Skip to content

API Reference

Core modes are the recommended default. Legacy provider-specific modes still work but are deprecated and will show warnings. See the Mode Migration Guide for details.

Core Clients

The main client classes for interacting with LLM providers.

Sync client wrapper that adds structured output support.

Source code in instructor/v2/core/client.py
class Instructor:
    """Sync client wrapper that adds structured output support."""

    client: Any | None
    create_fn: Callable[..., Any]
    mode: Mode
    default_model: str | None = None
    provider: Provider
    hooks: Hooks

    def __init__(
        self,
        client: Any | None,
        create: Callable[..., Any],
        mode: Mode = Mode.TOOLS,
        provider: Provider = Provider.OPENAI,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ):
        self.client = client
        self.create_fn = create
        self.mode = mode
        if mode == Mode.FUNCTIONS:
            Mode.warn_mode_functions_deprecation()

        self.kwargs = kwargs
        self.provider = provider
        self.hooks = hooks or Hooks()

        if mode in {
            Mode.RESPONSES_TOOLS,
            Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
        }:
            assert isinstance(client, (openai.OpenAI, openai.AsyncOpenAI))
            self.responses = Response(client=self)

    def on(
        self,
        hook_name: (
            HookName
            | Literal[
                "completion:kwargs",
                "completion:response",
                "completion:error",
                "completion:last_attempt",
                "parse:error",
            ]
        ),
        handler: Callable[[Any], None],
    ) -> None:
        self.hooks.on(hook_name, handler)

    def off(
        self,
        hook_name: (
            HookName
            | Literal[
                "completion:kwargs",
                "completion:response",
                "completion:error",
                "completion:last_attempt",
                "parse:error",
            ]
        ),
        handler: Callable[[Any], None],
    ) -> None:
        self.hooks.off(hook_name, handler)

    def clear(
        self,
        hook_name: (
            HookName
            | Literal[
                "completion:kwargs",
                "completion:response",
                "completion:error",
                "completion:last_attempt",
                "parse:error",
            ]
        )
        | None = None,
    ) -> None:
        self.hooks.clear(hook_name)

    @property
    def chat(self) -> Self:
        return self

    @property
    def completions(self) -> Self:
        return self

    @property
    def messages(self) -> Self:
        return self

    @overload
    def create(
        self: AsyncInstructor,
        response_model: type[T],
        messages: list[ChatCompletionMessageParam],
        max_retries: int | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,  # {{ edit_1 }}
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> Awaitable[T]: ...

    @overload
    def create(
        self: Self,
        response_model: type[T],
        messages: list[ChatCompletionMessageParam],
        max_retries: int | Retrying = 3,
        context: dict[str, Any] | None = None,  # {{ edit_1 }}
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> T: ...

    @overload
    def create(
        self: AsyncInstructor,
        response_model: None,
        messages: list[ChatCompletionMessageParam],
        max_retries: int | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,  # {{ edit_1 }}
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> Awaitable[Any]: ...

    @overload
    def create(
        self: Self,
        response_model: None,
        messages: list[ChatCompletionMessageParam],
        max_retries: int | Retrying = 3,
        context: dict[str, Any] | None = None,  # {{ edit_1 }}
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> Any: ...

    def create(
        self,
        response_model: type[T] | None,
        messages: list[ChatCompletionMessageParam],
        max_retries: int | Retrying | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> T | Any | Awaitable[T] | Awaitable[Any]:
        kwargs = self.handle_kwargs(kwargs)

        # Combine client hooks with per-call hooks
        combined_hooks = self.hooks
        if hooks is not None:
            combined_hooks = self.hooks + hooks

        return self.create_fn(
            response_model=response_model,
            messages=messages,
            max_retries=max_retries,
            context=context,
            strict=strict,
            hooks=combined_hooks,
            **kwargs,
        )

    @overload
    def create_partial(
        self: AsyncInstructor,
        response_model: type[T],
        messages: list[ChatCompletionMessageParam],
        max_retries: int | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,  # {{ edit_1 }}
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> AsyncGenerator[T, None]: ...

    @overload
    def create_partial(
        self: Self,
        response_model: type[T],
        messages: list[ChatCompletionMessageParam],
        max_retries: int | Retrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> Generator[T, None, None]: ...

    def create_partial(
        self,
        response_model: type[T],
        messages: list[ChatCompletionMessageParam],
        max_retries: int | Retrying | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> Generator[T, None, None] | AsyncGenerator[T, None]:
        kwargs["stream"] = True

        kwargs = self.handle_kwargs(kwargs)

        # Combine client hooks with per-call hooks
        combined_hooks = self.hooks
        if hooks is not None:
            combined_hooks = self.hooks + hooks

        response_model = Partial[response_model]  # type: ignore
        return self.create_fn(
            messages=messages,
            response_model=response_model,
            max_retries=max_retries,
            context=context,
            strict=strict,
            hooks=combined_hooks,
            **kwargs,
        )

    @overload
    def create_iterable(
        self: AsyncInstructor,
        messages: list[ChatCompletionMessageParam],
        response_model: type[T],
        max_retries: int | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> AsyncGenerator[T, None]: ...

    @overload
    def create_iterable(
        self: Self,
        messages: list[ChatCompletionMessageParam],
        response_model: type[T],
        max_retries: int | Retrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> Generator[T, None, None]: ...

    def create_iterable(
        self,
        messages: list[ChatCompletionMessageParam],
        response_model: type[T],
        max_retries: int | Retrying | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> Generator[T, None, None] | AsyncGenerator[T, None]:
        kwargs["stream"] = True
        kwargs = self.handle_kwargs(kwargs)

        # Combine client hooks with per-call hooks
        combined_hooks = self.hooks
        if hooks is not None:
            combined_hooks = self.hooks + hooks

        response_model = Iterable[response_model]  # type: ignore
        return self.create_fn(
            messages=messages,
            response_model=response_model,
            max_retries=max_retries,
            context=context,
            strict=strict,
            hooks=combined_hooks,
            **kwargs,
        )

    @overload
    def create_with_completion(
        self: AsyncInstructor,
        messages: list[ChatCompletionMessageParam],
        response_model: type[T],
        max_retries: int | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> Awaitable[tuple[T, Any]]: ...

    @overload
    def create_with_completion(
        self: Self,
        messages: list[ChatCompletionMessageParam],
        response_model: type[T],
        max_retries: int | Retrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> tuple[T, Any]: ...

    def create_with_completion(
        self,
        messages: list[ChatCompletionMessageParam],
        response_model: type[T],
        max_retries: int | Retrying | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> tuple[T, Any] | Awaitable[tuple[T, Any]]:
        kwargs = self.handle_kwargs(kwargs)

        # Combine client hooks with per-call hooks
        combined_hooks = self.hooks
        if hooks is not None:
            combined_hooks = self.hooks + hooks

        model = self.create_fn(
            messages=messages,
            response_model=response_model,
            max_retries=max_retries,
            context=context,
            strict=strict,
            hooks=combined_hooks,
            **kwargs,
        )
        return model, model._raw_response

    def handle_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]:
        """
        Handle and process keyword arguments for the API call.

        This method merges the provided kwargs with the default kwargs stored in the instance.
        It ensures that any kwargs passed to the method call take precedence over the default ones.
        """
        for key, value in self.kwargs.items():
            if key not in kwargs:
                kwargs[key] = value
        return kwargs

    def __getattr__(self, attr: str) -> Any:
        if attr not in {"create", "chat", "messages"}:
            return getattr(self.client, attr)

        return getattr(self, attr)

handle_kwargs(kwargs)

Handle and process keyword arguments for the API call.

This method merges the provided kwargs with the default kwargs stored in the instance. It ensures that any kwargs passed to the method call take precedence over the default ones.

Source code in instructor/v2/core/client.py
def handle_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]:
    """
    Handle and process keyword arguments for the API call.

    This method merges the provided kwargs with the default kwargs stored in the instance.
    It ensures that any kwargs passed to the method call take precedence over the default ones.
    """
    for key, value in self.kwargs.items():
        if key not in kwargs:
            kwargs[key] = value
    return kwargs

Bases: Instructor

Async client wrapper that adds structured output support.

Source code in instructor/v2/core/client.py
class AsyncInstructor(Instructor):
    """Async client wrapper that adds structured output support."""

    client: Any | None
    create_fn: Callable[..., Any]
    mode: Mode
    default_model: str | None = None
    provider: Provider
    hooks: Hooks

    def __init__(
        self,
        client: Any | None,
        create: Callable[..., Any],
        mode: Mode = Mode.TOOLS,
        provider: Provider = Provider.OPENAI,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ):
        self.client = client
        self.create_fn = create
        self.mode = mode
        self.kwargs = kwargs
        self.provider = provider
        self.hooks = hooks or Hooks()

        if mode in {
            Mode.RESPONSES_TOOLS,
            Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
        }:
            assert isinstance(client, (openai.OpenAI, openai.AsyncOpenAI))
            self.responses = AsyncResponse(client=self)

    async def create(  # type: ignore[override]
        self,
        response_model: type[T] | None,
        messages: list[ChatCompletionMessageParam],
        max_retries: int | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> T | Any:
        kwargs = self.handle_kwargs(kwargs)

        # Combine client hooks with per-call hooks
        combined_hooks = self.hooks
        if hooks is not None:
            combined_hooks = self.hooks + hooks

        # Check if the response model is an iterable type
        if (
            get_origin(response_model) in {Iterable}
            and get_args(response_model)
            and get_args(response_model)[0] is not None
            and self.mode not in Mode.parallel_modes()
        ):
            return self.create_iterable(
                messages=messages,
                response_model=get_args(response_model)[0],
                max_retries=max_retries,
                context=context,
                strict=strict,
                hooks=hooks,  # Pass the per-call hooks to create_iterable
                **kwargs,
            )

        return await self.create_fn(
            response_model=response_model,
            context=context,
            max_retries=max_retries,
            messages=messages,
            strict=strict,
            hooks=combined_hooks,
            **kwargs,
        )

    async def create_partial(  # type: ignore[override]
        self,
        response_model: type[T],
        messages: list[ChatCompletionMessageParam],
        max_retries: int | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> AsyncGenerator[T, None]:
        kwargs = self.handle_kwargs(kwargs)
        kwargs["stream"] = True

        # Combine client hooks with per-call hooks
        combined_hooks = self.hooks
        if hooks is not None:
            combined_hooks = self.hooks + hooks

        async for item in await self.create_fn(
            response_model=Partial[response_model],  # type: ignore
            context=context,
            max_retries=max_retries,
            messages=messages,
            strict=strict,
            hooks=combined_hooks,
            **kwargs,
        ):
            yield item

    async def create_iterable(  # type: ignore[override]
        self,
        messages: list[ChatCompletionMessageParam],
        response_model: type[T],
        max_retries: int | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> AsyncGenerator[T, None]:
        kwargs = self.handle_kwargs(kwargs)
        kwargs["stream"] = True

        # Combine client hooks with per-call hooks
        combined_hooks = self.hooks
        if hooks is not None:
            combined_hooks = self.hooks + hooks

        async for item in await self.create_fn(
            response_model=Iterable[response_model],
            context=context,
            max_retries=max_retries,
            messages=messages,
            strict=strict,
            hooks=combined_hooks,
            **kwargs,
        ):
            yield item

    async def create_with_completion(  # type: ignore[override]
        self,
        messages: list[ChatCompletionMessageParam],
        response_model: type[T],
        max_retries: int | AsyncRetrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        hooks: Hooks | None = None,
        **kwargs: Any,
    ) -> tuple[T, Any]:
        kwargs = self.handle_kwargs(kwargs)

        # Combine client hooks with per-call hooks
        combined_hooks = self.hooks
        if hooks is not None:
            combined_hooks = self.hooks + hooks

        response = await self.create_fn(
            response_model=response_model,
            context=context,
            max_retries=max_retries,
            messages=messages,
            strict=strict,
            hooks=combined_hooks,
            **kwargs,
        )
        return response, response._raw_response

Helper for responses API using a patched client.

Source code in instructor/v2/core/client.py
class Response:
    """Helper for responses API using a patched client."""

    def __init__(
        self,
        client: Instructor,
    ):
        self.client = client

    @staticmethod
    def _normalize_messages(
        messages: str | list[ChatCompletionMessageParam] | None,
        kwargs: dict[str, Any],
    ) -> str | list[ChatCompletionMessageParam]:
        if messages is None:
            if "input" not in kwargs:
                raise TypeError("Either 'messages' or 'input' must be provided")
            messages = kwargs.pop("input")
        elif "input" in kwargs:
            raise TypeError("Pass only one of 'messages' or 'input'")

        if isinstance(messages, str):
            return [{"role": "user", "content": messages}]
        return messages

    @overload
    def create(
        self,
        messages: str | list[ChatCompletionMessageParam] | None = None,
        response_model: type[T] = ...,
        max_retries: int | Retrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        **kwargs: Any,
    ) -> T: ...

    @overload
    def create(
        self,
        messages: str | list[ChatCompletionMessageParam] | None = None,
        response_model: None = None,
        max_retries: int | Retrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        **kwargs: Any,
    ) -> Any: ...

    def create(
        self,
        messages: str | list[ChatCompletionMessageParam] | None = None,
        response_model: type[T] | None = None,
        max_retries: int | Retrying = 3,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        **kwargs,
    ) -> T | Any:
        messages = self._normalize_messages(messages, kwargs)

        return self.client.create(
            response_model=response_model,
            context=context,
            max_retries=max_retries,
            strict=strict,
            messages=messages,
            **kwargs,
        )

    @overload
    def create_with_completion(
        self,
        messages: str | list[ChatCompletionMessageParam] | None = None,
        response_model: type[T] = ...,
        max_retries: int | Retrying = 3,
        **kwargs: Any,
    ) -> tuple[T, Any]: ...

    @overload
    def create_with_completion(
        self,
        messages: str | list[ChatCompletionMessageParam] | None = None,
        response_model: None = None,
        max_retries: int | Retrying = 3,
        **kwargs: Any,
    ) -> tuple[Any, Any]: ...

    def create_with_completion(
        self,
        messages: str | list[ChatCompletionMessageParam] | None = None,
        response_model: type[T] | None = None,
        max_retries: int | Retrying = 3,
        **kwargs,
    ) -> tuple[T, Any]:
        messages = self._normalize_messages(messages, kwargs)

        return self.client.create_with_completion(
            messages=messages,
            response_model=response_model,
            max_retries=max_retries,
            **kwargs,
        )

    @overload
    def create_iterable(
        self,
        messages: str | list[ChatCompletionMessageParam] | None = None,
        response_model: type[T] = ...,
        max_retries: int | Retrying = 3,
        **kwargs: Any,
    ) -> Generator[T, None, None]: ...

    @overload
    def create_iterable(
        self,
        messages: str | list[ChatCompletionMessageParam] | None = None,
        response_model: None = None,
        max_retries: int | Retrying = 3,
        **kwargs: Any,
    ) -> Generator[Any, None, None]: ...

    def create_iterable(
        self,
        messages: str | list[ChatCompletionMessageParam] | None = None,
        response_model: type[T] | None = None,
        max_retries: int | Retrying = 3,
        **kwargs,
    ) -> Generator[T, None, None]:
        messages = self._normalize_messages(messages, kwargs)

        return self.client.create_iterable(
            messages=messages,
            response_model=response_model,
            max_retries=max_retries,
            **kwargs,
        )

    @overload
    def create_partial(
        self,
        messages: str | list[ChatCompletionMessageParam] | None = None,
        response_model: type[T] = ...,
        max_retries: int | Retrying = 3,
        **kwargs: Any,
    ) -> Generator[T, None, None]: ...

    @overload
    def create_partial(
        self,
        messages: str | list[ChatCompletionMessageParam] | None = None,
        response_model: None = None,
        max_retries: int | Retrying = 3,
        **kwargs: Any,
    ) -> Generator[Any, None, None]: ...

    def create_partial(
        self,
        messages: str | list[ChatCompletionMessageParam] | None = None,
        response_model: type[T] | None = None,
        max_retries: int | Retrying = 3,
        **kwargs,
    ) -> Generator[T, None, None]:
        messages = self._normalize_messages(messages, kwargs)

        return self.client.create_partial(
            messages=messages,
            response_model=response_model,
            max_retries=max_retries,
            **kwargs,
        )

Client Creation

Functions to create Instructor clients from various providers.

Create an Instructor client from a model string.

Parameters:

Name Type Description Default
model Union[str, KnownModelName]

String in format "provider/model-name" (e.g., "openai/gpt-4", "anthropic/claude-3-sonnet", "google/gemini-pro")

required
async_client bool

Whether to return an async client

False
cache BaseCache | None

Optional cache adapter (e.g., AutoCache or RedisCache) to enable transparent response caching. Automatically flows through **kwargs to all provider implementations.

None
mode Union[Mode, None]

Override the default mode for the provider. If not specified, uses the recommended default mode for each provider.

None
**kwargs Any

Additional arguments passed to the provider client functions. This includes the cache parameter and any provider-specific options.

{}

Returns:

Type Description
Union[Instructor, AsyncInstructor]

Instructor or AsyncInstructor instance

Raises:

Type Description
ValueError

If provider is not supported or model string is invalid

ImportError

If required package for provider is not installed

Examples:

>>> import instructor
>>> from instructor.cache import AutoCache
>>>
>>> # Basic usage
>>> client = instructor.from_provider("openai/gpt-4")
>>> client = instructor.from_provider("anthropic/claude-3-sonnet")
>>>
>>> # With caching
>>> cache = AutoCache(maxsize=1000)
>>> client = instructor.from_provider("openai/gpt-4", cache=cache)
>>>
>>> # Async clients
>>> async_client = instructor.from_provider("openai/gpt-4", async_client=True)
Source code in instructor/v2/auto_client.py
def from_provider(
    model: Union[str, KnownModelName],  # noqa: UP007
    async_client: bool = False,
    cache: BaseCache | None = None,
    mode: Union[Mode, None] = None,  # noqa: ARG001, UP007
    **kwargs: Any,
) -> Union[Instructor, AsyncInstructor]:  # noqa: UP007
    """Create an Instructor client from a model string.

    Args:
        model: String in format "provider/model-name"
              (e.g., "openai/gpt-4", "anthropic/claude-3-sonnet", "google/gemini-pro")
        async_client: Whether to return an async client
        cache: Optional cache adapter (e.g., ``AutoCache`` or ``RedisCache``)
               to enable transparent response caching. Automatically flows through
               **kwargs to all provider implementations.
        mode: Override the default mode for the provider. If not specified, uses the
              recommended default mode for each provider.
        **kwargs: Additional arguments passed to the provider client functions.
                 This includes the cache parameter and any provider-specific options.

    Returns:
        Instructor or AsyncInstructor instance

    Raises:
        ValueError: If provider is not supported or model string is invalid
        ImportError: If required package for provider is not installed

    Examples:
        >>> import instructor
        >>> from instructor.cache import AutoCache
        >>>
        >>> # Basic usage
        >>> client = instructor.from_provider("openai/gpt-4")
        >>> client = instructor.from_provider("anthropic/claude-3-sonnet")
        >>>
        >>> # With caching
        >>> cache = AutoCache(maxsize=1000)
        >>> client = instructor.from_provider("openai/gpt-4", cache=cache)
        >>>
        >>> # Async clients
        >>> async_client = instructor.from_provider("openai/gpt-4", async_client=True)
    """
    # Add cache to kwargs if provided so it flows through to provider functions
    if cache is not None:
        kwargs["cache"] = cache

    try:
        provider, model_name = model.split("/", 1)
    except ValueError:
        from instructor.v2.core.errors import ConfigurationError

        raise ConfigurationError(
            'Model string must be in format "provider/model-name" '
            '(e.g. "openai/gpt-4" or "anthropic/claude-3-sonnet")'
        ) from None

    provider_info = {"provider": provider, "operation": "initialize"}
    logger.info(
        "Initializing %s provider with model %s",
        provider,
        model_name,
        extra=provider_info,
    )
    logger.debug(
        "Provider configuration: async_client=%s, mode=%s",
        async_client,
        mode,
        extra=provider_info,
    )
    api_key = None
    if "api_key" in kwargs:
        api_key = kwargs.pop("api_key")
        if api_key:
            logger.debug(
                "API key provided for %s provider (length: %d characters)",
                provider,
                len(api_key),
                extra=provider_info,
            )

    builder = _PROVIDER_BUILDERS.get(provider)
    if builder is None:
        from instructor.v2.core.errors import ConfigurationError

        logger.error(
            "Error initializing %s client: unsupported provider",
            provider,
            extra={**provider_info, "status": "error"},
        )
        raise ConfigurationError(
            f"Unsupported provider: {provider}. "
            f"Supported providers are: {supported_providers}"
        )

    return builder(
        provider=provider,
        model_name=model_name,
        async_client=async_client,
        mode=mode,
        api_key=api_key,
        kwargs=kwargs,
        provider_info=provider_info,
    )

Create an Instructor instance from an OpenAI client using v2 registry.

Parameters:

Name Type Description Default
client OpenAI | AsyncOpenAI

An instance of OpenAI client (sync or async)

required
mode Mode

The mode to use (defaults to Mode.TOOLS)

TOOLS
model str | None

Optional model to inject if not provided in requests

None
**kwargs Any

Additional keyword arguments to pass to the Instructor constructor

{}

Returns:

Type Description
Instructor | AsyncInstructor

An Instructor instance (sync or async depending on the client type)

Raises:

Type Description
ModeError

If mode is not registered for OpenAI

ClientError

If client is not a valid OpenAI client instance

Examples:

>>> import openai
>>> from instructor import Mode
>>> from instructor.v2.providers.openai import from_openai
>>>
>>> client = openai.OpenAI()
>>> instructor_client = from_openai(client, mode=Mode.TOOLS)
>>>
>>> # Or use JSON_SCHEMA mode for structured outputs
>>> instructor_client = from_openai(client, mode=Mode.JSON_SCHEMA)
Source code in instructor/v2/providers/openai/client.py
def from_openai(
    client: openai.OpenAI | openai.AsyncOpenAI,
    mode: Mode = Mode.TOOLS,
    model: str | None = None,
    **kwargs: Any,
) -> Instructor | AsyncInstructor:
    """Create an Instructor instance from an OpenAI client using v2 registry.

    Args:
        client: An instance of OpenAI client (sync or async)
        mode: The mode to use (defaults to Mode.TOOLS)
        model: Optional model to inject if not provided in requests
        **kwargs: Additional keyword arguments to pass to the Instructor constructor

    Returns:
        An Instructor instance (sync or async depending on the client type)

    Raises:
        ModeError: If mode is not registered for OpenAI
        ClientError: If client is not a valid OpenAI client instance

    Examples:
        >>> import openai
        >>> from instructor import Mode
        >>> from instructor.v2.providers.openai import from_openai
        >>>
        >>> client = openai.OpenAI()
        >>> instructor_client = from_openai(client, mode=Mode.TOOLS)
        >>>
        >>> # Or use JSON_SCHEMA mode for structured outputs
        >>> instructor_client = from_openai(client, mode=Mode.JSON_SCHEMA)
    """
    return _from_openai_compat(
        client=client,
        provider=Provider.OPENAI,
        mode=mode,
        model=model,
        **kwargs,
    )

Create an Instructor client from a LiteLLM completion function.

Source code in instructor/v2/providers/litellm/client.py
def from_litellm(
    completion: Callable[..., object] | Callable[..., Awaitable[Any]],
    mode: Mode = Mode.TOOLS,
    *,
    async_client: bool | None = None,
    **kwargs: Any,
) -> Instructor | AsyncInstructor:
    """Create an Instructor client from a LiteLLM completion function."""
    create = patch_v2(func=completion, provider=Provider.OPENAI, mode=mode)
    if async_client is None:
        async_client = inspect.iscoroutinefunction(completion)
    client_type = AsyncInstructor if async_client else Instructor
    return client_type(
        client=None,
        create=create,
        mode=mode,
        provider=Provider.OPENAI,
        **kwargs,
    )

DSL Components

Domain-specific language components for advanced patterns and data handling.

Compatibility exports for v2-owned iterable helpers.

IterableBase

Source code in instructor/v2/dsl/iterable.py
class IterableBase:
    task_type: ClassVar[Optional[type[BaseModel]]] = None

    @classmethod
    def from_streaming_response(
        cls,
        completion: Iterable[Any],
        stream_extractor: Callable[[Iterable[Any]], Generator[str, None, None]],
        task_parser: Callable[..., Generator[BaseModel, None, None]] | None = None,
        **kwargs: Any,
    ) -> Generator[BaseModel, None, None]:
        if stream_extractor is None:
            raise ValueError("stream_extractor is required for streaming responses")
        json_chunks = stream_extractor(completion)
        parser = task_parser or cls.tasks_from_chunks
        yield from parser(json_chunks, **kwargs)

    @classmethod
    async def from_streaming_response_async(
        cls,
        completion: AsyncGenerator[Any, None],
        stream_extractor: Callable[
            [AsyncGenerator[Any, None]], AsyncGenerator[str, None]
        ],
        task_parser: Callable[..., AsyncGenerator[BaseModel, None]] | None = None,
        **kwargs: Any,
    ) -> AsyncGenerator[BaseModel, None]:
        if stream_extractor is None:
            raise ValueError("stream_extractor is required for streaming responses")
        json_chunks = stream_extractor(completion)
        parser = task_parser or cls.tasks_from_chunks_async
        async for item in parser(json_chunks, **kwargs):
            yield item

    @classmethod
    async def tasks_from_task_list_chunks_async(
        cls, json_chunks: AsyncGenerator[str, None], **kwargs: Any
    ) -> AsyncGenerator[BaseModel, None]:
        """Process streaming chunks that contain a full tasks list."""

        async for chunk in json_chunks:
            if not chunk:
                continue
            json_response = json.loads(chunk)
            if not json_response["tasks"]:
                continue

            for item in json_response["tasks"]:
                obj = cls.extract_cls_task_type(json.dumps(item), **kwargs)
                yield obj

    @classmethod
    def tasks_from_task_list_chunks(
        cls, json_chunks: Iterable[str], **kwargs: Any
    ) -> Generator[BaseModel, None, None]:
        """Process streaming chunks that contain a full tasks list."""
        for chunk in json_chunks:
            if not chunk:
                continue
            json_response = json.loads(chunk)
            if not json_response["tasks"]:
                continue

            for item in json_response["tasks"]:
                obj = cls.extract_cls_task_type(json.dumps(item), **kwargs)
                yield obj

    @classmethod
    def tasks_from_chunks(
        cls, json_chunks: Iterable[str], **kwargs: Any
    ) -> Generator[BaseModel, None, None]:
        started = False
        potential_object = ""
        for chunk in json_chunks:
            potential_object += chunk
            if not started:
                if "[" in chunk:
                    started = True
                    potential_object = chunk[chunk.find("[") + 1 :]

            while True:
                task_json, potential_object = cls.get_object(potential_object, 0)
                if task_json:
                    assert cls.task_type is not None
                    obj = cls.extract_cls_task_type(task_json, **kwargs)
                    yield obj
                else:
                    break

    @classmethod
    async def tasks_from_chunks_async(
        cls, json_chunks: AsyncGenerator[str, None], **kwargs: Any
    ) -> AsyncGenerator[BaseModel, None]:
        started = False
        potential_object = ""
        async for chunk in json_chunks:
            potential_object += chunk
            if not started:
                if "[" in chunk:
                    started = True
                    potential_object = chunk[chunk.find("[") + 1 :]

            while True:
                task_json, potential_object = cls.get_object(potential_object, 0)
                if task_json:
                    assert cls.task_type is not None
                    obj = cls.extract_cls_task_type(task_json, **kwargs)
                    yield obj
                else:
                    break

    @classmethod
    def extract_cls_task_type(
        cls,
        task_json: str,
        **kwargs: Any,
    ):
        assert cls.task_type is not None
        if get_origin(cls.task_type) is Union:
            union_members = get_args(cls.task_type)
            for member in union_members:
                try:
                    obj = member.model_validate_json(task_json, **kwargs)
                    return obj
                except Exception:
                    pass
        else:
            return cls.task_type.model_validate_json(task_json, **kwargs)
        raise ValueError(
            f"Failed to extract task type with {task_json} for {cls.task_type}"
        )

    @staticmethod
    def extract_json(
        completion: Iterable[Any],
        stream_extractor: Callable[[Iterable[Any]], Generator[str, None, None]],
    ) -> Generator[str, None, None]:
        if stream_extractor is None:
            raise ValueError("stream_extractor is required for streaming responses")
        yield from stream_extractor(completion)

    @staticmethod
    async def extract_json_async(
        completion: AsyncGenerator[Any, None],
        stream_extractor: Callable[
            [AsyncGenerator[Any, None]], AsyncGenerator[str, None]
        ],
    ) -> AsyncGenerator[str, None]:
        if stream_extractor is None:
            raise ValueError("stream_extractor is required for streaming responses")
        async for chunk in stream_extractor(completion):
            yield chunk

    @staticmethod
    def get_object(s: str, stack: int) -> tuple[Optional[str], str]:
        start_index = s.find("{")
        for i, c in enumerate(s):
            if c == "{":
                stack += 1
            if c == "}":
                stack -= 1
                if stack == 0:
                    return s[start_index : i + 1], s[i + 2 :]
        return None, s

tasks_from_task_list_chunks(json_chunks, **kwargs) classmethod

Process streaming chunks that contain a full tasks list.

Source code in instructor/v2/dsl/iterable.py
@classmethod
def tasks_from_task_list_chunks(
    cls, json_chunks: Iterable[str], **kwargs: Any
) -> Generator[BaseModel, None, None]:
    """Process streaming chunks that contain a full tasks list."""
    for chunk in json_chunks:
        if not chunk:
            continue
        json_response = json.loads(chunk)
        if not json_response["tasks"]:
            continue

        for item in json_response["tasks"]:
            obj = cls.extract_cls_task_type(json.dumps(item), **kwargs)
            yield obj

tasks_from_task_list_chunks_async(json_chunks, **kwargs) async classmethod

Process streaming chunks that contain a full tasks list.

Source code in instructor/v2/dsl/iterable.py
@classmethod
async def tasks_from_task_list_chunks_async(
    cls, json_chunks: AsyncGenerator[str, None], **kwargs: Any
) -> AsyncGenerator[BaseModel, None]:
    """Process streaming chunks that contain a full tasks list."""

    async for chunk in json_chunks:
        if not chunk:
            continue
        json_response = json.loads(chunk)
        if not json_response["tasks"]:
            continue

        for item in json_response["tasks"]:
            obj = cls.extract_cls_task_type(json.dumps(item), **kwargs)
            yield obj

Compatibility exports for v2-owned partial helpers.

JsonCompleteness

Track completeness of JSON structures during streaming.

Uses a simple heuristic: if a value has a next sibling in the parsed structure, it must be complete. For the last sibling, we don't know until the parent completes - but that's fine because parent validation will cover it.

Example

tracker = JsonCompleteness()

Incomplete - missing closing brace

tracker.analyze('{"name": "Alice", "address": {"city": "NY') tracker.is_path_complete("") # False - root incomplete tracker.is_path_complete("name") # True - has next sibling "address" tracker.is_path_complete("address") # False - last sibling, unknown

Complete

tracker.analyze('{"name": "Alice"}') tracker.is_path_complete("") # True - root complete

Source code in instructor/v2/dsl/json_tracker.py
class JsonCompleteness:
    """
    Track completeness of JSON structures during streaming.

    Uses a simple heuristic: if a value has a next sibling in the parsed
    structure, it must be complete. For the last sibling, we don't know
    until the parent completes - but that's fine because parent validation
    will cover it.

    Example:
        tracker = JsonCompleteness()

        # Incomplete - missing closing brace
        tracker.analyze('{"name": "Alice", "address": {"city": "NY')
        tracker.is_path_complete("")  # False - root incomplete
        tracker.is_path_complete("name")  # True - has next sibling "address"
        tracker.is_path_complete("address")  # False - last sibling, unknown

        # Complete
        tracker.analyze('{"name": "Alice"}')
        tracker.is_path_complete("")  # True - root complete
    """

    def __init__(self) -> None:
        self._complete_paths: set[str] = set()

    def analyze(self, json_str: str) -> None:
        """Analyze a JSON string and determine completeness of each path."""
        self._complete_paths = set()

        if not json_str or not json_str.strip():
            return

        # Try strict parsing first - if it succeeds, JSON is complete
        try:
            parsed = from_json(json_str.encode())
            self._mark_all(parsed, "")
            return
        except ValueError:
            pass  # JSON is incomplete, continue with partial parsing

        # Root incomplete - use sibling heuristic
        try:
            parsed = from_json(json_str.encode(), partial_mode="trailing-strings")
        except ValueError:
            return

        self._check_siblings(parsed, "")

    def _mark_all(self, data: Any, path: str) -> None:
        """Recursively mark path and all children as complete."""
        self._complete_paths.add(path)
        if isinstance(data, dict):
            for key, value in data.items():
                child_path = f"{path}.{key}" if path else key
                self._mark_all(value, child_path)
        elif isinstance(data, list):
            for i, item in enumerate(data):
                self._mark_all(item, f"{path}[{i}]")

    def _check_siblings(self, data: Any, path: str) -> None:
        """
        Check completeness using sibling heuristic.

        If a value has a next sibling, it's complete (jiter had to finish
        parsing it to find the next sibling). Last sibling is unknown.
        """
        if isinstance(data, dict):
            keys = list(data.keys())
            for i, key in enumerate(keys):
                child_path = f"{path}.{key}" if path else key
                if i < len(keys) - 1:
                    # Has next sibling → complete
                    self._mark_all(data[key], child_path)
                else:
                    # Last sibling → recurse to check children
                    self._check_siblings(data[key], child_path)

        elif isinstance(data, list):
            for i, item in enumerate(data):
                child_path = f"{path}[{i}]"
                if i < len(data) - 1:
                    # Has next sibling → complete
                    self._mark_all(item, child_path)
                else:
                    # Last sibling → recurse
                    self._check_siblings(item, child_path)

    def is_path_complete(self, path: str) -> bool:
        """
        Check if the sub-structure at the given path is complete.

        Args:
            path: Dot-separated path (e.g., "user.address.city", "items[0]")
                  Use "" for root object.

        Returns:
            True if the structure at path is complete (closed), False otherwise.
        """
        return path in self._complete_paths

    def get_complete_paths(self) -> set[str]:
        """Return all paths that are complete."""
        return self._complete_paths.copy()

    def is_root_complete(self) -> bool:
        """Check if the root JSON structure is complete."""
        return "" in self._complete_paths

analyze(json_str)

Analyze a JSON string and determine completeness of each path.

Source code in instructor/v2/dsl/json_tracker.py
def analyze(self, json_str: str) -> None:
    """Analyze a JSON string and determine completeness of each path."""
    self._complete_paths = set()

    if not json_str or not json_str.strip():
        return

    # Try strict parsing first - if it succeeds, JSON is complete
    try:
        parsed = from_json(json_str.encode())
        self._mark_all(parsed, "")
        return
    except ValueError:
        pass  # JSON is incomplete, continue with partial parsing

    # Root incomplete - use sibling heuristic
    try:
        parsed = from_json(json_str.encode(), partial_mode="trailing-strings")
    except ValueError:
        return

    self._check_siblings(parsed, "")

get_complete_paths()

Return all paths that are complete.

Source code in instructor/v2/dsl/json_tracker.py
def get_complete_paths(self) -> set[str]:
    """Return all paths that are complete."""
    return self._complete_paths.copy()

is_path_complete(path)

Check if the sub-structure at the given path is complete.

Parameters:

Name Type Description Default
path str

Dot-separated path (e.g., "user.address.city", "items[0]") Use "" for root object.

required

Returns:

Type Description
bool

True if the structure at path is complete (closed), False otherwise.

Source code in instructor/v2/dsl/json_tracker.py
def is_path_complete(self, path: str) -> bool:
    """
    Check if the sub-structure at the given path is complete.

    Args:
        path: Dot-separated path (e.g., "user.address.city", "items[0]")
              Use "" for root object.

    Returns:
        True if the structure at path is complete (closed), False otherwise.
    """
    return path in self._complete_paths

is_root_complete()

Check if the root JSON structure is complete.

Source code in instructor/v2/dsl/json_tracker.py
def is_root_complete(self) -> bool:
    """Check if the root JSON structure is complete."""
    return "" in self._complete_paths

Partial

Bases: Generic[T_Model]

Generate a new class which has PartialBase as a base class.

Notes

This will enable partial validation of the model while streaming.

Example

Partial[SomeModel]

Source code in instructor/v2/dsl/partial.py
class Partial(Generic[T_Model]):
    """Generate a new class which has PartialBase as a base class.

    Notes:
        This will enable partial validation of the model while streaming.

    Example:
        Partial[SomeModel]
    """

    def __new__(
        cls,
        *args: object,  # noqa
        **kwargs: object,  # noqa
    ) -> Partial[T_Model]:
        """Cannot instantiate.

        Raises:
            TypeError: Direct instantiation not allowed.
        """
        raise TypeError("Cannot instantiate abstract Partial class.")

    def __init_subclass__(
        cls,
        *args: object,
        **kwargs: object,
    ) -> NoReturn:
        """Cannot subclass.

        Raises:
           TypeError: Subclassing not allowed.
        """
        raise TypeError(f"Cannot subclass {cls.__module__}.Partial")

    def __class_getitem__(
        cls,
        wrapped_class: type[T_Model] | tuple[type[T_Model], type[MakeFieldsOptional]],
    ) -> type[T_Model]:
        """Convert model to one that inherits from PartialBase.

        We don't make the fields optional at this point, we just wrap them with `Partial` so the names of the nested models will be
        `Partial{ModelName}`. We want the output of `model_json_schema()` to
        reflect the name change, but everything else should be the same as the
        original model. During validation, we'll generate a true partial model
        to support partially defined fields.

        """

        make_fields_optional = None
        if isinstance(wrapped_class, tuple):
            wrapped_class, make_fields_optional = wrapped_class

        def _wrap_models(field: FieldInfo) -> tuple[object, FieldInfo]:
            tmp_field = deepcopy(field)

            annotation = field.annotation

            # Handle generics (like List, Dict, etc.)
            if get_origin(annotation) is not None:
                # Get the generic base (like List, Dict) and its arguments (like User in List[User])
                generic_base = get_origin(annotation)
                generic_args = get_args(annotation)

                modified_args = tuple(_process_generic_arg(arg) for arg in generic_args)

                # Reconstruct the generic type with modified arguments
                if generic_base is UNION_TYPE:
                    tmp_field.annotation = reduce(or_, modified_args)
                else:
                    tmp_field.annotation = (
                        generic_base[modified_args] if generic_base else None
                    )
            # If the field is a BaseModel, then recursively convert it's
            # attributes to optionals.
            elif isinstance(annotation, type) and issubclass(annotation, BaseModel):
                # Prevent infinite recursion for self-referential models
                if annotation in _processing_models:
                    tmp_field.annotation = (
                        annotation  # Already processing, keep unwrapped
                    )
                else:
                    _processing_models.add(annotation)
                    try:
                        tmp_field.annotation = Partial[annotation]
                    finally:
                        _processing_models.discard(annotation)
            return tmp_field.annotation, tmp_field

        model_name = (
            wrapped_class.__name__
            if wrapped_class.__name__.startswith("Partial")
            else f"Partial{wrapped_class.__name__}"
        )

        partial_model = create_model(
            model_name,
            __base__=(wrapped_class, PartialBase),  # type: ignore
            __module__=wrapped_class.__module__,
            **{
                field_name: (
                    _make_field_optional(field_info)
                    if make_fields_optional is not None
                    else _wrap_models(field_info)
                )
                for field_name, field_info in wrapped_class.model_fields.items()
            },  # type: ignore
        )

        # Store reference to original model for final validation
        partial_model._original_model = wrapped_class  # type: ignore[attr-defined]

        return partial_model

__class_getitem__(wrapped_class)

Convert model to one that inherits from PartialBase.

We don't make the fields optional at this point, we just wrap them with Partial so the names of the nested models will be Partial{ModelName}. We want the output of model_json_schema() to reflect the name change, but everything else should be the same as the original model. During validation, we'll generate a true partial model to support partially defined fields.

Source code in instructor/v2/dsl/partial.py
def __class_getitem__(
    cls,
    wrapped_class: type[T_Model] | tuple[type[T_Model], type[MakeFieldsOptional]],
) -> type[T_Model]:
    """Convert model to one that inherits from PartialBase.

    We don't make the fields optional at this point, we just wrap them with `Partial` so the names of the nested models will be
    `Partial{ModelName}`. We want the output of `model_json_schema()` to
    reflect the name change, but everything else should be the same as the
    original model. During validation, we'll generate a true partial model
    to support partially defined fields.

    """

    make_fields_optional = None
    if isinstance(wrapped_class, tuple):
        wrapped_class, make_fields_optional = wrapped_class

    def _wrap_models(field: FieldInfo) -> tuple[object, FieldInfo]:
        tmp_field = deepcopy(field)

        annotation = field.annotation

        # Handle generics (like List, Dict, etc.)
        if get_origin(annotation) is not None:
            # Get the generic base (like List, Dict) and its arguments (like User in List[User])
            generic_base = get_origin(annotation)
            generic_args = get_args(annotation)

            modified_args = tuple(_process_generic_arg(arg) for arg in generic_args)

            # Reconstruct the generic type with modified arguments
            if generic_base is UNION_TYPE:
                tmp_field.annotation = reduce(or_, modified_args)
            else:
                tmp_field.annotation = (
                    generic_base[modified_args] if generic_base else None
                )
        # If the field is a BaseModel, then recursively convert it's
        # attributes to optionals.
        elif isinstance(annotation, type) and issubclass(annotation, BaseModel):
            # Prevent infinite recursion for self-referential models
            if annotation in _processing_models:
                tmp_field.annotation = (
                    annotation  # Already processing, keep unwrapped
                )
            else:
                _processing_models.add(annotation)
                try:
                    tmp_field.annotation = Partial[annotation]
                finally:
                    _processing_models.discard(annotation)
        return tmp_field.annotation, tmp_field

    model_name = (
        wrapped_class.__name__
        if wrapped_class.__name__.startswith("Partial")
        else f"Partial{wrapped_class.__name__}"
    )

    partial_model = create_model(
        model_name,
        __base__=(wrapped_class, PartialBase),  # type: ignore
        __module__=wrapped_class.__module__,
        **{
            field_name: (
                _make_field_optional(field_info)
                if make_fields_optional is not None
                else _wrap_models(field_info)
            )
            for field_name, field_info in wrapped_class.model_fields.items()
        },  # type: ignore
    )

    # Store reference to original model for final validation
    partial_model._original_model = wrapped_class  # type: ignore[attr-defined]

    return partial_model

__init_subclass__(*args, **kwargs)

Cannot subclass.

Raises:

Type Description
TypeError

Subclassing not allowed.

Source code in instructor/v2/dsl/partial.py
def __init_subclass__(
    cls,
    *args: object,
    **kwargs: object,
) -> NoReturn:
    """Cannot subclass.

    Raises:
       TypeError: Subclassing not allowed.
    """
    raise TypeError(f"Cannot subclass {cls.__module__}.Partial")

__new__(*args, **kwargs)

Cannot instantiate.

Raises:

Type Description
TypeError

Direct instantiation not allowed.

Source code in instructor/v2/dsl/partial.py
def __new__(
    cls,
    *args: object,  # noqa
    **kwargs: object,  # noqa
) -> Partial[T_Model]:
    """Cannot instantiate.

    Raises:
        TypeError: Direct instantiation not allowed.
    """
    raise TypeError("Cannot instantiate abstract Partial class.")

PartialBase

Bases: Generic[T_Model]

Source code in instructor/v2/dsl/partial.py
class PartialBase(Generic[T_Model]):
    @classmethod
    @cache
    def get_partial_model(cls) -> type[T_Model]:
        """Return a partial model for holding incomplete streaming data.

        With completeness-based validation, we use model_construct() to build
        partial objects without validation. This method creates a model with
        all fields optional and stores a reference to the original model
        for validation when JSON is complete.
        """
        assert issubclass(cls, BaseModel), (
            f"{cls.__name__} must be a subclass of BaseModel"
        )

        model_name = (
            cls.__name__
            if cls.__name__.startswith("Partial")
            else f"Partial{cls.__name__}"
        )

        # Create partial model with optional fields
        partial_model = create_model(
            model_name,
            __base__=cls,
            __module__=cls.__module__,
            **{
                field_name: _make_field_optional(field_info)
                for field_name, field_info in cls.model_fields.items()
            },  # type: ignore[all]
        )

        # Store reference to original model for validation of complete objects
        original = getattr(cls, "_original_model", cls)
        partial_model._original_model = original  # type: ignore[attr-defined]

        return partial_model

    @classmethod
    def from_streaming_response(
        cls,
        completion: Iterable[Any],
        stream_extractor: Callable[[Iterable[Any]], Generator[str, None, None]],
        chunk_parser: Callable[..., Generator[T_Model, None, None]] | None = None,
        **kwargs: Any,
    ) -> Generator[T_Model, None, None]:
        if stream_extractor is None:
            raise ValueError("stream_extractor is required for streaming responses")
        json_chunks = stream_extractor(completion)
        parser = chunk_parser or cls.model_from_chunks
        yield from parser(json_chunks, **kwargs)

    @classmethod
    async def from_streaming_response_async(
        cls,
        completion: AsyncGenerator[Any, None],
        stream_extractor: Callable[
            [AsyncGenerator[Any, None]], AsyncGenerator[str, None]
        ],
        chunk_parser: Callable[..., AsyncGenerator[T_Model, None]] | None = None,
        **kwargs: Any,
    ) -> AsyncGenerator[T_Model, None]:
        if stream_extractor is None:
            raise ValueError("stream_extractor is required for streaming responses")
        json_chunks = stream_extractor(completion)
        parser = chunk_parser or cls.model_from_chunks_async
        async for item in parser(json_chunks, **kwargs):
            yield item

    @classmethod
    def model_from_chunks(
        cls, json_chunks: Iterable[Any], **kwargs: Any
    ) -> Generator[T_Model, None, None]:
        potential_object = ""
        partial_model = cls.get_partial_model()
        # Always use trailing-strings mode to preserve incomplete data during streaming
        # PartialLiteralMixin is deprecated - completeness-based validation handles Literals
        partial_mode = "trailing-strings"
        final_obj = None
        for chunk in json_chunks:
            if chunk is None:
                continue
            if not isinstance(chunk, str):
                try:
                    chunk = str(chunk)
                except Exception:
                    continue
            potential_object += remove_control_chars(chunk)
            obj = process_potential_object(
                potential_object, partial_mode, partial_model, **kwargs
            )
            final_obj = obj
            yield obj

        # Final validation: only validate if the JSON is structurally complete
        # If JSON is incomplete (stream ended mid-object), skip validation
        if final_obj is not None:
            original_model = getattr(cls, "_original_model", None)
            if original_model is not None:
                if is_json_complete(potential_object.strip() or "{}"):
                    original_model.model_validate(
                        final_obj.model_dump(exclude_none=True), **kwargs
                    )

    @classmethod
    async def model_from_chunks_async(
        cls, json_chunks: AsyncGenerator[str, None], **kwargs: Any
    ) -> AsyncGenerator[T_Model, None]:
        potential_object = ""
        partial_model = cls.get_partial_model()
        # Always use trailing-strings mode to preserve incomplete data during streaming
        # PartialLiteralMixin is deprecated - completeness-based validation handles Literals
        partial_mode = "trailing-strings"
        final_obj = None
        async for chunk in json_chunks:
            if chunk is None:
                continue
            if not isinstance(chunk, str):
                try:
                    chunk = str(chunk)
                except Exception:
                    continue
            potential_object += remove_control_chars(chunk)
            obj = process_potential_object(
                potential_object, partial_mode, partial_model, **kwargs
            )
            final_obj = obj
            yield obj

        # Final validation: only validate if the JSON is structurally complete
        # If JSON is incomplete (stream ended mid-object), skip validation
        if final_obj is not None:
            original_model = getattr(cls, "_original_model", None)
            if original_model is not None:
                if is_json_complete(potential_object.strip() or "{}"):
                    original_model.model_validate(
                        final_obj.model_dump(exclude_none=True), **kwargs
                    )

    @staticmethod
    def extract_json(
        completion: Iterable[Any],
        stream_extractor: Callable[[Iterable[Any]], Generator[str, None, None]] | Any,
        on_event: Callable[..., Any] | None = None,
    ) -> Generator[str, None, None]:
        from instructor.v2.core.mode import Mode

        if stream_extractor in {
            Mode.RESPONSES_TOOLS,
            Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
        }:
            from openai.types.responses import (
                ResponseFunctionCallArgumentsDeltaEvent,
                ResponseReasoningSummaryTextDeltaEvent,
                ResponseReasoningSummaryTextDoneEvent,
            )

            for chunk in completion:
                if isinstance(chunk, ResponseFunctionCallArgumentsDeltaEvent):
                    yield chunk.delta
                elif on_event and isinstance(
                    chunk,
                    (
                        ResponseReasoningSummaryTextDeltaEvent,
                        ResponseReasoningSummaryTextDoneEvent,
                    ),
                ):
                    on_event(chunk)
            return
        if stream_extractor is None:
            raise ValueError("stream_extractor is required for streaming responses")
        yield from stream_extractor(completion)

    @staticmethod
    async def extract_json_async(
        completion: AsyncGenerator[Any, None],
        stream_extractor: Callable[
            [AsyncGenerator[Any, None]], AsyncGenerator[str, None]
        ]
        | Any,
        on_event: Callable[..., Any] | None = None,
    ) -> AsyncGenerator[str, None]:
        import inspect

        from instructor.v2.core.mode import Mode

        if stream_extractor in {
            Mode.RESPONSES_TOOLS,
            Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
        }:
            from openai.types.responses import (
                ResponseFunctionCallArgumentsDeltaEvent,
                ResponseReasoningSummaryTextDeltaEvent,
                ResponseReasoningSummaryTextDoneEvent,
            )

            async for chunk in completion:
                if isinstance(chunk, ResponseFunctionCallArgumentsDeltaEvent):
                    yield chunk.delta
                elif on_event and isinstance(
                    chunk,
                    (
                        ResponseReasoningSummaryTextDeltaEvent,
                        ResponseReasoningSummaryTextDoneEvent,
                    ),
                ):
                    maybe_awaitable = on_event(chunk)
                    if inspect.isawaitable(maybe_awaitable):
                        await maybe_awaitable
            return
        if stream_extractor is None:
            raise ValueError("stream_extractor is required for streaming responses")
        async for chunk in stream_extractor(completion):
            yield chunk

get_partial_model() cached classmethod

Return a partial model for holding incomplete streaming data.

With completeness-based validation, we use model_construct() to build partial objects without validation. This method creates a model with all fields optional and stores a reference to the original model for validation when JSON is complete.

Source code in instructor/v2/dsl/partial.py
@classmethod
@cache
def get_partial_model(cls) -> type[T_Model]:
    """Return a partial model for holding incomplete streaming data.

    With completeness-based validation, we use model_construct() to build
    partial objects without validation. This method creates a model with
    all fields optional and stores a reference to the original model
    for validation when JSON is complete.
    """
    assert issubclass(cls, BaseModel), (
        f"{cls.__name__} must be a subclass of BaseModel"
    )

    model_name = (
        cls.__name__
        if cls.__name__.startswith("Partial")
        else f"Partial{cls.__name__}"
    )

    # Create partial model with optional fields
    partial_model = create_model(
        model_name,
        __base__=cls,
        __module__=cls.__module__,
        **{
            field_name: _make_field_optional(field_info)
            for field_name, field_info in cls.model_fields.items()
        },  # type: ignore[all]
    )

    # Store reference to original model for validation of complete objects
    original = getattr(cls, "_original_model", cls)
    partial_model._original_model = original  # type: ignore[attr-defined]

    return partial_model

PartialLiteralMixin

DEPRECATED: This mixin is no longer necessary.

With completeness-based validation, Literal and Enum types are handled automatically during streaming: - Incomplete JSON: no validation runs, partial values are stored as-is - Complete JSON: full validation against original model

You can safely remove this mixin from your models.

Source code in instructor/v2/dsl/partial.py
class PartialLiteralMixin:
    """DEPRECATED: This mixin is no longer necessary.

    With completeness-based validation, Literal and Enum types are handled
    automatically during streaming:
    - Incomplete JSON: no validation runs, partial values are stored as-is
    - Complete JSON: full validation against original model

    You can safely remove this mixin from your models.
    """

    def __init_subclass__(cls, **kwargs: Any) -> None:
        super().__init_subclass__(**kwargs)
        warnings.warn(
            "PartialLiteralMixin is deprecated and no longer necessary. "
            "Completeness-based validation now handles Literal and Enum types "
            "automatically during streaming. You can safely remove this mixin.",
            DeprecationWarning,
            stacklevel=2,
        )

is_json_complete(json_str)

Check if a JSON string represents a complete structure.

Uses jiter in strict mode - parsing fails if JSON is incomplete.

Source code in instructor/v2/dsl/json_tracker.py
def is_json_complete(json_str: str) -> bool:
    """
    Check if a JSON string represents a complete structure.

    Uses jiter in strict mode - parsing fails if JSON is incomplete.
    """
    if not json_str or not json_str.strip():
        return False
    try:
        from_json(json_str.encode())  # No partial_mode = strict parsing
        return True
    except ValueError:
        return False

process_potential_object(potential_object, partial_mode, partial_model, **kwargs)

Process a potential JSON object using completeness-based validation.

  • If JSON is complete (closed braces/brackets): validate against original model
  • If JSON is incomplete: build partial object using model_construct (no validation)

Note: Pydantic v2.10+ has experimental_allow_partial but it doesn't support BaseModel constraints during partial validation (only TypedDict). If Pydantic adds BaseModel support in the future, this could potentially be simplified. See: https://docs.pydantic.dev/latest/concepts/partial_validation/

Source code in instructor/v2/dsl/partial.py
def process_potential_object(potential_object, partial_mode, partial_model, **kwargs):
    """Process a potential JSON object using completeness-based validation.

    - If JSON is complete (closed braces/brackets): validate against original model
    - If JSON is incomplete: build partial object using model_construct (no validation)

    Note: Pydantic v2.10+ has `experimental_allow_partial` but it doesn't support
    BaseModel constraints during partial validation (only TypedDict). If Pydantic
    adds BaseModel support in the future, this could potentially be simplified.
    See: https://docs.pydantic.dev/latest/concepts/partial_validation/
    """
    json_str = potential_object.strip() or "{}"
    parsed = from_json(json_str.encode(), partial_mode=partial_mode)

    tracker = JsonCompleteness()
    tracker.analyze(json_str)

    # Get original model for validation
    original_model = getattr(partial_model, "_original_model", None)

    # Check if root is complete AND has actual data (not just empty {})
    root_complete = tracker.is_root_complete()
    has_data = bool(parsed) if isinstance(parsed, dict) else True

    validation_kwargs = {
        key: value
        for key, value in kwargs.items()
        if key
        in {"context", "strict", "extra", "from_attributes", "by_alias", "by_name"}
    }

    if root_complete and has_data and original_model is not None:
        # Root object is complete with data - validate against original model
        return original_model.model_validate(parsed, **validation_kwargs)
    else:
        # Object is incomplete or empty - build instance using model_construct (no validation)
        model_for_construct = (
            original_model if original_model is not None else partial_model
        )
        return _build_partial_object(parsed, model_for_construct, tracker, "", **kwargs)

Compatibility exports for v2-owned parallel helpers.

Mode

Bases: Enum

Mode enumeration for patching LLM API clients.

Each mode determines how the library formats and structures requests to different provider APIs and how it processes their responses.

Source code in instructor/v2/core/mode.py
class Mode(enum.Enum):
    """
    Mode enumeration for patching LLM API clients.

    Each mode determines how the library formats and structures requests
    to different provider APIs and how it processes their responses.
    """

    # OpenAI modes
    FUNCTIONS = "function_call"  # Deprecated
    PARALLEL_TOOLS = "parallel_tool_call"
    TOOLS = "tool_call"
    TOOLS_STRICT = "tools_strict"
    JSON = "json_mode"
    JSON_O1 = "json_o1"
    MD_JSON = "markdown_json_mode"
    JSON_SCHEMA = "json_schema_mode"

    # Add new modes to support responses api
    RESPONSES_TOOLS = "responses_tools"
    RESPONSES_TOOLS_WITH_INBUILT_TOOLS = "responses_tools_with_inbuilt_tools"

    # XAI modes
    XAI_JSON = "xai_json"
    XAI_TOOLS = "xai_tools"

    # Anthropic modes
    ANTHROPIC_TOOLS = "anthropic_tools"
    ANTHROPIC_REASONING_TOOLS = "anthropic_reasoning_tools"
    ANTHROPIC_JSON = "anthropic_json"
    ANTHROPIC_PARALLEL_TOOLS = "anthropic_parallel_tools"

    # Mistral modes
    MISTRAL_TOOLS = "mistral_tools"
    MISTRAL_STRUCTURED_OUTPUTS = "mistral_structured_outputs"

    # Vertex AI & Google modes
    VERTEXAI_TOOLS = "vertexai_tools"
    VERTEXAI_JSON = "vertexai_json"
    VERTEXAI_PARALLEL_TOOLS = "vertexai_parallel_tools"
    GEMINI_JSON = "gemini_json"
    GEMINI_TOOLS = "gemini_tools"
    GENAI_TOOLS = "genai_tools"
    GENAI_JSON = "genai_json"
    GENAI_STRUCTURED_OUTPUTS = (
        "genai_structured_outputs"  # Backwards compatibility alias
    )

    # Cohere modes
    COHERE_TOOLS = "cohere_tools"
    COHERE_JSON_SCHEMA = "json_object"

    # Cerebras modes
    CEREBRAS_TOOLS = "cerebras_tools"
    CEREBRAS_JSON = "cerebras_json"

    # Fireworks modes
    FIREWORKS_TOOLS = "fireworks_tools"
    FIREWORKS_JSON = "fireworks_json"

    # Other providers
    WRITER_TOOLS = "writer_tools"
    WRITER_JSON = "writer_json"
    BEDROCK_TOOLS = "bedrock_tools"
    BEDROCK_JSON = "bedrock_json"
    PERPLEXITY_JSON = "perplexity_json"
    OPENROUTER_STRUCTURED_OUTPUTS = "openrouter_structured_outputs"

    # Classification helpers
    @classmethod
    def tool_modes(cls) -> set["Mode"]:
        """Returns a set of all tool-based modes."""
        return {
            cls.FUNCTIONS,
            cls.PARALLEL_TOOLS,
            cls.TOOLS,
            cls.TOOLS_STRICT,
            cls.ANTHROPIC_TOOLS,
            cls.ANTHROPIC_REASONING_TOOLS,
            cls.ANTHROPIC_PARALLEL_TOOLS,
            cls.MISTRAL_TOOLS,
            cls.VERTEXAI_TOOLS,
            cls.VERTEXAI_PARALLEL_TOOLS,
            cls.GEMINI_TOOLS,
            cls.COHERE_TOOLS,
            cls.CEREBRAS_TOOLS,
            cls.FIREWORKS_TOOLS,
            cls.WRITER_TOOLS,
            cls.BEDROCK_TOOLS,
            cls.OPENROUTER_STRUCTURED_OUTPUTS,
            cls.MISTRAL_STRUCTURED_OUTPUTS,
            cls.XAI_TOOLS,
            cls.GENAI_TOOLS,
            cls.RESPONSES_TOOLS,
            cls.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
        }

    @classmethod
    def json_modes(cls) -> set["Mode"]:
        """Returns a set of all JSON-based modes."""
        return {
            cls.JSON,
            cls.JSON_O1,
            cls.MD_JSON,
            cls.JSON_SCHEMA,
            cls.ANTHROPIC_JSON,
            cls.VERTEXAI_JSON,
            cls.GEMINI_JSON,
            cls.COHERE_JSON_SCHEMA,
            cls.CEREBRAS_JSON,
            cls.FIREWORKS_JSON,
            cls.WRITER_JSON,
            cls.BEDROCK_JSON,
            cls.PERPLEXITY_JSON,
            cls.OPENROUTER_STRUCTURED_OUTPUTS,
            cls.MISTRAL_STRUCTURED_OUTPUTS,
            cls.XAI_JSON,
        }

    @classmethod
    def parallel_modes(cls) -> set["Mode"]:
        """Return canonical and compatibility aliases for parallel tool modes."""
        return {
            cls.PARALLEL_TOOLS,
            cls.ANTHROPIC_PARALLEL_TOOLS,
            cls.VERTEXAI_PARALLEL_TOOLS,
        }

    @classmethod
    def warn_mode_functions_deprecation(cls):
        """
        Warn about FUNCTIONS mode deprecation.

        Shows the warning only once per session to avoid spamming logs
        with the same message.
        """
        global _functions_deprecation_shown
        if not _functions_deprecation_shown:
            warnings.warn(
                "The FUNCTIONS mode is deprecated and will be removed in future versions",
                DeprecationWarning,
                stacklevel=2,
            )
            _functions_deprecation_shown = True

    @classmethod
    def warn_anthropic_reasoning_tools_deprecation(cls):
        """
        Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

        ANTHROPIC_TOOLS now supports extended thinking/reasoning via the
        'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'}
        instead of Mode.ANTHROPIC_REASONING_TOOLS.

        Shows the warning only once per session to avoid spamming logs
        with the same message.
        """
        global _reasoning_tools_deprecation_shown
        if not _reasoning_tools_deprecation_shown:
            warnings.warn(
                "Mode.ANTHROPIC_REASONING_TOOLS is deprecated. "
                "Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled', 'budget_tokens': ...} instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            _reasoning_tools_deprecation_shown = True

    @classmethod
    def warn_deprecated_mode(cls, mode: "Mode") -> None:
        """Warn about provider-specific mode deprecation.

        Uses a single warning per mode per process to reduce noise.
        """
        if mode not in DEPRECATED_TO_CORE:
            return
        if mode in _deprecated_modes_warned:
            return
        _deprecated_modes_warned.add(mode)
        replacement = DEPRECATED_TO_CORE[mode]
        warnings.warn(
            f"Mode.{mode.name} is deprecated and will be removed in v3.0. "
            f"Use Mode.{replacement.name} instead. "
            "The provider is determined by the client (from_openai, from_anthropic, etc.), "
            "not by the mode.",
            DeprecationWarning,
            stacklevel=3,
        )

json_modes() classmethod

Returns a set of all JSON-based modes.

Source code in instructor/v2/core/mode.py
@classmethod
def json_modes(cls) -> set["Mode"]:
    """Returns a set of all JSON-based modes."""
    return {
        cls.JSON,
        cls.JSON_O1,
        cls.MD_JSON,
        cls.JSON_SCHEMA,
        cls.ANTHROPIC_JSON,
        cls.VERTEXAI_JSON,
        cls.GEMINI_JSON,
        cls.COHERE_JSON_SCHEMA,
        cls.CEREBRAS_JSON,
        cls.FIREWORKS_JSON,
        cls.WRITER_JSON,
        cls.BEDROCK_JSON,
        cls.PERPLEXITY_JSON,
        cls.OPENROUTER_STRUCTURED_OUTPUTS,
        cls.MISTRAL_STRUCTURED_OUTPUTS,
        cls.XAI_JSON,
    }

parallel_modes() classmethod

Return canonical and compatibility aliases for parallel tool modes.

Source code in instructor/v2/core/mode.py
@classmethod
def parallel_modes(cls) -> set["Mode"]:
    """Return canonical and compatibility aliases for parallel tool modes."""
    return {
        cls.PARALLEL_TOOLS,
        cls.ANTHROPIC_PARALLEL_TOOLS,
        cls.VERTEXAI_PARALLEL_TOOLS,
    }

tool_modes() classmethod

Returns a set of all tool-based modes.

Source code in instructor/v2/core/mode.py
@classmethod
def tool_modes(cls) -> set["Mode"]:
    """Returns a set of all tool-based modes."""
    return {
        cls.FUNCTIONS,
        cls.PARALLEL_TOOLS,
        cls.TOOLS,
        cls.TOOLS_STRICT,
        cls.ANTHROPIC_TOOLS,
        cls.ANTHROPIC_REASONING_TOOLS,
        cls.ANTHROPIC_PARALLEL_TOOLS,
        cls.MISTRAL_TOOLS,
        cls.VERTEXAI_TOOLS,
        cls.VERTEXAI_PARALLEL_TOOLS,
        cls.GEMINI_TOOLS,
        cls.COHERE_TOOLS,
        cls.CEREBRAS_TOOLS,
        cls.FIREWORKS_TOOLS,
        cls.WRITER_TOOLS,
        cls.BEDROCK_TOOLS,
        cls.OPENROUTER_STRUCTURED_OUTPUTS,
        cls.MISTRAL_STRUCTURED_OUTPUTS,
        cls.XAI_TOOLS,
        cls.GENAI_TOOLS,
        cls.RESPONSES_TOOLS,
        cls.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
    }

warn_anthropic_reasoning_tools_deprecation() classmethod

Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

ANTHROPIC_TOOLS now supports extended thinking/reasoning via the 'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'} instead of Mode.ANTHROPIC_REASONING_TOOLS.

Shows the warning only once per session to avoid spamming logs with the same message.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_anthropic_reasoning_tools_deprecation(cls):
    """
    Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

    ANTHROPIC_TOOLS now supports extended thinking/reasoning via the
    'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'}
    instead of Mode.ANTHROPIC_REASONING_TOOLS.

    Shows the warning only once per session to avoid spamming logs
    with the same message.
    """
    global _reasoning_tools_deprecation_shown
    if not _reasoning_tools_deprecation_shown:
        warnings.warn(
            "Mode.ANTHROPIC_REASONING_TOOLS is deprecated. "
            "Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled', 'budget_tokens': ...} instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        _reasoning_tools_deprecation_shown = True

warn_deprecated_mode(mode) classmethod

Warn about provider-specific mode deprecation.

Uses a single warning per mode per process to reduce noise.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_deprecated_mode(cls, mode: "Mode") -> None:
    """Warn about provider-specific mode deprecation.

    Uses a single warning per mode per process to reduce noise.
    """
    if mode not in DEPRECATED_TO_CORE:
        return
    if mode in _deprecated_modes_warned:
        return
    _deprecated_modes_warned.add(mode)
    replacement = DEPRECATED_TO_CORE[mode]
    warnings.warn(
        f"Mode.{mode.name} is deprecated and will be removed in v3.0. "
        f"Use Mode.{replacement.name} instead. "
        "The provider is determined by the client (from_openai, from_anthropic, etc.), "
        "not by the mode.",
        DeprecationWarning,
        stacklevel=3,
    )

warn_mode_functions_deprecation() classmethod

Warn about FUNCTIONS mode deprecation.

Shows the warning only once per session to avoid spamming logs with the same message.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_mode_functions_deprecation(cls):
    """
    Warn about FUNCTIONS mode deprecation.

    Shows the warning only once per session to avoid spamming logs
    with the same message.
    """
    global _functions_deprecation_shown
    if not _functions_deprecation_shown:
        warnings.warn(
            "The FUNCTIONS mode is deprecated and will be removed in future versions",
            DeprecationWarning,
            stacklevel=2,
        )
        _functions_deprecation_shown = True

AnthropicParallelModel(typehint)

Compatibility shim for the Anthropic-owned parallel model.

Source code in instructor/v2/dsl/parallel.py
def AnthropicParallelModel(typehint: type[Iterable[T]]) -> ParallelBase[T]:
    """Compatibility shim for the Anthropic-owned parallel model."""
    from instructor.v2.providers.anthropic.parallel import (
        AnthropicParallelModel as factory,
    )

    return factory(typehint)

VertexAIParallelModel(typehint)

Compatibility shim for the VertexAI-owned parallel model.

Source code in instructor/v2/dsl/parallel.py
def VertexAIParallelModel(typehint: type[Iterable[T]]) -> ParallelBase[T]:
    """Compatibility shim for the VertexAI-owned parallel model."""
    from instructor.v2.providers.vertexai.parallel import (
        VertexAIParallelModel as factory,
    )

    return factory(typehint)

handle_anthropic_parallel_model(typehint)

Compatibility shim for Anthropic-owned parallel schema generation.

Source code in instructor/v2/dsl/parallel.py
def handle_anthropic_parallel_model(
    typehint: type[Iterable[T]],
) -> list[dict[str, Any]]:
    """Compatibility shim for Anthropic-owned parallel schema generation."""
    from instructor.v2.providers.anthropic.parallel import handle_parallel_model

    return handle_parallel_model(typehint)

Compatibility exports for v2-owned Maybe helpers.

MaybeBase

Bases: BaseModel, Generic[T]

Extract a result from a model, if any, otherwise set the error and message fields.

Source code in instructor/v2/dsl/maybe.py
class MaybeBase(BaseModel, Generic[T]):
    """
    Extract a result from a model, if any, otherwise set the error and message fields.
    """

    result: Optional[T]
    error: bool = Field(default=False)
    message: Optional[str]

    def __bool__(self) -> bool:
        return self.result is not None

Maybe(model)

Create a Maybe model for a given Pydantic model. This allows you to return a model that includes fields for result, error, and message for sitatations where the data may not be present in the context.

Usage

from pydantic import BaseModel, Field
from instructor import Maybe

class User(BaseModel):
    name: str = Field(description="The name of the person")
    age: int = Field(description="The age of the person")
    role: str = Field(description="The role of the person")

MaybeUser = Maybe(User)

Result

class MaybeUser(BaseModel):
    result: Optional[User]
    error: bool = Field(default=False)
    message: Optional[str]

    def __bool__(self):
        return self.result is not None

Parameters:

Name Type Description Default
model Type[BaseModel]

The Pydantic model to wrap with Maybe.

required

Returns:

Name Type Description
MaybeModel Type[BaseModel]

A new Pydantic model that includes fields for result, error, and message.

Source code in instructor/v2/dsl/maybe.py
def Maybe(model: type[T]) -> type[MaybeBase[T]]:
    """
    Create a Maybe model for a given Pydantic model. This allows you to return a model that includes fields for `result`, `error`, and `message` for sitatations where the data may not be present in the context.

    ## Usage

    ```python
    from pydantic import BaseModel, Field
    from instructor import Maybe

    class User(BaseModel):
        name: str = Field(description="The name of the person")
        age: int = Field(description="The age of the person")
        role: str = Field(description="The role of the person")

    MaybeUser = Maybe(User)
    ```

    ## Result

    ```python
    class MaybeUser(BaseModel):
        result: Optional[User]
        error: bool = Field(default=False)
        message: Optional[str]

        def __bool__(self):
            return self.result is not None
    ```

    Parameters:
        model (Type[BaseModel]): The Pydantic model to wrap with Maybe.

    Returns:
        MaybeModel (Type[BaseModel]): A new Pydantic model that includes fields for `result`, `error`, and `message`.
    """
    return create_model(
        f"Maybe{model.__name__}",
        __base__=MaybeBase,
        result=(
            Optional[model],
            Field(
                default=None,
                description="Correctly extracted result from the model, if any, otherwise None",
            ),
        ),
        error=(bool, Field(default=False)),
        message=(
            Optional[str],
            Field(
                default=None,
                description="Error message if no result was found, should be short and concise",
            ),
        ),
    )

Compatibility exports for v2-owned citation helpers.

CitationMixin

Bases: BaseModel

Helpful mixing that can use validation_context={"context": context} in from_response to find the span of the substring_phrase in the context.

Usage

from pydantic import BaseModel, Field
from instructor import CitationMixin

class User(BaseModel):
    name: str = Field(description="The name of the person")
    age: int = Field(description="The age of the person")
    role: str = Field(description="The role of the person")


context = "Betty was a student. Jason was a student. Jason is 20 years old"

user = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
        {
            "role": "user",
            "content": "Extract jason from {context}",
        },
    response_model=User,
    validation_context={"context": context},
    ]
)

for quote in user.substring_quotes:
    assert quote in context

print(user.model_dump())

Result

{
    "name": "Jason Liu",
    "age": 20,
    "role": "student",
    "substring_quotes": [
        "Jason was a student",
        "Jason is 20 years old",
    ]
}
Source code in instructor/v2/dsl/citation.py
class CitationMixin(BaseModel):
    """
    Helpful mixing that can use `validation_context={"context": context}` in `from_response` to find the span of the substring_phrase in the context.

    ## Usage

    ```python
    from pydantic import BaseModel, Field
    from instructor import CitationMixin

    class User(BaseModel):
        name: str = Field(description="The name of the person")
        age: int = Field(description="The age of the person")
        role: str = Field(description="The role of the person")


    context = "Betty was a student. Jason was a student. Jason is 20 years old"

    user = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {
                "role": "user",
                "content": "Extract jason from {context}",
            },
        response_model=User,
        validation_context={"context": context},
        ]
    )

    for quote in user.substring_quotes:
        assert quote in context

    print(user.model_dump())
    ```

    ## Result
    ```
    {
        "name": "Jason Liu",
        "age": 20,
        "role": "student",
        "substring_quotes": [
            "Jason was a student",
            "Jason is 20 years old",
        ]
    }
    ```

    """

    substring_quotes: list[str] = Field(
        description="List of unique and specific substrings of the quote that was used to answer the question.",
    )

    @model_validator(mode="after")  # type: ignore[misc]
    def validate_sources(self, info: ValidationInfo) -> "CitationMixin":
        """
        For each substring_phrase, find the span of the substring_phrase in the context.
        If the span is not found, remove the substring_phrase from the list.
        """
        if info.context is None:
            return self

        # Get the context from the info
        text_chunks = info.context.get("context", None)

        # Get the spans of the substring_phrase in the context
        spans = list(self.get_spans(text_chunks))
        # Replace the substring_phrase with the actual substring
        self.substring_quotes = [text_chunks[span[0] : span[1]] for span in spans]
        return self

    def _get_span(
        self, quote: str, context: str, errs: int = 5
    ) -> Generator[tuple[int, int], None, None]:
        import regex

        minor = quote
        major = context

        errs_ = 0
        s = regex.search(f"({minor}){{e<={errs_}}}", major)
        while s is None and errs_ <= errs:
            errs_ += 1
            s = regex.search(f"({minor}){{e<={errs_}}}", major)

        if s is not None:
            yield from s.spans()

    def get_spans(self, context: str) -> Generator[tuple[int, int], None, None]:
        for quote in self.substring_quotes:
            yield from self._get_span(quote, context)

validate_sources(info)

For each substring_phrase, find the span of the substring_phrase in the context. If the span is not found, remove the substring_phrase from the list.

Source code in instructor/v2/dsl/citation.py
@model_validator(mode="after")  # type: ignore[misc]
def validate_sources(self, info: ValidationInfo) -> "CitationMixin":
    """
    For each substring_phrase, find the span of the substring_phrase in the context.
    If the span is not found, remove the substring_phrase from the list.
    """
    if info.context is None:
        return self

    # Get the context from the info
    text_chunks = info.context.get("context", None)

    # Get the spans of the substring_phrase in the context
    spans = list(self.get_spans(text_chunks))
    # Replace the substring_phrase with the actual substring
    self.substring_quotes = [text_chunks[span[0] : span[1]] for span in spans]
    return self

Function Calls & Schema

Classes and functions for defining and working with function call schemas.

Backwards compatibility module for instructor.function_calls.

This module re-exports everything from instructor.processing.function_calls for backwards compatibility.

IncompleteOutputException

Bases: InstructorError

Exception raised when LLM output is truncated due to token limits.

This exception occurs when the LLM hits the max_tokens limit before completing its response. This is particularly common with: - Large structured outputs - Very detailed responses - Low max_tokens settings

Attributes:

Name Type Description
last_completion

The partial/incomplete response from the LLM before truncation occurred

Common Solutions
  • Increase max_tokens in your request
  • Simplify your response model
  • Use streaming with Partial models to get incomplete data
  • Break down complex extractions into smaller tasks

Examples:

try:
    response = client.chat.completions.create(
        response_model=DetailedReport,
        max_tokens=100,  # Too low
        ...
    )
except IncompleteOutputException as e:
    print(f"Output truncated. Partial data: {e.last_completion}")
    # Retry with higher max_tokens
    response = client.chat.completions.create(
        response_model=DetailedReport,
        max_tokens=2000,
        ...
    )
See Also
  • instructor.dsl.Partial: For handling partial/incomplete responses
Source code in instructor/v2/core/errors.py
class IncompleteOutputException(InstructorError):
    """Exception raised when LLM output is truncated due to token limits.

    This exception occurs when the LLM hits the max_tokens limit before
    completing its response. This is particularly common with:
    - Large structured outputs
    - Very detailed responses
    - Low max_tokens settings

    Attributes:
        last_completion: The partial/incomplete response from the LLM
            before truncation occurred

    Common Solutions:
        - Increase max_tokens in your request
        - Simplify your response model
        - Use streaming with Partial models to get incomplete data
        - Break down complex extractions into smaller tasks

    Examples:
        ```python
        try:
            response = client.chat.completions.create(
                response_model=DetailedReport,
                max_tokens=100,  # Too low
                ...
            )
        except IncompleteOutputException as e:
            print(f"Output truncated. Partial data: {e.last_completion}")
            # Retry with higher max_tokens
            response = client.chat.completions.create(
                response_model=DetailedReport,
                max_tokens=2000,
                ...
            )
        ```

    See Also:
        - instructor.dsl.Partial: For handling partial/incomplete responses
    """

    def __init__(
        self,
        *args: Any,
        last_completion: Any | None = None,
        message: str = "The output is incomplete due to a max_tokens length limit.",
        **kwargs: dict[str, Any],
    ):
        self.last_completion = last_completion
        super().__init__(message, *args, **kwargs)

Mode

Bases: Enum

Mode enumeration for patching LLM API clients.

Each mode determines how the library formats and structures requests to different provider APIs and how it processes their responses.

Source code in instructor/v2/core/mode.py
class Mode(enum.Enum):
    """
    Mode enumeration for patching LLM API clients.

    Each mode determines how the library formats and structures requests
    to different provider APIs and how it processes their responses.
    """

    # OpenAI modes
    FUNCTIONS = "function_call"  # Deprecated
    PARALLEL_TOOLS = "parallel_tool_call"
    TOOLS = "tool_call"
    TOOLS_STRICT = "tools_strict"
    JSON = "json_mode"
    JSON_O1 = "json_o1"
    MD_JSON = "markdown_json_mode"
    JSON_SCHEMA = "json_schema_mode"

    # Add new modes to support responses api
    RESPONSES_TOOLS = "responses_tools"
    RESPONSES_TOOLS_WITH_INBUILT_TOOLS = "responses_tools_with_inbuilt_tools"

    # XAI modes
    XAI_JSON = "xai_json"
    XAI_TOOLS = "xai_tools"

    # Anthropic modes
    ANTHROPIC_TOOLS = "anthropic_tools"
    ANTHROPIC_REASONING_TOOLS = "anthropic_reasoning_tools"
    ANTHROPIC_JSON = "anthropic_json"
    ANTHROPIC_PARALLEL_TOOLS = "anthropic_parallel_tools"

    # Mistral modes
    MISTRAL_TOOLS = "mistral_tools"
    MISTRAL_STRUCTURED_OUTPUTS = "mistral_structured_outputs"

    # Vertex AI & Google modes
    VERTEXAI_TOOLS = "vertexai_tools"
    VERTEXAI_JSON = "vertexai_json"
    VERTEXAI_PARALLEL_TOOLS = "vertexai_parallel_tools"
    GEMINI_JSON = "gemini_json"
    GEMINI_TOOLS = "gemini_tools"
    GENAI_TOOLS = "genai_tools"
    GENAI_JSON = "genai_json"
    GENAI_STRUCTURED_OUTPUTS = (
        "genai_structured_outputs"  # Backwards compatibility alias
    )

    # Cohere modes
    COHERE_TOOLS = "cohere_tools"
    COHERE_JSON_SCHEMA = "json_object"

    # Cerebras modes
    CEREBRAS_TOOLS = "cerebras_tools"
    CEREBRAS_JSON = "cerebras_json"

    # Fireworks modes
    FIREWORKS_TOOLS = "fireworks_tools"
    FIREWORKS_JSON = "fireworks_json"

    # Other providers
    WRITER_TOOLS = "writer_tools"
    WRITER_JSON = "writer_json"
    BEDROCK_TOOLS = "bedrock_tools"
    BEDROCK_JSON = "bedrock_json"
    PERPLEXITY_JSON = "perplexity_json"
    OPENROUTER_STRUCTURED_OUTPUTS = "openrouter_structured_outputs"

    # Classification helpers
    @classmethod
    def tool_modes(cls) -> set["Mode"]:
        """Returns a set of all tool-based modes."""
        return {
            cls.FUNCTIONS,
            cls.PARALLEL_TOOLS,
            cls.TOOLS,
            cls.TOOLS_STRICT,
            cls.ANTHROPIC_TOOLS,
            cls.ANTHROPIC_REASONING_TOOLS,
            cls.ANTHROPIC_PARALLEL_TOOLS,
            cls.MISTRAL_TOOLS,
            cls.VERTEXAI_TOOLS,
            cls.VERTEXAI_PARALLEL_TOOLS,
            cls.GEMINI_TOOLS,
            cls.COHERE_TOOLS,
            cls.CEREBRAS_TOOLS,
            cls.FIREWORKS_TOOLS,
            cls.WRITER_TOOLS,
            cls.BEDROCK_TOOLS,
            cls.OPENROUTER_STRUCTURED_OUTPUTS,
            cls.MISTRAL_STRUCTURED_OUTPUTS,
            cls.XAI_TOOLS,
            cls.GENAI_TOOLS,
            cls.RESPONSES_TOOLS,
            cls.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
        }

    @classmethod
    def json_modes(cls) -> set["Mode"]:
        """Returns a set of all JSON-based modes."""
        return {
            cls.JSON,
            cls.JSON_O1,
            cls.MD_JSON,
            cls.JSON_SCHEMA,
            cls.ANTHROPIC_JSON,
            cls.VERTEXAI_JSON,
            cls.GEMINI_JSON,
            cls.COHERE_JSON_SCHEMA,
            cls.CEREBRAS_JSON,
            cls.FIREWORKS_JSON,
            cls.WRITER_JSON,
            cls.BEDROCK_JSON,
            cls.PERPLEXITY_JSON,
            cls.OPENROUTER_STRUCTURED_OUTPUTS,
            cls.MISTRAL_STRUCTURED_OUTPUTS,
            cls.XAI_JSON,
        }

    @classmethod
    def parallel_modes(cls) -> set["Mode"]:
        """Return canonical and compatibility aliases for parallel tool modes."""
        return {
            cls.PARALLEL_TOOLS,
            cls.ANTHROPIC_PARALLEL_TOOLS,
            cls.VERTEXAI_PARALLEL_TOOLS,
        }

    @classmethod
    def warn_mode_functions_deprecation(cls):
        """
        Warn about FUNCTIONS mode deprecation.

        Shows the warning only once per session to avoid spamming logs
        with the same message.
        """
        global _functions_deprecation_shown
        if not _functions_deprecation_shown:
            warnings.warn(
                "The FUNCTIONS mode is deprecated and will be removed in future versions",
                DeprecationWarning,
                stacklevel=2,
            )
            _functions_deprecation_shown = True

    @classmethod
    def warn_anthropic_reasoning_tools_deprecation(cls):
        """
        Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

        ANTHROPIC_TOOLS now supports extended thinking/reasoning via the
        'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'}
        instead of Mode.ANTHROPIC_REASONING_TOOLS.

        Shows the warning only once per session to avoid spamming logs
        with the same message.
        """
        global _reasoning_tools_deprecation_shown
        if not _reasoning_tools_deprecation_shown:
            warnings.warn(
                "Mode.ANTHROPIC_REASONING_TOOLS is deprecated. "
                "Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled', 'budget_tokens': ...} instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            _reasoning_tools_deprecation_shown = True

    @classmethod
    def warn_deprecated_mode(cls, mode: "Mode") -> None:
        """Warn about provider-specific mode deprecation.

        Uses a single warning per mode per process to reduce noise.
        """
        if mode not in DEPRECATED_TO_CORE:
            return
        if mode in _deprecated_modes_warned:
            return
        _deprecated_modes_warned.add(mode)
        replacement = DEPRECATED_TO_CORE[mode]
        warnings.warn(
            f"Mode.{mode.name} is deprecated and will be removed in v3.0. "
            f"Use Mode.{replacement.name} instead. "
            "The provider is determined by the client (from_openai, from_anthropic, etc.), "
            "not by the mode.",
            DeprecationWarning,
            stacklevel=3,
        )

json_modes() classmethod

Returns a set of all JSON-based modes.

Source code in instructor/v2/core/mode.py
@classmethod
def json_modes(cls) -> set["Mode"]:
    """Returns a set of all JSON-based modes."""
    return {
        cls.JSON,
        cls.JSON_O1,
        cls.MD_JSON,
        cls.JSON_SCHEMA,
        cls.ANTHROPIC_JSON,
        cls.VERTEXAI_JSON,
        cls.GEMINI_JSON,
        cls.COHERE_JSON_SCHEMA,
        cls.CEREBRAS_JSON,
        cls.FIREWORKS_JSON,
        cls.WRITER_JSON,
        cls.BEDROCK_JSON,
        cls.PERPLEXITY_JSON,
        cls.OPENROUTER_STRUCTURED_OUTPUTS,
        cls.MISTRAL_STRUCTURED_OUTPUTS,
        cls.XAI_JSON,
    }

parallel_modes() classmethod

Return canonical and compatibility aliases for parallel tool modes.

Source code in instructor/v2/core/mode.py
@classmethod
def parallel_modes(cls) -> set["Mode"]:
    """Return canonical and compatibility aliases for parallel tool modes."""
    return {
        cls.PARALLEL_TOOLS,
        cls.ANTHROPIC_PARALLEL_TOOLS,
        cls.VERTEXAI_PARALLEL_TOOLS,
    }

tool_modes() classmethod

Returns a set of all tool-based modes.

Source code in instructor/v2/core/mode.py
@classmethod
def tool_modes(cls) -> set["Mode"]:
    """Returns a set of all tool-based modes."""
    return {
        cls.FUNCTIONS,
        cls.PARALLEL_TOOLS,
        cls.TOOLS,
        cls.TOOLS_STRICT,
        cls.ANTHROPIC_TOOLS,
        cls.ANTHROPIC_REASONING_TOOLS,
        cls.ANTHROPIC_PARALLEL_TOOLS,
        cls.MISTRAL_TOOLS,
        cls.VERTEXAI_TOOLS,
        cls.VERTEXAI_PARALLEL_TOOLS,
        cls.GEMINI_TOOLS,
        cls.COHERE_TOOLS,
        cls.CEREBRAS_TOOLS,
        cls.FIREWORKS_TOOLS,
        cls.WRITER_TOOLS,
        cls.BEDROCK_TOOLS,
        cls.OPENROUTER_STRUCTURED_OUTPUTS,
        cls.MISTRAL_STRUCTURED_OUTPUTS,
        cls.XAI_TOOLS,
        cls.GENAI_TOOLS,
        cls.RESPONSES_TOOLS,
        cls.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
    }

warn_anthropic_reasoning_tools_deprecation() classmethod

Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

ANTHROPIC_TOOLS now supports extended thinking/reasoning via the 'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'} instead of Mode.ANTHROPIC_REASONING_TOOLS.

Shows the warning only once per session to avoid spamming logs with the same message.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_anthropic_reasoning_tools_deprecation(cls):
    """
    Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

    ANTHROPIC_TOOLS now supports extended thinking/reasoning via the
    'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'}
    instead of Mode.ANTHROPIC_REASONING_TOOLS.

    Shows the warning only once per session to avoid spamming logs
    with the same message.
    """
    global _reasoning_tools_deprecation_shown
    if not _reasoning_tools_deprecation_shown:
        warnings.warn(
            "Mode.ANTHROPIC_REASONING_TOOLS is deprecated. "
            "Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled', 'budget_tokens': ...} instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        _reasoning_tools_deprecation_shown = True

warn_deprecated_mode(mode) classmethod

Warn about provider-specific mode deprecation.

Uses a single warning per mode per process to reduce noise.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_deprecated_mode(cls, mode: "Mode") -> None:
    """Warn about provider-specific mode deprecation.

    Uses a single warning per mode per process to reduce noise.
    """
    if mode not in DEPRECATED_TO_CORE:
        return
    if mode in _deprecated_modes_warned:
        return
    _deprecated_modes_warned.add(mode)
    replacement = DEPRECATED_TO_CORE[mode]
    warnings.warn(
        f"Mode.{mode.name} is deprecated and will be removed in v3.0. "
        f"Use Mode.{replacement.name} instead. "
        "The provider is determined by the client (from_openai, from_anthropic, etc.), "
        "not by the mode.",
        DeprecationWarning,
        stacklevel=3,
    )

warn_mode_functions_deprecation() classmethod

Warn about FUNCTIONS mode deprecation.

Shows the warning only once per session to avoid spamming logs with the same message.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_mode_functions_deprecation(cls):
    """
    Warn about FUNCTIONS mode deprecation.

    Shows the warning only once per session to avoid spamming logs
    with the same message.
    """
    global _functions_deprecation_shown
    if not _functions_deprecation_shown:
        warnings.warn(
            "The FUNCTIONS mode is deprecated and will be removed in future versions",
            DeprecationWarning,
            stacklevel=2,
        )
        _functions_deprecation_shown = True

Provider

Bases: Enum

Supported provider identifiers.

Source code in instructor/v2/core/providers.py
class Provider(Enum):
    """Supported provider identifiers."""

    OPENAI = "openai"
    AZURE_OPENAI = "azure_openai"
    VERTEXAI = "vertexai"
    ANTHROPIC = "anthropic"
    ANYSCALE = "anyscale"
    TOGETHER = "together"
    GROQ = "groq"
    MISTRAL = "mistral"
    COHERE = "cohere"
    GEMINI = "gemini"
    GENAI = "genai"
    DATABRICKS = "databricks"
    CEREBRAS = "cerebras"
    DEEPSEEK = "deepseek"
    FIREWORKS = "fireworks"
    WRITER = "writer"
    XAI = "xai"
    OLLAMA = "ollama"
    LITELLM = "litellm"
    GOOGLE = "google"
    GENERATIVE_AI = "generative-ai"
    UNKNOWN = "unknown"
    BEDROCK = "bedrock"
    PERPLEXITY = "perplexity"
    OPENROUTER = "openrouter"

ResponseSchema

Bases: BaseModel

Source code in instructor/v2/core/function_calls.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
class ResponseSchema(BaseModel):
    # Ignore classproperty, since Pydantic doesn't understand it like it would a normal property.
    model_config = ConfigDict(ignored_types=(classproperty,))

    @classproperty
    def openai_schema(cls) -> dict[str, Any]:
        """
        Return the schema in the format of OpenAI's schema as jsonschema

        Note:
            Its important to add a docstring to describe how to best use this class, it will be included in the description attribute and be part of the prompt.

        Returns:
            model_json_schema (dict): A dictionary in the format of OpenAI's schema as jsonschema
        """
        from instructor.v2.providers.openai.schema import generate_openai_schema

        return generate_openai_schema(cls)

    @classproperty
    def anthropic_schema(cls) -> dict[str, Any]:
        from instructor.v2.providers.anthropic.schema import generate_anthropic_schema

        return generate_anthropic_schema(cls)

    @classproperty
    def gemini_schema(cls) -> Any:
        from instructor.v2.providers.gemini.schema import generate_gemini_schema

        return generate_gemini_schema(cls)

    @classmethod
    def from_response(
        cls: type[Self],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
        mode: Mode = Mode.TOOLS,
        provider: Provider = Provider.OPENAI,
    ) -> Self:
        """Execute the function from the response of an openai chat completion

        Parameters:
            completion (openai.ChatCompletion): The response from an openai chat completion
            strict (bool): Whether to use strict json parsing
            mode (Mode): The completion mode
            provider (Provider): The provider for handler lookup

        Returns:
            cls (ResponseSchema): An instance of the class
        """

        import importlib

        from instructor.v2.core.registry import mode_registry

        importlib.import_module("instructor.v2")

        provider = provider_from_mode(mode, provider)
        mode = normalize_mode_for_provider(mode, provider)
        handlers = mode_registry.get_handlers(provider, mode)
        return handlers.response_parser(
            response=completion,
            response_model=cls,
            validation_context=validation_context,
            strict=strict,
            stream=False,
            is_async=False,
        )

    @classmethod
    def _parse_with_registry(
        cls: type[Self],
        completion: Any,
        *,
        mode: Mode,
        provider: Provider,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
        warning: str | None = None,
    ) -> Self:
        if warning:
            warnings.warn(warning, DeprecationWarning, stacklevel=2)
        return cls.from_response(
            completion,
            validation_context=validation_context,
            strict=strict,
            mode=mode,
            provider=provider,
        )

    @classmethod
    def parse_genai_structured_outputs(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy GenAI structured parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.JSON,
            provider=Provider.GENAI,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_genai_structured_outputs is deprecated. "
                "Use process_response(..., provider=Provider.GENAI, mode=Mode.JSON)."
            ),
        )

    @classmethod
    def parse_genai_tools(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy GenAI tools parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.TOOLS,
            provider=Provider.GENAI,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_genai_tools is deprecated. "
                "Use process_response(..., provider=Provider.GENAI, mode=Mode.TOOLS)."
            ),
        )

    @classmethod
    def parse_cohere_json_schema(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ):
        """Legacy Cohere JSON schema parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.JSON_SCHEMA,
            provider=Provider.COHERE,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_cohere_json_schema is deprecated. "
                "Use process_response(..., provider=Provider.COHERE, mode=Mode.JSON_SCHEMA)."
            ),
        )

    @classmethod
    def parse_anthropic_tools(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy Anthropic tools parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.ANTHROPIC_TOOLS,
            provider=Provider.ANTHROPIC,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_anthropic_tools is deprecated. "
                "Use process_response(..., provider=Provider.ANTHROPIC, mode=Mode.TOOLS) "
                "or ResponseSchema.from_response with core modes."
            ),
        )

    @classmethod
    def parse_anthropic_json(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy Anthropic JSON parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.ANTHROPIC_JSON,
            provider=Provider.ANTHROPIC,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_anthropic_json is deprecated. "
                "Use process_response(..., provider=Provider.ANTHROPIC, mode=Mode.JSON) "
                "or ResponseSchema.from_response with core modes."
            ),
        )

    @classmethod
    def parse_bedrock_json(
        cls: type[ResponseSchema],
        completion: Any,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy Bedrock JSON parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.MD_JSON,
            provider=Provider.BEDROCK,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_bedrock_json is deprecated. "
                "Use process_response(..., provider=Provider.BEDROCK, mode=Mode.MD_JSON)."
            ),
        )

    @classmethod
    def parse_bedrock_tools(
        cls: type[ResponseSchema],
        completion: Any,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy Bedrock tools parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.TOOLS,
            provider=Provider.BEDROCK,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_bedrock_tools is deprecated. "
                "Use process_response(..., provider=Provider.BEDROCK, mode=Mode.TOOLS)."
            ),
        )

    @classmethod
    def parse_gemini_json(
        cls: type[ResponseSchema],
        completion: Any,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy Gemini JSON parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.MD_JSON,
            provider=Provider.GEMINI,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_gemini_json is deprecated. "
                "Use process_response(..., provider=Provider.GEMINI, mode=Mode.MD_JSON)."
            ),
        )

    @classmethod
    def parse_gemini_tools(
        cls: type[ResponseSchema],
        completion: Any,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy Gemini tools parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.TOOLS,
            provider=Provider.GEMINI,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_gemini_tools is deprecated. "
                "Use process_response(..., provider=Provider.GEMINI, mode=Mode.TOOLS)."
            ),
        )

    @classmethod
    def parse_vertexai_tools(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
    ) -> BaseModel:
        """Legacy VertexAI tools parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.TOOLS,
            provider=Provider.VERTEXAI,
            validation_context=validation_context,
            strict=False,
            warning=(
                "ResponseSchema.parse_vertexai_tools is deprecated. "
                "Use process_response(..., provider=Provider.VERTEXAI, mode=Mode.TOOLS)."
            ),
        )

    @classmethod
    def parse_vertexai_json(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy VertexAI JSON parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.MD_JSON,
            provider=Provider.VERTEXAI,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_vertexai_json is deprecated. "
                "Use process_response(..., provider=Provider.VERTEXAI, mode=Mode.MD_JSON)."
            ),
        )

    @classmethod
    def parse_cohere_tools(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy Cohere tools parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.TOOLS,
            provider=Provider.COHERE,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_cohere_tools is deprecated. "
                "Use process_response(..., provider=Provider.COHERE, mode=Mode.TOOLS)."
            ),
        )

    @classmethod
    def parse_writer_tools(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy Writer tools parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.TOOLS,
            provider=Provider.WRITER,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_writer_tools is deprecated. "
                "Use process_response(..., provider=Provider.WRITER, mode=Mode.TOOLS)."
            ),
        )

    @classmethod
    def parse_writer_json(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy Writer JSON parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.MD_JSON,
            provider=Provider.WRITER,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_writer_json is deprecated. "
                "Use process_response(..., provider=Provider.WRITER, mode=Mode.MD_JSON)."
            ),
        )

    @classmethod
    def parse_functions(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy OpenAI FUNCTIONS parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.FUNCTIONS,
            provider=Provider.OPENAI,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_functions is deprecated. "
                "Use process_response(..., mode=Mode.TOOLS) or ResponseSchema.from_response."
            ),
        )

    @classmethod
    def parse_responses_tools(
        cls: type[ResponseSchema],
        completion: Any,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy OpenAI Responses Tools parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.RESPONSES_TOOLS,
            provider=Provider.OPENAI,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_responses_tools is deprecated. "
                "Use process_response(..., mode=Mode.RESPONSES_TOOLS) or ResponseSchema.from_response."
            ),
        )

    @classmethod
    def parse_tools(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy OpenAI tools parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.TOOLS,
            provider=Provider.OPENAI,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_tools is deprecated. "
                "Use process_response(..., mode=Mode.TOOLS) or ResponseSchema.from_response."
            ),
        )

    @classmethod
    def parse_mistral_structured_outputs(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy Mistral structured-output parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.JSON_SCHEMA,
            provider=Provider.MISTRAL,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_mistral_structured_outputs is deprecated. "
                "Use process_response(..., provider=Provider.MISTRAL, mode=Mode.JSON_SCHEMA)."
            ),
        )

    @classmethod
    def parse_json(
        cls: type[ResponseSchema],
        completion: ChatCompletion,
        validation_context: dict[str, Any] | None = None,
        strict: bool | None = None,
    ) -> BaseModel:
        """Legacy JSON parser (deprecated)."""
        return cls._parse_with_registry(
            completion,
            mode=Mode.JSON,
            provider=Provider.OPENAI,
            validation_context=validation_context,
            strict=strict,
            warning=(
                "ResponseSchema.parse_json is deprecated. "
                "Use process_response(..., mode=Mode.JSON) or ResponseSchema.from_response."
            ),
        )

from_response(completion, validation_context=None, strict=None, mode=Mode.TOOLS, provider=Provider.OPENAI) classmethod

Execute the function from the response of an openai chat completion

Parameters:

Name Type Description Default
completion ChatCompletion

The response from an openai chat completion

required
strict bool

Whether to use strict json parsing

None
mode Mode

The completion mode

TOOLS
provider Provider

The provider for handler lookup

OPENAI

Returns:

Name Type Description
cls ResponseSchema

An instance of the class

Source code in instructor/v2/core/function_calls.py
@classmethod
def from_response(
    cls: type[Self],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
    mode: Mode = Mode.TOOLS,
    provider: Provider = Provider.OPENAI,
) -> Self:
    """Execute the function from the response of an openai chat completion

    Parameters:
        completion (openai.ChatCompletion): The response from an openai chat completion
        strict (bool): Whether to use strict json parsing
        mode (Mode): The completion mode
        provider (Provider): The provider for handler lookup

    Returns:
        cls (ResponseSchema): An instance of the class
    """

    import importlib

    from instructor.v2.core.registry import mode_registry

    importlib.import_module("instructor.v2")

    provider = provider_from_mode(mode, provider)
    mode = normalize_mode_for_provider(mode, provider)
    handlers = mode_registry.get_handlers(provider, mode)
    return handlers.response_parser(
        response=completion,
        response_model=cls,
        validation_context=validation_context,
        strict=strict,
        stream=False,
        is_async=False,
    )

openai_schema()

Return the schema in the format of OpenAI's schema as jsonschema

Note

Its important to add a docstring to describe how to best use this class, it will be included in the description attribute and be part of the prompt.

Returns:

Name Type Description
model_json_schema dict

A dictionary in the format of OpenAI's schema as jsonschema

Source code in instructor/v2/core/function_calls.py
@classproperty
def openai_schema(cls) -> dict[str, Any]:
    """
    Return the schema in the format of OpenAI's schema as jsonschema

    Note:
        Its important to add a docstring to describe how to best use this class, it will be included in the description attribute and be part of the prompt.

    Returns:
        model_json_schema (dict): A dictionary in the format of OpenAI's schema as jsonschema
    """
    from instructor.v2.providers.openai.schema import generate_openai_schema

    return generate_openai_schema(cls)

parse_anthropic_json(completion, validation_context=None, strict=None) classmethod

Legacy Anthropic JSON parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_anthropic_json(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy Anthropic JSON parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.ANTHROPIC_JSON,
        provider=Provider.ANTHROPIC,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_anthropic_json is deprecated. "
            "Use process_response(..., provider=Provider.ANTHROPIC, mode=Mode.JSON) "
            "or ResponseSchema.from_response with core modes."
        ),
    )

parse_anthropic_tools(completion, validation_context=None, strict=None) classmethod

Legacy Anthropic tools parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_anthropic_tools(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy Anthropic tools parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.ANTHROPIC_TOOLS,
        provider=Provider.ANTHROPIC,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_anthropic_tools is deprecated. "
            "Use process_response(..., provider=Provider.ANTHROPIC, mode=Mode.TOOLS) "
            "or ResponseSchema.from_response with core modes."
        ),
    )

parse_bedrock_json(completion, validation_context=None, strict=None) classmethod

Legacy Bedrock JSON parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_bedrock_json(
    cls: type[ResponseSchema],
    completion: Any,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy Bedrock JSON parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.MD_JSON,
        provider=Provider.BEDROCK,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_bedrock_json is deprecated. "
            "Use process_response(..., provider=Provider.BEDROCK, mode=Mode.MD_JSON)."
        ),
    )

parse_bedrock_tools(completion, validation_context=None, strict=None) classmethod

Legacy Bedrock tools parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_bedrock_tools(
    cls: type[ResponseSchema],
    completion: Any,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy Bedrock tools parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.TOOLS,
        provider=Provider.BEDROCK,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_bedrock_tools is deprecated. "
            "Use process_response(..., provider=Provider.BEDROCK, mode=Mode.TOOLS)."
        ),
    )

parse_cohere_json_schema(completion, validation_context=None, strict=None) classmethod

Legacy Cohere JSON schema parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_cohere_json_schema(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
):
    """Legacy Cohere JSON schema parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.JSON_SCHEMA,
        provider=Provider.COHERE,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_cohere_json_schema is deprecated. "
            "Use process_response(..., provider=Provider.COHERE, mode=Mode.JSON_SCHEMA)."
        ),
    )

parse_cohere_tools(completion, validation_context=None, strict=None) classmethod

Legacy Cohere tools parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_cohere_tools(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy Cohere tools parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.TOOLS,
        provider=Provider.COHERE,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_cohere_tools is deprecated. "
            "Use process_response(..., provider=Provider.COHERE, mode=Mode.TOOLS)."
        ),
    )

parse_functions(completion, validation_context=None, strict=None) classmethod

Legacy OpenAI FUNCTIONS parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_functions(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy OpenAI FUNCTIONS parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.FUNCTIONS,
        provider=Provider.OPENAI,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_functions is deprecated. "
            "Use process_response(..., mode=Mode.TOOLS) or ResponseSchema.from_response."
        ),
    )

parse_gemini_json(completion, validation_context=None, strict=None) classmethod

Legacy Gemini JSON parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_gemini_json(
    cls: type[ResponseSchema],
    completion: Any,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy Gemini JSON parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.MD_JSON,
        provider=Provider.GEMINI,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_gemini_json is deprecated. "
            "Use process_response(..., provider=Provider.GEMINI, mode=Mode.MD_JSON)."
        ),
    )

parse_gemini_tools(completion, validation_context=None, strict=None) classmethod

Legacy Gemini tools parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_gemini_tools(
    cls: type[ResponseSchema],
    completion: Any,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy Gemini tools parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.TOOLS,
        provider=Provider.GEMINI,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_gemini_tools is deprecated. "
            "Use process_response(..., provider=Provider.GEMINI, mode=Mode.TOOLS)."
        ),
    )

parse_genai_structured_outputs(completion, validation_context=None, strict=None) classmethod

Legacy GenAI structured parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_genai_structured_outputs(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy GenAI structured parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.JSON,
        provider=Provider.GENAI,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_genai_structured_outputs is deprecated. "
            "Use process_response(..., provider=Provider.GENAI, mode=Mode.JSON)."
        ),
    )

parse_genai_tools(completion, validation_context=None, strict=None) classmethod

Legacy GenAI tools parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_genai_tools(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy GenAI tools parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.TOOLS,
        provider=Provider.GENAI,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_genai_tools is deprecated. "
            "Use process_response(..., provider=Provider.GENAI, mode=Mode.TOOLS)."
        ),
    )

parse_json(completion, validation_context=None, strict=None) classmethod

Legacy JSON parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_json(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy JSON parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.JSON,
        provider=Provider.OPENAI,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_json is deprecated. "
            "Use process_response(..., mode=Mode.JSON) or ResponseSchema.from_response."
        ),
    )

parse_mistral_structured_outputs(completion, validation_context=None, strict=None) classmethod

Legacy Mistral structured-output parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_mistral_structured_outputs(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy Mistral structured-output parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.JSON_SCHEMA,
        provider=Provider.MISTRAL,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_mistral_structured_outputs is deprecated. "
            "Use process_response(..., provider=Provider.MISTRAL, mode=Mode.JSON_SCHEMA)."
        ),
    )

parse_responses_tools(completion, validation_context=None, strict=None) classmethod

Legacy OpenAI Responses Tools parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_responses_tools(
    cls: type[ResponseSchema],
    completion: Any,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy OpenAI Responses Tools parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.RESPONSES_TOOLS,
        provider=Provider.OPENAI,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_responses_tools is deprecated. "
            "Use process_response(..., mode=Mode.RESPONSES_TOOLS) or ResponseSchema.from_response."
        ),
    )

parse_tools(completion, validation_context=None, strict=None) classmethod

Legacy OpenAI tools parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_tools(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy OpenAI tools parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.TOOLS,
        provider=Provider.OPENAI,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_tools is deprecated. "
            "Use process_response(..., mode=Mode.TOOLS) or ResponseSchema.from_response."
        ),
    )

parse_vertexai_json(completion, validation_context=None, strict=None) classmethod

Legacy VertexAI JSON parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_vertexai_json(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy VertexAI JSON parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.MD_JSON,
        provider=Provider.VERTEXAI,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_vertexai_json is deprecated. "
            "Use process_response(..., provider=Provider.VERTEXAI, mode=Mode.MD_JSON)."
        ),
    )

parse_vertexai_tools(completion, validation_context=None) classmethod

Legacy VertexAI tools parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_vertexai_tools(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
) -> BaseModel:
    """Legacy VertexAI tools parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.TOOLS,
        provider=Provider.VERTEXAI,
        validation_context=validation_context,
        strict=False,
        warning=(
            "ResponseSchema.parse_vertexai_tools is deprecated. "
            "Use process_response(..., provider=Provider.VERTEXAI, mode=Mode.TOOLS)."
        ),
    )

parse_writer_json(completion, validation_context=None, strict=None) classmethod

Legacy Writer JSON parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_writer_json(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy Writer JSON parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.MD_JSON,
        provider=Provider.WRITER,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_writer_json is deprecated. "
            "Use process_response(..., provider=Provider.WRITER, mode=Mode.MD_JSON)."
        ),
    )

parse_writer_tools(completion, validation_context=None, strict=None) classmethod

Legacy Writer tools parser (deprecated).

Source code in instructor/v2/core/function_calls.py
@classmethod
def parse_writer_tools(
    cls: type[ResponseSchema],
    completion: ChatCompletion,
    validation_context: dict[str, Any] | None = None,
    strict: bool | None = None,
) -> BaseModel:
    """Legacy Writer tools parser (deprecated)."""
    return cls._parse_with_registry(
        completion,
        mode=Mode.TOOLS,
        provider=Provider.WRITER,
        validation_context=validation_context,
        strict=strict,
        warning=(
            "ResponseSchema.parse_writer_tools is deprecated. "
            "Use process_response(..., provider=Provider.WRITER, mode=Mode.TOOLS)."
        ),
    )

classproperty

Bases: Generic[R_co]

Descriptor for class-level properties.

Source code in instructor/v2/core/utils.py
class classproperty(Generic[R_co]):
    """Descriptor for class-level properties."""

    def __init__(self, method: Callable[[Any], R_co]) -> None:
        self.cproperty = method

    def __get__(self, instance: object, cls: type[Any]) -> R_co:
        return self.cproperty(cls)

normalize_mode_for_provider(mode, _provider)

Apply provider-specific mode overrides before registry lookup.

Source code in instructor/v2/core/providers.py
def normalize_mode_for_provider(mode: Mode, _provider: Provider) -> Mode:
    """Apply provider-specific mode overrides before registry lookup."""
    if mode in DEPRECATED_TO_CORE:
        Mode.warn_deprecated_mode(mode)
        return DEPRECATED_TO_CORE[mode]
    return mode

provider_from_mode(mode, default=Provider.OPENAI)

Infer provider from a provider-specific Mode.

Source code in instructor/v2/core/providers.py
def provider_from_mode(mode: Mode, default: Provider = Provider.OPENAI) -> Provider:
    """Infer provider from a provider-specific Mode."""
    mapping = {
        Mode.ANTHROPIC_TOOLS: Provider.ANTHROPIC,
        Mode.ANTHROPIC_JSON: Provider.ANTHROPIC,
        Mode.ANTHROPIC_PARALLEL_TOOLS: Provider.ANTHROPIC,
        Mode.ANTHROPIC_REASONING_TOOLS: Provider.ANTHROPIC,
        Mode.COHERE_TOOLS: Provider.COHERE,
        Mode.COHERE_JSON_SCHEMA: Provider.COHERE,
        Mode.MISTRAL_TOOLS: Provider.MISTRAL,
        Mode.MISTRAL_STRUCTURED_OUTPUTS: Provider.MISTRAL,
        Mode.VERTEXAI_TOOLS: Provider.VERTEXAI,
        Mode.VERTEXAI_JSON: Provider.VERTEXAI,
        Mode.VERTEXAI_PARALLEL_TOOLS: Provider.VERTEXAI,
        Mode.GEMINI_TOOLS: Provider.GEMINI,
        Mode.GEMINI_JSON: Provider.GEMINI,
        Mode.GENAI_TOOLS: Provider.GENAI,
        Mode.GENAI_JSON: Provider.GENAI,
        Mode.GENAI_STRUCTURED_OUTPUTS: Provider.GENAI,
        Mode.XAI_TOOLS: Provider.XAI,
        Mode.XAI_JSON: Provider.XAI,
        Mode.CEREBRAS_TOOLS: Provider.CEREBRAS,
        Mode.CEREBRAS_JSON: Provider.CEREBRAS,
        Mode.FIREWORKS_TOOLS: Provider.FIREWORKS,
        Mode.FIREWORKS_JSON: Provider.FIREWORKS,
        Mode.WRITER_TOOLS: Provider.WRITER,
        Mode.WRITER_JSON: Provider.WRITER,
        Mode.BEDROCK_TOOLS: Provider.BEDROCK,
        Mode.BEDROCK_JSON: Provider.BEDROCK,
        Mode.PERPLEXITY_JSON: Provider.PERPLEXITY,
        Mode.OPENROUTER_STRUCTURED_OUTPUTS: Provider.OPENROUTER,
    }
    return mapping.get(mode, default)

response_schema(cls)

Wrap a Pydantic model class to add ResponseSchema behavior.

Source code in instructor/v2/core/function_calls.py
def response_schema(cls: type[Model]) -> type[Model]:
    """Wrap a Pydantic model class to add ResponseSchema behavior."""
    if not inspect.isclass(cls) or not issubclass(cls, BaseModel):
        got = cls.__name__ if inspect.isclass(cls) else type(cls).__name__
        raise TypeError(
            f"response_model must be a subclass of pydantic.BaseModel, got {got}"
        )

    # Create the wrapped model
    schema = cast(
        type[BaseModel],
        wraps(cls, updated=())(
            create_model(
                cls.__name__ if hasattr(cls, "__name__") else str(cls),
                __base__=(cls, ResponseSchema),
            )
        ),
    )

    return cast(type[Model], schema)

Compatibility wrapper for the provider-owned OpenAI schema helper.

Source code in instructor/v2/core/schema.py
def generate_openai_schema(model: type[BaseModel]) -> dict[str, Any]:
    """Compatibility wrapper for the provider-owned OpenAI schema helper."""
    return _generate_openai_schema(model)

Compatibility wrapper for the provider-owned Anthropic schema helper.

Source code in instructor/v2/core/schema.py
def generate_anthropic_schema(model: type[BaseModel]) -> dict[str, Any]:
    """Compatibility wrapper for the provider-owned Anthropic schema helper."""
    return _generate_anthropic_schema(model)

Compatibility wrapper for the provider-owned Gemini schema helper.

Source code in instructor/v2/core/schema.py
def generate_gemini_schema(model: type[BaseModel]) -> Any:
    """Compatibility wrapper for the provider-owned Gemini schema helper."""
    return _generate_gemini_schema(model)

Validation

Validation utilities for LLM outputs and async validation support.

Validation components for instructor.

AsyncValidationContext

Carry context through async validation hooks.

Source code in instructor/v2/validation/async_validators.py
class AsyncValidationContext:
    """Carry context through async validation hooks."""

    context: dict[str, Any]

    def __init__(self, context: dict[str, Any]):
        self.context = context

AsyncValidationError

Bases: ValueError, InstructorError

Exception raised during async validation.

This exception is used specifically for errors that occur during asynchronous validation operations. It inherits from both ValueError and InstructorError to maintain compatibility with existing code.

Attributes:

Name Type Description
errors list[ValueError]

List of ValueError instances from failed validations

Examples:

from instructor.v2.validation import async_field_validator

class Model(BaseModel):
    urls: list[str]

    @async_field_validator('urls')
    async def validate_urls(cls, v):
        # Async validation logic
        ...

try:
    response = await client.chat.completions.create(
        response_model=Model,
        ...
    )
except AsyncValidationError as e:
    print(f"Async validation failed: {e.errors}")
Source code in instructor/v2/core/errors.py
class AsyncValidationError(ValueError, InstructorError):
    """Exception raised during async validation.

    This exception is used specifically for errors that occur during
    asynchronous validation operations. It inherits from both ValueError
    and InstructorError to maintain compatibility with existing code.

    Attributes:
        errors: List of ValueError instances from failed validations

    Examples:
        ```python
        from instructor.v2.validation import async_field_validator

        class Model(BaseModel):
            urls: list[str]

            @async_field_validator('urls')
            async def validate_urls(cls, v):
                # Async validation logic
                ...

        try:
            response = await client.chat.completions.create(
                response_model=Model,
                ...
            )
        except AsyncValidationError as e:
            print(f"Async validation failed: {e.errors}")
        ```
    """

    errors: list[ValueError]

Validator

Bases: ResponseSchema

Describe whether a candidate attribute is valid and how to repair it.

Source code in instructor/v2/core/validators.py
class Validator(ResponseSchema):
    """Describe whether a candidate attribute is valid and how to repair it."""

    is_valid: bool = Field(
        description="Whether the attribute is valid based on the requirements",
    )
    reason: Optional[str] = Field(
        default=None,
        description="The error message if the attribute is not valid, otherwise None",
    )
    fixed_value: Optional[str] = Field(
        default=None,
        description="If the attribute is not valid, suggest a new value for the attribute",
    )

async_field_validator(field, *fields)

Mark a callable as an async field validator.

Source code in instructor/v2/validation/async_validators.py
def async_field_validator(field: str, *fields: str) -> Callable[[T], T]:
    """Mark a callable as an async field validator."""
    field_names = field, *fields

    def decorator(func: T) -> T:
        params = signature(func).parameters
        requires_validation_context = False
        if len(params) == 3:
            if "info" not in params:
                raise ValueError(
                    "Async validator can only have a value parameter and an optional info parameter"
                )
            if params["info"].annotation != ValidationInfo:
                raise ValueError(
                    "Async validator info parameter must be of type ValidationInfo"
                )
            requires_validation_context = True

        setattr(
            func, ASYNC_VALIDATOR_KEY, (field_names, func, requires_validation_context)
        )
        return func

    return decorator

async_model_validator()

Mark a callable as an async model validator.

Source code in instructor/v2/validation/async_validators.py
def async_model_validator() -> Callable[[T], T]:
    """Mark a callable as an async model validator."""

    def decorator(func: T) -> T:
        params = signature(func).parameters
        requires_validation_context = False
        if len(params) > 2:
            raise ValueError("Invalid Parameter Count!")

        if len(params) == 2:
            if "info" not in params:
                raise ValueError(
                    "Async validator can only have a value parameter and an optional info parameter"
                )
            if params["info"].annotation != ValidationInfo:
                raise ValueError(
                    "Async validator info parameter must be of type ValidationInfo"
                )
            requires_validation_context = True

        setattr(
            func,
            ASYNC_MODEL_VALIDATOR_KEY,
            (func, requires_validation_context),
        )
        return func

    return decorator

llm_validator(statement, client, allow_override=False, model='gpt-3.5-turbo', temperature=0)

Create a validator that uses an LLM to validate an attribute.

Source code in instructor/v2/validation/llm_validators.py
def llm_validator(
    statement: str,
    client: Instructor,
    allow_override: bool = False,
    model: str = "gpt-3.5-turbo",
    temperature: float = 0,
) -> Callable[[str], str]:
    """Create a validator that uses an LLM to validate an attribute."""

    def llm(v: str) -> str:
        resp = client.chat.completions.create(
            response_model=Validator,
            messages=[
                {
                    "role": "system",
                    "content": "You are a world class validation model. Capable to determine if the following value is valid for the statement, if it is not, explain why and suggest a new value.",
                },
                {
                    "role": "user",
                    "content": f"Does `{v}` follow the rules: {statement}",
                },
            ],
            model=model,
            temperature=temperature,
        )

        if not resp.is_valid:
            if allow_override and resp.fixed_value is not None:
                return resp.fixed_value
            assert resp.is_valid, resp.reason

        return v

    return llm

openai_moderation(client)

Create a validator backed by the OpenAI moderation endpoint.

Source code in instructor/v2/validation/llm_validators.py
def openai_moderation(client: OpenAI) -> Callable[[str], str]:
    """Create a validator backed by the OpenAI moderation endpoint."""

    def validate_message_with_openai_mod(v: str) -> str:
        response = client.moderations.create(input=v)
        out = response.results[0]
        cats = out.categories.model_dump()
        if out.flagged:
            raise ValueError(
                f"`{v}` was flagged for {', '.join(cat for cat in cats if cats[cat])}"
            )

        return v

    return validate_message_with_openai_mod

Create a validator that uses an LLM to validate an attribute.

Source code in instructor/v2/validation/llm_validators.py
def llm_validator(
    statement: str,
    client: Instructor,
    allow_override: bool = False,
    model: str = "gpt-3.5-turbo",
    temperature: float = 0,
) -> Callable[[str], str]:
    """Create a validator that uses an LLM to validate an attribute."""

    def llm(v: str) -> str:
        resp = client.chat.completions.create(
            response_model=Validator,
            messages=[
                {
                    "role": "system",
                    "content": "You are a world class validation model. Capable to determine if the following value is valid for the statement, if it is not, explain why and suggest a new value.",
                },
                {
                    "role": "user",
                    "content": f"Does `{v}` follow the rules: {statement}",
                },
            ],
            model=model,
            temperature=temperature,
        )

        if not resp.is_valid:
            if allow_override and resp.fixed_value is not None:
                return resp.fixed_value
            assert resp.is_valid, resp.reason

        return v

    return llm

Create a validator backed by the OpenAI moderation endpoint.

Source code in instructor/v2/validation/llm_validators.py
def openai_moderation(client: OpenAI) -> Callable[[str], str]:
    """Create a validator backed by the OpenAI moderation endpoint."""

    def validate_message_with_openai_mod(v: str) -> str:
        response = client.moderations.create(input=v)
        out = response.results[0]
        cats = out.categories.model_dump()
        if out.flagged:
            raise ValueError(
                f"`{v}` was flagged for {', '.join(cat for cat in cats if cats[cat])}"
            )

        return v

    return validate_message_with_openai_mod

Batch Processing

Batch processing utilities for handling multiple requests efficiently.

Unified Batch Processing API for Multiple Providers

This module provides a unified interface for batch processing across OpenAI and Anthropic providers. The API uses a Maybe/Result-like pattern with custom_id tracking for type-safe handling of batch results.

Supported Providers: - OpenAI: 50% cost savings on batch requests - Anthropic: 50% cost savings on batch requests (Message Batches API)

Features: - Type-safe Maybe/Result pattern for handling successes and errors - Custom ID tracking for correlating results to original requests - Unified interface across all providers - Helper functions for filtering and extracting results

Example usage

from instructor.batch import BatchProcessor, filter_successful, extract_results from pydantic import BaseModel

class User(BaseModel): name: str age: int

processor = BatchProcessor("openai/gpt-4o-mini", User) batch_id = processor.submit_batch("requests.jsonl")

Results are BatchSuccess[T] | BatchError union types

all_results = processor.retrieve_results(batch_id) successful_results = filter_successful(all_results) extracted_users = extract_results(all_results)

Documentation: - OpenAI Batch API: https://platform.openai.com/docs/guides/batch - Anthropic Message Batches: https://docs.anthropic.com/en/api/creating-message-batches

BatchError

Bases: BaseModel

Error information for failed batch requests

Source code in instructor/batch/models.py
class BatchError(BaseModel):
    """Error information for failed batch requests"""

    custom_id: str
    error_type: str
    error_message: str
    success: bool = False
    raw_data: dict[str, Any] | None = None

BatchErrorInfo

Bases: BaseModel

Batch-level error information

Source code in instructor/batch/models.py
class BatchErrorInfo(BaseModel):
    """Batch-level error information"""

    error_type: str | None = None
    error_message: str | None = None
    error_code: str | None = None

BatchFiles

Bases: BaseModel

File references for batch job

Source code in instructor/batch/models.py
class BatchFiles(BaseModel):
    """File references for batch job"""

    input_file_id: str | None = None
    output_file_id: str | None = None
    error_file_id: str | None = None
    results_url: str | None = None  # Anthropic

BatchJob

Legacy BatchJob class for backward compatibility

Source code in instructor/batch/__init__.py
class BatchJob:
    """Legacy BatchJob class for backward compatibility"""

    @classmethod
    def parse_from_file(
        cls, file_path: str, response_model: type[T]
    ) -> tuple[list[T], list[dict[Any, Any]]]:
        with open(file_path) as file:
            content = file.read()
        return cls.parse_from_string(content, response_model)

    @classmethod
    def parse_from_string(
        cls, content: str, response_model: type[T]
    ) -> tuple[list[T], list[dict[Any, Any]]]:
        """Enhanced parser that works with all providers using JSON schema"""
        import json

        res: list[T] = []
        error_objs: list[dict[Any, Any]] = []

        lines = content.strip().split("\n")
        for line in lines:
            if not line.strip():
                continue

            try:
                data = json.loads(line)
                extracted_data = cls._extract_structured_data(data)

                if extracted_data:
                    try:
                        result = response_model(**extracted_data)
                        res.append(result)
                    except Exception:
                        error_objs.append(data)
                else:
                    error_objs.append(data)

            except Exception:
                error_objs.append({"error": "Failed to parse JSON", "raw_line": line})

        return res, error_objs

    @classmethod
    def _extract_structured_data(cls, data: dict[str, Any]) -> Optional[dict[str, Any]]:
        """Extract structured data from various provider response formats"""
        import json

        try:
            # Try OpenAI JSON schema format first
            if "response" in data and "body" in data["response"]:
                choices = data["response"]["body"].get("choices", [])
                if choices:
                    message = choices[0].get("message", {})

                    # JSON schema response
                    if "content" in message:
                        content = message["content"]
                        if isinstance(content, str):
                            return json.loads(content)

                    # Tool calls (legacy)
                    if "tool_calls" in message:
                        tool_call = message["tool_calls"][0]
                        return json.loads(tool_call["function"]["arguments"])

            # Try Anthropic format
            if "result" in data and "message" in data["result"]:
                content = data["result"]["message"]["content"]
                if isinstance(content, list) and len(content) > 0:
                    # Tool use response
                    for item in content:
                        if item.get("type") == "tool_use":
                            return item.get("input", {})
                    # Text response with JSON
                    for item in content:
                        if item.get("type") == "text":
                            text = item.get("text", "")
                            return json.loads(text)

        except Exception:
            pass

        return None

parse_from_string(content, response_model) classmethod

Enhanced parser that works with all providers using JSON schema

Source code in instructor/batch/__init__.py
@classmethod
def parse_from_string(
    cls, content: str, response_model: type[T]
) -> tuple[list[T], list[dict[Any, Any]]]:
    """Enhanced parser that works with all providers using JSON schema"""
    import json

    res: list[T] = []
    error_objs: list[dict[Any, Any]] = []

    lines = content.strip().split("\n")
    for line in lines:
        if not line.strip():
            continue

        try:
            data = json.loads(line)
            extracted_data = cls._extract_structured_data(data)

            if extracted_data:
                try:
                    result = response_model(**extracted_data)
                    res.append(result)
                except Exception:
                    error_objs.append(data)
            else:
                error_objs.append(data)

        except Exception:
            error_objs.append({"error": "Failed to parse JSON", "raw_line": line})

    return res, error_objs

BatchJobInfo

Bases: BaseModel

Enhanced unified batch job information with comprehensive provider support

Source code in instructor/batch/models.py
class BatchJobInfo(BaseModel):
    """Enhanced unified batch job information with comprehensive provider support"""

    # Core identifiers
    id: str
    provider: str

    # Status information
    status: BatchStatus
    raw_status: str  # Original provider status

    # Timing information
    timestamps: BatchTimestamps

    # Request tracking
    request_counts: BatchRequestCounts

    # File references
    files: BatchFiles

    # Error information
    error: BatchErrorInfo | None = None

    # Provider-specific data
    metadata: dict[str, Any] = Field(default_factory=dict)
    raw_data: dict[str, Any] | None = None

    # Additional fields
    model: str | None = None
    endpoint: str | None = None
    completion_window: str | None = None

    @classmethod
    def from_openai(cls, batch_data: dict[str, Any]) -> BatchJobInfo:
        """Create from OpenAI batch response"""
        # Normalize status
        status_map = {
            "validating": BatchStatus.PENDING,
            "in_progress": BatchStatus.PROCESSING,
            "finalizing": BatchStatus.PROCESSING,
            "completed": BatchStatus.COMPLETED,
            "failed": BatchStatus.FAILED,
            "expired": BatchStatus.EXPIRED,
            "cancelled": BatchStatus.CANCELLED,
            "cancelling": BatchStatus.CANCELLED,
        }

        # Parse timestamps
        timestamps = BatchTimestamps(
            created_at=(
                datetime.fromtimestamp(batch_data["created_at"], tz=timezone.utc)
                if batch_data.get("created_at")
                else None
            ),
            started_at=(
                datetime.fromtimestamp(batch_data["in_progress_at"], tz=timezone.utc)
                if batch_data.get("in_progress_at")
                else None
            ),
            completed_at=(
                datetime.fromtimestamp(batch_data["completed_at"], tz=timezone.utc)
                if batch_data.get("completed_at")
                else None
            ),
            failed_at=(
                datetime.fromtimestamp(batch_data["failed_at"], tz=timezone.utc)
                if batch_data.get("failed_at")
                else None
            ),
            cancelled_at=(
                datetime.fromtimestamp(batch_data["cancelled_at"], tz=timezone.utc)
                if batch_data.get("cancelled_at")
                else None
            ),
            expired_at=(
                datetime.fromtimestamp(batch_data["expired_at"], tz=timezone.utc)
                if batch_data.get("expired_at")
                else None
            ),
            expires_at=(
                datetime.fromtimestamp(batch_data["expires_at"], tz=timezone.utc)
                if batch_data.get("expires_at")
                else None
            ),
        )

        # Parse request counts
        request_counts_data = batch_data.get("request_counts", {})
        request_counts = BatchRequestCounts(
            total=request_counts_data.get("total"),
            completed=request_counts_data.get("completed"),
            failed=request_counts_data.get("failed"),
        )

        # Parse files
        files = BatchFiles(
            input_file_id=batch_data.get("input_file_id"),
            output_file_id=batch_data.get("output_file_id"),
            error_file_id=batch_data.get("error_file_id"),
        )

        # Parse error information
        error = None
        if batch_data.get("errors"):
            error_data = batch_data["errors"]
            error = BatchErrorInfo(
                error_type=error_data.get("type"),
                error_message=error_data.get("message"),
                error_code=error_data.get("code"),
            )

        return cls(
            id=batch_data["id"],
            provider="openai",
            status=status_map.get(batch_data["status"], BatchStatus.PENDING),
            raw_status=batch_data["status"],
            timestamps=timestamps,
            request_counts=request_counts,
            files=files,
            error=error,
            metadata=batch_data.get("metadata", {}),
            raw_data=batch_data,
            endpoint=batch_data.get("endpoint"),
            completion_window=batch_data.get("completion_window"),
        )

    @classmethod
    def from_anthropic(cls, batch_data: dict[str, Any]) -> BatchJobInfo:
        """Create from Anthropic batch response"""
        # Normalize status
        status_map = {
            "in_progress": BatchStatus.PROCESSING,
            "ended": BatchStatus.COMPLETED,
            "failed": BatchStatus.FAILED,
            "cancelled": BatchStatus.CANCELLED,
            "expired": BatchStatus.EXPIRED,
        }

        # Parse timestamps
        def parse_iso_timestamp(timestamp_value):
            if not timestamp_value:
                return None
            try:
                # Handle different timestamp format variations
                if isinstance(timestamp_value, datetime):
                    return timestamp_value
                elif isinstance(timestamp_value, str):
                    return datetime.fromisoformat(
                        timestamp_value.replace("Z", "+00:00")
                    )
                else:
                    return None
            except (ValueError, AttributeError):
                return None

        timestamps = BatchTimestamps(
            created_at=parse_iso_timestamp(batch_data.get("created_at")),
            started_at=parse_iso_timestamp(
                batch_data.get("created_at")
            ),  # Anthropic doesn't provide started_at, use created_at
            cancelled_at=parse_iso_timestamp(batch_data.get("cancel_initiated_at")),
            completed_at=parse_iso_timestamp(batch_data.get("ended_at")),
            expires_at=parse_iso_timestamp(batch_data.get("expires_at")),
        )

        # Parse request counts
        request_counts_data = batch_data.get("request_counts", {})
        request_counts = BatchRequestCounts(
            processing=request_counts_data.get("processing"),
            succeeded=request_counts_data.get("succeeded"),
            errored=request_counts_data.get("errored"),
            cancelled=request_counts_data.get(
                "canceled"
            ),  # Note: Anthropic uses "canceled"
            expired=request_counts_data.get("expired"),
            total=request_counts_data.get("processing", 0)
            + request_counts_data.get("succeeded", 0)
            + request_counts_data.get("errored", 0),
        )

        # Parse files
        files = BatchFiles(
            results_url=batch_data.get("results_url"),
        )

        return cls(
            id=batch_data["id"],
            provider="anthropic",
            status=status_map.get(batch_data["processing_status"], BatchStatus.PENDING),
            raw_status=batch_data["processing_status"],
            timestamps=timestamps,
            request_counts=request_counts,
            files=files,
            raw_data=batch_data,
        )

from_anthropic(batch_data) classmethod

Create from Anthropic batch response

Source code in instructor/batch/models.py
@classmethod
def from_anthropic(cls, batch_data: dict[str, Any]) -> BatchJobInfo:
    """Create from Anthropic batch response"""
    # Normalize status
    status_map = {
        "in_progress": BatchStatus.PROCESSING,
        "ended": BatchStatus.COMPLETED,
        "failed": BatchStatus.FAILED,
        "cancelled": BatchStatus.CANCELLED,
        "expired": BatchStatus.EXPIRED,
    }

    # Parse timestamps
    def parse_iso_timestamp(timestamp_value):
        if not timestamp_value:
            return None
        try:
            # Handle different timestamp format variations
            if isinstance(timestamp_value, datetime):
                return timestamp_value
            elif isinstance(timestamp_value, str):
                return datetime.fromisoformat(
                    timestamp_value.replace("Z", "+00:00")
                )
            else:
                return None
        except (ValueError, AttributeError):
            return None

    timestamps = BatchTimestamps(
        created_at=parse_iso_timestamp(batch_data.get("created_at")),
        started_at=parse_iso_timestamp(
            batch_data.get("created_at")
        ),  # Anthropic doesn't provide started_at, use created_at
        cancelled_at=parse_iso_timestamp(batch_data.get("cancel_initiated_at")),
        completed_at=parse_iso_timestamp(batch_data.get("ended_at")),
        expires_at=parse_iso_timestamp(batch_data.get("expires_at")),
    )

    # Parse request counts
    request_counts_data = batch_data.get("request_counts", {})
    request_counts = BatchRequestCounts(
        processing=request_counts_data.get("processing"),
        succeeded=request_counts_data.get("succeeded"),
        errored=request_counts_data.get("errored"),
        cancelled=request_counts_data.get(
            "canceled"
        ),  # Note: Anthropic uses "canceled"
        expired=request_counts_data.get("expired"),
        total=request_counts_data.get("processing", 0)
        + request_counts_data.get("succeeded", 0)
        + request_counts_data.get("errored", 0),
    )

    # Parse files
    files = BatchFiles(
        results_url=batch_data.get("results_url"),
    )

    return cls(
        id=batch_data["id"],
        provider="anthropic",
        status=status_map.get(batch_data["processing_status"], BatchStatus.PENDING),
        raw_status=batch_data["processing_status"],
        timestamps=timestamps,
        request_counts=request_counts,
        files=files,
        raw_data=batch_data,
    )

from_openai(batch_data) classmethod

Create from OpenAI batch response

Source code in instructor/batch/models.py
@classmethod
def from_openai(cls, batch_data: dict[str, Any]) -> BatchJobInfo:
    """Create from OpenAI batch response"""
    # Normalize status
    status_map = {
        "validating": BatchStatus.PENDING,
        "in_progress": BatchStatus.PROCESSING,
        "finalizing": BatchStatus.PROCESSING,
        "completed": BatchStatus.COMPLETED,
        "failed": BatchStatus.FAILED,
        "expired": BatchStatus.EXPIRED,
        "cancelled": BatchStatus.CANCELLED,
        "cancelling": BatchStatus.CANCELLED,
    }

    # Parse timestamps
    timestamps = BatchTimestamps(
        created_at=(
            datetime.fromtimestamp(batch_data["created_at"], tz=timezone.utc)
            if batch_data.get("created_at")
            else None
        ),
        started_at=(
            datetime.fromtimestamp(batch_data["in_progress_at"], tz=timezone.utc)
            if batch_data.get("in_progress_at")
            else None
        ),
        completed_at=(
            datetime.fromtimestamp(batch_data["completed_at"], tz=timezone.utc)
            if batch_data.get("completed_at")
            else None
        ),
        failed_at=(
            datetime.fromtimestamp(batch_data["failed_at"], tz=timezone.utc)
            if batch_data.get("failed_at")
            else None
        ),
        cancelled_at=(
            datetime.fromtimestamp(batch_data["cancelled_at"], tz=timezone.utc)
            if batch_data.get("cancelled_at")
            else None
        ),
        expired_at=(
            datetime.fromtimestamp(batch_data["expired_at"], tz=timezone.utc)
            if batch_data.get("expired_at")
            else None
        ),
        expires_at=(
            datetime.fromtimestamp(batch_data["expires_at"], tz=timezone.utc)
            if batch_data.get("expires_at")
            else None
        ),
    )

    # Parse request counts
    request_counts_data = batch_data.get("request_counts", {})
    request_counts = BatchRequestCounts(
        total=request_counts_data.get("total"),
        completed=request_counts_data.get("completed"),
        failed=request_counts_data.get("failed"),
    )

    # Parse files
    files = BatchFiles(
        input_file_id=batch_data.get("input_file_id"),
        output_file_id=batch_data.get("output_file_id"),
        error_file_id=batch_data.get("error_file_id"),
    )

    # Parse error information
    error = None
    if batch_data.get("errors"):
        error_data = batch_data["errors"]
        error = BatchErrorInfo(
            error_type=error_data.get("type"),
            error_message=error_data.get("message"),
            error_code=error_data.get("code"),
        )

    return cls(
        id=batch_data["id"],
        provider="openai",
        status=status_map.get(batch_data["status"], BatchStatus.PENDING),
        raw_status=batch_data["status"],
        timestamps=timestamps,
        request_counts=request_counts,
        files=files,
        error=error,
        metadata=batch_data.get("metadata", {}),
        raw_data=batch_data,
        endpoint=batch_data.get("endpoint"),
        completion_window=batch_data.get("completion_window"),
    )

BatchProcessor

Bases: Generic[T]

Unified batch processor that works across all providers

Source code in instructor/batch/processor.py
class BatchProcessor(Generic[T]):
    """Unified batch processor that works across all providers"""

    def __init__(self, model: str, response_model: type[T]):
        self.model = model
        self.response_model = response_model

        # Parse provider from model string
        try:
            self.provider_name, self.model_name = model.split("/", 1)
        except ValueError as err:
            raise ValueError(
                'Model string must be in format "provider/model-name" '
                '(e.g. "openai/gpt-4" or "anthropic/claude-3-sonnet")'
            ) from err

        # Get the batch provider instance
        self.provider = get_provider(self.provider_name)

    def create_batch_from_messages(
        self,
        messages_list: list[list[dict[str, Any]]],
        file_path: str | None = None,
        max_tokens: int | None = 1000,
        temperature: float | None = 0.1,
    ) -> str | io.BytesIO:
        """Create batch file from list of message conversations

        Args:
            messages_list: List of message conversations, each as a list of message dicts
            file_path: Path to save the batch request file. If None, returns BytesIO buffer
            max_tokens: Maximum tokens per request
            temperature: Temperature for generation

        Returns:
            The file path where the batch was saved, or BytesIO buffer if file_path is None
        """
        if file_path is not None:
            if os.path.exists(file_path):
                os.remove(file_path)

            batch_requests = []
            for i, messages in enumerate(messages_list):
                batch_request = BatchRequest[T](
                    custom_id=f"request-{i}",
                    messages=messages,
                    response_model=self.response_model,
                    model=self.model_name,
                    max_tokens=max_tokens,
                    temperature=temperature,
                )
                batch_request.save_to_file(file_path, self.provider_name)
                batch_requests.append(batch_request)

            print(f"Created batch file {file_path} with {len(batch_requests)} requests")
            return file_path
        else:
            # Create BytesIO buffer - caller is responsible for cleanup
            buffer = io.BytesIO()
            batch_requests = []
            for i, messages in enumerate(messages_list):
                batch_request = BatchRequest[T](
                    custom_id=f"request-{i}",
                    messages=messages,
                    response_model=self.response_model,
                    model=self.model_name,
                    max_tokens=max_tokens,
                    temperature=temperature,
                )
                batch_request.save_to_file(buffer, self.provider_name)
                batch_requests.append(batch_request)

            print(f"Created batch buffer with {len(batch_requests)} requests")
            buffer.seek(0)  # Reset buffer position for reading
            return buffer

    def submit_batch(
        self,
        file_path_or_buffer: str | io.BytesIO,
        metadata: dict[str, Any] | None = None,
        **kwargs,
    ) -> str:
        """Submit batch job to the provider and return job ID

        Args:
            file_path_or_buffer: Path to the batch request file or BytesIO buffer
            metadata: Optional metadata to attach to the batch job
            **kwargs: Additional provider-specific arguments
        """
        if metadata is None:
            metadata = {"description": "Instructor batch job"}

        return self.provider.submit_batch(
            file_path_or_buffer, metadata=metadata, **kwargs
        )

    def get_batch_status(self, batch_id: str) -> dict[str, Any]:
        """Get batch job status from the provider"""
        return self.provider.get_status(batch_id)

    def retrieve_results(self, batch_id: str) -> list[BatchResult]:
        """Retrieve and parse batch results from the provider"""
        results_content = self.provider.retrieve_results(batch_id)
        return self.parse_results(results_content)

    def list_batches(self, limit: int = 10) -> list[BatchJobInfo]:
        """List batch jobs for the current provider

        Args:
            limit: Maximum number of batch jobs to return

        Returns:
            List of BatchJobInfo objects with normalized batch information
        """
        return self.provider.list_batches(limit)

    def get_results(
        self, batch_id: str, file_path: str | None = None
    ) -> list[BatchResult]:
        """Get batch results, optionally saving raw results to a file

        Args:
            batch_id: The batch job ID
            file_path: Optional file path to save raw results. If provided,
                      raw results will be saved to this file. If not provided,
                      results are only kept in memory.

        Returns:
            List of BatchResult objects (BatchSuccess[T] or BatchError)
        """
        # Retrieve results directly to memory
        results_content = self.retrieve_results(batch_id)

        # If file path is provided, save raw results to file
        if file_path is not None:
            self.provider.download_results(batch_id, file_path)

        return results_content

    def cancel_batch(self, batch_id: str) -> dict[str, Any]:
        """Cancel a batch job

        Args:
            batch_id: The batch job ID to cancel

        Returns:
            Dict containing the cancelled batch information
        """
        return self.provider.cancel_batch(batch_id)

    def delete_batch(self, batch_id: str) -> dict[str, Any]:
        """Delete a batch job (only available for completed batches)

        Args:
            batch_id: The batch job ID to delete

        Returns:
            Dict containing the deletion confirmation
        """
        return self.provider.delete_batch(batch_id)

    def parse_results(self, results_content: str) -> list[BatchResult]:
        """Parse batch results from content string into Maybe-like results with custom_id tracking"""
        results: list[BatchResult] = []

        lines = results_content.strip().split("\n")
        for line in lines:
            if not line.strip():
                continue

            try:
                data = json.loads(line)
                custom_id = data.get("custom_id", "unknown")
                extracted_data = self._extract_from_response(data)

                if extracted_data:
                    try:
                        # Parse into response model
                        result = self.response_model(**extracted_data)
                        batch_result = BatchSuccess[T](
                            custom_id=custom_id, result=result
                        )
                        results.append(batch_result)
                    except Exception as e:
                        error_result = BatchError(
                            custom_id=custom_id,
                            error_type="parsing_error",
                            error_message=f"Failed to parse into {self.response_model.__name__}: {e}",
                            raw_data=extracted_data,
                        )
                        results.append(error_result)
                else:
                    # Check if this is a provider error response
                    error_message = "Unknown error"
                    error_type = "extraction_error"

                    if self.provider_name == "anthropic" and "result" in data:
                        result = data["result"]
                        if result.get("type") == "error":
                            error_info = result.get("error", {})
                            if isinstance(error_info, dict) and "error" in error_info:
                                error_details = error_info["error"]
                                error_message = error_details.get(
                                    "message", "Unknown Anthropic error"
                                )
                                error_type = error_details.get(
                                    "type", "anthropic_error"
                                )
                            else:
                                error_message = str(error_info)
                                error_type = "anthropic_error"

                    error_result = BatchError(
                        custom_id=custom_id,
                        error_type=error_type,
                        error_message=error_message,
                        raw_data=data,
                    )
                    results.append(error_result)

            except Exception as e:
                error_result = BatchError(
                    custom_id="unknown",
                    error_type="json_parse_error",
                    error_message=f"Failed to parse JSON: {e}",
                    raw_data={"raw_line": line},
                )
                results.append(error_result)

        return results

    def _extract_from_response(self, data: dict[str, Any]) -> dict[str, Any] | None:
        """Extract structured data from provider-specific response format"""
        try:
            if self.provider_name == "openai":
                # OpenAI JSON schema response
                content = data["response"]["body"]["choices"][0]["message"]["content"]
                return json.loads(content)

            elif self.provider_name == "anthropic":
                # Anthropic batch response format
                if "result" not in data:
                    return None

                result = data["result"]

                # Check if result is an error
                if result.get("type") == "error":
                    # Return None to indicate error, let caller handle
                    return None

                # Handle successful message result
                if result.get("type") == "succeeded" and "message" in result:
                    content = result["message"]["content"]
                    if isinstance(content, list) and len(content) > 0:
                        # Try tool_use first
                        for item in content:
                            if item.get("type") == "tool_use":
                                return item.get("input", {})

                        # Fallback to text content and parse JSON
                        for item in content:
                            if item.get("type") == "text":
                                text = item.get("text", "")
                                try:
                                    return json.loads(text)
                                except json.JSONDecodeError:
                                    continue

                return None

        except Exception:
            return None

        return None

cancel_batch(batch_id)

Cancel a batch job

Parameters:

Name Type Description Default
batch_id str

The batch job ID to cancel

required

Returns:

Type Description
dict[str, Any]

Dict containing the cancelled batch information

Source code in instructor/batch/processor.py
def cancel_batch(self, batch_id: str) -> dict[str, Any]:
    """Cancel a batch job

    Args:
        batch_id: The batch job ID to cancel

    Returns:
        Dict containing the cancelled batch information
    """
    return self.provider.cancel_batch(batch_id)

create_batch_from_messages(messages_list, file_path=None, max_tokens=1000, temperature=0.1)

Create batch file from list of message conversations

Parameters:

Name Type Description Default
messages_list list[list[dict[str, Any]]]

List of message conversations, each as a list of message dicts

required
file_path str | None

Path to save the batch request file. If None, returns BytesIO buffer

None
max_tokens int | None

Maximum tokens per request

1000
temperature float | None

Temperature for generation

0.1

Returns:

Type Description
str | BytesIO

The file path where the batch was saved, or BytesIO buffer if file_path is None

Source code in instructor/batch/processor.py
def create_batch_from_messages(
    self,
    messages_list: list[list[dict[str, Any]]],
    file_path: str | None = None,
    max_tokens: int | None = 1000,
    temperature: float | None = 0.1,
) -> str | io.BytesIO:
    """Create batch file from list of message conversations

    Args:
        messages_list: List of message conversations, each as a list of message dicts
        file_path: Path to save the batch request file. If None, returns BytesIO buffer
        max_tokens: Maximum tokens per request
        temperature: Temperature for generation

    Returns:
        The file path where the batch was saved, or BytesIO buffer if file_path is None
    """
    if file_path is not None:
        if os.path.exists(file_path):
            os.remove(file_path)

        batch_requests = []
        for i, messages in enumerate(messages_list):
            batch_request = BatchRequest[T](
                custom_id=f"request-{i}",
                messages=messages,
                response_model=self.response_model,
                model=self.model_name,
                max_tokens=max_tokens,
                temperature=temperature,
            )
            batch_request.save_to_file(file_path, self.provider_name)
            batch_requests.append(batch_request)

        print(f"Created batch file {file_path} with {len(batch_requests)} requests")
        return file_path
    else:
        # Create BytesIO buffer - caller is responsible for cleanup
        buffer = io.BytesIO()
        batch_requests = []
        for i, messages in enumerate(messages_list):
            batch_request = BatchRequest[T](
                custom_id=f"request-{i}",
                messages=messages,
                response_model=self.response_model,
                model=self.model_name,
                max_tokens=max_tokens,
                temperature=temperature,
            )
            batch_request.save_to_file(buffer, self.provider_name)
            batch_requests.append(batch_request)

        print(f"Created batch buffer with {len(batch_requests)} requests")
        buffer.seek(0)  # Reset buffer position for reading
        return buffer

delete_batch(batch_id)

Delete a batch job (only available for completed batches)

Parameters:

Name Type Description Default
batch_id str

The batch job ID to delete

required

Returns:

Type Description
dict[str, Any]

Dict containing the deletion confirmation

Source code in instructor/batch/processor.py
def delete_batch(self, batch_id: str) -> dict[str, Any]:
    """Delete a batch job (only available for completed batches)

    Args:
        batch_id: The batch job ID to delete

    Returns:
        Dict containing the deletion confirmation
    """
    return self.provider.delete_batch(batch_id)

get_batch_status(batch_id)

Get batch job status from the provider

Source code in instructor/batch/processor.py
def get_batch_status(self, batch_id: str) -> dict[str, Any]:
    """Get batch job status from the provider"""
    return self.provider.get_status(batch_id)

get_results(batch_id, file_path=None)

Get batch results, optionally saving raw results to a file

Parameters:

Name Type Description Default
batch_id str

The batch job ID

required
file_path str | None

Optional file path to save raw results. If provided, raw results will be saved to this file. If not provided, results are only kept in memory.

None

Returns:

Type Description
list[BatchResult]

List of BatchResult objects (BatchSuccess[T] or BatchError)

Source code in instructor/batch/processor.py
def get_results(
    self, batch_id: str, file_path: str | None = None
) -> list[BatchResult]:
    """Get batch results, optionally saving raw results to a file

    Args:
        batch_id: The batch job ID
        file_path: Optional file path to save raw results. If provided,
                  raw results will be saved to this file. If not provided,
                  results are only kept in memory.

    Returns:
        List of BatchResult objects (BatchSuccess[T] or BatchError)
    """
    # Retrieve results directly to memory
    results_content = self.retrieve_results(batch_id)

    # If file path is provided, save raw results to file
    if file_path is not None:
        self.provider.download_results(batch_id, file_path)

    return results_content

list_batches(limit=10)

List batch jobs for the current provider

Parameters:

Name Type Description Default
limit int

Maximum number of batch jobs to return

10

Returns:

Type Description
list[BatchJobInfo]

List of BatchJobInfo objects with normalized batch information

Source code in instructor/batch/processor.py
def list_batches(self, limit: int = 10) -> list[BatchJobInfo]:
    """List batch jobs for the current provider

    Args:
        limit: Maximum number of batch jobs to return

    Returns:
        List of BatchJobInfo objects with normalized batch information
    """
    return self.provider.list_batches(limit)

parse_results(results_content)

Parse batch results from content string into Maybe-like results with custom_id tracking

Source code in instructor/batch/processor.py
def parse_results(self, results_content: str) -> list[BatchResult]:
    """Parse batch results from content string into Maybe-like results with custom_id tracking"""
    results: list[BatchResult] = []

    lines = results_content.strip().split("\n")
    for line in lines:
        if not line.strip():
            continue

        try:
            data = json.loads(line)
            custom_id = data.get("custom_id", "unknown")
            extracted_data = self._extract_from_response(data)

            if extracted_data:
                try:
                    # Parse into response model
                    result = self.response_model(**extracted_data)
                    batch_result = BatchSuccess[T](
                        custom_id=custom_id, result=result
                    )
                    results.append(batch_result)
                except Exception as e:
                    error_result = BatchError(
                        custom_id=custom_id,
                        error_type="parsing_error",
                        error_message=f"Failed to parse into {self.response_model.__name__}: {e}",
                        raw_data=extracted_data,
                    )
                    results.append(error_result)
            else:
                # Check if this is a provider error response
                error_message = "Unknown error"
                error_type = "extraction_error"

                if self.provider_name == "anthropic" and "result" in data:
                    result = data["result"]
                    if result.get("type") == "error":
                        error_info = result.get("error", {})
                        if isinstance(error_info, dict) and "error" in error_info:
                            error_details = error_info["error"]
                            error_message = error_details.get(
                                "message", "Unknown Anthropic error"
                            )
                            error_type = error_details.get(
                                "type", "anthropic_error"
                            )
                        else:
                            error_message = str(error_info)
                            error_type = "anthropic_error"

                error_result = BatchError(
                    custom_id=custom_id,
                    error_type=error_type,
                    error_message=error_message,
                    raw_data=data,
                )
                results.append(error_result)

        except Exception as e:
            error_result = BatchError(
                custom_id="unknown",
                error_type="json_parse_error",
                error_message=f"Failed to parse JSON: {e}",
                raw_data={"raw_line": line},
            )
            results.append(error_result)

    return results

retrieve_results(batch_id)

Retrieve and parse batch results from the provider

Source code in instructor/batch/processor.py
def retrieve_results(self, batch_id: str) -> list[BatchResult]:
    """Retrieve and parse batch results from the provider"""
    results_content = self.provider.retrieve_results(batch_id)
    return self.parse_results(results_content)

submit_batch(file_path_or_buffer, metadata=None, **kwargs)

Submit batch job to the provider and return job ID

Parameters:

Name Type Description Default
file_path_or_buffer str | BytesIO

Path to the batch request file or BytesIO buffer

required
metadata dict[str, Any] | None

Optional metadata to attach to the batch job

None
**kwargs

Additional provider-specific arguments

{}
Source code in instructor/batch/processor.py
def submit_batch(
    self,
    file_path_or_buffer: str | io.BytesIO,
    metadata: dict[str, Any] | None = None,
    **kwargs,
) -> str:
    """Submit batch job to the provider and return job ID

    Args:
        file_path_or_buffer: Path to the batch request file or BytesIO buffer
        metadata: Optional metadata to attach to the batch job
        **kwargs: Additional provider-specific arguments
    """
    if metadata is None:
        metadata = {"description": "Instructor batch job"}

    return self.provider.submit_batch(
        file_path_or_buffer, metadata=metadata, **kwargs
    )

BatchRequest

Bases: BaseModel, Generic[T]

Unified batch request that works across all providers using JSON schema

Source code in instructor/batch/request.py
class BatchRequest(BaseModel, Generic[T]):
    """Unified batch request that works across all providers using JSON schema"""

    custom_id: str
    messages: list[dict[str, Any]]
    response_model: type[T]
    model: str
    max_tokens: int | None = Field(default=1000)
    temperature: float | None = Field(default=0.1)

    model_config = ConfigDict(arbitrary_types_allowed=True)

    def get_json_schema(self) -> dict[str, Any]:
        """Generate JSON schema from response_model"""
        return self.response_model.model_json_schema()

    def to_openai_format(self) -> dict[str, Any]:
        """Convert to OpenAI batch format with JSON schema"""
        schema = self.get_json_schema()

        # OpenAI strict mode requires additionalProperties to be false
        def make_strict_schema(schema_dict):
            """Recursively add additionalProperties: false for OpenAI strict mode"""
            if isinstance(schema_dict, dict):
                if "type" in schema_dict:
                    if schema_dict["type"] == "object":
                        schema_dict["additionalProperties"] = False
                    elif schema_dict["type"] == "array" and "items" in schema_dict:
                        schema_dict["items"] = make_strict_schema(schema_dict["items"])

                # Recursively process properties
                if "properties" in schema_dict:
                    for prop_name, prop_schema in schema_dict["properties"].items():
                        schema_dict["properties"][prop_name] = make_strict_schema(
                            prop_schema
                        )

                # Process definitions/defs
                for key in ["definitions", "$defs"]:
                    if key in schema_dict:
                        for def_name, def_schema in schema_dict[key].items():
                            schema_dict[key][def_name] = make_strict_schema(def_schema)

            return schema_dict

        strict_schema = make_strict_schema(schema.copy())

        return {
            "custom_id": self.custom_id,
            "method": "POST",
            "url": "/v1/chat/completions",
            "body": {
                "model": self.model,
                "messages": self.messages,
                "max_tokens": self.max_tokens,
                "temperature": self.temperature,
                "response_format": {
                    "type": "json_schema",
                    "json_schema": {
                        "name": self.response_model.__name__,
                        "strict": True,
                        "schema": strict_schema,
                    },
                },
            },
        }

    def to_anthropic_format(self) -> dict[str, Any]:
        """Convert to Anthropic batch format with JSON schema"""
        schema = self.get_json_schema()

        # Ensure schema has proper format for Anthropic
        if "type" not in schema:
            schema["type"] = "object"
        if "additionalProperties" not in schema:
            schema["additionalProperties"] = False

        # Extract system message and convert to system parameter
        system_message = None
        filtered_messages = []

        for message in self.messages:
            if message.get("role") == "system":
                system_message = message.get("content", "")
            else:
                filtered_messages.append(message)

        params = {
            "model": self.model,
            "max_tokens": self.max_tokens,
            "temperature": self.temperature,
            "messages": filtered_messages,
            "tools": [
                {
                    "name": "extract_data",
                    "description": f"Extract data matching the {self.response_model.__name__} schema",
                    "input_schema": schema,
                }
            ],
            "tool_choice": {"type": "tool", "name": "extract_data"},
        }

        # Add system parameter if system message exists
        if system_message:
            params["system"] = system_message

        return {
            "custom_id": self.custom_id,
            "params": params,
        }

    def save_to_file(
        self, file_path_or_buffer: str | io.BytesIO, provider: str
    ) -> None:
        """Save batch request to file or BytesIO buffer in provider-specific format"""
        if provider == "openai":
            data = self.to_openai_format()
        elif provider == "anthropic":
            data = self.to_anthropic_format()
        else:
            raise ValueError(f"Unsupported provider: {provider}")

        json_line = json.dumps(data) + "\n"

        if isinstance(file_path_or_buffer, str):
            with open(file_path_or_buffer, "a") as f:
                f.write(json_line)
        elif isinstance(file_path_or_buffer, io.BytesIO):
            file_path_or_buffer.write(json_line.encode("utf-8"))
        else:
            raise ValueError(
                f"Unsupported file_path_or_buffer type: {type(file_path_or_buffer)}"
            )

get_json_schema()

Generate JSON schema from response_model

Source code in instructor/batch/request.py
def get_json_schema(self) -> dict[str, Any]:
    """Generate JSON schema from response_model"""
    return self.response_model.model_json_schema()

save_to_file(file_path_or_buffer, provider)

Save batch request to file or BytesIO buffer in provider-specific format

Source code in instructor/batch/request.py
def save_to_file(
    self, file_path_or_buffer: str | io.BytesIO, provider: str
) -> None:
    """Save batch request to file or BytesIO buffer in provider-specific format"""
    if provider == "openai":
        data = self.to_openai_format()
    elif provider == "anthropic":
        data = self.to_anthropic_format()
    else:
        raise ValueError(f"Unsupported provider: {provider}")

    json_line = json.dumps(data) + "\n"

    if isinstance(file_path_or_buffer, str):
        with open(file_path_or_buffer, "a") as f:
            f.write(json_line)
    elif isinstance(file_path_or_buffer, io.BytesIO):
        file_path_or_buffer.write(json_line.encode("utf-8"))
    else:
        raise ValueError(
            f"Unsupported file_path_or_buffer type: {type(file_path_or_buffer)}"
        )

to_anthropic_format()

Convert to Anthropic batch format with JSON schema

Source code in instructor/batch/request.py
def to_anthropic_format(self) -> dict[str, Any]:
    """Convert to Anthropic batch format with JSON schema"""
    schema = self.get_json_schema()

    # Ensure schema has proper format for Anthropic
    if "type" not in schema:
        schema["type"] = "object"
    if "additionalProperties" not in schema:
        schema["additionalProperties"] = False

    # Extract system message and convert to system parameter
    system_message = None
    filtered_messages = []

    for message in self.messages:
        if message.get("role") == "system":
            system_message = message.get("content", "")
        else:
            filtered_messages.append(message)

    params = {
        "model": self.model,
        "max_tokens": self.max_tokens,
        "temperature": self.temperature,
        "messages": filtered_messages,
        "tools": [
            {
                "name": "extract_data",
                "description": f"Extract data matching the {self.response_model.__name__} schema",
                "input_schema": schema,
            }
        ],
        "tool_choice": {"type": "tool", "name": "extract_data"},
    }

    # Add system parameter if system message exists
    if system_message:
        params["system"] = system_message

    return {
        "custom_id": self.custom_id,
        "params": params,
    }

to_openai_format()

Convert to OpenAI batch format with JSON schema

Source code in instructor/batch/request.py
def to_openai_format(self) -> dict[str, Any]:
    """Convert to OpenAI batch format with JSON schema"""
    schema = self.get_json_schema()

    # OpenAI strict mode requires additionalProperties to be false
    def make_strict_schema(schema_dict):
        """Recursively add additionalProperties: false for OpenAI strict mode"""
        if isinstance(schema_dict, dict):
            if "type" in schema_dict:
                if schema_dict["type"] == "object":
                    schema_dict["additionalProperties"] = False
                elif schema_dict["type"] == "array" and "items" in schema_dict:
                    schema_dict["items"] = make_strict_schema(schema_dict["items"])

            # Recursively process properties
            if "properties" in schema_dict:
                for prop_name, prop_schema in schema_dict["properties"].items():
                    schema_dict["properties"][prop_name] = make_strict_schema(
                        prop_schema
                    )

            # Process definitions/defs
            for key in ["definitions", "$defs"]:
                if key in schema_dict:
                    for def_name, def_schema in schema_dict[key].items():
                        schema_dict[key][def_name] = make_strict_schema(def_schema)

        return schema_dict

    strict_schema = make_strict_schema(schema.copy())

    return {
        "custom_id": self.custom_id,
        "method": "POST",
        "url": "/v1/chat/completions",
        "body": {
            "model": self.model,
            "messages": self.messages,
            "max_tokens": self.max_tokens,
            "temperature": self.temperature,
            "response_format": {
                "type": "json_schema",
                "json_schema": {
                    "name": self.response_model.__name__,
                    "strict": True,
                    "schema": strict_schema,
                },
            },
        },
    }

BatchRequestCounts

Bases: BaseModel

Unified request counts across providers

Source code in instructor/batch/models.py
class BatchRequestCounts(BaseModel):
    """Unified request counts across providers"""

    total: int | None = None

    # OpenAI fields
    completed: int | None = None
    failed: int | None = None

    # Anthropic fields
    processing: int | None = None
    succeeded: int | None = None
    errored: int | None = None
    cancelled: int | None = None
    expired: int | None = None

BatchStatus

Bases: str, Enum

Normalized batch status across providers

Source code in instructor/batch/models.py
class BatchStatus(str, Enum):
    """Normalized batch status across providers"""

    PENDING = "pending"
    PROCESSING = "processing"
    COMPLETED = "completed"
    FAILED = "failed"
    CANCELLED = "cancelled"
    EXPIRED = "expired"

BatchSuccess

Bases: BaseModel, Generic[T]

Successful batch result with custom_id

Source code in instructor/batch/models.py
class BatchSuccess(BaseModel, Generic[T]):
    """Successful batch result with custom_id"""

    custom_id: str
    result: T
    success: bool = True

    model_config = ConfigDict(arbitrary_types_allowed=True)

BatchTimestamps

Bases: BaseModel

Comprehensive timestamp tracking

Source code in instructor/batch/models.py
class BatchTimestamps(BaseModel):
    """Comprehensive timestamp tracking"""

    created_at: datetime | None = None
    started_at: datetime | None = None  # in_progress_at, processing start
    completed_at: datetime | None = None  # completed_at, ended_at
    failed_at: datetime | None = None
    cancelled_at: datetime | None = None
    expired_at: datetime | None = None
    expires_at: datetime | None = None

extract_results(results)

Extract just the result objects from successful results

Source code in instructor/batch/utils.py
def extract_results(results: list[BatchResult]) -> list[T]:
    """Extract just the result objects from successful results"""
    return [cast(BatchSuccess[T], r).result for r in results if r.success]

filter_errors(results)

Filter to only error results

Source code in instructor/batch/utils.py
def filter_errors(results: list[BatchResult]) -> list[BatchError]:
    """Filter to only error results"""
    return cast(list[BatchError], [r for r in results if not r.success])

filter_successful(results)

Filter to only successful results

Source code in instructor/batch/utils.py
def filter_successful(results: list[BatchResult]) -> list[BatchSuccess[T]]:
    """Filter to only successful results"""
    return cast(list[BatchSuccess[T]], [r for r in results if r.success])

get_results_by_custom_id(results)

Create a dictionary mapping custom_id to results

Source code in instructor/batch/utils.py
def get_results_by_custom_id(results: list[BatchResult]) -> dict[str, BatchResult]:
    """Create a dictionary mapping custom_id to results"""
    return {r.custom_id: r for r in results}

Bases: Generic[T]

Unified batch processor that works across all providers

Source code in instructor/batch/processor.py
class BatchProcessor(Generic[T]):
    """Unified batch processor that works across all providers"""

    def __init__(self, model: str, response_model: type[T]):
        self.model = model
        self.response_model = response_model

        # Parse provider from model string
        try:
            self.provider_name, self.model_name = model.split("/", 1)
        except ValueError as err:
            raise ValueError(
                'Model string must be in format "provider/model-name" '
                '(e.g. "openai/gpt-4" or "anthropic/claude-3-sonnet")'
            ) from err

        # Get the batch provider instance
        self.provider = get_provider(self.provider_name)

    def create_batch_from_messages(
        self,
        messages_list: list[list[dict[str, Any]]],
        file_path: str | None = None,
        max_tokens: int | None = 1000,
        temperature: float | None = 0.1,
    ) -> str | io.BytesIO:
        """Create batch file from list of message conversations

        Args:
            messages_list: List of message conversations, each as a list of message dicts
            file_path: Path to save the batch request file. If None, returns BytesIO buffer
            max_tokens: Maximum tokens per request
            temperature: Temperature for generation

        Returns:
            The file path where the batch was saved, or BytesIO buffer if file_path is None
        """
        if file_path is not None:
            if os.path.exists(file_path):
                os.remove(file_path)

            batch_requests = []
            for i, messages in enumerate(messages_list):
                batch_request = BatchRequest[T](
                    custom_id=f"request-{i}",
                    messages=messages,
                    response_model=self.response_model,
                    model=self.model_name,
                    max_tokens=max_tokens,
                    temperature=temperature,
                )
                batch_request.save_to_file(file_path, self.provider_name)
                batch_requests.append(batch_request)

            print(f"Created batch file {file_path} with {len(batch_requests)} requests")
            return file_path
        else:
            # Create BytesIO buffer - caller is responsible for cleanup
            buffer = io.BytesIO()
            batch_requests = []
            for i, messages in enumerate(messages_list):
                batch_request = BatchRequest[T](
                    custom_id=f"request-{i}",
                    messages=messages,
                    response_model=self.response_model,
                    model=self.model_name,
                    max_tokens=max_tokens,
                    temperature=temperature,
                )
                batch_request.save_to_file(buffer, self.provider_name)
                batch_requests.append(batch_request)

            print(f"Created batch buffer with {len(batch_requests)} requests")
            buffer.seek(0)  # Reset buffer position for reading
            return buffer

    def submit_batch(
        self,
        file_path_or_buffer: str | io.BytesIO,
        metadata: dict[str, Any] | None = None,
        **kwargs,
    ) -> str:
        """Submit batch job to the provider and return job ID

        Args:
            file_path_or_buffer: Path to the batch request file or BytesIO buffer
            metadata: Optional metadata to attach to the batch job
            **kwargs: Additional provider-specific arguments
        """
        if metadata is None:
            metadata = {"description": "Instructor batch job"}

        return self.provider.submit_batch(
            file_path_or_buffer, metadata=metadata, **kwargs
        )

    def get_batch_status(self, batch_id: str) -> dict[str, Any]:
        """Get batch job status from the provider"""
        return self.provider.get_status(batch_id)

    def retrieve_results(self, batch_id: str) -> list[BatchResult]:
        """Retrieve and parse batch results from the provider"""
        results_content = self.provider.retrieve_results(batch_id)
        return self.parse_results(results_content)

    def list_batches(self, limit: int = 10) -> list[BatchJobInfo]:
        """List batch jobs for the current provider

        Args:
            limit: Maximum number of batch jobs to return

        Returns:
            List of BatchJobInfo objects with normalized batch information
        """
        return self.provider.list_batches(limit)

    def get_results(
        self, batch_id: str, file_path: str | None = None
    ) -> list[BatchResult]:
        """Get batch results, optionally saving raw results to a file

        Args:
            batch_id: The batch job ID
            file_path: Optional file path to save raw results. If provided,
                      raw results will be saved to this file. If not provided,
                      results are only kept in memory.

        Returns:
            List of BatchResult objects (BatchSuccess[T] or BatchError)
        """
        # Retrieve results directly to memory
        results_content = self.retrieve_results(batch_id)

        # If file path is provided, save raw results to file
        if file_path is not None:
            self.provider.download_results(batch_id, file_path)

        return results_content

    def cancel_batch(self, batch_id: str) -> dict[str, Any]:
        """Cancel a batch job

        Args:
            batch_id: The batch job ID to cancel

        Returns:
            Dict containing the cancelled batch information
        """
        return self.provider.cancel_batch(batch_id)

    def delete_batch(self, batch_id: str) -> dict[str, Any]:
        """Delete a batch job (only available for completed batches)

        Args:
            batch_id: The batch job ID to delete

        Returns:
            Dict containing the deletion confirmation
        """
        return self.provider.delete_batch(batch_id)

    def parse_results(self, results_content: str) -> list[BatchResult]:
        """Parse batch results from content string into Maybe-like results with custom_id tracking"""
        results: list[BatchResult] = []

        lines = results_content.strip().split("\n")
        for line in lines:
            if not line.strip():
                continue

            try:
                data = json.loads(line)
                custom_id = data.get("custom_id", "unknown")
                extracted_data = self._extract_from_response(data)

                if extracted_data:
                    try:
                        # Parse into response model
                        result = self.response_model(**extracted_data)
                        batch_result = BatchSuccess[T](
                            custom_id=custom_id, result=result
                        )
                        results.append(batch_result)
                    except Exception as e:
                        error_result = BatchError(
                            custom_id=custom_id,
                            error_type="parsing_error",
                            error_message=f"Failed to parse into {self.response_model.__name__}: {e}",
                            raw_data=extracted_data,
                        )
                        results.append(error_result)
                else:
                    # Check if this is a provider error response
                    error_message = "Unknown error"
                    error_type = "extraction_error"

                    if self.provider_name == "anthropic" and "result" in data:
                        result = data["result"]
                        if result.get("type") == "error":
                            error_info = result.get("error", {})
                            if isinstance(error_info, dict) and "error" in error_info:
                                error_details = error_info["error"]
                                error_message = error_details.get(
                                    "message", "Unknown Anthropic error"
                                )
                                error_type = error_details.get(
                                    "type", "anthropic_error"
                                )
                            else:
                                error_message = str(error_info)
                                error_type = "anthropic_error"

                    error_result = BatchError(
                        custom_id=custom_id,
                        error_type=error_type,
                        error_message=error_message,
                        raw_data=data,
                    )
                    results.append(error_result)

            except Exception as e:
                error_result = BatchError(
                    custom_id="unknown",
                    error_type="json_parse_error",
                    error_message=f"Failed to parse JSON: {e}",
                    raw_data={"raw_line": line},
                )
                results.append(error_result)

        return results

    def _extract_from_response(self, data: dict[str, Any]) -> dict[str, Any] | None:
        """Extract structured data from provider-specific response format"""
        try:
            if self.provider_name == "openai":
                # OpenAI JSON schema response
                content = data["response"]["body"]["choices"][0]["message"]["content"]
                return json.loads(content)

            elif self.provider_name == "anthropic":
                # Anthropic batch response format
                if "result" not in data:
                    return None

                result = data["result"]

                # Check if result is an error
                if result.get("type") == "error":
                    # Return None to indicate error, let caller handle
                    return None

                # Handle successful message result
                if result.get("type") == "succeeded" and "message" in result:
                    content = result["message"]["content"]
                    if isinstance(content, list) and len(content) > 0:
                        # Try tool_use first
                        for item in content:
                            if item.get("type") == "tool_use":
                                return item.get("input", {})

                        # Fallback to text content and parse JSON
                        for item in content:
                            if item.get("type") == "text":
                                text = item.get("text", "")
                                try:
                                    return json.loads(text)
                                except json.JSONDecodeError:
                                    continue

                return None

        except Exception:
            return None

        return None

cancel_batch(batch_id)

Cancel a batch job

Parameters:

Name Type Description Default
batch_id str

The batch job ID to cancel

required

Returns:

Type Description
dict[str, Any]

Dict containing the cancelled batch information

Source code in instructor/batch/processor.py
def cancel_batch(self, batch_id: str) -> dict[str, Any]:
    """Cancel a batch job

    Args:
        batch_id: The batch job ID to cancel

    Returns:
        Dict containing the cancelled batch information
    """
    return self.provider.cancel_batch(batch_id)

create_batch_from_messages(messages_list, file_path=None, max_tokens=1000, temperature=0.1)

Create batch file from list of message conversations

Parameters:

Name Type Description Default
messages_list list[list[dict[str, Any]]]

List of message conversations, each as a list of message dicts

required
file_path str | None

Path to save the batch request file. If None, returns BytesIO buffer

None
max_tokens int | None

Maximum tokens per request

1000
temperature float | None

Temperature for generation

0.1

Returns:

Type Description
str | BytesIO

The file path where the batch was saved, or BytesIO buffer if file_path is None

Source code in instructor/batch/processor.py
def create_batch_from_messages(
    self,
    messages_list: list[list[dict[str, Any]]],
    file_path: str | None = None,
    max_tokens: int | None = 1000,
    temperature: float | None = 0.1,
) -> str | io.BytesIO:
    """Create batch file from list of message conversations

    Args:
        messages_list: List of message conversations, each as a list of message dicts
        file_path: Path to save the batch request file. If None, returns BytesIO buffer
        max_tokens: Maximum tokens per request
        temperature: Temperature for generation

    Returns:
        The file path where the batch was saved, or BytesIO buffer if file_path is None
    """
    if file_path is not None:
        if os.path.exists(file_path):
            os.remove(file_path)

        batch_requests = []
        for i, messages in enumerate(messages_list):
            batch_request = BatchRequest[T](
                custom_id=f"request-{i}",
                messages=messages,
                response_model=self.response_model,
                model=self.model_name,
                max_tokens=max_tokens,
                temperature=temperature,
            )
            batch_request.save_to_file(file_path, self.provider_name)
            batch_requests.append(batch_request)

        print(f"Created batch file {file_path} with {len(batch_requests)} requests")
        return file_path
    else:
        # Create BytesIO buffer - caller is responsible for cleanup
        buffer = io.BytesIO()
        batch_requests = []
        for i, messages in enumerate(messages_list):
            batch_request = BatchRequest[T](
                custom_id=f"request-{i}",
                messages=messages,
                response_model=self.response_model,
                model=self.model_name,
                max_tokens=max_tokens,
                temperature=temperature,
            )
            batch_request.save_to_file(buffer, self.provider_name)
            batch_requests.append(batch_request)

        print(f"Created batch buffer with {len(batch_requests)} requests")
        buffer.seek(0)  # Reset buffer position for reading
        return buffer

delete_batch(batch_id)

Delete a batch job (only available for completed batches)

Parameters:

Name Type Description Default
batch_id str

The batch job ID to delete

required

Returns:

Type Description
dict[str, Any]

Dict containing the deletion confirmation

Source code in instructor/batch/processor.py
def delete_batch(self, batch_id: str) -> dict[str, Any]:
    """Delete a batch job (only available for completed batches)

    Args:
        batch_id: The batch job ID to delete

    Returns:
        Dict containing the deletion confirmation
    """
    return self.provider.delete_batch(batch_id)

get_batch_status(batch_id)

Get batch job status from the provider

Source code in instructor/batch/processor.py
def get_batch_status(self, batch_id: str) -> dict[str, Any]:
    """Get batch job status from the provider"""
    return self.provider.get_status(batch_id)

get_results(batch_id, file_path=None)

Get batch results, optionally saving raw results to a file

Parameters:

Name Type Description Default
batch_id str

The batch job ID

required
file_path str | None

Optional file path to save raw results. If provided, raw results will be saved to this file. If not provided, results are only kept in memory.

None

Returns:

Type Description
list[BatchResult]

List of BatchResult objects (BatchSuccess[T] or BatchError)

Source code in instructor/batch/processor.py
def get_results(
    self, batch_id: str, file_path: str | None = None
) -> list[BatchResult]:
    """Get batch results, optionally saving raw results to a file

    Args:
        batch_id: The batch job ID
        file_path: Optional file path to save raw results. If provided,
                  raw results will be saved to this file. If not provided,
                  results are only kept in memory.

    Returns:
        List of BatchResult objects (BatchSuccess[T] or BatchError)
    """
    # Retrieve results directly to memory
    results_content = self.retrieve_results(batch_id)

    # If file path is provided, save raw results to file
    if file_path is not None:
        self.provider.download_results(batch_id, file_path)

    return results_content

list_batches(limit=10)

List batch jobs for the current provider

Parameters:

Name Type Description Default
limit int

Maximum number of batch jobs to return

10

Returns:

Type Description
list[BatchJobInfo]

List of BatchJobInfo objects with normalized batch information

Source code in instructor/batch/processor.py
def list_batches(self, limit: int = 10) -> list[BatchJobInfo]:
    """List batch jobs for the current provider

    Args:
        limit: Maximum number of batch jobs to return

    Returns:
        List of BatchJobInfo objects with normalized batch information
    """
    return self.provider.list_batches(limit)

parse_results(results_content)

Parse batch results from content string into Maybe-like results with custom_id tracking

Source code in instructor/batch/processor.py
def parse_results(self, results_content: str) -> list[BatchResult]:
    """Parse batch results from content string into Maybe-like results with custom_id tracking"""
    results: list[BatchResult] = []

    lines = results_content.strip().split("\n")
    for line in lines:
        if not line.strip():
            continue

        try:
            data = json.loads(line)
            custom_id = data.get("custom_id", "unknown")
            extracted_data = self._extract_from_response(data)

            if extracted_data:
                try:
                    # Parse into response model
                    result = self.response_model(**extracted_data)
                    batch_result = BatchSuccess[T](
                        custom_id=custom_id, result=result
                    )
                    results.append(batch_result)
                except Exception as e:
                    error_result = BatchError(
                        custom_id=custom_id,
                        error_type="parsing_error",
                        error_message=f"Failed to parse into {self.response_model.__name__}: {e}",
                        raw_data=extracted_data,
                    )
                    results.append(error_result)
            else:
                # Check if this is a provider error response
                error_message = "Unknown error"
                error_type = "extraction_error"

                if self.provider_name == "anthropic" and "result" in data:
                    result = data["result"]
                    if result.get("type") == "error":
                        error_info = result.get("error", {})
                        if isinstance(error_info, dict) and "error" in error_info:
                            error_details = error_info["error"]
                            error_message = error_details.get(
                                "message", "Unknown Anthropic error"
                            )
                            error_type = error_details.get(
                                "type", "anthropic_error"
                            )
                        else:
                            error_message = str(error_info)
                            error_type = "anthropic_error"

                error_result = BatchError(
                    custom_id=custom_id,
                    error_type=error_type,
                    error_message=error_message,
                    raw_data=data,
                )
                results.append(error_result)

        except Exception as e:
            error_result = BatchError(
                custom_id="unknown",
                error_type="json_parse_error",
                error_message=f"Failed to parse JSON: {e}",
                raw_data={"raw_line": line},
            )
            results.append(error_result)

    return results

retrieve_results(batch_id)

Retrieve and parse batch results from the provider

Source code in instructor/batch/processor.py
def retrieve_results(self, batch_id: str) -> list[BatchResult]:
    """Retrieve and parse batch results from the provider"""
    results_content = self.provider.retrieve_results(batch_id)
    return self.parse_results(results_content)

submit_batch(file_path_or_buffer, metadata=None, **kwargs)

Submit batch job to the provider and return job ID

Parameters:

Name Type Description Default
file_path_or_buffer str | BytesIO

Path to the batch request file or BytesIO buffer

required
metadata dict[str, Any] | None

Optional metadata to attach to the batch job

None
**kwargs

Additional provider-specific arguments

{}
Source code in instructor/batch/processor.py
def submit_batch(
    self,
    file_path_or_buffer: str | io.BytesIO,
    metadata: dict[str, Any] | None = None,
    **kwargs,
) -> str:
    """Submit batch job to the provider and return job ID

    Args:
        file_path_or_buffer: Path to the batch request file or BytesIO buffer
        metadata: Optional metadata to attach to the batch job
        **kwargs: Additional provider-specific arguments
    """
    if metadata is None:
        metadata = {"description": "Instructor batch job"}

    return self.provider.submit_batch(
        file_path_or_buffer, metadata=metadata, **kwargs
    )

Bases: BaseModel, Generic[T]

Unified batch request that works across all providers using JSON schema

Source code in instructor/batch/request.py
class BatchRequest(BaseModel, Generic[T]):
    """Unified batch request that works across all providers using JSON schema"""

    custom_id: str
    messages: list[dict[str, Any]]
    response_model: type[T]
    model: str
    max_tokens: int | None = Field(default=1000)
    temperature: float | None = Field(default=0.1)

    model_config = ConfigDict(arbitrary_types_allowed=True)

    def get_json_schema(self) -> dict[str, Any]:
        """Generate JSON schema from response_model"""
        return self.response_model.model_json_schema()

    def to_openai_format(self) -> dict[str, Any]:
        """Convert to OpenAI batch format with JSON schema"""
        schema = self.get_json_schema()

        # OpenAI strict mode requires additionalProperties to be false
        def make_strict_schema(schema_dict):
            """Recursively add additionalProperties: false for OpenAI strict mode"""
            if isinstance(schema_dict, dict):
                if "type" in schema_dict:
                    if schema_dict["type"] == "object":
                        schema_dict["additionalProperties"] = False
                    elif schema_dict["type"] == "array" and "items" in schema_dict:
                        schema_dict["items"] = make_strict_schema(schema_dict["items"])

                # Recursively process properties
                if "properties" in schema_dict:
                    for prop_name, prop_schema in schema_dict["properties"].items():
                        schema_dict["properties"][prop_name] = make_strict_schema(
                            prop_schema
                        )

                # Process definitions/defs
                for key in ["definitions", "$defs"]:
                    if key in schema_dict:
                        for def_name, def_schema in schema_dict[key].items():
                            schema_dict[key][def_name] = make_strict_schema(def_schema)

            return schema_dict

        strict_schema = make_strict_schema(schema.copy())

        return {
            "custom_id": self.custom_id,
            "method": "POST",
            "url": "/v1/chat/completions",
            "body": {
                "model": self.model,
                "messages": self.messages,
                "max_tokens": self.max_tokens,
                "temperature": self.temperature,
                "response_format": {
                    "type": "json_schema",
                    "json_schema": {
                        "name": self.response_model.__name__,
                        "strict": True,
                        "schema": strict_schema,
                    },
                },
            },
        }

    def to_anthropic_format(self) -> dict[str, Any]:
        """Convert to Anthropic batch format with JSON schema"""
        schema = self.get_json_schema()

        # Ensure schema has proper format for Anthropic
        if "type" not in schema:
            schema["type"] = "object"
        if "additionalProperties" not in schema:
            schema["additionalProperties"] = False

        # Extract system message and convert to system parameter
        system_message = None
        filtered_messages = []

        for message in self.messages:
            if message.get("role") == "system":
                system_message = message.get("content", "")
            else:
                filtered_messages.append(message)

        params = {
            "model": self.model,
            "max_tokens": self.max_tokens,
            "temperature": self.temperature,
            "messages": filtered_messages,
            "tools": [
                {
                    "name": "extract_data",
                    "description": f"Extract data matching the {self.response_model.__name__} schema",
                    "input_schema": schema,
                }
            ],
            "tool_choice": {"type": "tool", "name": "extract_data"},
        }

        # Add system parameter if system message exists
        if system_message:
            params["system"] = system_message

        return {
            "custom_id": self.custom_id,
            "params": params,
        }

    def save_to_file(
        self, file_path_or_buffer: str | io.BytesIO, provider: str
    ) -> None:
        """Save batch request to file or BytesIO buffer in provider-specific format"""
        if provider == "openai":
            data = self.to_openai_format()
        elif provider == "anthropic":
            data = self.to_anthropic_format()
        else:
            raise ValueError(f"Unsupported provider: {provider}")

        json_line = json.dumps(data) + "\n"

        if isinstance(file_path_or_buffer, str):
            with open(file_path_or_buffer, "a") as f:
                f.write(json_line)
        elif isinstance(file_path_or_buffer, io.BytesIO):
            file_path_or_buffer.write(json_line.encode("utf-8"))
        else:
            raise ValueError(
                f"Unsupported file_path_or_buffer type: {type(file_path_or_buffer)}"
            )

get_json_schema()

Generate JSON schema from response_model

Source code in instructor/batch/request.py
def get_json_schema(self) -> dict[str, Any]:
    """Generate JSON schema from response_model"""
    return self.response_model.model_json_schema()

save_to_file(file_path_or_buffer, provider)

Save batch request to file or BytesIO buffer in provider-specific format

Source code in instructor/batch/request.py
def save_to_file(
    self, file_path_or_buffer: str | io.BytesIO, provider: str
) -> None:
    """Save batch request to file or BytesIO buffer in provider-specific format"""
    if provider == "openai":
        data = self.to_openai_format()
    elif provider == "anthropic":
        data = self.to_anthropic_format()
    else:
        raise ValueError(f"Unsupported provider: {provider}")

    json_line = json.dumps(data) + "\n"

    if isinstance(file_path_or_buffer, str):
        with open(file_path_or_buffer, "a") as f:
            f.write(json_line)
    elif isinstance(file_path_or_buffer, io.BytesIO):
        file_path_or_buffer.write(json_line.encode("utf-8"))
    else:
        raise ValueError(
            f"Unsupported file_path_or_buffer type: {type(file_path_or_buffer)}"
        )

to_anthropic_format()

Convert to Anthropic batch format with JSON schema

Source code in instructor/batch/request.py
def to_anthropic_format(self) -> dict[str, Any]:
    """Convert to Anthropic batch format with JSON schema"""
    schema = self.get_json_schema()

    # Ensure schema has proper format for Anthropic
    if "type" not in schema:
        schema["type"] = "object"
    if "additionalProperties" not in schema:
        schema["additionalProperties"] = False

    # Extract system message and convert to system parameter
    system_message = None
    filtered_messages = []

    for message in self.messages:
        if message.get("role") == "system":
            system_message = message.get("content", "")
        else:
            filtered_messages.append(message)

    params = {
        "model": self.model,
        "max_tokens": self.max_tokens,
        "temperature": self.temperature,
        "messages": filtered_messages,
        "tools": [
            {
                "name": "extract_data",
                "description": f"Extract data matching the {self.response_model.__name__} schema",
                "input_schema": schema,
            }
        ],
        "tool_choice": {"type": "tool", "name": "extract_data"},
    }

    # Add system parameter if system message exists
    if system_message:
        params["system"] = system_message

    return {
        "custom_id": self.custom_id,
        "params": params,
    }

to_openai_format()

Convert to OpenAI batch format with JSON schema

Source code in instructor/batch/request.py
def to_openai_format(self) -> dict[str, Any]:
    """Convert to OpenAI batch format with JSON schema"""
    schema = self.get_json_schema()

    # OpenAI strict mode requires additionalProperties to be false
    def make_strict_schema(schema_dict):
        """Recursively add additionalProperties: false for OpenAI strict mode"""
        if isinstance(schema_dict, dict):
            if "type" in schema_dict:
                if schema_dict["type"] == "object":
                    schema_dict["additionalProperties"] = False
                elif schema_dict["type"] == "array" and "items" in schema_dict:
                    schema_dict["items"] = make_strict_schema(schema_dict["items"])

            # Recursively process properties
            if "properties" in schema_dict:
                for prop_name, prop_schema in schema_dict["properties"].items():
                    schema_dict["properties"][prop_name] = make_strict_schema(
                        prop_schema
                    )

            # Process definitions/defs
            for key in ["definitions", "$defs"]:
                if key in schema_dict:
                    for def_name, def_schema in schema_dict[key].items():
                        schema_dict[key][def_name] = make_strict_schema(def_schema)

        return schema_dict

    strict_schema = make_strict_schema(schema.copy())

    return {
        "custom_id": self.custom_id,
        "method": "POST",
        "url": "/v1/chat/completions",
        "body": {
            "model": self.model,
            "messages": self.messages,
            "max_tokens": self.max_tokens,
            "temperature": self.temperature,
            "response_format": {
                "type": "json_schema",
                "json_schema": {
                    "name": self.response_model.__name__,
                    "strict": True,
                    "schema": strict_schema,
                },
            },
        },
    }

Legacy BatchJob class for backward compatibility

Source code in instructor/batch/__init__.py
class BatchJob:
    """Legacy BatchJob class for backward compatibility"""

    @classmethod
    def parse_from_file(
        cls, file_path: str, response_model: type[T]
    ) -> tuple[list[T], list[dict[Any, Any]]]:
        with open(file_path) as file:
            content = file.read()
        return cls.parse_from_string(content, response_model)

    @classmethod
    def parse_from_string(
        cls, content: str, response_model: type[T]
    ) -> tuple[list[T], list[dict[Any, Any]]]:
        """Enhanced parser that works with all providers using JSON schema"""
        import json

        res: list[T] = []
        error_objs: list[dict[Any, Any]] = []

        lines = content.strip().split("\n")
        for line in lines:
            if not line.strip():
                continue

            try:
                data = json.loads(line)
                extracted_data = cls._extract_structured_data(data)

                if extracted_data:
                    try:
                        result = response_model(**extracted_data)
                        res.append(result)
                    except Exception:
                        error_objs.append(data)
                else:
                    error_objs.append(data)

            except Exception:
                error_objs.append({"error": "Failed to parse JSON", "raw_line": line})

        return res, error_objs

    @classmethod
    def _extract_structured_data(cls, data: dict[str, Any]) -> Optional[dict[str, Any]]:
        """Extract structured data from various provider response formats"""
        import json

        try:
            # Try OpenAI JSON schema format first
            if "response" in data and "body" in data["response"]:
                choices = data["response"]["body"].get("choices", [])
                if choices:
                    message = choices[0].get("message", {})

                    # JSON schema response
                    if "content" in message:
                        content = message["content"]
                        if isinstance(content, str):
                            return json.loads(content)

                    # Tool calls (legacy)
                    if "tool_calls" in message:
                        tool_call = message["tool_calls"][0]
                        return json.loads(tool_call["function"]["arguments"])

            # Try Anthropic format
            if "result" in data and "message" in data["result"]:
                content = data["result"]["message"]["content"]
                if isinstance(content, list) and len(content) > 0:
                    # Tool use response
                    for item in content:
                        if item.get("type") == "tool_use":
                            return item.get("input", {})
                    # Text response with JSON
                    for item in content:
                        if item.get("type") == "text":
                            text = item.get("text", "")
                            return json.loads(text)

        except Exception:
            pass

        return None

parse_from_string(content, response_model) classmethod

Enhanced parser that works with all providers using JSON schema

Source code in instructor/batch/__init__.py
@classmethod
def parse_from_string(
    cls, content: str, response_model: type[T]
) -> tuple[list[T], list[dict[Any, Any]]]:
    """Enhanced parser that works with all providers using JSON schema"""
    import json

    res: list[T] = []
    error_objs: list[dict[Any, Any]] = []

    lines = content.strip().split("\n")
    for line in lines:
        if not line.strip():
            continue

        try:
            data = json.loads(line)
            extracted_data = cls._extract_structured_data(data)

            if extracted_data:
                try:
                    result = response_model(**extracted_data)
                    res.append(result)
                except Exception:
                    error_objs.append(data)
            else:
                error_objs.append(data)

        except Exception:
            error_objs.append({"error": "Failed to parse JSON", "raw_line": line})

    return res, error_objs

Distillation

Tools for distillation and fine-tuning workflows.

Instructions

Source code in instructor/distil.py
class Instructions:
    def __init__(
        self,
        name: Optional[str] = None,
        id: Optional[str] = None,
        log_handlers: Optional[list[logging.Handler]] = None,
        finetune_format: FinetuneFormat = FinetuneFormat.MESSAGES,
        indent: int = 2,
        include_code_body: bool = False,
        openai_client: Optional[OpenAI] = None,
    ) -> None:
        """
        Instructions for distillation and dispatch.

        :param name: Name of the instructions.
        :param id: ID of the instructions.
        :param log_handlers: List of log handlers to use.
        :param finetune_format: Format to use for finetuning.
        :param indent: Indentation to use for finetuning.
        :param include_code_body: Whether to include the code body in the finetuning.
        """
        self.name = name
        self.id = id or str(uuid.uuid4())
        self.unique_id = str(uuid.uuid4())
        self.finetune_format = finetune_format
        self.indent = indent
        self.include_code_body = include_code_body
        self.client = openai_client or OpenAI()

        self.logger = logging.getLogger(self.name)
        for handler in log_handlers or []:
            self.logger.addHandler(handler)

    def distil(
        self,
        *args: Any,
        name: Optional[str] = None,
        mode: Literal["distil", "dispatch"] = "distil",
        model: str = "gpt-3.5-turbo",
        fine_tune_format: Optional[FinetuneFormat] = None,
    ) -> Union[
        Callable[P, Union[T_Retval, ChatCompletion]],
        Callable[[Callable[P, T_Retval]], Callable[P, Union[T_Retval, ChatCompletion]]],
    ]:
        """
        Decorator to track the function call and response, supports distillation and dispatch modes.

        If used without arguments, it must be used as a decorator.

        :Example:

        >>> @distil
        >>> def my_function() -> MyModel:
        >>>     return MyModel()
        >>>
        >>> @distil(name="my_function")
        >>> def my_function() -> MyModel:
        >>>     return MyModel()

        :param fn: Function to track.
        :param name: Name of the function to track. Defaults to the function name.
        :param mode: Mode to use for distillation. Defaults to "distil".
        """
        allowed_modes = {"distil", "dispatch"}
        assert mode in allowed_modes, f"Must be in {allowed_modes}"

        if fine_tune_format is None:
            fine_tune_format = self.finetune_format

        def _wrap_distil(
            fn: Callable[P, T_Retval],
        ) -> Callable[P, Union[T_Retval, ChatCompletion]]:
            msg = f"Return type hint for {fn} must subclass `pydantic.BaseModel'"
            assert is_return_type_base_model_or_instance(fn), msg
            return_base_model = inspect.signature(fn).return_annotation

            @functools.wraps(fn)
            def _dispatch(*args: P.args, **kwargs: P.kwargs) -> ChatCompletion:
                openai_kwargs = self.openai_kwargs(
                    name=name if name else fn.__name__,  # type: ignore
                    fn=fn,
                    args=args,
                    kwargs=kwargs,
                    base_model=return_base_model,
                )
                return self.client.chat.completions.create(
                    **openai_kwargs,
                    model=model,
                    response_model=return_base_model,  # type: ignore - TODO figure out why `response_model` is not recognized
                )

            @functools.wraps(fn)
            def _distil(*args: P.args, **kwargs: P.kwargs) -> T_Retval:
                resp = fn(*args, **kwargs)
                self.track(
                    fn,
                    args,
                    kwargs,
                    resp,
                    name=name,
                    finetune_format=fine_tune_format,
                )
                return resp

            return _dispatch if mode == "dispatch" else _distil

        if len(args) == 1 and callable(args[0]):
            return _wrap_distil(args[0])  # type: ignore

        return _wrap_distil

    @validate_call
    def track(
        self,
        fn: Callable[..., Any],
        args: tuple[Any, ...],
        kwargs: dict[str, Any],
        resp: BaseModel,
        name: Optional[str] = None,
        finetune_format: FinetuneFormat = FinetuneFormat.MESSAGES,
    ) -> None:
        """
        Track the function call and response in a log file, later used for finetuning.

        :param fn: Function to track.
        :param args: Arguments passed to the function.
        :param kwargs: Keyword arguments passed to the function.
        :param resp: Response returned by the function.
        :param name: Name of the function to track. Defaults to the function name.
        :param finetune_format: Format to use for finetuning. Defaults to "raw".
        """
        name = name if name else fn.__name__  # type: ignore
        base_model = type(resp)

        if finetune_format == FinetuneFormat.MESSAGES:
            openai_function_call = response_schema(base_model).openai_schema
            openai_kwargs = self.openai_kwargs(name, fn, args, kwargs, base_model)
            openai_kwargs["messages"].append(
                {
                    "role": "assistant",
                    "function_call": {
                        "name": base_model.__name__,
                        "arguments": resp.model_dump_json(indent=self.indent),
                    },
                }
            )
            openai_kwargs["functions"] = [openai_function_call]
            self.logger.info(json.dumps(openai_kwargs))

        if finetune_format == FinetuneFormat.RAW:
            function_body = dict(
                fn_name=name,
                fn_repr=format_function(fn),
                args=args,
                kwargs=kwargs,
                resp=resp.model_dump(),
                schema=base_model.model_json_schema(),
            )
            self.logger.info(json.dumps(function_body))

    def openai_kwargs(
        self,
        name: str,
        fn: Callable[..., Any],
        args: tuple[Any, ...],
        kwargs: dict[str, Any],
        base_model: type[BaseModel],
    ) -> OpenAIChatKwargs:
        if self.include_code_body:
            func_def = format_function(fn)
        else:
            func_def = get_signature_from_fn(fn)

        str_args = ", ".join(map(str, args))
        str_kwargs = (
            ", ".join(f"{k}={json.dumps(v)}" for k, v in kwargs.items()) or None
        )
        call_args = ", ".join(filter(None, [str_args, str_kwargs]))

        function_body: OpenAIChatKwargs = {
            "messages": [
                {
                    "role": "system",
                    "content": f"Predict the results of this function:\n\n{func_def}",
                },
                {
                    "role": "user",
                    "content": f"Return `{name}({call_args})`",
                },
            ],
        }
        return function_body

__init__(name=None, id=None, log_handlers=None, finetune_format=FinetuneFormat.MESSAGES, indent=2, include_code_body=False, openai_client=None)

Instructions for distillation and dispatch.

:param name: Name of the instructions. :param id: ID of the instructions. :param log_handlers: List of log handlers to use. :param finetune_format: Format to use for finetuning. :param indent: Indentation to use for finetuning. :param include_code_body: Whether to include the code body in the finetuning.

Source code in instructor/distil.py
def __init__(
    self,
    name: Optional[str] = None,
    id: Optional[str] = None,
    log_handlers: Optional[list[logging.Handler]] = None,
    finetune_format: FinetuneFormat = FinetuneFormat.MESSAGES,
    indent: int = 2,
    include_code_body: bool = False,
    openai_client: Optional[OpenAI] = None,
) -> None:
    """
    Instructions for distillation and dispatch.

    :param name: Name of the instructions.
    :param id: ID of the instructions.
    :param log_handlers: List of log handlers to use.
    :param finetune_format: Format to use for finetuning.
    :param indent: Indentation to use for finetuning.
    :param include_code_body: Whether to include the code body in the finetuning.
    """
    self.name = name
    self.id = id or str(uuid.uuid4())
    self.unique_id = str(uuid.uuid4())
    self.finetune_format = finetune_format
    self.indent = indent
    self.include_code_body = include_code_body
    self.client = openai_client or OpenAI()

    self.logger = logging.getLogger(self.name)
    for handler in log_handlers or []:
        self.logger.addHandler(handler)

distil(*args, name=None, mode='distil', model='gpt-3.5-turbo', fine_tune_format=None)

Decorator to track the function call and response, supports distillation and dispatch modes.

If used without arguments, it must be used as a decorator.

:Example:

@distil def my_function() -> MyModel: return MyModel()

@distil(name="my_function") def my_function() -> MyModel: return MyModel()

:param fn: Function to track. :param name: Name of the function to track. Defaults to the function name. :param mode: Mode to use for distillation. Defaults to "distil".

Source code in instructor/distil.py
def distil(
    self,
    *args: Any,
    name: Optional[str] = None,
    mode: Literal["distil", "dispatch"] = "distil",
    model: str = "gpt-3.5-turbo",
    fine_tune_format: Optional[FinetuneFormat] = None,
) -> Union[
    Callable[P, Union[T_Retval, ChatCompletion]],
    Callable[[Callable[P, T_Retval]], Callable[P, Union[T_Retval, ChatCompletion]]],
]:
    """
    Decorator to track the function call and response, supports distillation and dispatch modes.

    If used without arguments, it must be used as a decorator.

    :Example:

    >>> @distil
    >>> def my_function() -> MyModel:
    >>>     return MyModel()
    >>>
    >>> @distil(name="my_function")
    >>> def my_function() -> MyModel:
    >>>     return MyModel()

    :param fn: Function to track.
    :param name: Name of the function to track. Defaults to the function name.
    :param mode: Mode to use for distillation. Defaults to "distil".
    """
    allowed_modes = {"distil", "dispatch"}
    assert mode in allowed_modes, f"Must be in {allowed_modes}"

    if fine_tune_format is None:
        fine_tune_format = self.finetune_format

    def _wrap_distil(
        fn: Callable[P, T_Retval],
    ) -> Callable[P, Union[T_Retval, ChatCompletion]]:
        msg = f"Return type hint for {fn} must subclass `pydantic.BaseModel'"
        assert is_return_type_base_model_or_instance(fn), msg
        return_base_model = inspect.signature(fn).return_annotation

        @functools.wraps(fn)
        def _dispatch(*args: P.args, **kwargs: P.kwargs) -> ChatCompletion:
            openai_kwargs = self.openai_kwargs(
                name=name if name else fn.__name__,  # type: ignore
                fn=fn,
                args=args,
                kwargs=kwargs,
                base_model=return_base_model,
            )
            return self.client.chat.completions.create(
                **openai_kwargs,
                model=model,
                response_model=return_base_model,  # type: ignore - TODO figure out why `response_model` is not recognized
            )

        @functools.wraps(fn)
        def _distil(*args: P.args, **kwargs: P.kwargs) -> T_Retval:
            resp = fn(*args, **kwargs)
            self.track(
                fn,
                args,
                kwargs,
                resp,
                name=name,
                finetune_format=fine_tune_format,
            )
            return resp

        return _dispatch if mode == "dispatch" else _distil

    if len(args) == 1 and callable(args[0]):
        return _wrap_distil(args[0])  # type: ignore

    return _wrap_distil

track(fn, args, kwargs, resp, name=None, finetune_format=FinetuneFormat.MESSAGES)

Track the function call and response in a log file, later used for finetuning.

:param fn: Function to track. :param args: Arguments passed to the function. :param kwargs: Keyword arguments passed to the function. :param resp: Response returned by the function. :param name: Name of the function to track. Defaults to the function name. :param finetune_format: Format to use for finetuning. Defaults to "raw".

Source code in instructor/distil.py
@validate_call
def track(
    self,
    fn: Callable[..., Any],
    args: tuple[Any, ...],
    kwargs: dict[str, Any],
    resp: BaseModel,
    name: Optional[str] = None,
    finetune_format: FinetuneFormat = FinetuneFormat.MESSAGES,
) -> None:
    """
    Track the function call and response in a log file, later used for finetuning.

    :param fn: Function to track.
    :param args: Arguments passed to the function.
    :param kwargs: Keyword arguments passed to the function.
    :param resp: Response returned by the function.
    :param name: Name of the function to track. Defaults to the function name.
    :param finetune_format: Format to use for finetuning. Defaults to "raw".
    """
    name = name if name else fn.__name__  # type: ignore
    base_model = type(resp)

    if finetune_format == FinetuneFormat.MESSAGES:
        openai_function_call = response_schema(base_model).openai_schema
        openai_kwargs = self.openai_kwargs(name, fn, args, kwargs, base_model)
        openai_kwargs["messages"].append(
            {
                "role": "assistant",
                "function_call": {
                    "name": base_model.__name__,
                    "arguments": resp.model_dump_json(indent=self.indent),
                },
            }
        )
        openai_kwargs["functions"] = [openai_function_call]
        self.logger.info(json.dumps(openai_kwargs))

    if finetune_format == FinetuneFormat.RAW:
        function_body = dict(
            fn_name=name,
            fn_repr=format_function(fn),
            args=args,
            kwargs=kwargs,
            resp=resp.model_dump(),
            schema=base_model.model_json_schema(),
        )
        self.logger.info(json.dumps(function_body))

format_function(func) cached

Format a function as a string with docstring and body.

Source code in instructor/distil.py
@functools.lru_cache
def format_function(func: Callable[..., Any]) -> str:
    """
    Format a function as a string with docstring and body.
    """
    source_lines = inspect.getsourcelines(func)
    definition = " ".join(source_lines[0]).strip()

    docstring = inspect.getdoc(func)
    if docstring:
        formatted_docstring = f'"""\n{docstring}\n"""'
    else:
        formatted_docstring = ""

    body = inspect.getsource(func)
    body = body.replace(f"def {func.__name__}", "")  # type: ignore

    return f"{definition}\n{formatted_docstring}\n{body}"

get_signature_from_fn(fn)

Get the function signature as a string.

:Example:

def my_function(a: int, b: int) -> int: return a + b

get_signature_from_fn(my_function) "def my_function(a: int, b: int) -> int"

:param fn: Function to get the signature for. :return: Function signature as a string.

Source code in instructor/distil.py
def get_signature_from_fn(fn: Callable[..., Any]) -> str:
    """
    Get the function signature as a string.

    :Example:

    >>> def my_function(a: int, b: int) -> int:
    >>>     return a + b
    >>>
    >>> get_signature_from_fn(my_function)
    "def my_function(a: int, b: int) -> int"

    :param fn: Function to get the signature for.
    :return: Function signature as a string.
    """
    sig = inspect.signature(fn)
    lines = f"def {fn.__name__}{sig}"  # type: ignore
    docstring = inspect.getdoc(fn)
    if docstring:
        formatted_docstring = f'"""\n{docstring}\n"""'
    else:
        formatted_docstring = ""
    return f"{lines}\n{formatted_docstring}"

is_return_type_base_model_or_instance(func)

Check if the return type of a function is a pydantic BaseModel or an instance of it.

:param func: Function to check. :return: True if the return type is a pydantic BaseModel or an instance of it.

Source code in instructor/distil.py
def is_return_type_base_model_or_instance(func: Callable[..., Any]) -> bool:
    """
    Check if the return type of a function is a pydantic BaseModel or an instance of it.

    :param func: Function to check.
    :return: True if the return type is a pydantic BaseModel or an instance of it.
    """
    return_type = inspect.signature(func).return_annotation
    assert return_type != inspect.Signature.empty, (
        "Must have a return type hint that is a pydantic BaseModel"
    )
    return inspect.isclass(return_type) and issubclass(return_type, BaseModel)

Bases: Enum

Source code in instructor/distil.py
class FinetuneFormat(enum.Enum):
    MESSAGES = "messages"
    RAW = "raw"
Source code in instructor/distil.py
class Instructions:
    def __init__(
        self,
        name: Optional[str] = None,
        id: Optional[str] = None,
        log_handlers: Optional[list[logging.Handler]] = None,
        finetune_format: FinetuneFormat = FinetuneFormat.MESSAGES,
        indent: int = 2,
        include_code_body: bool = False,
        openai_client: Optional[OpenAI] = None,
    ) -> None:
        """
        Instructions for distillation and dispatch.

        :param name: Name of the instructions.
        :param id: ID of the instructions.
        :param log_handlers: List of log handlers to use.
        :param finetune_format: Format to use for finetuning.
        :param indent: Indentation to use for finetuning.
        :param include_code_body: Whether to include the code body in the finetuning.
        """
        self.name = name
        self.id = id or str(uuid.uuid4())
        self.unique_id = str(uuid.uuid4())
        self.finetune_format = finetune_format
        self.indent = indent
        self.include_code_body = include_code_body
        self.client = openai_client or OpenAI()

        self.logger = logging.getLogger(self.name)
        for handler in log_handlers or []:
            self.logger.addHandler(handler)

    def distil(
        self,
        *args: Any,
        name: Optional[str] = None,
        mode: Literal["distil", "dispatch"] = "distil",
        model: str = "gpt-3.5-turbo",
        fine_tune_format: Optional[FinetuneFormat] = None,
    ) -> Union[
        Callable[P, Union[T_Retval, ChatCompletion]],
        Callable[[Callable[P, T_Retval]], Callable[P, Union[T_Retval, ChatCompletion]]],
    ]:
        """
        Decorator to track the function call and response, supports distillation and dispatch modes.

        If used without arguments, it must be used as a decorator.

        :Example:

        >>> @distil
        >>> def my_function() -> MyModel:
        >>>     return MyModel()
        >>>
        >>> @distil(name="my_function")
        >>> def my_function() -> MyModel:
        >>>     return MyModel()

        :param fn: Function to track.
        :param name: Name of the function to track. Defaults to the function name.
        :param mode: Mode to use for distillation. Defaults to "distil".
        """
        allowed_modes = {"distil", "dispatch"}
        assert mode in allowed_modes, f"Must be in {allowed_modes}"

        if fine_tune_format is None:
            fine_tune_format = self.finetune_format

        def _wrap_distil(
            fn: Callable[P, T_Retval],
        ) -> Callable[P, Union[T_Retval, ChatCompletion]]:
            msg = f"Return type hint for {fn} must subclass `pydantic.BaseModel'"
            assert is_return_type_base_model_or_instance(fn), msg
            return_base_model = inspect.signature(fn).return_annotation

            @functools.wraps(fn)
            def _dispatch(*args: P.args, **kwargs: P.kwargs) -> ChatCompletion:
                openai_kwargs = self.openai_kwargs(
                    name=name if name else fn.__name__,  # type: ignore
                    fn=fn,
                    args=args,
                    kwargs=kwargs,
                    base_model=return_base_model,
                )
                return self.client.chat.completions.create(
                    **openai_kwargs,
                    model=model,
                    response_model=return_base_model,  # type: ignore - TODO figure out why `response_model` is not recognized
                )

            @functools.wraps(fn)
            def _distil(*args: P.args, **kwargs: P.kwargs) -> T_Retval:
                resp = fn(*args, **kwargs)
                self.track(
                    fn,
                    args,
                    kwargs,
                    resp,
                    name=name,
                    finetune_format=fine_tune_format,
                )
                return resp

            return _dispatch if mode == "dispatch" else _distil

        if len(args) == 1 and callable(args[0]):
            return _wrap_distil(args[0])  # type: ignore

        return _wrap_distil

    @validate_call
    def track(
        self,
        fn: Callable[..., Any],
        args: tuple[Any, ...],
        kwargs: dict[str, Any],
        resp: BaseModel,
        name: Optional[str] = None,
        finetune_format: FinetuneFormat = FinetuneFormat.MESSAGES,
    ) -> None:
        """
        Track the function call and response in a log file, later used for finetuning.

        :param fn: Function to track.
        :param args: Arguments passed to the function.
        :param kwargs: Keyword arguments passed to the function.
        :param resp: Response returned by the function.
        :param name: Name of the function to track. Defaults to the function name.
        :param finetune_format: Format to use for finetuning. Defaults to "raw".
        """
        name = name if name else fn.__name__  # type: ignore
        base_model = type(resp)

        if finetune_format == FinetuneFormat.MESSAGES:
            openai_function_call = response_schema(base_model).openai_schema
            openai_kwargs = self.openai_kwargs(name, fn, args, kwargs, base_model)
            openai_kwargs["messages"].append(
                {
                    "role": "assistant",
                    "function_call": {
                        "name": base_model.__name__,
                        "arguments": resp.model_dump_json(indent=self.indent),
                    },
                }
            )
            openai_kwargs["functions"] = [openai_function_call]
            self.logger.info(json.dumps(openai_kwargs))

        if finetune_format == FinetuneFormat.RAW:
            function_body = dict(
                fn_name=name,
                fn_repr=format_function(fn),
                args=args,
                kwargs=kwargs,
                resp=resp.model_dump(),
                schema=base_model.model_json_schema(),
            )
            self.logger.info(json.dumps(function_body))

    def openai_kwargs(
        self,
        name: str,
        fn: Callable[..., Any],
        args: tuple[Any, ...],
        kwargs: dict[str, Any],
        base_model: type[BaseModel],
    ) -> OpenAIChatKwargs:
        if self.include_code_body:
            func_def = format_function(fn)
        else:
            func_def = get_signature_from_fn(fn)

        str_args = ", ".join(map(str, args))
        str_kwargs = (
            ", ".join(f"{k}={json.dumps(v)}" for k, v in kwargs.items()) or None
        )
        call_args = ", ".join(filter(None, [str_args, str_kwargs]))

        function_body: OpenAIChatKwargs = {
            "messages": [
                {
                    "role": "system",
                    "content": f"Predict the results of this function:\n\n{func_def}",
                },
                {
                    "role": "user",
                    "content": f"Return `{name}({call_args})`",
                },
            ],
        }
        return function_body

__init__(name=None, id=None, log_handlers=None, finetune_format=FinetuneFormat.MESSAGES, indent=2, include_code_body=False, openai_client=None)

Instructions for distillation and dispatch.

:param name: Name of the instructions. :param id: ID of the instructions. :param log_handlers: List of log handlers to use. :param finetune_format: Format to use for finetuning. :param indent: Indentation to use for finetuning. :param include_code_body: Whether to include the code body in the finetuning.

Source code in instructor/distil.py
def __init__(
    self,
    name: Optional[str] = None,
    id: Optional[str] = None,
    log_handlers: Optional[list[logging.Handler]] = None,
    finetune_format: FinetuneFormat = FinetuneFormat.MESSAGES,
    indent: int = 2,
    include_code_body: bool = False,
    openai_client: Optional[OpenAI] = None,
) -> None:
    """
    Instructions for distillation and dispatch.

    :param name: Name of the instructions.
    :param id: ID of the instructions.
    :param log_handlers: List of log handlers to use.
    :param finetune_format: Format to use for finetuning.
    :param indent: Indentation to use for finetuning.
    :param include_code_body: Whether to include the code body in the finetuning.
    """
    self.name = name
    self.id = id or str(uuid.uuid4())
    self.unique_id = str(uuid.uuid4())
    self.finetune_format = finetune_format
    self.indent = indent
    self.include_code_body = include_code_body
    self.client = openai_client or OpenAI()

    self.logger = logging.getLogger(self.name)
    for handler in log_handlers or []:
        self.logger.addHandler(handler)

distil(*args, name=None, mode='distil', model='gpt-3.5-turbo', fine_tune_format=None)

Decorator to track the function call and response, supports distillation and dispatch modes.

If used without arguments, it must be used as a decorator.

:Example:

@distil def my_function() -> MyModel: return MyModel()

@distil(name="my_function") def my_function() -> MyModel: return MyModel()

:param fn: Function to track. :param name: Name of the function to track. Defaults to the function name. :param mode: Mode to use for distillation. Defaults to "distil".

Source code in instructor/distil.py
def distil(
    self,
    *args: Any,
    name: Optional[str] = None,
    mode: Literal["distil", "dispatch"] = "distil",
    model: str = "gpt-3.5-turbo",
    fine_tune_format: Optional[FinetuneFormat] = None,
) -> Union[
    Callable[P, Union[T_Retval, ChatCompletion]],
    Callable[[Callable[P, T_Retval]], Callable[P, Union[T_Retval, ChatCompletion]]],
]:
    """
    Decorator to track the function call and response, supports distillation and dispatch modes.

    If used without arguments, it must be used as a decorator.

    :Example:

    >>> @distil
    >>> def my_function() -> MyModel:
    >>>     return MyModel()
    >>>
    >>> @distil(name="my_function")
    >>> def my_function() -> MyModel:
    >>>     return MyModel()

    :param fn: Function to track.
    :param name: Name of the function to track. Defaults to the function name.
    :param mode: Mode to use for distillation. Defaults to "distil".
    """
    allowed_modes = {"distil", "dispatch"}
    assert mode in allowed_modes, f"Must be in {allowed_modes}"

    if fine_tune_format is None:
        fine_tune_format = self.finetune_format

    def _wrap_distil(
        fn: Callable[P, T_Retval],
    ) -> Callable[P, Union[T_Retval, ChatCompletion]]:
        msg = f"Return type hint for {fn} must subclass `pydantic.BaseModel'"
        assert is_return_type_base_model_or_instance(fn), msg
        return_base_model = inspect.signature(fn).return_annotation

        @functools.wraps(fn)
        def _dispatch(*args: P.args, **kwargs: P.kwargs) -> ChatCompletion:
            openai_kwargs = self.openai_kwargs(
                name=name if name else fn.__name__,  # type: ignore
                fn=fn,
                args=args,
                kwargs=kwargs,
                base_model=return_base_model,
            )
            return self.client.chat.completions.create(
                **openai_kwargs,
                model=model,
                response_model=return_base_model,  # type: ignore - TODO figure out why `response_model` is not recognized
            )

        @functools.wraps(fn)
        def _distil(*args: P.args, **kwargs: P.kwargs) -> T_Retval:
            resp = fn(*args, **kwargs)
            self.track(
                fn,
                args,
                kwargs,
                resp,
                name=name,
                finetune_format=fine_tune_format,
            )
            return resp

        return _dispatch if mode == "dispatch" else _distil

    if len(args) == 1 and callable(args[0]):
        return _wrap_distil(args[0])  # type: ignore

    return _wrap_distil

track(fn, args, kwargs, resp, name=None, finetune_format=FinetuneFormat.MESSAGES)

Track the function call and response in a log file, later used for finetuning.

:param fn: Function to track. :param args: Arguments passed to the function. :param kwargs: Keyword arguments passed to the function. :param resp: Response returned by the function. :param name: Name of the function to track. Defaults to the function name. :param finetune_format: Format to use for finetuning. Defaults to "raw".

Source code in instructor/distil.py
@validate_call
def track(
    self,
    fn: Callable[..., Any],
    args: tuple[Any, ...],
    kwargs: dict[str, Any],
    resp: BaseModel,
    name: Optional[str] = None,
    finetune_format: FinetuneFormat = FinetuneFormat.MESSAGES,
) -> None:
    """
    Track the function call and response in a log file, later used for finetuning.

    :param fn: Function to track.
    :param args: Arguments passed to the function.
    :param kwargs: Keyword arguments passed to the function.
    :param resp: Response returned by the function.
    :param name: Name of the function to track. Defaults to the function name.
    :param finetune_format: Format to use for finetuning. Defaults to "raw".
    """
    name = name if name else fn.__name__  # type: ignore
    base_model = type(resp)

    if finetune_format == FinetuneFormat.MESSAGES:
        openai_function_call = response_schema(base_model).openai_schema
        openai_kwargs = self.openai_kwargs(name, fn, args, kwargs, base_model)
        openai_kwargs["messages"].append(
            {
                "role": "assistant",
                "function_call": {
                    "name": base_model.__name__,
                    "arguments": resp.model_dump_json(indent=self.indent),
                },
            }
        )
        openai_kwargs["functions"] = [openai_function_call]
        self.logger.info(json.dumps(openai_kwargs))

    if finetune_format == FinetuneFormat.RAW:
        function_body = dict(
            fn_name=name,
            fn_repr=format_function(fn),
            args=args,
            kwargs=kwargs,
            resp=resp.model_dump(),
            schema=base_model.model_json_schema(),
        )
        self.logger.info(json.dumps(function_body))

Multimodal

Support for image and audio content in LLM requests.

Compatibility exports for v2-owned multimodal helpers.

Audio

Bases: BaseModel

Represents an audio that can be loaded from a URL or file path.

Source code in instructor/v2/core/multimodal.py
class Audio(BaseModel):
    """Represents an audio that can be loaded from a URL or file path."""

    source: Union[str, Path] = Field(description="URL or file path of the audio")  # noqa: UP007
    data: Union[str, None] = Field(  # noqa: UP007
        None, description="Base64 encoded audio data", repr=False
    )
    media_type: str = Field(description="MIME type of the audio")

    @classmethod
    def autodetect(cls, source: str | Path) -> Audio:
        """Attempt to autodetect an audio from a source string or Path."""
        if isinstance(source, str):
            if cls.is_base64(source):
                return cls.from_base64(source)
            if source.startswith(("http://", "https://")):
                return cls.from_url(source)
            if source.startswith("gs://"):
                return cls.from_gs_url(source)
            # Since detecting the max length of a file universally cross-platform is difficult,
            # we'll just try/catch the Path conversion and file check
            try:
                path = Path(source)
                if path.is_file():
                    return cls.from_path(path)
            except OSError:
                pass  # Fall through to error

            raise ValueError("Unable to determine audio source")

        if isinstance(source, Path):
            return cls.from_path(source)

    @classmethod
    def autodetect_safely(cls, source: Union[str, Path]) -> Union[Audio, str]:  # noqa: UP007
        """Safely attempt to autodetect an audio from a source string or path.

        Args:
            source (Union[str,path]): The source string or path.
        Returns:
            An Audio if the source is detected to be a valid audio, otherwise
            the source itself as a string.
        """
        try:
            return cls.autodetect(source)
        except ValueError:
            return str(source)

    @classmethod
    def is_base64(cls, s: str) -> bool:
        return bool(re.match(r"^data:audio/[a-zA-Z0-9+-]+;base64,", s))

    @classmethod
    def from_base64(cls, data_uri: str) -> Audio:
        header, encoded = data_uri.split(",", 1)
        media_type = header.split(":")[1].split(";")[0]
        if media_type not in VALID_AUDIO_MIME_TYPES:
            raise ValueError(f"Unsupported audio format: {media_type}")
        return cls(
            source=data_uri,
            media_type=media_type,
            data=encoded,
        )

    @classmethod
    def from_url(cls, url: str) -> Audio:
        """Create an Audio instance from a URL."""
        if url.startswith("gs://"):
            return cls.from_gs_url(url)
        response = requests.get(url)
        content_type = response.headers.get("content-type")
        assert content_type in VALID_AUDIO_MIME_TYPES, (
            f"Invalid audio format. Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
        )

        data = base64.b64encode(response.content).decode("utf-8")
        return cls(source=url, data=data, media_type=content_type)

    @classmethod
    def from_path(cls, path: Union[str, Path]) -> Audio:  # noqa: UP007
        """Create an Audio instance from a file path."""
        path = Path(path)
        assert path.is_file(), f"Audio file not found: {path}"

        mime_type = mimetypes.guess_type(str(path))[0]

        if mime_type == "audio/x-wav":
            mime_type = "audio/wav"

        if (
            mime_type == "audio/vnd.dlna.adts"
        ):  # <--- this is the case for aac audio files in Windows
            mime_type = "audio/aac"

        assert mime_type in VALID_AUDIO_MIME_TYPES, (
            f"Invalid audio format. Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
        )

        data = base64.b64encode(path.read_bytes()).decode("utf-8")
        return cls(source=str(path), data=data, media_type=mime_type)

    @classmethod
    def from_gs_url(cls, data_uri: str, timeout: int = 30) -> Audio:
        """
        Create an Audio instance from a Google Cloud Storage URL.

        Args:
            data_uri: GCS URL starting with gs://
            timeout: Request timeout in seconds (default: 30)
        """
        if not data_uri.startswith("gs://"):
            raise ValueError("URL must start with gs://")

        public_url = f"https://storage.googleapis.com/{data_uri[5:]}"

        try:
            response = requests.get(public_url, timeout=timeout)
            response.raise_for_status()
            media_type = response.headers.get("Content-Type")
            if media_type not in VALID_AUDIO_MIME_TYPES:
                raise ValueError(f"Unsupported audio format: {media_type}")

            data = base64.b64encode(response.content).decode("utf-8")

            return cls(source=data_uri, media_type=media_type, data=data)
        except requests.RequestException as e:
            raise ValueError(
                "Failed to access GCS audio (must be publicly readable)"
            ) from e

    def to_openai(self, mode: Mode) -> dict[str, Any]:
        from instructor.v2.providers.openai.multimodal import audio_to_openai

        return audio_to_openai(self, mode)

    def to_anthropic(self) -> dict[str, Any]:
        from instructor.v2.providers.anthropic.multimodal import audio_to_anthropic

        return audio_to_anthropic(self)

    def to_genai(self):
        from instructor.v2.providers.genai.multimodal import audio_to_genai

        return audio_to_genai(self)

autodetect(source) classmethod

Attempt to autodetect an audio from a source string or Path.

Source code in instructor/v2/core/multimodal.py
@classmethod
def autodetect(cls, source: str | Path) -> Audio:
    """Attempt to autodetect an audio from a source string or Path."""
    if isinstance(source, str):
        if cls.is_base64(source):
            return cls.from_base64(source)
        if source.startswith(("http://", "https://")):
            return cls.from_url(source)
        if source.startswith("gs://"):
            return cls.from_gs_url(source)
        # Since detecting the max length of a file universally cross-platform is difficult,
        # we'll just try/catch the Path conversion and file check
        try:
            path = Path(source)
            if path.is_file():
                return cls.from_path(path)
        except OSError:
            pass  # Fall through to error

        raise ValueError("Unable to determine audio source")

    if isinstance(source, Path):
        return cls.from_path(source)

autodetect_safely(source) classmethod

Safely attempt to autodetect an audio from a source string or path.

Parameters:

Name Type Description Default
source Union[str, path]

The source string or path.

required

Returns: An Audio if the source is detected to be a valid audio, otherwise the source itself as a string.

Source code in instructor/v2/core/multimodal.py
@classmethod
def autodetect_safely(cls, source: Union[str, Path]) -> Union[Audio, str]:  # noqa: UP007
    """Safely attempt to autodetect an audio from a source string or path.

    Args:
        source (Union[str,path]): The source string or path.
    Returns:
        An Audio if the source is detected to be a valid audio, otherwise
        the source itself as a string.
    """
    try:
        return cls.autodetect(source)
    except ValueError:
        return str(source)

from_gs_url(data_uri, timeout=30) classmethod

Create an Audio instance from a Google Cloud Storage URL.

Parameters:

Name Type Description Default
data_uri str

GCS URL starting with gs://

required
timeout int

Request timeout in seconds (default: 30)

30
Source code in instructor/v2/core/multimodal.py
@classmethod
def from_gs_url(cls, data_uri: str, timeout: int = 30) -> Audio:
    """
    Create an Audio instance from a Google Cloud Storage URL.

    Args:
        data_uri: GCS URL starting with gs://
        timeout: Request timeout in seconds (default: 30)
    """
    if not data_uri.startswith("gs://"):
        raise ValueError("URL must start with gs://")

    public_url = f"https://storage.googleapis.com/{data_uri[5:]}"

    try:
        response = requests.get(public_url, timeout=timeout)
        response.raise_for_status()
        media_type = response.headers.get("Content-Type")
        if media_type not in VALID_AUDIO_MIME_TYPES:
            raise ValueError(f"Unsupported audio format: {media_type}")

        data = base64.b64encode(response.content).decode("utf-8")

        return cls(source=data_uri, media_type=media_type, data=data)
    except requests.RequestException as e:
        raise ValueError(
            "Failed to access GCS audio (must be publicly readable)"
        ) from e

from_path(path) classmethod

Create an Audio instance from a file path.

Source code in instructor/v2/core/multimodal.py
@classmethod
def from_path(cls, path: Union[str, Path]) -> Audio:  # noqa: UP007
    """Create an Audio instance from a file path."""
    path = Path(path)
    assert path.is_file(), f"Audio file not found: {path}"

    mime_type = mimetypes.guess_type(str(path))[0]

    if mime_type == "audio/x-wav":
        mime_type = "audio/wav"

    if (
        mime_type == "audio/vnd.dlna.adts"
    ):  # <--- this is the case for aac audio files in Windows
        mime_type = "audio/aac"

    assert mime_type in VALID_AUDIO_MIME_TYPES, (
        f"Invalid audio format. Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
    )

    data = base64.b64encode(path.read_bytes()).decode("utf-8")
    return cls(source=str(path), data=data, media_type=mime_type)

from_url(url) classmethod

Create an Audio instance from a URL.

Source code in instructor/v2/core/multimodal.py
@classmethod
def from_url(cls, url: str) -> Audio:
    """Create an Audio instance from a URL."""
    if url.startswith("gs://"):
        return cls.from_gs_url(url)
    response = requests.get(url)
    content_type = response.headers.get("content-type")
    assert content_type in VALID_AUDIO_MIME_TYPES, (
        f"Invalid audio format. Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
    )

    data = base64.b64encode(response.content).decode("utf-8")
    return cls(source=url, data=data, media_type=content_type)

Image

Bases: BaseModel

Image content loaded from a URL, path, or base64 data.

Source code in instructor/v2/core/multimodal.py
class Image(BaseModel):
    """Image content loaded from a URL, path, or base64 data."""

    source: Union[str, Path] = Field(  # noqa: UP007
        description="URL, file path, or base64 data of the image"
    )
    media_type: str = Field(description="MIME type of the image")
    data: Union[str, None] = Field(  # noqa: UP007
        None, description="Base64 encoded image data", repr=False
    )

    @classmethod
    def autodetect(cls, source: str | Path) -> Image:
        """Attempt to autodetect an image from a source string or Path."""
        if isinstance(source, str):
            if cls.is_base64(source):
                return cls.from_base64(source)
            if source.startswith(("http://", "https://")):
                return cls.from_url(source)
            if source.startswith("gs://"):
                return cls.from_gs_url(source)
            # Since detecting the max length of a file universally cross-platform is difficult,
            # we'll just try/catch the Path conversion and file check
            try:
                path = Path(source)
                if path.is_file():
                    return cls.from_path(path)
            except OSError:
                pass  # Fall through to raw base64 attempt

            return cls.from_raw_base64(source)

        if isinstance(source, Path):
            return cls.from_path(source)

    @classmethod
    def autodetect_safely(cls, source: Union[str, Path]) -> Union[Image, str]:  # noqa: UP007
        """Safely attempt to autodetect an image from a source string or path.

        Args:
            source (Union[str,path]): The source string or path.
        Returns:
            An Image if the source is detected to be a valid image, otherwise
            the source itself as a string.
        """
        try:
            return cls.autodetect(source)
        except ValueError:
            return str(source)

    @classmethod
    def is_base64(cls, s: str) -> bool:
        return bool(re.match(r"^data:image/[a-zA-Z]+;base64,", s))

    @classmethod  # Caching likely unnecessary
    def from_base64(cls, data_uri: str) -> Image:
        header, encoded = data_uri.split(",", 1)
        media_type = header.split(":")[1].split(";")[0]
        if media_type not in VALID_MIME_TYPES:
            raise MultimodalError(
                f"Unsupported image format: {media_type}. Supported formats: {', '.join(VALID_MIME_TYPES)}",
                content_type="image",
            )
        return cls(
            source=data_uri,
            media_type=media_type,
            data=encoded,
        )

    @classmethod
    def from_gs_url(cls, data_uri: str, timeout: int = 30) -> Image:
        """
        Create an Image instance from a Google Cloud Storage URL.

        Args:
            data_uri: GCS URL starting with gs://
            timeout: Request timeout in seconds (default: 30)
        """
        if not data_uri.startswith("gs://"):
            raise ValueError("URL must start with gs://")

        public_url = f"https://storage.googleapis.com/{data_uri[5:]}"

        try:
            response = requests.get(public_url, timeout=timeout)
            response.raise_for_status()
            media_type = response.headers.get("Content-Type")
            if media_type not in VALID_MIME_TYPES:
                raise ValueError(f"Unsupported image format: {media_type}")

            data = base64.b64encode(response.content).decode("utf-8")

            return cls(source=data_uri, media_type=media_type, data=data)
        except requests.RequestException as e:
            raise ValueError(
                "Failed to access GCS image (must be publicly readable)"
            ) from e

    @classmethod  # Caching likely unnecessary
    def from_raw_base64(cls, data: str) -> Image:
        try:
            decoded = base64.b64decode(data)

            # Detect image type from file signature (magic bytes)
            # This replaces imghdr which was removed in Python 3.13
            img_type = None
            if decoded.startswith(b"\xff\xd8\xff"):
                img_type = "jpeg"
            elif decoded.startswith(b"\x89PNG\r\n\x1a\n"):
                img_type = "png"
            elif decoded.startswith(b"GIF87a") or decoded.startswith(b"GIF89a"):
                img_type = "gif"
            elif decoded.startswith(b"RIFF") and decoded[8:12] == b"WEBP":
                img_type = "webp"

            if img_type:
                media_type = f"image/{img_type}"
                if media_type in VALID_MIME_TYPES:
                    return cls(
                        source=data,
                        media_type=media_type,
                        data=data,
                    )
            raise ValueError(f"Unsupported image type: {img_type}")
        except Exception as e:
            raise ValueError(f"Invalid or unsupported base64 image data") from e

    @classmethod
    @lru_cache
    def from_url(cls, url: str) -> Image:
        if url.startswith("gs://"):
            return cls.from_gs_url(url)
        if cls.is_base64(url):
            return cls.from_base64(url)

        parsed_url = urlparse(url)
        media_type, _ = mimetypes.guess_type(parsed_url.path)

        if not media_type:
            try:
                response = requests.head(url, allow_redirects=True)
                media_type = response.headers.get("Content-Type")
            except requests.RequestException as e:
                raise ValueError(f"Failed to fetch image from URL") from e

        if media_type not in VALID_MIME_TYPES:
            raise ValueError(f"Unsupported image format: {media_type}")
        return cls(source=url, media_type=media_type, data=None)

    @classmethod
    @lru_cache
    def from_path(cls, path: Union[str, Path]) -> Image:  # noqa: UP007
        path = Path(path)
        if not path.is_file():
            raise FileNotFoundError(f"Image file not found: {path}")

        if path.stat().st_size == 0:
            raise ValueError("Image file is empty")

        media_type, _ = mimetypes.guess_type(str(path))
        if media_type not in VALID_MIME_TYPES:
            raise ValueError(f"Unsupported image format: {media_type}")

        data = base64.b64encode(path.read_bytes()).decode("utf-8")
        return cls(source=path, media_type=media_type, data=data)

    @staticmethod
    @lru_cache
    def url_to_base64(url: str) -> str:
        """Cachable helper method for getting image url and encoding to base64."""
        response = requests.get(url)
        response.raise_for_status()
        data = base64.b64encode(response.content).decode("utf-8")
        return data

    def to_anthropic(self) -> dict[str, Any]:
        from instructor.v2.providers.anthropic.multimodal import image_to_anthropic

        return image_to_anthropic(self)

    def to_openai(self, mode: Mode) -> dict[str, Any]:
        from instructor.v2.providers.openai.multimodal import image_to_openai

        return image_to_openai(self, mode)

    def to_genai(self):
        from instructor.v2.providers.genai.multimodal import image_to_genai

        return image_to_genai(self)

autodetect(source) classmethod

Attempt to autodetect an image from a source string or Path.

Source code in instructor/v2/core/multimodal.py
@classmethod
def autodetect(cls, source: str | Path) -> Image:
    """Attempt to autodetect an image from a source string or Path."""
    if isinstance(source, str):
        if cls.is_base64(source):
            return cls.from_base64(source)
        if source.startswith(("http://", "https://")):
            return cls.from_url(source)
        if source.startswith("gs://"):
            return cls.from_gs_url(source)
        # Since detecting the max length of a file universally cross-platform is difficult,
        # we'll just try/catch the Path conversion and file check
        try:
            path = Path(source)
            if path.is_file():
                return cls.from_path(path)
        except OSError:
            pass  # Fall through to raw base64 attempt

        return cls.from_raw_base64(source)

    if isinstance(source, Path):
        return cls.from_path(source)

autodetect_safely(source) classmethod

Safely attempt to autodetect an image from a source string or path.

Parameters:

Name Type Description Default
source Union[str, path]

The source string or path.

required

Returns: An Image if the source is detected to be a valid image, otherwise the source itself as a string.

Source code in instructor/v2/core/multimodal.py
@classmethod
def autodetect_safely(cls, source: Union[str, Path]) -> Union[Image, str]:  # noqa: UP007
    """Safely attempt to autodetect an image from a source string or path.

    Args:
        source (Union[str,path]): The source string or path.
    Returns:
        An Image if the source is detected to be a valid image, otherwise
        the source itself as a string.
    """
    try:
        return cls.autodetect(source)
    except ValueError:
        return str(source)

from_gs_url(data_uri, timeout=30) classmethod

Create an Image instance from a Google Cloud Storage URL.

Parameters:

Name Type Description Default
data_uri str

GCS URL starting with gs://

required
timeout int

Request timeout in seconds (default: 30)

30
Source code in instructor/v2/core/multimodal.py
@classmethod
def from_gs_url(cls, data_uri: str, timeout: int = 30) -> Image:
    """
    Create an Image instance from a Google Cloud Storage URL.

    Args:
        data_uri: GCS URL starting with gs://
        timeout: Request timeout in seconds (default: 30)
    """
    if not data_uri.startswith("gs://"):
        raise ValueError("URL must start with gs://")

    public_url = f"https://storage.googleapis.com/{data_uri[5:]}"

    try:
        response = requests.get(public_url, timeout=timeout)
        response.raise_for_status()
        media_type = response.headers.get("Content-Type")
        if media_type not in VALID_MIME_TYPES:
            raise ValueError(f"Unsupported image format: {media_type}")

        data = base64.b64encode(response.content).decode("utf-8")

        return cls(source=data_uri, media_type=media_type, data=data)
    except requests.RequestException as e:
        raise ValueError(
            "Failed to access GCS image (must be publicly readable)"
        ) from e

url_to_base64(url) cached staticmethod

Cachable helper method for getting image url and encoding to base64.

Source code in instructor/v2/core/multimodal.py
@staticmethod
@lru_cache
def url_to_base64(url: str) -> str:
    """Cachable helper method for getting image url and encoding to base64."""
    response = requests.get(url)
    response.raise_for_status()
    data = base64.b64encode(response.content).decode("utf-8")
    return data

ImageWithCacheControl

Bases: Image

Image with Anthropic prompt caching support.

Source code in instructor/v2/core/multimodal.py
class ImageWithCacheControl(Image):
    """Image with Anthropic prompt caching support."""

    cache_control: OptionalCacheControlType = Field(
        None, description="Optional Anthropic cache control image"
    )

    @classmethod
    def from_image_params(cls, image_params: ImageParams) -> Image:
        source = image_params["source"]
        cache_control = image_params.get("cache_control")
        base_image = Image.autodetect(source)
        return cls(
            source=base_image.source,
            media_type=base_image.media_type,
            data=base_image.data,
            cache_control=cache_control,
        )

    def to_anthropic(self) -> dict[str, Any]:
        from instructor.v2.providers.anthropic.multimodal import (
            image_with_cache_control_to_anthropic,
        )

        return image_with_cache_control_to_anthropic(self)

Mode

Bases: Enum

Mode enumeration for patching LLM API clients.

Each mode determines how the library formats and structures requests to different provider APIs and how it processes their responses.

Source code in instructor/v2/core/mode.py
class Mode(enum.Enum):
    """
    Mode enumeration for patching LLM API clients.

    Each mode determines how the library formats and structures requests
    to different provider APIs and how it processes their responses.
    """

    # OpenAI modes
    FUNCTIONS = "function_call"  # Deprecated
    PARALLEL_TOOLS = "parallel_tool_call"
    TOOLS = "tool_call"
    TOOLS_STRICT = "tools_strict"
    JSON = "json_mode"
    JSON_O1 = "json_o1"
    MD_JSON = "markdown_json_mode"
    JSON_SCHEMA = "json_schema_mode"

    # Add new modes to support responses api
    RESPONSES_TOOLS = "responses_tools"
    RESPONSES_TOOLS_WITH_INBUILT_TOOLS = "responses_tools_with_inbuilt_tools"

    # XAI modes
    XAI_JSON = "xai_json"
    XAI_TOOLS = "xai_tools"

    # Anthropic modes
    ANTHROPIC_TOOLS = "anthropic_tools"
    ANTHROPIC_REASONING_TOOLS = "anthropic_reasoning_tools"
    ANTHROPIC_JSON = "anthropic_json"
    ANTHROPIC_PARALLEL_TOOLS = "anthropic_parallel_tools"

    # Mistral modes
    MISTRAL_TOOLS = "mistral_tools"
    MISTRAL_STRUCTURED_OUTPUTS = "mistral_structured_outputs"

    # Vertex AI & Google modes
    VERTEXAI_TOOLS = "vertexai_tools"
    VERTEXAI_JSON = "vertexai_json"
    VERTEXAI_PARALLEL_TOOLS = "vertexai_parallel_tools"
    GEMINI_JSON = "gemini_json"
    GEMINI_TOOLS = "gemini_tools"
    GENAI_TOOLS = "genai_tools"
    GENAI_JSON = "genai_json"
    GENAI_STRUCTURED_OUTPUTS = (
        "genai_structured_outputs"  # Backwards compatibility alias
    )

    # Cohere modes
    COHERE_TOOLS = "cohere_tools"
    COHERE_JSON_SCHEMA = "json_object"

    # Cerebras modes
    CEREBRAS_TOOLS = "cerebras_tools"
    CEREBRAS_JSON = "cerebras_json"

    # Fireworks modes
    FIREWORKS_TOOLS = "fireworks_tools"
    FIREWORKS_JSON = "fireworks_json"

    # Other providers
    WRITER_TOOLS = "writer_tools"
    WRITER_JSON = "writer_json"
    BEDROCK_TOOLS = "bedrock_tools"
    BEDROCK_JSON = "bedrock_json"
    PERPLEXITY_JSON = "perplexity_json"
    OPENROUTER_STRUCTURED_OUTPUTS = "openrouter_structured_outputs"

    # Classification helpers
    @classmethod
    def tool_modes(cls) -> set["Mode"]:
        """Returns a set of all tool-based modes."""
        return {
            cls.FUNCTIONS,
            cls.PARALLEL_TOOLS,
            cls.TOOLS,
            cls.TOOLS_STRICT,
            cls.ANTHROPIC_TOOLS,
            cls.ANTHROPIC_REASONING_TOOLS,
            cls.ANTHROPIC_PARALLEL_TOOLS,
            cls.MISTRAL_TOOLS,
            cls.VERTEXAI_TOOLS,
            cls.VERTEXAI_PARALLEL_TOOLS,
            cls.GEMINI_TOOLS,
            cls.COHERE_TOOLS,
            cls.CEREBRAS_TOOLS,
            cls.FIREWORKS_TOOLS,
            cls.WRITER_TOOLS,
            cls.BEDROCK_TOOLS,
            cls.OPENROUTER_STRUCTURED_OUTPUTS,
            cls.MISTRAL_STRUCTURED_OUTPUTS,
            cls.XAI_TOOLS,
            cls.GENAI_TOOLS,
            cls.RESPONSES_TOOLS,
            cls.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
        }

    @classmethod
    def json_modes(cls) -> set["Mode"]:
        """Returns a set of all JSON-based modes."""
        return {
            cls.JSON,
            cls.JSON_O1,
            cls.MD_JSON,
            cls.JSON_SCHEMA,
            cls.ANTHROPIC_JSON,
            cls.VERTEXAI_JSON,
            cls.GEMINI_JSON,
            cls.COHERE_JSON_SCHEMA,
            cls.CEREBRAS_JSON,
            cls.FIREWORKS_JSON,
            cls.WRITER_JSON,
            cls.BEDROCK_JSON,
            cls.PERPLEXITY_JSON,
            cls.OPENROUTER_STRUCTURED_OUTPUTS,
            cls.MISTRAL_STRUCTURED_OUTPUTS,
            cls.XAI_JSON,
        }

    @classmethod
    def parallel_modes(cls) -> set["Mode"]:
        """Return canonical and compatibility aliases for parallel tool modes."""
        return {
            cls.PARALLEL_TOOLS,
            cls.ANTHROPIC_PARALLEL_TOOLS,
            cls.VERTEXAI_PARALLEL_TOOLS,
        }

    @classmethod
    def warn_mode_functions_deprecation(cls):
        """
        Warn about FUNCTIONS mode deprecation.

        Shows the warning only once per session to avoid spamming logs
        with the same message.
        """
        global _functions_deprecation_shown
        if not _functions_deprecation_shown:
            warnings.warn(
                "The FUNCTIONS mode is deprecated and will be removed in future versions",
                DeprecationWarning,
                stacklevel=2,
            )
            _functions_deprecation_shown = True

    @classmethod
    def warn_anthropic_reasoning_tools_deprecation(cls):
        """
        Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

        ANTHROPIC_TOOLS now supports extended thinking/reasoning via the
        'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'}
        instead of Mode.ANTHROPIC_REASONING_TOOLS.

        Shows the warning only once per session to avoid spamming logs
        with the same message.
        """
        global _reasoning_tools_deprecation_shown
        if not _reasoning_tools_deprecation_shown:
            warnings.warn(
                "Mode.ANTHROPIC_REASONING_TOOLS is deprecated. "
                "Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled', 'budget_tokens': ...} instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            _reasoning_tools_deprecation_shown = True

    @classmethod
    def warn_deprecated_mode(cls, mode: "Mode") -> None:
        """Warn about provider-specific mode deprecation.

        Uses a single warning per mode per process to reduce noise.
        """
        if mode not in DEPRECATED_TO_CORE:
            return
        if mode in _deprecated_modes_warned:
            return
        _deprecated_modes_warned.add(mode)
        replacement = DEPRECATED_TO_CORE[mode]
        warnings.warn(
            f"Mode.{mode.name} is deprecated and will be removed in v3.0. "
            f"Use Mode.{replacement.name} instead. "
            "The provider is determined by the client (from_openai, from_anthropic, etc.), "
            "not by the mode.",
            DeprecationWarning,
            stacklevel=3,
        )

json_modes() classmethod

Returns a set of all JSON-based modes.

Source code in instructor/v2/core/mode.py
@classmethod
def json_modes(cls) -> set["Mode"]:
    """Returns a set of all JSON-based modes."""
    return {
        cls.JSON,
        cls.JSON_O1,
        cls.MD_JSON,
        cls.JSON_SCHEMA,
        cls.ANTHROPIC_JSON,
        cls.VERTEXAI_JSON,
        cls.GEMINI_JSON,
        cls.COHERE_JSON_SCHEMA,
        cls.CEREBRAS_JSON,
        cls.FIREWORKS_JSON,
        cls.WRITER_JSON,
        cls.BEDROCK_JSON,
        cls.PERPLEXITY_JSON,
        cls.OPENROUTER_STRUCTURED_OUTPUTS,
        cls.MISTRAL_STRUCTURED_OUTPUTS,
        cls.XAI_JSON,
    }

parallel_modes() classmethod

Return canonical and compatibility aliases for parallel tool modes.

Source code in instructor/v2/core/mode.py
@classmethod
def parallel_modes(cls) -> set["Mode"]:
    """Return canonical and compatibility aliases for parallel tool modes."""
    return {
        cls.PARALLEL_TOOLS,
        cls.ANTHROPIC_PARALLEL_TOOLS,
        cls.VERTEXAI_PARALLEL_TOOLS,
    }

tool_modes() classmethod

Returns a set of all tool-based modes.

Source code in instructor/v2/core/mode.py
@classmethod
def tool_modes(cls) -> set["Mode"]:
    """Returns a set of all tool-based modes."""
    return {
        cls.FUNCTIONS,
        cls.PARALLEL_TOOLS,
        cls.TOOLS,
        cls.TOOLS_STRICT,
        cls.ANTHROPIC_TOOLS,
        cls.ANTHROPIC_REASONING_TOOLS,
        cls.ANTHROPIC_PARALLEL_TOOLS,
        cls.MISTRAL_TOOLS,
        cls.VERTEXAI_TOOLS,
        cls.VERTEXAI_PARALLEL_TOOLS,
        cls.GEMINI_TOOLS,
        cls.COHERE_TOOLS,
        cls.CEREBRAS_TOOLS,
        cls.FIREWORKS_TOOLS,
        cls.WRITER_TOOLS,
        cls.BEDROCK_TOOLS,
        cls.OPENROUTER_STRUCTURED_OUTPUTS,
        cls.MISTRAL_STRUCTURED_OUTPUTS,
        cls.XAI_TOOLS,
        cls.GENAI_TOOLS,
        cls.RESPONSES_TOOLS,
        cls.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
    }

warn_anthropic_reasoning_tools_deprecation() classmethod

Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

ANTHROPIC_TOOLS now supports extended thinking/reasoning via the 'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'} instead of Mode.ANTHROPIC_REASONING_TOOLS.

Shows the warning only once per session to avoid spamming logs with the same message.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_anthropic_reasoning_tools_deprecation(cls):
    """
    Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

    ANTHROPIC_TOOLS now supports extended thinking/reasoning via the
    'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'}
    instead of Mode.ANTHROPIC_REASONING_TOOLS.

    Shows the warning only once per session to avoid spamming logs
    with the same message.
    """
    global _reasoning_tools_deprecation_shown
    if not _reasoning_tools_deprecation_shown:
        warnings.warn(
            "Mode.ANTHROPIC_REASONING_TOOLS is deprecated. "
            "Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled', 'budget_tokens': ...} instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        _reasoning_tools_deprecation_shown = True

warn_deprecated_mode(mode) classmethod

Warn about provider-specific mode deprecation.

Uses a single warning per mode per process to reduce noise.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_deprecated_mode(cls, mode: "Mode") -> None:
    """Warn about provider-specific mode deprecation.

    Uses a single warning per mode per process to reduce noise.
    """
    if mode not in DEPRECATED_TO_CORE:
        return
    if mode in _deprecated_modes_warned:
        return
    _deprecated_modes_warned.add(mode)
    replacement = DEPRECATED_TO_CORE[mode]
    warnings.warn(
        f"Mode.{mode.name} is deprecated and will be removed in v3.0. "
        f"Use Mode.{replacement.name} instead. "
        "The provider is determined by the client (from_openai, from_anthropic, etc.), "
        "not by the mode.",
        DeprecationWarning,
        stacklevel=3,
    )

warn_mode_functions_deprecation() classmethod

Warn about FUNCTIONS mode deprecation.

Shows the warning only once per session to avoid spamming logs with the same message.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_mode_functions_deprecation(cls):
    """
    Warn about FUNCTIONS mode deprecation.

    Shows the warning only once per session to avoid spamming logs
    with the same message.
    """
    global _functions_deprecation_shown
    if not _functions_deprecation_shown:
        warnings.warn(
            "The FUNCTIONS mode is deprecated and will be removed in future versions",
            DeprecationWarning,
            stacklevel=2,
        )
        _functions_deprecation_shown = True

MultimodalError

Bases: ValueError, InstructorError

Exception raised for multimodal content processing errors.

This exception is raised when there are issues processing multimodal content (images, audio, PDFs, etc.), such as: - Unsupported file formats - File not found - Invalid base64 encoding - Provider doesn't support multimodal content

Note: This exception inherits from both ValueError and InstructorError to maintain backwards compatibility with code that catches ValueError.

Attributes:

Name Type Description
content_type

The type of content that failed (e.g., 'image', 'audio', 'pdf')

file_path

The file path if applicable

Examples:

from instructor import Image

try:
    response = client.chat.completions.create(
        response_model=Analysis,
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": "Analyze this image"},
                Image.from_path("/invalid/path.jpg")
            ]
        }]
    )
except MultimodalError as e:
    print(f"Multimodal error with {e.content_type}: {e}")
    if e.file_path:
        print(f"File path: {e.file_path}")

Backwards compatible with ValueError:

try:
    img = Image.from_path("/path/to/image.jpg")
except ValueError as e:
    # Still catches MultimodalError
    print(f"Image error: {e}")

Source code in instructor/v2/core/errors.py
class MultimodalError(ValueError, InstructorError):
    """Exception raised for multimodal content processing errors.

    This exception is raised when there are issues processing multimodal
    content (images, audio, PDFs, etc.), such as:
    - Unsupported file formats
    - File not found
    - Invalid base64 encoding
    - Provider doesn't support multimodal content

    Note: This exception inherits from both ValueError and InstructorError
    to maintain backwards compatibility with code that catches ValueError.

    Attributes:
        content_type: The type of content that failed (e.g., 'image', 'audio', 'pdf')
        file_path: The file path if applicable

    Examples:
        ```python
        from instructor import Image

        try:
            response = client.chat.completions.create(
                response_model=Analysis,
                messages=[{
                    "role": "user",
                    "content": [
                        {"type": "text", "text": "Analyze this image"},
                        Image.from_path("/invalid/path.jpg")
                    ]
                }]
            )
        except MultimodalError as e:
            print(f"Multimodal error with {e.content_type}: {e}")
            if e.file_path:
                print(f"File path: {e.file_path}")
        ```

        Backwards compatible with ValueError:
        ```python
        try:
            img = Image.from_path("/path/to/image.jpg")
        except ValueError as e:
            # Still catches MultimodalError
            print(f"Image error: {e}")
        ```
    """

    def __init__(
        self,
        message: str,
        *args: Any,
        content_type: str | None = None,
        file_path: str | None = None,
        **kwargs: Any,
    ):
        self.content_type = content_type
        self.file_path = file_path
        context_parts = []
        if content_type:
            context_parts.append(f"content_type: {content_type}")
        if file_path:
            context_parts.append(f"file: {file_path}")
        context = f" ({', '.join(context_parts)})" if context_parts else ""
        super().__init__(f"{message}{context}", *args, **kwargs)

PDF

Bases: BaseModel

Source code in instructor/v2/core/multimodal.py
class PDF(BaseModel):
    source: str | Path = Field(description="URL, file path, or base64 data of the PDF")
    media_type: str = Field(
        description="MIME type of the PDF", default="application/pdf"
    )
    data: str | None = Field(None, description="Base64 encoded PDF data", repr=False)

    @classmethod
    def autodetect(cls, source: str | Path) -> PDF:
        """Attempt to autodetect a PDF from a source string or Path.
        Args:
            source (Union[str,path]): The source string or path.
        Returns:
            A PDF if the source is detected to be a valid PDF.
        Raises:
            ValueError: If the source is not detected to be a valid PDF.
        """
        if isinstance(source, str):
            if cls.is_base64(source):
                return cls.from_base64(source)
            elif source.startswith(("http://", "https://")):
                return cls.from_url(source)
            elif source.startswith("gs://"):
                return cls.from_gs_url(source)

            try:
                if Path(source).is_file():
                    return cls.from_path(source)
            except FileNotFoundError as err:
                raise MultimodalError(
                    "PDF file not found",
                    content_type="pdf",
                    file_path=str(source),
                ) from err
            except OSError as e:
                if e.errno == 63:  # File name too long
                    raise MultimodalError(
                        "PDF file name too long",
                        content_type="pdf",
                        file_path=str(source),
                    ) from e
                raise MultimodalError(
                    "Unable to read PDF file",
                    content_type="pdf",
                    file_path=str(source),
                ) from e

            return cls.from_raw_base64(source)
        elif isinstance(source, Path):
            return cls.from_path(source)

    @classmethod
    def autodetect_safely(cls, source: Union[str, Path]) -> Union[PDF, str]:  # noqa: UP007
        """Safely attempt to autodetect a PDF from a source string or path.

        Args:
            source (Union[str,path]): The source string or path.
        Returns:
            A PDF if the source is detected to be a valid PDF, otherwise
            the source itself as a string.
        """
        try:
            return cls.autodetect(source)
        except ValueError:
            return str(source)

    @classmethod
    def is_base64(cls, s: str) -> bool:
        return bool(re.match(r"^data:application/pdf;base64,", s))

    @classmethod
    def from_base64(cls, data_uri: str) -> PDF:
        header, encoded = data_uri.split(",", 1)
        media_type = header.split(":")[1].split(";")[0]
        if media_type not in VALID_PDF_MIME_TYPES:
            raise ValueError(f"Unsupported PDF format: {media_type}")
        return cls(
            source=data_uri,
            media_type=media_type,
            data=encoded,
        )

    @classmethod
    @lru_cache
    def from_path(cls, path: str | Path) -> PDF:
        path = Path(path)
        if not path.is_file():
            raise FileNotFoundError(f"PDF file not found: {path}")

        if path.stat().st_size == 0:
            raise ValueError("PDF file is empty")

        media_type, _ = mimetypes.guess_type(str(path))
        if media_type not in VALID_PDF_MIME_TYPES:
            raise ValueError(f"Unsupported PDF format: {media_type}")

        data = base64.b64encode(path.read_bytes()).decode("utf-8")
        return cls(source=path, media_type=media_type, data=data)

    @classmethod
    def from_raw_base64(cls, data: str) -> PDF:
        try:
            decoded = base64.b64decode(data)
            # Check if it's a valid PDF by looking for the PDF header
            if decoded.startswith(b"%PDF-"):
                return cls(
                    source=data,
                    media_type="application/pdf",
                    data=data,
                )
            raise ValueError("Invalid PDF format")
        except Exception as e:
            raise ValueError("Invalid or unsupported base64 PDF data") from e

    @classmethod
    def from_gs_url(cls, data_uri: str, timeout: int = 30) -> PDF:
        """
        Create a PDF instance from a Google Cloud Storage URL.

        Args:
            data_uri: GCS URL starting with gs://
            timeout: Request timeout in seconds (default: 30)
        """
        if not data_uri.startswith("gs://"):
            raise ValueError("URL must start with gs://")

        public_url = f"https://storage.googleapis.com/{data_uri[5:]}"

        try:
            response = requests.get(public_url, timeout=timeout)
            response.raise_for_status()
            media_type = response.headers.get("Content-Type", "application/pdf")
            if media_type not in VALID_PDF_MIME_TYPES:
                raise ValueError(f"Unsupported PDF format: {media_type}")

            data = base64.b64encode(response.content).decode("utf-8")

            return cls(source=data_uri, media_type=media_type, data=data)
        except requests.RequestException as e:
            raise ValueError(
                "Failed to access GCS PDF (must be publicly readable)"
            ) from e

    @classmethod
    @lru_cache
    def from_url(cls, url: str) -> PDF:
        if url.startswith("gs://"):
            return cls.from_gs_url(url)
        parsed_url = urlparse(url)
        media_type, _ = mimetypes.guess_type(parsed_url.path)

        if not media_type:
            try:
                response = requests.head(url, allow_redirects=True)
                media_type = response.headers.get("Content-Type")
            except requests.RequestException as e:
                raise ValueError("Failed to fetch PDF from URL") from e

        if media_type not in VALID_PDF_MIME_TYPES:
            raise ValueError(f"Unsupported PDF format: {media_type}")
        return cls(source=url, media_type=media_type, data=None)

    def to_mistral(self) -> dict[str, Any]:
        from instructor.v2.providers.mistral.multimodal import pdf_to_mistral

        return pdf_to_mistral(self)

    def to_openai(self, mode: Mode) -> dict[str, Any]:
        from instructor.v2.providers.openai.multimodal import pdf_to_openai

        return pdf_to_openai(self, mode)

    def to_anthropic(self) -> dict[str, Any]:
        from instructor.v2.providers.anthropic.multimodal import pdf_to_anthropic

        return pdf_to_anthropic(self)

    def to_genai(self):
        from instructor.v2.providers.genai.multimodal import pdf_to_genai

        return pdf_to_genai(self)

    def to_bedrock(self, name: str | None = None) -> dict[str, Any]:
        """Convert to Bedrock's document format."""
        # Determine the document name
        if name is None:
            if isinstance(self.source, Path):
                name = self.source.name
            elif isinstance(self.source, str):
                # Try to extract filename from path or URL
                if self.source.startswith(("http://", "https://", "gs://")):
                    name = Path(urlparse(self.source).path).name or "document"
                else:
                    name = (
                        Path(self.source).name
                        if Path(self.source).exists()
                        else "document"
                    )
            else:
                name = "document"

        # Sanitize name according to Bedrock requirements
        # Only allow alphanumeric, whitespace (max one in row), hyphens, parentheses, square brackets
        name = re.sub(r"[^\w\s\-\(\)\[\]]", "", name)
        name = re.sub(r"\s+", " ", name)  # Consolidate whitespace
        name = name.strip()

        # Handle S3 URIs
        if isinstance(self.source, str) and self.source.startswith("s3://"):
            # Parse S3 URI: s3://bucket/key
            s3_match = re.match(r"s3://([^/]+)/(.*)", self.source)
            if not s3_match:
                raise ValueError(f"Invalid S3 URI format: {self.source}")

            bucket = s3_match.group(1)
            key = s3_match.group(2)

            # Note: bucketOwner is optional but recommended for cross-account access
            return {
                "document": {
                    "format": "pdf",
                    "name": name,
                    "source": {
                        "s3Location": {
                            "uri": self.source
                            # "bucketOwner": "account-id"  # Optional, can be added by user
                        }
                    },
                }
            }

        # Handle bytes-based sources (base64 only)
        if not self.data:
            raise ValueError(
                "PDF data is missing. Provide base64-encoded data or use an s3:// source."
            )
        else:
            # Decode base64 data to bytes
            pdf_bytes = base64.b64decode(self.data)

        return {
            "document": {"format": "pdf", "name": name, "source": {"bytes": pdf_bytes}}
        }

autodetect(source) classmethod

Attempt to autodetect a PDF from a source string or Path. Args: source (Union[str,path]): The source string or path. Returns: A PDF if the source is detected to be a valid PDF. Raises: ValueError: If the source is not detected to be a valid PDF.

Source code in instructor/v2/core/multimodal.py
@classmethod
def autodetect(cls, source: str | Path) -> PDF:
    """Attempt to autodetect a PDF from a source string or Path.
    Args:
        source (Union[str,path]): The source string or path.
    Returns:
        A PDF if the source is detected to be a valid PDF.
    Raises:
        ValueError: If the source is not detected to be a valid PDF.
    """
    if isinstance(source, str):
        if cls.is_base64(source):
            return cls.from_base64(source)
        elif source.startswith(("http://", "https://")):
            return cls.from_url(source)
        elif source.startswith("gs://"):
            return cls.from_gs_url(source)

        try:
            if Path(source).is_file():
                return cls.from_path(source)
        except FileNotFoundError as err:
            raise MultimodalError(
                "PDF file not found",
                content_type="pdf",
                file_path=str(source),
            ) from err
        except OSError as e:
            if e.errno == 63:  # File name too long
                raise MultimodalError(
                    "PDF file name too long",
                    content_type="pdf",
                    file_path=str(source),
                ) from e
            raise MultimodalError(
                "Unable to read PDF file",
                content_type="pdf",
                file_path=str(source),
            ) from e

        return cls.from_raw_base64(source)
    elif isinstance(source, Path):
        return cls.from_path(source)

autodetect_safely(source) classmethod

Safely attempt to autodetect a PDF from a source string or path.

Parameters:

Name Type Description Default
source Union[str, path]

The source string or path.

required

Returns: A PDF if the source is detected to be a valid PDF, otherwise the source itself as a string.

Source code in instructor/v2/core/multimodal.py
@classmethod
def autodetect_safely(cls, source: Union[str, Path]) -> Union[PDF, str]:  # noqa: UP007
    """Safely attempt to autodetect a PDF from a source string or path.

    Args:
        source (Union[str,path]): The source string or path.
    Returns:
        A PDF if the source is detected to be a valid PDF, otherwise
        the source itself as a string.
    """
    try:
        return cls.autodetect(source)
    except ValueError:
        return str(source)

from_gs_url(data_uri, timeout=30) classmethod

Create a PDF instance from a Google Cloud Storage URL.

Parameters:

Name Type Description Default
data_uri str

GCS URL starting with gs://

required
timeout int

Request timeout in seconds (default: 30)

30
Source code in instructor/v2/core/multimodal.py
@classmethod
def from_gs_url(cls, data_uri: str, timeout: int = 30) -> PDF:
    """
    Create a PDF instance from a Google Cloud Storage URL.

    Args:
        data_uri: GCS URL starting with gs://
        timeout: Request timeout in seconds (default: 30)
    """
    if not data_uri.startswith("gs://"):
        raise ValueError("URL must start with gs://")

    public_url = f"https://storage.googleapis.com/{data_uri[5:]}"

    try:
        response = requests.get(public_url, timeout=timeout)
        response.raise_for_status()
        media_type = response.headers.get("Content-Type", "application/pdf")
        if media_type not in VALID_PDF_MIME_TYPES:
            raise ValueError(f"Unsupported PDF format: {media_type}")

        data = base64.b64encode(response.content).decode("utf-8")

        return cls(source=data_uri, media_type=media_type, data=data)
    except requests.RequestException as e:
        raise ValueError(
            "Failed to access GCS PDF (must be publicly readable)"
        ) from e

to_bedrock(name=None)

Convert to Bedrock's document format.

Source code in instructor/v2/core/multimodal.py
def to_bedrock(self, name: str | None = None) -> dict[str, Any]:
    """Convert to Bedrock's document format."""
    # Determine the document name
    if name is None:
        if isinstance(self.source, Path):
            name = self.source.name
        elif isinstance(self.source, str):
            # Try to extract filename from path or URL
            if self.source.startswith(("http://", "https://", "gs://")):
                name = Path(urlparse(self.source).path).name or "document"
            else:
                name = (
                    Path(self.source).name
                    if Path(self.source).exists()
                    else "document"
                )
        else:
            name = "document"

    # Sanitize name according to Bedrock requirements
    # Only allow alphanumeric, whitespace (max one in row), hyphens, parentheses, square brackets
    name = re.sub(r"[^\w\s\-\(\)\[\]]", "", name)
    name = re.sub(r"\s+", " ", name)  # Consolidate whitespace
    name = name.strip()

    # Handle S3 URIs
    if isinstance(self.source, str) and self.source.startswith("s3://"):
        # Parse S3 URI: s3://bucket/key
        s3_match = re.match(r"s3://([^/]+)/(.*)", self.source)
        if not s3_match:
            raise ValueError(f"Invalid S3 URI format: {self.source}")

        bucket = s3_match.group(1)
        key = s3_match.group(2)

        # Note: bucketOwner is optional but recommended for cross-account access
        return {
            "document": {
                "format": "pdf",
                "name": name,
                "source": {
                    "s3Location": {
                        "uri": self.source
                        # "bucketOwner": "account-id"  # Optional, can be added by user
                    }
                },
            }
        }

    # Handle bytes-based sources (base64 only)
    if not self.data:
        raise ValueError(
            "PDF data is missing. Provide base64-encoded data or use an s3:// source."
        )
    else:
        # Decode base64 data to bytes
        pdf_bytes = base64.b64decode(self.data)

    return {
        "document": {"format": "pdf", "name": name, "source": {"bytes": pdf_bytes}}
    }

PDFWithCacheControl

Bases: PDF

PDF with Anthropic prompt caching support.

Source code in instructor/v2/core/multimodal.py
class PDFWithCacheControl(PDF):
    """PDF with Anthropic prompt caching support."""

    def to_anthropic(self) -> dict[str, Any]:
        from instructor.v2.providers.anthropic.multimodal import (
            pdf_with_cache_control_to_anthropic,
        )

        return pdf_with_cache_control_to_anthropic(self)

autodetect_media(source)

Autodetect images, audio, or PDFs from a given source.

Parameters:

Name Type Description Default
source str | Path | Image | Audio | PDF

URL, file path, Path, or data URI to inspect.

required

Returns:

Type Description
Image | Audio | PDF | str

The detected :class:Image, :class:Audio, or :class:PDF instance.

Image | Audio | PDF | str

If detection fails, the original source is returned.

Source code in instructor/v2/core/multimodal.py
def autodetect_media(
    source: str | Path | Image | Audio | PDF,
) -> Image | Audio | PDF | str:
    """Autodetect images, audio, or PDFs from a given source.

    Args:
        source: URL, file path, Path, or data URI to inspect.

    Returns:
        The detected :class:`Image`, :class:`Audio`, or :class:`PDF` instance.
        If detection fails, the original source is returned.
    """
    if isinstance(source, (Image, Audio, PDF)):
        return source

    # Normalize once for cheap checks and mimetype guess
    source = str(source)

    if source.startswith("data:image/"):
        return Image.autodetect_safely(source)
    if source.startswith("data:audio/"):
        return Audio.autodetect_safely(source)
    if source.startswith("data:application/pdf"):
        return PDF.autodetect_safely(source)

    media_type, _ = mimetypes.guess_type(source)
    if media_type in VALID_MIME_TYPES:
        return Image.autodetect_safely(source)
    if media_type in VALID_AUDIO_MIME_TYPES:
        return Audio.autodetect_safely(source)
    if media_type in VALID_PDF_MIME_TYPES:
        return PDF.autodetect_safely(source)

    for cls in (Image, Audio, PDF):
        item = cls.autodetect_safely(source)  # type: ignore[arg-type]
        if not isinstance(item, str):
            return item
    return source

convert_contents(contents, mode)

Convert content items to the appropriate format based on the specified mode.

Source code in instructor/v2/core/multimodal.py
def convert_contents(
    contents: Union[  # noqa: UP007
        str,
        dict[str, Any],
        Image,
        Audio,
        list[Union[str, dict[str, Any], Image, Audio]],  # noqa: UP007
    ],
    mode: Mode,
) -> Union[str, list[dict[str, Any]]]:  # noqa: UP007
    """Convert content items to the appropriate format based on the specified mode."""
    if isinstance(contents, str):
        return contents
    if isinstance(contents, (Image, Audio, PDF)) or isinstance(contents, dict):
        contents = [contents]

    converted_contents: list[dict[str, Union[str, Image]]] = []  # noqa: UP007
    text_file_type = (
        "input_text"
        if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}
        else "text"
    )
    for content in contents:
        if isinstance(content, str):
            converted_contents.append({"type": text_file_type, "text": content})
        elif isinstance(content, dict):
            converted_contents.append(content)
        elif isinstance(content, (Image, Audio, PDF)):
            if mode in {
                Mode.ANTHROPIC_JSON,
                Mode.ANTHROPIC_TOOLS,
                Mode.ANTHROPIC_REASONING_TOOLS,
            }:
                converted_contents.append(content.to_anthropic())
            elif mode in {Mode.GEMINI_JSON, Mode.GEMINI_TOOLS}:
                raise NotImplementedError("Gemini is not supported yet")
            elif mode in {
                Mode.MISTRAL_STRUCTURED_OUTPUTS,
                Mode.MISTRAL_TOOLS,
            } and isinstance(content, (PDF)):
                converted_contents.append(content.to_mistral())  # type: ignore
            else:
                converted_contents.append(content.to_openai(mode))
        else:
            raise ValueError(f"Unsupported content type: {type(content)}")
    return converted_contents

convert_messages(messages, mode, autodetect_images=False)

Convert messages to the appropriate format based on the specified mode.

Source code in instructor/v2/core/multimodal.py
def convert_messages(
    messages: list[
        dict[
            str,
            Union[  # noqa: UP007
                str,
                dict[str, Any],
                Image,
                Audio,
                PDF,
                list[Union[str, dict[str, Any], Image, Audio, PDF]],  # noqa: UP007
            ],
        ]
    ],
    mode: Mode,
    autodetect_images: bool = False,
) -> list[dict[str, Any]]:
    """Convert messages to the appropriate format based on the specified mode."""
    converted_messages = []

    def is_image_params(x: Any) -> bool:
        return isinstance(x, dict) and x.get("type") == "image" and "source" in x  # type: ignore

    for message in messages:
        if "type" in message:
            if message["type"] in {"audio", "image"}:
                converted_messages.append(message)  # type: ignore
                continue
            else:
                raise ValueError(f"Unsupported message type: {message['type']}")
        role = message["role"]
        content = message["content"] or []
        other_kwargs = {
            k: v for k, v in message.items() if k not in ["role", "content", "type"]
        }
        if autodetect_images:
            if isinstance(content, list):
                new_content: list[str | dict[str, Any] | Image | Audio | PDF] = []  # noqa: UP007
                for item in content:
                    if isinstance(item, str):
                        new_content.append(autodetect_media(item))
                    elif is_image_params(item):
                        new_content.append(
                            ImageWithCacheControl.from_image_params(
                                cast(ImageParams, item)
                            )
                        )
                    else:
                        new_content.append(item)
                content = new_content
            elif isinstance(content, str):
                content = autodetect_media(content)
            elif is_image_params(content):
                content = ImageWithCacheControl.from_image_params(
                    cast(ImageParams, content)
                )
        if isinstance(content, str):
            converted_messages.append(  # type: ignore
                {"role": role, "content": content, **other_kwargs}
            )
        else:
            # At this point content is narrowed to non-str types accepted by convert_contents
            converted_content = convert_contents(content, mode)  # type: ignore
            converted_messages.append(  # type: ignore
                {"role": role, "content": converted_content, **other_kwargs}
            )
    return converted_messages  # type: ignore

extract_genai_multimodal_content(contents, autodetect_images=True)

Compatibility wrapper for the GenAI-owned multimodal converter.

Source code in instructor/v2/core/multimodal.py
def extract_genai_multimodal_content(
    contents: list[Any],
    autodetect_images: bool = True,
):
    """Compatibility wrapper for the GenAI-owned multimodal converter."""
    from instructor.v2.providers.genai.multimodal import extract_multimodal_content

    return extract_multimodal_content(contents, autodetect_images)

Bases: BaseModel

Image content loaded from a URL, path, or base64 data.

Source code in instructor/v2/core/multimodal.py
class Image(BaseModel):
    """Image content loaded from a URL, path, or base64 data."""

    source: Union[str, Path] = Field(  # noqa: UP007
        description="URL, file path, or base64 data of the image"
    )
    media_type: str = Field(description="MIME type of the image")
    data: Union[str, None] = Field(  # noqa: UP007
        None, description="Base64 encoded image data", repr=False
    )

    @classmethod
    def autodetect(cls, source: str | Path) -> Image:
        """Attempt to autodetect an image from a source string or Path."""
        if isinstance(source, str):
            if cls.is_base64(source):
                return cls.from_base64(source)
            if source.startswith(("http://", "https://")):
                return cls.from_url(source)
            if source.startswith("gs://"):
                return cls.from_gs_url(source)
            # Since detecting the max length of a file universally cross-platform is difficult,
            # we'll just try/catch the Path conversion and file check
            try:
                path = Path(source)
                if path.is_file():
                    return cls.from_path(path)
            except OSError:
                pass  # Fall through to raw base64 attempt

            return cls.from_raw_base64(source)

        if isinstance(source, Path):
            return cls.from_path(source)

    @classmethod
    def autodetect_safely(cls, source: Union[str, Path]) -> Union[Image, str]:  # noqa: UP007
        """Safely attempt to autodetect an image from a source string or path.

        Args:
            source (Union[str,path]): The source string or path.
        Returns:
            An Image if the source is detected to be a valid image, otherwise
            the source itself as a string.
        """
        try:
            return cls.autodetect(source)
        except ValueError:
            return str(source)

    @classmethod
    def is_base64(cls, s: str) -> bool:
        return bool(re.match(r"^data:image/[a-zA-Z]+;base64,", s))

    @classmethod  # Caching likely unnecessary
    def from_base64(cls, data_uri: str) -> Image:
        header, encoded = data_uri.split(",", 1)
        media_type = header.split(":")[1].split(";")[0]
        if media_type not in VALID_MIME_TYPES:
            raise MultimodalError(
                f"Unsupported image format: {media_type}. Supported formats: {', '.join(VALID_MIME_TYPES)}",
                content_type="image",
            )
        return cls(
            source=data_uri,
            media_type=media_type,
            data=encoded,
        )

    @classmethod
    def from_gs_url(cls, data_uri: str, timeout: int = 30) -> Image:
        """
        Create an Image instance from a Google Cloud Storage URL.

        Args:
            data_uri: GCS URL starting with gs://
            timeout: Request timeout in seconds (default: 30)
        """
        if not data_uri.startswith("gs://"):
            raise ValueError("URL must start with gs://")

        public_url = f"https://storage.googleapis.com/{data_uri[5:]}"

        try:
            response = requests.get(public_url, timeout=timeout)
            response.raise_for_status()
            media_type = response.headers.get("Content-Type")
            if media_type not in VALID_MIME_TYPES:
                raise ValueError(f"Unsupported image format: {media_type}")

            data = base64.b64encode(response.content).decode("utf-8")

            return cls(source=data_uri, media_type=media_type, data=data)
        except requests.RequestException as e:
            raise ValueError(
                "Failed to access GCS image (must be publicly readable)"
            ) from e

    @classmethod  # Caching likely unnecessary
    def from_raw_base64(cls, data: str) -> Image:
        try:
            decoded = base64.b64decode(data)

            # Detect image type from file signature (magic bytes)
            # This replaces imghdr which was removed in Python 3.13
            img_type = None
            if decoded.startswith(b"\xff\xd8\xff"):
                img_type = "jpeg"
            elif decoded.startswith(b"\x89PNG\r\n\x1a\n"):
                img_type = "png"
            elif decoded.startswith(b"GIF87a") or decoded.startswith(b"GIF89a"):
                img_type = "gif"
            elif decoded.startswith(b"RIFF") and decoded[8:12] == b"WEBP":
                img_type = "webp"

            if img_type:
                media_type = f"image/{img_type}"
                if media_type in VALID_MIME_TYPES:
                    return cls(
                        source=data,
                        media_type=media_type,
                        data=data,
                    )
            raise ValueError(f"Unsupported image type: {img_type}")
        except Exception as e:
            raise ValueError(f"Invalid or unsupported base64 image data") from e

    @classmethod
    @lru_cache
    def from_url(cls, url: str) -> Image:
        if url.startswith("gs://"):
            return cls.from_gs_url(url)
        if cls.is_base64(url):
            return cls.from_base64(url)

        parsed_url = urlparse(url)
        media_type, _ = mimetypes.guess_type(parsed_url.path)

        if not media_type:
            try:
                response = requests.head(url, allow_redirects=True)
                media_type = response.headers.get("Content-Type")
            except requests.RequestException as e:
                raise ValueError(f"Failed to fetch image from URL") from e

        if media_type not in VALID_MIME_TYPES:
            raise ValueError(f"Unsupported image format: {media_type}")
        return cls(source=url, media_type=media_type, data=None)

    @classmethod
    @lru_cache
    def from_path(cls, path: Union[str, Path]) -> Image:  # noqa: UP007
        path = Path(path)
        if not path.is_file():
            raise FileNotFoundError(f"Image file not found: {path}")

        if path.stat().st_size == 0:
            raise ValueError("Image file is empty")

        media_type, _ = mimetypes.guess_type(str(path))
        if media_type not in VALID_MIME_TYPES:
            raise ValueError(f"Unsupported image format: {media_type}")

        data = base64.b64encode(path.read_bytes()).decode("utf-8")
        return cls(source=path, media_type=media_type, data=data)

    @staticmethod
    @lru_cache
    def url_to_base64(url: str) -> str:
        """Cachable helper method for getting image url and encoding to base64."""
        response = requests.get(url)
        response.raise_for_status()
        data = base64.b64encode(response.content).decode("utf-8")
        return data

    def to_anthropic(self) -> dict[str, Any]:
        from instructor.v2.providers.anthropic.multimodal import image_to_anthropic

        return image_to_anthropic(self)

    def to_openai(self, mode: Mode) -> dict[str, Any]:
        from instructor.v2.providers.openai.multimodal import image_to_openai

        return image_to_openai(self, mode)

    def to_genai(self):
        from instructor.v2.providers.genai.multimodal import image_to_genai

        return image_to_genai(self)

autodetect(source) classmethod

Attempt to autodetect an image from a source string or Path.

Source code in instructor/v2/core/multimodal.py
@classmethod
def autodetect(cls, source: str | Path) -> Image:
    """Attempt to autodetect an image from a source string or Path."""
    if isinstance(source, str):
        if cls.is_base64(source):
            return cls.from_base64(source)
        if source.startswith(("http://", "https://")):
            return cls.from_url(source)
        if source.startswith("gs://"):
            return cls.from_gs_url(source)
        # Since detecting the max length of a file universally cross-platform is difficult,
        # we'll just try/catch the Path conversion and file check
        try:
            path = Path(source)
            if path.is_file():
                return cls.from_path(path)
        except OSError:
            pass  # Fall through to raw base64 attempt

        return cls.from_raw_base64(source)

    if isinstance(source, Path):
        return cls.from_path(source)

autodetect_safely(source) classmethod

Safely attempt to autodetect an image from a source string or path.

Parameters:

Name Type Description Default
source Union[str, path]

The source string or path.

required

Returns: An Image if the source is detected to be a valid image, otherwise the source itself as a string.

Source code in instructor/v2/core/multimodal.py
@classmethod
def autodetect_safely(cls, source: Union[str, Path]) -> Union[Image, str]:  # noqa: UP007
    """Safely attempt to autodetect an image from a source string or path.

    Args:
        source (Union[str,path]): The source string or path.
    Returns:
        An Image if the source is detected to be a valid image, otherwise
        the source itself as a string.
    """
    try:
        return cls.autodetect(source)
    except ValueError:
        return str(source)

from_gs_url(data_uri, timeout=30) classmethod

Create an Image instance from a Google Cloud Storage URL.

Parameters:

Name Type Description Default
data_uri str

GCS URL starting with gs://

required
timeout int

Request timeout in seconds (default: 30)

30
Source code in instructor/v2/core/multimodal.py
@classmethod
def from_gs_url(cls, data_uri: str, timeout: int = 30) -> Image:
    """
    Create an Image instance from a Google Cloud Storage URL.

    Args:
        data_uri: GCS URL starting with gs://
        timeout: Request timeout in seconds (default: 30)
    """
    if not data_uri.startswith("gs://"):
        raise ValueError("URL must start with gs://")

    public_url = f"https://storage.googleapis.com/{data_uri[5:]}"

    try:
        response = requests.get(public_url, timeout=timeout)
        response.raise_for_status()
        media_type = response.headers.get("Content-Type")
        if media_type not in VALID_MIME_TYPES:
            raise ValueError(f"Unsupported image format: {media_type}")

        data = base64.b64encode(response.content).decode("utf-8")

        return cls(source=data_uri, media_type=media_type, data=data)
    except requests.RequestException as e:
        raise ValueError(
            "Failed to access GCS image (must be publicly readable)"
        ) from e

url_to_base64(url) cached staticmethod

Cachable helper method for getting image url and encoding to base64.

Source code in instructor/v2/core/multimodal.py
@staticmethod
@lru_cache
def url_to_base64(url: str) -> str:
    """Cachable helper method for getting image url and encoding to base64."""
    response = requests.get(url)
    response.raise_for_status()
    data = base64.b64encode(response.content).decode("utf-8")
    return data

Bases: BaseModel

Represents an audio that can be loaded from a URL or file path.

Source code in instructor/v2/core/multimodal.py
class Audio(BaseModel):
    """Represents an audio that can be loaded from a URL or file path."""

    source: Union[str, Path] = Field(description="URL or file path of the audio")  # noqa: UP007
    data: Union[str, None] = Field(  # noqa: UP007
        None, description="Base64 encoded audio data", repr=False
    )
    media_type: str = Field(description="MIME type of the audio")

    @classmethod
    def autodetect(cls, source: str | Path) -> Audio:
        """Attempt to autodetect an audio from a source string or Path."""
        if isinstance(source, str):
            if cls.is_base64(source):
                return cls.from_base64(source)
            if source.startswith(("http://", "https://")):
                return cls.from_url(source)
            if source.startswith("gs://"):
                return cls.from_gs_url(source)
            # Since detecting the max length of a file universally cross-platform is difficult,
            # we'll just try/catch the Path conversion and file check
            try:
                path = Path(source)
                if path.is_file():
                    return cls.from_path(path)
            except OSError:
                pass  # Fall through to error

            raise ValueError("Unable to determine audio source")

        if isinstance(source, Path):
            return cls.from_path(source)

    @classmethod
    def autodetect_safely(cls, source: Union[str, Path]) -> Union[Audio, str]:  # noqa: UP007
        """Safely attempt to autodetect an audio from a source string or path.

        Args:
            source (Union[str,path]): The source string or path.
        Returns:
            An Audio if the source is detected to be a valid audio, otherwise
            the source itself as a string.
        """
        try:
            return cls.autodetect(source)
        except ValueError:
            return str(source)

    @classmethod
    def is_base64(cls, s: str) -> bool:
        return bool(re.match(r"^data:audio/[a-zA-Z0-9+-]+;base64,", s))

    @classmethod
    def from_base64(cls, data_uri: str) -> Audio:
        header, encoded = data_uri.split(",", 1)
        media_type = header.split(":")[1].split(";")[0]
        if media_type not in VALID_AUDIO_MIME_TYPES:
            raise ValueError(f"Unsupported audio format: {media_type}")
        return cls(
            source=data_uri,
            media_type=media_type,
            data=encoded,
        )

    @classmethod
    def from_url(cls, url: str) -> Audio:
        """Create an Audio instance from a URL."""
        if url.startswith("gs://"):
            return cls.from_gs_url(url)
        response = requests.get(url)
        content_type = response.headers.get("content-type")
        assert content_type in VALID_AUDIO_MIME_TYPES, (
            f"Invalid audio format. Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
        )

        data = base64.b64encode(response.content).decode("utf-8")
        return cls(source=url, data=data, media_type=content_type)

    @classmethod
    def from_path(cls, path: Union[str, Path]) -> Audio:  # noqa: UP007
        """Create an Audio instance from a file path."""
        path = Path(path)
        assert path.is_file(), f"Audio file not found: {path}"

        mime_type = mimetypes.guess_type(str(path))[0]

        if mime_type == "audio/x-wav":
            mime_type = "audio/wav"

        if (
            mime_type == "audio/vnd.dlna.adts"
        ):  # <--- this is the case for aac audio files in Windows
            mime_type = "audio/aac"

        assert mime_type in VALID_AUDIO_MIME_TYPES, (
            f"Invalid audio format. Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
        )

        data = base64.b64encode(path.read_bytes()).decode("utf-8")
        return cls(source=str(path), data=data, media_type=mime_type)

    @classmethod
    def from_gs_url(cls, data_uri: str, timeout: int = 30) -> Audio:
        """
        Create an Audio instance from a Google Cloud Storage URL.

        Args:
            data_uri: GCS URL starting with gs://
            timeout: Request timeout in seconds (default: 30)
        """
        if not data_uri.startswith("gs://"):
            raise ValueError("URL must start with gs://")

        public_url = f"https://storage.googleapis.com/{data_uri[5:]}"

        try:
            response = requests.get(public_url, timeout=timeout)
            response.raise_for_status()
            media_type = response.headers.get("Content-Type")
            if media_type not in VALID_AUDIO_MIME_TYPES:
                raise ValueError(f"Unsupported audio format: {media_type}")

            data = base64.b64encode(response.content).decode("utf-8")

            return cls(source=data_uri, media_type=media_type, data=data)
        except requests.RequestException as e:
            raise ValueError(
                "Failed to access GCS audio (must be publicly readable)"
            ) from e

    def to_openai(self, mode: Mode) -> dict[str, Any]:
        from instructor.v2.providers.openai.multimodal import audio_to_openai

        return audio_to_openai(self, mode)

    def to_anthropic(self) -> dict[str, Any]:
        from instructor.v2.providers.anthropic.multimodal import audio_to_anthropic

        return audio_to_anthropic(self)

    def to_genai(self):
        from instructor.v2.providers.genai.multimodal import audio_to_genai

        return audio_to_genai(self)

autodetect(source) classmethod

Attempt to autodetect an audio from a source string or Path.

Source code in instructor/v2/core/multimodal.py
@classmethod
def autodetect(cls, source: str | Path) -> Audio:
    """Attempt to autodetect an audio from a source string or Path."""
    if isinstance(source, str):
        if cls.is_base64(source):
            return cls.from_base64(source)
        if source.startswith(("http://", "https://")):
            return cls.from_url(source)
        if source.startswith("gs://"):
            return cls.from_gs_url(source)
        # Since detecting the max length of a file universally cross-platform is difficult,
        # we'll just try/catch the Path conversion and file check
        try:
            path = Path(source)
            if path.is_file():
                return cls.from_path(path)
        except OSError:
            pass  # Fall through to error

        raise ValueError("Unable to determine audio source")

    if isinstance(source, Path):
        return cls.from_path(source)

autodetect_safely(source) classmethod

Safely attempt to autodetect an audio from a source string or path.

Parameters:

Name Type Description Default
source Union[str, path]

The source string or path.

required

Returns: An Audio if the source is detected to be a valid audio, otherwise the source itself as a string.

Source code in instructor/v2/core/multimodal.py
@classmethod
def autodetect_safely(cls, source: Union[str, Path]) -> Union[Audio, str]:  # noqa: UP007
    """Safely attempt to autodetect an audio from a source string or path.

    Args:
        source (Union[str,path]): The source string or path.
    Returns:
        An Audio if the source is detected to be a valid audio, otherwise
        the source itself as a string.
    """
    try:
        return cls.autodetect(source)
    except ValueError:
        return str(source)

from_gs_url(data_uri, timeout=30) classmethod

Create an Audio instance from a Google Cloud Storage URL.

Parameters:

Name Type Description Default
data_uri str

GCS URL starting with gs://

required
timeout int

Request timeout in seconds (default: 30)

30
Source code in instructor/v2/core/multimodal.py
@classmethod
def from_gs_url(cls, data_uri: str, timeout: int = 30) -> Audio:
    """
    Create an Audio instance from a Google Cloud Storage URL.

    Args:
        data_uri: GCS URL starting with gs://
        timeout: Request timeout in seconds (default: 30)
    """
    if not data_uri.startswith("gs://"):
        raise ValueError("URL must start with gs://")

    public_url = f"https://storage.googleapis.com/{data_uri[5:]}"

    try:
        response = requests.get(public_url, timeout=timeout)
        response.raise_for_status()
        media_type = response.headers.get("Content-Type")
        if media_type not in VALID_AUDIO_MIME_TYPES:
            raise ValueError(f"Unsupported audio format: {media_type}")

        data = base64.b64encode(response.content).decode("utf-8")

        return cls(source=data_uri, media_type=media_type, data=data)
    except requests.RequestException as e:
        raise ValueError(
            "Failed to access GCS audio (must be publicly readable)"
        ) from e

from_path(path) classmethod

Create an Audio instance from a file path.

Source code in instructor/v2/core/multimodal.py
@classmethod
def from_path(cls, path: Union[str, Path]) -> Audio:  # noqa: UP007
    """Create an Audio instance from a file path."""
    path = Path(path)
    assert path.is_file(), f"Audio file not found: {path}"

    mime_type = mimetypes.guess_type(str(path))[0]

    if mime_type == "audio/x-wav":
        mime_type = "audio/wav"

    if (
        mime_type == "audio/vnd.dlna.adts"
    ):  # <--- this is the case for aac audio files in Windows
        mime_type = "audio/aac"

    assert mime_type in VALID_AUDIO_MIME_TYPES, (
        f"Invalid audio format. Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
    )

    data = base64.b64encode(path.read_bytes()).decode("utf-8")
    return cls(source=str(path), data=data, media_type=mime_type)

from_url(url) classmethod

Create an Audio instance from a URL.

Source code in instructor/v2/core/multimodal.py
@classmethod
def from_url(cls, url: str) -> Audio:
    """Create an Audio instance from a URL."""
    if url.startswith("gs://"):
        return cls.from_gs_url(url)
    response = requests.get(url)
    content_type = response.headers.get("content-type")
    assert content_type in VALID_AUDIO_MIME_TYPES, (
        f"Invalid audio format. Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
    )

    data = base64.b64encode(response.content).decode("utf-8")
    return cls(source=url, data=data, media_type=content_type)

Mode & Provider

Enumerations for modes and providers.

Bases: Enum

Mode enumeration for patching LLM API clients.

Each mode determines how the library formats and structures requests to different provider APIs and how it processes their responses.

Source code in instructor/v2/core/mode.py
class Mode(enum.Enum):
    """
    Mode enumeration for patching LLM API clients.

    Each mode determines how the library formats and structures requests
    to different provider APIs and how it processes their responses.
    """

    # OpenAI modes
    FUNCTIONS = "function_call"  # Deprecated
    PARALLEL_TOOLS = "parallel_tool_call"
    TOOLS = "tool_call"
    TOOLS_STRICT = "tools_strict"
    JSON = "json_mode"
    JSON_O1 = "json_o1"
    MD_JSON = "markdown_json_mode"
    JSON_SCHEMA = "json_schema_mode"

    # Add new modes to support responses api
    RESPONSES_TOOLS = "responses_tools"
    RESPONSES_TOOLS_WITH_INBUILT_TOOLS = "responses_tools_with_inbuilt_tools"

    # XAI modes
    XAI_JSON = "xai_json"
    XAI_TOOLS = "xai_tools"

    # Anthropic modes
    ANTHROPIC_TOOLS = "anthropic_tools"
    ANTHROPIC_REASONING_TOOLS = "anthropic_reasoning_tools"
    ANTHROPIC_JSON = "anthropic_json"
    ANTHROPIC_PARALLEL_TOOLS = "anthropic_parallel_tools"

    # Mistral modes
    MISTRAL_TOOLS = "mistral_tools"
    MISTRAL_STRUCTURED_OUTPUTS = "mistral_structured_outputs"

    # Vertex AI & Google modes
    VERTEXAI_TOOLS = "vertexai_tools"
    VERTEXAI_JSON = "vertexai_json"
    VERTEXAI_PARALLEL_TOOLS = "vertexai_parallel_tools"
    GEMINI_JSON = "gemini_json"
    GEMINI_TOOLS = "gemini_tools"
    GENAI_TOOLS = "genai_tools"
    GENAI_JSON = "genai_json"
    GENAI_STRUCTURED_OUTPUTS = (
        "genai_structured_outputs"  # Backwards compatibility alias
    )

    # Cohere modes
    COHERE_TOOLS = "cohere_tools"
    COHERE_JSON_SCHEMA = "json_object"

    # Cerebras modes
    CEREBRAS_TOOLS = "cerebras_tools"
    CEREBRAS_JSON = "cerebras_json"

    # Fireworks modes
    FIREWORKS_TOOLS = "fireworks_tools"
    FIREWORKS_JSON = "fireworks_json"

    # Other providers
    WRITER_TOOLS = "writer_tools"
    WRITER_JSON = "writer_json"
    BEDROCK_TOOLS = "bedrock_tools"
    BEDROCK_JSON = "bedrock_json"
    PERPLEXITY_JSON = "perplexity_json"
    OPENROUTER_STRUCTURED_OUTPUTS = "openrouter_structured_outputs"

    # Classification helpers
    @classmethod
    def tool_modes(cls) -> set["Mode"]:
        """Returns a set of all tool-based modes."""
        return {
            cls.FUNCTIONS,
            cls.PARALLEL_TOOLS,
            cls.TOOLS,
            cls.TOOLS_STRICT,
            cls.ANTHROPIC_TOOLS,
            cls.ANTHROPIC_REASONING_TOOLS,
            cls.ANTHROPIC_PARALLEL_TOOLS,
            cls.MISTRAL_TOOLS,
            cls.VERTEXAI_TOOLS,
            cls.VERTEXAI_PARALLEL_TOOLS,
            cls.GEMINI_TOOLS,
            cls.COHERE_TOOLS,
            cls.CEREBRAS_TOOLS,
            cls.FIREWORKS_TOOLS,
            cls.WRITER_TOOLS,
            cls.BEDROCK_TOOLS,
            cls.OPENROUTER_STRUCTURED_OUTPUTS,
            cls.MISTRAL_STRUCTURED_OUTPUTS,
            cls.XAI_TOOLS,
            cls.GENAI_TOOLS,
            cls.RESPONSES_TOOLS,
            cls.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
        }

    @classmethod
    def json_modes(cls) -> set["Mode"]:
        """Returns a set of all JSON-based modes."""
        return {
            cls.JSON,
            cls.JSON_O1,
            cls.MD_JSON,
            cls.JSON_SCHEMA,
            cls.ANTHROPIC_JSON,
            cls.VERTEXAI_JSON,
            cls.GEMINI_JSON,
            cls.COHERE_JSON_SCHEMA,
            cls.CEREBRAS_JSON,
            cls.FIREWORKS_JSON,
            cls.WRITER_JSON,
            cls.BEDROCK_JSON,
            cls.PERPLEXITY_JSON,
            cls.OPENROUTER_STRUCTURED_OUTPUTS,
            cls.MISTRAL_STRUCTURED_OUTPUTS,
            cls.XAI_JSON,
        }

    @classmethod
    def parallel_modes(cls) -> set["Mode"]:
        """Return canonical and compatibility aliases for parallel tool modes."""
        return {
            cls.PARALLEL_TOOLS,
            cls.ANTHROPIC_PARALLEL_TOOLS,
            cls.VERTEXAI_PARALLEL_TOOLS,
        }

    @classmethod
    def warn_mode_functions_deprecation(cls):
        """
        Warn about FUNCTIONS mode deprecation.

        Shows the warning only once per session to avoid spamming logs
        with the same message.
        """
        global _functions_deprecation_shown
        if not _functions_deprecation_shown:
            warnings.warn(
                "The FUNCTIONS mode is deprecated and will be removed in future versions",
                DeprecationWarning,
                stacklevel=2,
            )
            _functions_deprecation_shown = True

    @classmethod
    def warn_anthropic_reasoning_tools_deprecation(cls):
        """
        Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

        ANTHROPIC_TOOLS now supports extended thinking/reasoning via the
        'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'}
        instead of Mode.ANTHROPIC_REASONING_TOOLS.

        Shows the warning only once per session to avoid spamming logs
        with the same message.
        """
        global _reasoning_tools_deprecation_shown
        if not _reasoning_tools_deprecation_shown:
            warnings.warn(
                "Mode.ANTHROPIC_REASONING_TOOLS is deprecated. "
                "Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled', 'budget_tokens': ...} instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            _reasoning_tools_deprecation_shown = True

    @classmethod
    def warn_deprecated_mode(cls, mode: "Mode") -> None:
        """Warn about provider-specific mode deprecation.

        Uses a single warning per mode per process to reduce noise.
        """
        if mode not in DEPRECATED_TO_CORE:
            return
        if mode in _deprecated_modes_warned:
            return
        _deprecated_modes_warned.add(mode)
        replacement = DEPRECATED_TO_CORE[mode]
        warnings.warn(
            f"Mode.{mode.name} is deprecated and will be removed in v3.0. "
            f"Use Mode.{replacement.name} instead. "
            "The provider is determined by the client (from_openai, from_anthropic, etc.), "
            "not by the mode.",
            DeprecationWarning,
            stacklevel=3,
        )

json_modes() classmethod

Returns a set of all JSON-based modes.

Source code in instructor/v2/core/mode.py
@classmethod
def json_modes(cls) -> set["Mode"]:
    """Returns a set of all JSON-based modes."""
    return {
        cls.JSON,
        cls.JSON_O1,
        cls.MD_JSON,
        cls.JSON_SCHEMA,
        cls.ANTHROPIC_JSON,
        cls.VERTEXAI_JSON,
        cls.GEMINI_JSON,
        cls.COHERE_JSON_SCHEMA,
        cls.CEREBRAS_JSON,
        cls.FIREWORKS_JSON,
        cls.WRITER_JSON,
        cls.BEDROCK_JSON,
        cls.PERPLEXITY_JSON,
        cls.OPENROUTER_STRUCTURED_OUTPUTS,
        cls.MISTRAL_STRUCTURED_OUTPUTS,
        cls.XAI_JSON,
    }

parallel_modes() classmethod

Return canonical and compatibility aliases for parallel tool modes.

Source code in instructor/v2/core/mode.py
@classmethod
def parallel_modes(cls) -> set["Mode"]:
    """Return canonical and compatibility aliases for parallel tool modes."""
    return {
        cls.PARALLEL_TOOLS,
        cls.ANTHROPIC_PARALLEL_TOOLS,
        cls.VERTEXAI_PARALLEL_TOOLS,
    }

tool_modes() classmethod

Returns a set of all tool-based modes.

Source code in instructor/v2/core/mode.py
@classmethod
def tool_modes(cls) -> set["Mode"]:
    """Returns a set of all tool-based modes."""
    return {
        cls.FUNCTIONS,
        cls.PARALLEL_TOOLS,
        cls.TOOLS,
        cls.TOOLS_STRICT,
        cls.ANTHROPIC_TOOLS,
        cls.ANTHROPIC_REASONING_TOOLS,
        cls.ANTHROPIC_PARALLEL_TOOLS,
        cls.MISTRAL_TOOLS,
        cls.VERTEXAI_TOOLS,
        cls.VERTEXAI_PARALLEL_TOOLS,
        cls.GEMINI_TOOLS,
        cls.COHERE_TOOLS,
        cls.CEREBRAS_TOOLS,
        cls.FIREWORKS_TOOLS,
        cls.WRITER_TOOLS,
        cls.BEDROCK_TOOLS,
        cls.OPENROUTER_STRUCTURED_OUTPUTS,
        cls.MISTRAL_STRUCTURED_OUTPUTS,
        cls.XAI_TOOLS,
        cls.GENAI_TOOLS,
        cls.RESPONSES_TOOLS,
        cls.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
    }

warn_anthropic_reasoning_tools_deprecation() classmethod

Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

ANTHROPIC_TOOLS now supports extended thinking/reasoning via the 'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'} instead of Mode.ANTHROPIC_REASONING_TOOLS.

Shows the warning only once per session to avoid spamming logs with the same message.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_anthropic_reasoning_tools_deprecation(cls):
    """
    Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

    ANTHROPIC_TOOLS now supports extended thinking/reasoning via the
    'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'}
    instead of Mode.ANTHROPIC_REASONING_TOOLS.

    Shows the warning only once per session to avoid spamming logs
    with the same message.
    """
    global _reasoning_tools_deprecation_shown
    if not _reasoning_tools_deprecation_shown:
        warnings.warn(
            "Mode.ANTHROPIC_REASONING_TOOLS is deprecated. "
            "Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled', 'budget_tokens': ...} instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        _reasoning_tools_deprecation_shown = True

warn_deprecated_mode(mode) classmethod

Warn about provider-specific mode deprecation.

Uses a single warning per mode per process to reduce noise.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_deprecated_mode(cls, mode: "Mode") -> None:
    """Warn about provider-specific mode deprecation.

    Uses a single warning per mode per process to reduce noise.
    """
    if mode not in DEPRECATED_TO_CORE:
        return
    if mode in _deprecated_modes_warned:
        return
    _deprecated_modes_warned.add(mode)
    replacement = DEPRECATED_TO_CORE[mode]
    warnings.warn(
        f"Mode.{mode.name} is deprecated and will be removed in v3.0. "
        f"Use Mode.{replacement.name} instead. "
        "The provider is determined by the client (from_openai, from_anthropic, etc.), "
        "not by the mode.",
        DeprecationWarning,
        stacklevel=3,
    )

warn_mode_functions_deprecation() classmethod

Warn about FUNCTIONS mode deprecation.

Shows the warning only once per session to avoid spamming logs with the same message.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_mode_functions_deprecation(cls):
    """
    Warn about FUNCTIONS mode deprecation.

    Shows the warning only once per session to avoid spamming logs
    with the same message.
    """
    global _functions_deprecation_shown
    if not _functions_deprecation_shown:
        warnings.warn(
            "The FUNCTIONS mode is deprecated and will be removed in future versions",
            DeprecationWarning,
            stacklevel=2,
        )
        _functions_deprecation_shown = True

Bases: Enum

Supported provider identifiers.

Source code in instructor/v2/core/providers.py
class Provider(Enum):
    """Supported provider identifiers."""

    OPENAI = "openai"
    AZURE_OPENAI = "azure_openai"
    VERTEXAI = "vertexai"
    ANTHROPIC = "anthropic"
    ANYSCALE = "anyscale"
    TOGETHER = "together"
    GROQ = "groq"
    MISTRAL = "mistral"
    COHERE = "cohere"
    GEMINI = "gemini"
    GENAI = "genai"
    DATABRICKS = "databricks"
    CEREBRAS = "cerebras"
    DEEPSEEK = "deepseek"
    FIREWORKS = "fireworks"
    WRITER = "writer"
    XAI = "xai"
    OLLAMA = "ollama"
    LITELLM = "litellm"
    GOOGLE = "google"
    GENERATIVE_AI = "generative-ai"
    UNKNOWN = "unknown"
    BEDROCK = "bedrock"
    PERPLEXITY = "perplexity"
    OPENROUTER = "openrouter"

Exceptions

Exception classes for error handling.

Compatibility exports for v2-owned exception helpers.

AsyncValidationError

Bases: ValueError, InstructorError

Exception raised during async validation.

This exception is used specifically for errors that occur during asynchronous validation operations. It inherits from both ValueError and InstructorError to maintain compatibility with existing code.

Attributes:

Name Type Description
errors list[ValueError]

List of ValueError instances from failed validations

Examples:

from instructor.v2.validation import async_field_validator

class Model(BaseModel):
    urls: list[str]

    @async_field_validator('urls')
    async def validate_urls(cls, v):
        # Async validation logic
        ...

try:
    response = await client.chat.completions.create(
        response_model=Model,
        ...
    )
except AsyncValidationError as e:
    print(f"Async validation failed: {e.errors}")
Source code in instructor/v2/core/errors.py
class AsyncValidationError(ValueError, InstructorError):
    """Exception raised during async validation.

    This exception is used specifically for errors that occur during
    asynchronous validation operations. It inherits from both ValueError
    and InstructorError to maintain compatibility with existing code.

    Attributes:
        errors: List of ValueError instances from failed validations

    Examples:
        ```python
        from instructor.v2.validation import async_field_validator

        class Model(BaseModel):
            urls: list[str]

            @async_field_validator('urls')
            async def validate_urls(cls, v):
                # Async validation logic
                ...

        try:
            response = await client.chat.completions.create(
                response_model=Model,
                ...
            )
        except AsyncValidationError as e:
            print(f"Async validation failed: {e.errors}")
        ```
    """

    errors: list[ValueError]

ClientError

Bases: InstructorError

Exception raised for client initialization or usage errors.

This exception covers errors related to improper client usage or initialization that don't fit other categories.

Common Scenarios
  • Passing invalid client object to from_* functions
  • Missing required client configuration
  • Attempting operations on improperly initialized clients

Examples:

try:
    # Invalid client type
    client = instructor.from_openai("not_a_client")
except ClientError as e:
    print(f"Client error: {e}")
Source code in instructor/v2/core/errors.py
class ClientError(InstructorError):
    """Exception raised for client initialization or usage errors.

    This exception covers errors related to improper client usage or
    initialization that don't fit other categories.

    Common Scenarios:
        - Passing invalid client object to from_* functions
        - Missing required client configuration
        - Attempting operations on improperly initialized clients

    Examples:
        ```python
        try:
            # Invalid client type
            client = instructor.from_openai("not_a_client")
        except ClientError as e:
            print(f"Client error: {e}")
        ```
    """

    pass

ConfigurationError

Bases: InstructorError

Exception raised for configuration-related errors.

This exception occurs when there are issues with how Instructor is configured or initialized, such as: - Missing required dependencies - Invalid parameters - Incompatible settings - Improper client initialization

Common Scenarios
  • Missing provider SDK (e.g., anthropic package not installed)
  • Invalid model string format in from_provider()
  • Incompatible parameter combinations
  • Invalid max_retries configuration

Examples:

try:
    # Missing provider SDK
    client = instructor.from_provider("anthropic/claude-3")
except ConfigurationError as e:
    print(f"Configuration issue: {e}")
    # e.g., "The anthropic package is required..."

try:
    # Invalid model string
    client = instructor.from_provider("invalid-format")
except ConfigurationError as e:
    print(f"Configuration issue: {e}")
    # e.g., "Model string must be in format 'provider/model-name'"
Source code in instructor/v2/core/errors.py
class ConfigurationError(InstructorError):
    """Exception raised for configuration-related errors.

    This exception occurs when there are issues with how Instructor
    is configured or initialized, such as:
    - Missing required dependencies
    - Invalid parameters
    - Incompatible settings
    - Improper client initialization

    Common Scenarios:
        - Missing provider SDK (e.g., anthropic package not installed)
        - Invalid model string format in from_provider()
        - Incompatible parameter combinations
        - Invalid max_retries configuration

    Examples:
        ```python
        try:
            # Missing provider SDK
            client = instructor.from_provider("anthropic/claude-3")
        except ConfigurationError as e:
            print(f"Configuration issue: {e}")
            # e.g., "The anthropic package is required..."

        try:
            # Invalid model string
            client = instructor.from_provider("invalid-format")
        except ConfigurationError as e:
            print(f"Configuration issue: {e}")
            # e.g., "Model string must be in format 'provider/model-name'"
        ```
    """

    pass

FailedAttempt

Bases: NamedTuple

Represents a single failed retry attempt.

This immutable tuple stores information about a failed attempt during the retry process, allowing users to inspect what went wrong across multiple retry attempts.

Attributes:

Name Type Description
attempt_number int

The sequential number of this attempt (1-indexed)

exception Exception

The exception that caused this attempt to fail

completion Any | None

Optional partial completion data from the LLM before the failure occurred. This can be useful for debugging or implementing custom recovery logic.

Examples:

from instructor.v2.core.errors import InstructorRetryException

try:
    response = client.chat.completions.create(...)
except InstructorRetryException as e:
    for attempt in e.failed_attempts:
        print(f"Attempt {attempt.attempt_number} failed:")
        print(f"  Error: {attempt.exception}")
        print(f"  Partial data: {attempt.completion}")
Source code in instructor/v2/core/errors.py
class FailedAttempt(NamedTuple):
    """Represents a single failed retry attempt.

    This immutable tuple stores information about a failed attempt during
    the retry process, allowing users to inspect what went wrong across
    multiple retry attempts.

    Attributes:
        attempt_number: The sequential number of this attempt (1-indexed)
        exception: The exception that caused this attempt to fail
        completion: Optional partial completion data from the LLM before
            the failure occurred. This can be useful for debugging or
            implementing custom recovery logic.

    Examples:
        ```python
        from instructor.v2.core.errors import InstructorRetryException

        try:
            response = client.chat.completions.create(...)
        except InstructorRetryException as e:
            for attempt in e.failed_attempts:
                print(f"Attempt {attempt.attempt_number} failed:")
                print(f"  Error: {attempt.exception}")
                print(f"  Partial data: {attempt.completion}")
        ```
    """

    attempt_number: int
    exception: Exception
    completion: Any | None = None

IncompleteOutputException

Bases: InstructorError

Exception raised when LLM output is truncated due to token limits.

This exception occurs when the LLM hits the max_tokens limit before completing its response. This is particularly common with: - Large structured outputs - Very detailed responses - Low max_tokens settings

Attributes:

Name Type Description
last_completion

The partial/incomplete response from the LLM before truncation occurred

Common Solutions
  • Increase max_tokens in your request
  • Simplify your response model
  • Use streaming with Partial models to get incomplete data
  • Break down complex extractions into smaller tasks

Examples:

try:
    response = client.chat.completions.create(
        response_model=DetailedReport,
        max_tokens=100,  # Too low
        ...
    )
except IncompleteOutputException as e:
    print(f"Output truncated. Partial data: {e.last_completion}")
    # Retry with higher max_tokens
    response = client.chat.completions.create(
        response_model=DetailedReport,
        max_tokens=2000,
        ...
    )
See Also
  • instructor.dsl.Partial: For handling partial/incomplete responses
Source code in instructor/v2/core/errors.py
class IncompleteOutputException(InstructorError):
    """Exception raised when LLM output is truncated due to token limits.

    This exception occurs when the LLM hits the max_tokens limit before
    completing its response. This is particularly common with:
    - Large structured outputs
    - Very detailed responses
    - Low max_tokens settings

    Attributes:
        last_completion: The partial/incomplete response from the LLM
            before truncation occurred

    Common Solutions:
        - Increase max_tokens in your request
        - Simplify your response model
        - Use streaming with Partial models to get incomplete data
        - Break down complex extractions into smaller tasks

    Examples:
        ```python
        try:
            response = client.chat.completions.create(
                response_model=DetailedReport,
                max_tokens=100,  # Too low
                ...
            )
        except IncompleteOutputException as e:
            print(f"Output truncated. Partial data: {e.last_completion}")
            # Retry with higher max_tokens
            response = client.chat.completions.create(
                response_model=DetailedReport,
                max_tokens=2000,
                ...
            )
        ```

    See Also:
        - instructor.dsl.Partial: For handling partial/incomplete responses
    """

    def __init__(
        self,
        *args: Any,
        last_completion: Any | None = None,
        message: str = "The output is incomplete due to a max_tokens length limit.",
        **kwargs: dict[str, Any],
    ):
        self.last_completion = last_completion
        super().__init__(message, *args, **kwargs)

InstructorError

Bases: Exception

Base exception for all Instructor-specific errors.

This is the root exception class for the Instructor library. All custom exceptions in Instructor inherit from this class, allowing you to catch any Instructor-related error with a single except clause.

Attributes:

Name Type Description
failed_attempts list[FailedAttempt] | None

Optional list of FailedAttempt objects tracking retry attempts that failed before this exception was raised. Each attempt includes the attempt number, exception, and partial completion data.

Examples:

Catch all Instructor errors:

try:
    response = client.chat.completions.create(...)
except InstructorError as e:
    logger.error(f"Instructor error: {e}")
    # Handle any Instructor-specific error

Create error from another exception:

try:
    # some operation
except ValueError as e:
    raise InstructorError.from_exception(e)

See Also
  • FailedAttempt: NamedTuple containing retry attempt information
  • InstructorRetryException: Raised when retries are exhausted
Source code in instructor/v2/core/errors.py
class InstructorError(Exception):
    """Base exception for all Instructor-specific errors.

    This is the root exception class for the Instructor library. All custom
    exceptions in Instructor inherit from this class, allowing you to catch
    any Instructor-related error with a single except clause.

    Attributes:
        failed_attempts: Optional list of FailedAttempt objects tracking
            retry attempts that failed before this exception was raised.
            Each attempt includes the attempt number, exception, and
            partial completion data.

    Examples:
        Catch all Instructor errors:
        ```python
        try:
            response = client.chat.completions.create(...)
        except InstructorError as e:
            logger.error(f"Instructor error: {e}")
            # Handle any Instructor-specific error
        ```

        Create error from another exception:
        ```python
        try:
            # some operation
        except ValueError as e:
            raise InstructorError.from_exception(e)
        ```

    See Also:
        - FailedAttempt: NamedTuple containing retry attempt information
        - InstructorRetryException: Raised when retries are exhausted
    """

    failed_attempts: list[FailedAttempt] | None = None

    @classmethod
    def from_exception(
        cls, exception: Exception, failed_attempts: list[FailedAttempt] | None = None
    ):
        """Create an InstructorError from another exception.

        Args:
            exception: The original exception to wrap
            failed_attempts: Optional list of failed retry attempts

        Returns:
            A new instance of this exception class with the message from
            the original exception
        """
        return cls(str(exception), failed_attempts=failed_attempts)

    def __init__(
        self,
        *args: Any,
        failed_attempts: list[FailedAttempt] | None = None,
        **kwargs: dict[str, Any],
    ):
        self.failed_attempts = failed_attempts
        super().__init__(*args, **kwargs)

    def __str__(self) -> str:
        # If no failed attempts, use the standard exception string representation
        if not self.failed_attempts:
            return super().__str__()

        template = Template(
            dedent(
                """
                <failed_attempts>
                {% for attempt in failed_attempts %}
                <generation number="{{ attempt.attempt_number }}">
                <exception>
                    {{ attempt.exception }}
                </exception>
                <completion>
                    {{ attempt.completion }}
                </completion>
                </generation>
                {% endfor %}
                </failed_attempts>

                <last_exception>
                    {{ last_exception }}
                </last_exception>
                """
            ).strip()
        )
        return template.render(
            last_exception=super().__str__(), failed_attempts=self.failed_attempts
        )

from_exception(exception, failed_attempts=None) classmethod

Create an InstructorError from another exception.

Parameters:

Name Type Description Default
exception Exception

The original exception to wrap

required
failed_attempts list[FailedAttempt] | None

Optional list of failed retry attempts

None

Returns:

Type Description

A new instance of this exception class with the message from

the original exception

Source code in instructor/v2/core/errors.py
@classmethod
def from_exception(
    cls, exception: Exception, failed_attempts: list[FailedAttempt] | None = None
):
    """Create an InstructorError from another exception.

    Args:
        exception: The original exception to wrap
        failed_attempts: Optional list of failed retry attempts

    Returns:
        A new instance of this exception class with the message from
        the original exception
    """
    return cls(str(exception), failed_attempts=failed_attempts)

InstructorRetryException

Bases: InstructorError

Exception raised when all retry attempts have been exhausted.

This exception is raised after the maximum number of retries has been reached without successfully validating the LLM response. It contains detailed information about all failed attempts, making it useful for debugging and implementing custom recovery logic.

Attributes:

Name Type Description
last_completion

The final (unsuccessful) completion from the LLM

messages

The conversation history sent to the LLM (deprecated, use create_kwargs instead)

n_attempts

The total number of attempts made

total_usage

The cumulative token usage across all attempts

create_kwargs

The parameters used in the create() call, including model, messages, temperature, etc.

failed_attempts list[FailedAttempt] | None

List of FailedAttempt objects with details about each failed retry

Common Causes
  • Response model too strict for the LLM's capabilities
  • Ambiguous or contradictory requirements
  • LLM model not powerful enough for the task
  • Insufficient context or examples in the prompt

Examples:

try:
    response = client.chat.completions.create(
        response_model=StrictModel,
        max_retries=3,
        ...
    )
except InstructorRetryException as e:
    print(f"Failed after {e.n_attempts} attempts")
    print(f"Total tokens used: {e.total_usage}")
    print(f"Model used: {e.create_kwargs.get('model')}")

    # Inspect failed attempts
    for attempt in e.failed_attempts:
        print(f"Attempt {attempt.attempt_number}: {attempt.exception}")

    # Implement fallback strategy
    response = fallback_handler(e.last_completion)
See Also
  • FailedAttempt: Contains details about each retry attempt
  • ValidationError: Raised when response validation fails
Source code in instructor/v2/core/errors.py
class InstructorRetryException(InstructorError):
    """Exception raised when all retry attempts have been exhausted.

    This exception is raised after the maximum number of retries has been
    reached without successfully validating the LLM response. It contains
    detailed information about all failed attempts, making it useful for
    debugging and implementing custom recovery logic.

    Attributes:
        last_completion: The final (unsuccessful) completion from the LLM
        messages: The conversation history sent to the LLM (deprecated,
            use create_kwargs instead)
        n_attempts: The total number of attempts made
        total_usage: The cumulative token usage across all attempts
        create_kwargs: The parameters used in the create() call, including
            model, messages, temperature, etc.
        failed_attempts: List of FailedAttempt objects with details about
            each failed retry

    Common Causes:
        - Response model too strict for the LLM's capabilities
        - Ambiguous or contradictory requirements
        - LLM model not powerful enough for the task
        - Insufficient context or examples in the prompt

    Examples:
        ```python
        try:
            response = client.chat.completions.create(
                response_model=StrictModel,
                max_retries=3,
                ...
            )
        except InstructorRetryException as e:
            print(f"Failed after {e.n_attempts} attempts")
            print(f"Total tokens used: {e.total_usage}")
            print(f"Model used: {e.create_kwargs.get('model')}")

            # Inspect failed attempts
            for attempt in e.failed_attempts:
                print(f"Attempt {attempt.attempt_number}: {attempt.exception}")

            # Implement fallback strategy
            response = fallback_handler(e.last_completion)
        ```

    See Also:
        - FailedAttempt: Contains details about each retry attempt
        - ValidationError: Raised when response validation fails
    """

    def __init__(
        self,
        *args: Any,
        last_completion: Any | None = None,
        messages: list[Any] | None = None,
        n_attempts: int,
        total_usage: int,
        create_kwargs: dict[str, Any] | None = None,
        failed_attempts: list[FailedAttempt] | None = None,
        **kwargs: dict[str, Any],
    ):
        self.last_completion = last_completion
        self.messages = messages
        self.n_attempts = n_attempts
        self.total_usage = total_usage
        self.create_kwargs = create_kwargs
        super().__init__(*args, failed_attempts=failed_attempts, **kwargs)

ModeError

Bases: InstructorError

Exception raised when an invalid mode is used for a provider.

Different LLM providers support different modes (e.g., TOOLS, JSON, FUNCTIONS). This exception is raised when you try to use a mode that isn't supported by the current provider.

Attributes:

Name Type Description
mode

The invalid mode that was attempted

provider

The provider name

valid_modes

List of modes supported by this provider

Examples:

try:
    client = instructor.from_openai(
        openai_client,
        mode=instructor.Mode.ANTHROPIC_TOOLS  # Wrong for OpenAI
    )
except ModeError as e:
    print(f"Invalid mode '{e.mode}' for {e.provider}")
    print(f"Use one of: {', '.join(e.valid_modes)}")
    # Retry with valid mode
    client = instructor.from_openai(
        openai_client,
        mode=instructor.Mode.TOOLS
    )
See Also
  • instructor.Mode: Enum of all available modes
Source code in instructor/v2/core/errors.py
class ModeError(InstructorError):
    """Exception raised when an invalid mode is used for a provider.

    Different LLM providers support different modes (e.g., TOOLS, JSON,
    FUNCTIONS). This exception is raised when you try to use a mode that
    isn't supported by the current provider.

    Attributes:
        mode: The invalid mode that was attempted
        provider: The provider name
        valid_modes: List of modes supported by this provider

    Examples:
        ```python
        try:
            client = instructor.from_openai(
                openai_client,
                mode=instructor.Mode.ANTHROPIC_TOOLS  # Wrong for OpenAI
            )
        except ModeError as e:
            print(f"Invalid mode '{e.mode}' for {e.provider}")
            print(f"Use one of: {', '.join(e.valid_modes)}")
            # Retry with valid mode
            client = instructor.from_openai(
                openai_client,
                mode=instructor.Mode.TOOLS
            )
        ```

    See Also:
        - instructor.Mode: Enum of all available modes
    """

    def __init__(
        self,
        mode: str,
        provider: str,
        valid_modes: list[str],
        *args: Any,
        **kwargs: Any,
    ):
        self.mode = mode
        self.provider = provider
        self.valid_modes = valid_modes
        message = f"Invalid mode '{mode}' for provider '{provider}'. Valid modes: {', '.join(valid_modes)}"
        super().__init__(message, *args, **kwargs)

MultimodalError

Bases: ValueError, InstructorError

Exception raised for multimodal content processing errors.

This exception is raised when there are issues processing multimodal content (images, audio, PDFs, etc.), such as: - Unsupported file formats - File not found - Invalid base64 encoding - Provider doesn't support multimodal content

Note: This exception inherits from both ValueError and InstructorError to maintain backwards compatibility with code that catches ValueError.

Attributes:

Name Type Description
content_type

The type of content that failed (e.g., 'image', 'audio', 'pdf')

file_path

The file path if applicable

Examples:

from instructor import Image

try:
    response = client.chat.completions.create(
        response_model=Analysis,
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": "Analyze this image"},
                Image.from_path("/invalid/path.jpg")
            ]
        }]
    )
except MultimodalError as e:
    print(f"Multimodal error with {e.content_type}: {e}")
    if e.file_path:
        print(f"File path: {e.file_path}")

Backwards compatible with ValueError:

try:
    img = Image.from_path("/path/to/image.jpg")
except ValueError as e:
    # Still catches MultimodalError
    print(f"Image error: {e}")

Source code in instructor/v2/core/errors.py
class MultimodalError(ValueError, InstructorError):
    """Exception raised for multimodal content processing errors.

    This exception is raised when there are issues processing multimodal
    content (images, audio, PDFs, etc.), such as:
    - Unsupported file formats
    - File not found
    - Invalid base64 encoding
    - Provider doesn't support multimodal content

    Note: This exception inherits from both ValueError and InstructorError
    to maintain backwards compatibility with code that catches ValueError.

    Attributes:
        content_type: The type of content that failed (e.g., 'image', 'audio', 'pdf')
        file_path: The file path if applicable

    Examples:
        ```python
        from instructor import Image

        try:
            response = client.chat.completions.create(
                response_model=Analysis,
                messages=[{
                    "role": "user",
                    "content": [
                        {"type": "text", "text": "Analyze this image"},
                        Image.from_path("/invalid/path.jpg")
                    ]
                }]
            )
        except MultimodalError as e:
            print(f"Multimodal error with {e.content_type}: {e}")
            if e.file_path:
                print(f"File path: {e.file_path}")
        ```

        Backwards compatible with ValueError:
        ```python
        try:
            img = Image.from_path("/path/to/image.jpg")
        except ValueError as e:
            # Still catches MultimodalError
            print(f"Image error: {e}")
        ```
    """

    def __init__(
        self,
        message: str,
        *args: Any,
        content_type: str | None = None,
        file_path: str | None = None,
        **kwargs: Any,
    ):
        self.content_type = content_type
        self.file_path = file_path
        context_parts = []
        if content_type:
            context_parts.append(f"content_type: {content_type}")
        if file_path:
            context_parts.append(f"file: {file_path}")
        context = f" ({', '.join(context_parts)})" if context_parts else ""
        super().__init__(f"{message}{context}", *args, **kwargs)

ProviderError

Bases: InstructorError

Exception raised for provider-specific errors.

This exception is used to wrap errors specific to LLM providers (OpenAI, Anthropic, etc.) and provides context about which provider caused the error.

Attributes:

Name Type Description
provider

The name of the provider that raised the error (e.g., "openai", "anthropic", "gemini")

Common Causes
  • API authentication failures
  • Rate limiting
  • Invalid model names
  • Provider-specific API errors
  • Network connectivity issues

Examples:

try:
    client = instructor.from_openai(openai_client)
    response = client.chat.completions.create(...)
except ProviderError as e:
    print(f"Provider {e.provider} error: {e}")
    # Implement provider-specific error handling
    if e.provider == "openai":
        # Handle OpenAI-specific errors
        pass
Source code in instructor/v2/core/errors.py
class ProviderError(InstructorError):
    """Exception raised for provider-specific errors.

    This exception is used to wrap errors specific to LLM providers
    (OpenAI, Anthropic, etc.) and provides context about which provider
    caused the error.

    Attributes:
        provider: The name of the provider that raised the error
            (e.g., "openai", "anthropic", "gemini")

    Common Causes:
        - API authentication failures
        - Rate limiting
        - Invalid model names
        - Provider-specific API errors
        - Network connectivity issues

    Examples:
        ```python
        try:
            client = instructor.from_openai(openai_client)
            response = client.chat.completions.create(...)
        except ProviderError as e:
            print(f"Provider {e.provider} error: {e}")
            # Implement provider-specific error handling
            if e.provider == "openai":
                # Handle OpenAI-specific errors
                pass
        ```
    """

    def __init__(self, provider: str, message: str, *args: Any, **kwargs: Any):
        self.provider = provider
        super().__init__(f"{provider}: {message}", *args, **kwargs)

ResponseParsingError

Bases: ValueError, InstructorError

Exception raised when unable to parse the LLM response.

This exception occurs when the LLM's raw response cannot be parsed into the expected format. Common scenarios include: - Malformed JSON in JSON mode - Missing required fields in the response - Unexpected response structure - Invalid tool call format

Note: This exception inherits from both ValueError and InstructorError to maintain backwards compatibility with code that catches ValueError.

Attributes:

Name Type Description
mode

The mode being used when parsing failed

raw_response

The raw response that failed to parse (if available)

Examples:

try:
    response = client.chat.completions.create(
        response_model=User,
        mode=instructor.Mode.JSON,
        ...
    )
except ResponseParsingError as e:
    print(f"Failed to parse response in {e.mode} mode")
    print(f"Raw response: {e.raw_response}")
    # May indicate the model doesn't support this mode well

Backwards compatible with ValueError:

try:
    response = client.chat.completions.create(...)
except ValueError as e:
    # Still catches ResponseParsingError
    print(f"Parsing error: {e}")

Source code in instructor/v2/core/errors.py
class ResponseParsingError(ValueError, InstructorError):
    """Exception raised when unable to parse the LLM response.

    This exception occurs when the LLM's raw response cannot be parsed
    into the expected format. Common scenarios include:
    - Malformed JSON in JSON mode
    - Missing required fields in the response
    - Unexpected response structure
    - Invalid tool call format

    Note: This exception inherits from both ValueError and InstructorError
    to maintain backwards compatibility with code that catches ValueError.

    Attributes:
        mode: The mode being used when parsing failed
        raw_response: The raw response that failed to parse (if available)

    Examples:
        ```python
        try:
            response = client.chat.completions.create(
                response_model=User,
                mode=instructor.Mode.JSON,
                ...
            )
        except ResponseParsingError as e:
            print(f"Failed to parse response in {e.mode} mode")
            print(f"Raw response: {e.raw_response}")
            # May indicate the model doesn't support this mode well
        ```

        Backwards compatible with ValueError:
        ```python
        try:
            response = client.chat.completions.create(...)
        except ValueError as e:
            # Still catches ResponseParsingError
            print(f"Parsing error: {e}")
        ```
    """

    def __init__(
        self,
        message: str,
        *args: Any,
        mode: str | None = None,
        raw_response: Any | None = None,
        **kwargs: Any,
    ):
        self.mode = mode
        self.raw_response = raw_response
        context = f" (mode: {mode})" if mode else ""
        super().__init__(f"{message}{context}", *args, **kwargs)

ValidationError

Bases: InstructorError

Exception raised when LLM response validation fails.

This exception occurs when the LLM's response doesn't meet the validation requirements defined in your Pydantic model, such as: - Field validation failures - Type mismatches - Custom validator failures - Missing required fields

Note: This is distinct from Pydantic's ValidationError and provides Instructor-specific context through the failed_attempts attribute.

Examples:

from pydantic import BaseModel, field_validator

class User(BaseModel):
    age: int

    @field_validator('age')
    def age_must_be_positive(cls, v):
        if v < 0:
            raise ValueError('Age must be positive')
        return v

try:
    response = client.chat.completions.create(
        response_model=User,
        ...
    )
except ValidationError as e:
    print(f"Validation failed: {e}")
    # Validation errors are automatically retried
See Also
  • InstructorRetryException: Raised when validation fails repeatedly
Source code in instructor/v2/core/errors.py
class ValidationError(InstructorError):
    """Exception raised when LLM response validation fails.

    This exception occurs when the LLM's response doesn't meet the
    validation requirements defined in your Pydantic model, such as:
    - Field validation failures
    - Type mismatches
    - Custom validator failures
    - Missing required fields

    Note: This is distinct from Pydantic's ValidationError and provides
    Instructor-specific context through the failed_attempts attribute.

    Examples:
        ```python
        from pydantic import BaseModel, field_validator

        class User(BaseModel):
            age: int

            @field_validator('age')
            def age_must_be_positive(cls, v):
                if v < 0:
                    raise ValueError('Age must be positive')
                return v

        try:
            response = client.chat.completions.create(
                response_model=User,
                ...
            )
        except ValidationError as e:
            print(f"Validation failed: {e}")
            # Validation errors are automatically retried
        ```

    See Also:
        - InstructorRetryException: Raised when validation fails repeatedly
    """

    pass

Hooks

Event hooks system for monitoring and intercepting LLM interactions.

Compatibility exports for v2-owned hook helpers.

CompletionErrorHandler

Bases: Protocol

Protocol for completion error and last attempt handlers.

Source code in instructor/v2/core/hooks.py
class CompletionErrorHandler(Protocol):
    """Protocol for completion error and last attempt handlers."""

    def __call__(self, error: Exception) -> None: ...

CompletionKwargsHandler

Bases: Protocol

Protocol for completion kwargs handlers.

Source code in instructor/v2/core/hooks.py
class CompletionKwargsHandler(Protocol):
    """Protocol for completion kwargs handlers."""

    def __call__(self, *args: Any, **kwargs: Any) -> None: ...

CompletionResponseHandler

Bases: Protocol

Protocol for completion response handlers.

Source code in instructor/v2/core/hooks.py
class CompletionResponseHandler(Protocol):
    """Protocol for completion response handlers."""

    def __call__(self, response: Any) -> None: ...

Hooks

Hooks class for handling and emitting events related to completion processes.

This class provides a mechanism to register event handlers and emit events for various stages of the completion process.

Source code in instructor/v2/core/hooks.py
class Hooks:
    """
    Hooks class for handling and emitting events related to completion processes.

    This class provides a mechanism to register event handlers and emit events
    for various stages of the completion process.
    """

    def __init__(self) -> None:
        """Initialize the hooks container."""
        self._handlers: defaultdict[HookName, list[HandlerType]] = defaultdict(list)

    def on(
        self,
        hook_name: HookNameType,
        handler: HandlerType,
    ) -> None:
        """
        Register an event handler for a specific event.

        This method allows you to attach a handler function to a specific event.
        When the event is emitted, all registered handlers for that event will be called.

        Args:
            hook_name: The event to listen for. This can be either a HookName enum
                       value or a string representation of the event name.
            handler: The function to be called when the event is emitted.

        Raises:
            ValueError: If the hook_name is not a valid HookName enum or string representation.

        Example:
            >>> def on_completion_kwargs(*args: Any, **kwargs: Any) -> None:
            ...     print(f"Completion kwargs: {args}, {kwargs}")
            >>> hooks = Hooks()
            >>> hooks.on(HookName.COMPLETION_KWARGS, on_completion_kwargs)
            >>> hooks.emit_completion_arguments(model="gpt-3.5-turbo", temperature=0.7)
            Completion kwargs: (), {'model': 'gpt-3.5-turbo', 'temperature': 0.7}
        """
        hook_name = self.get_hook_name(hook_name)
        self._handlers[hook_name].append(handler)

    def get_hook_name(self, hook_name: HookNameType) -> HookName:
        """
        Convert a string hook name to its corresponding enum value.

        Args:
            hook_name: Either a HookName enum value or string representation.

        Returns:
            The corresponding HookName enum value.

        Raises:
            ValueError: If the string doesn't match any HookName enum value.
        """
        if isinstance(hook_name, str):
            try:
                return HookName(hook_name)
            except ValueError as err:
                raise ValueError(f"Invalid hook name: {hook_name}") from err
        return hook_name

    def emit(self, hook_name: HookName, *args: Any, **kwargs: Any) -> None:
        """
        Generic method to emit events for any hook type.

        Args:
            hook_name: The hook to emit
            *args: Positional arguments to pass to handlers
            **kwargs: Keyword arguments to pass to handlers
        """
        for handler in self._handlers[hook_name]:
            try:
                if kwargs:
                    try:
                        sig = inspect.signature(handler)
                        sig.bind(*args, **kwargs)
                    except TypeError:
                        handler(*args)  # type: ignore
                        continue
                handler(*args, **kwargs)  # type: ignore
            except Exception:
                error_traceback = traceback.format_exc()
                warnings.warn(
                    f"Error in {hook_name.value} handler:\n{error_traceback}",
                    stacklevel=2,
                )

    def emit_completion_arguments(self, *args: Any, **kwargs: Any) -> None:
        """
        Emit a completion arguments event.

        Args:
            *args: Positional arguments to pass to handlers
            **kwargs: Keyword arguments to pass to handlers
        """
        self.emit(HookName.COMPLETION_KWARGS, *args, **kwargs)

    def emit_completion_response(self, response: Any) -> None:
        """
        Emit a completion response event.

        Args:
            response: The completion response to pass to handlers
        """
        self.emit(HookName.COMPLETION_RESPONSE, response)

    def emit_completion_error(self, error: Exception, **kwargs: Any) -> None:
        """
        Emit a completion error event.

        Args:
            error: The exception to pass to handlers
            **kwargs: Optional metadata (attempt_number, max_attempts, is_last_attempt)
        """
        self.emit(HookName.COMPLETION_ERROR, error, **kwargs)

    def emit_completion_last_attempt(self, error: Exception, **kwargs: Any) -> None:
        """
        Emit a completion last attempt event.

        Args:
            error: The exception to pass to handlers
            **kwargs: Optional metadata (attempt_number, max_attempts, is_last_attempt)
        """
        self.emit(HookName.COMPLETION_LAST_ATTEMPT, error, **kwargs)

    def emit_parse_error(self, error: Exception) -> None:
        """
        Emit a parse error event.

        Args:
            error: The exception to pass to handlers
        """
        self.emit(HookName.PARSE_ERROR, error)

    def off(
        self,
        hook_name: HookNameType,
        handler: HandlerType,
    ) -> None:
        """
        Remove a specific handler from an event.

        Args:
            hook_name: The name of the hook.
            handler: The handler to remove.
        """
        hook_name = self.get_hook_name(hook_name)
        if hook_name in self._handlers:
            if handler in self._handlers[hook_name]:
                self._handlers[hook_name].remove(handler)
                if not self._handlers[hook_name]:
                    del self._handlers[hook_name]

    def clear(
        self,
        hook_name: HookNameType | None = None,
    ) -> None:
        """
        Clear handlers for a specific event or all events.

        Args:
            hook_name: The name of the event to clear handlers for.
                      If None, all handlers are cleared.
        """
        if hook_name is not None:
            hook_name = self.get_hook_name(hook_name)
            self._handlers.pop(hook_name, None)
        else:
            self._handlers.clear()

    def __add__(self, other: Hooks) -> Hooks:
        """
        Combine two Hooks instances into a new one.

        This creates a new Hooks instance that contains all handlers from both
        the current instance and the other instance. Handlers are combined by
        appending the other's handlers after the current instance's handlers.

        Args:
            other: Another Hooks instance to combine with this one.

        Returns:
            A new Hooks instance containing all handlers from both instances.

        Example:
            >>> hooks1 = Hooks()
            >>> hooks2 = Hooks()
            >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
            >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
            >>> combined = hooks1 + hooks2
            >>> combined.emit_completion_arguments()  # Prints both "Hook 1" and "Hook 2"
        """
        if not isinstance(other, Hooks):
            return NotImplemented

        combined = Hooks()

        # Copy handlers from self
        for hook_name, handlers in self._handlers.items():
            combined._handlers[hook_name].extend(handlers.copy())

        # Add handlers from other
        for hook_name, handlers in other._handlers.items():
            combined._handlers[hook_name].extend(handlers.copy())

        return combined

    def __iadd__(self, other: Hooks) -> Hooks:
        """
        Add another Hooks instance to this one in-place.

        This modifies the current instance by adding all handlers from the other
        instance. The other instance's handlers are appended after the current
        instance's handlers for each event type.

        Args:
            other: Another Hooks instance to add to this one.

        Returns:
            This Hooks instance (for method chaining).

        Example:
            >>> hooks1 = Hooks()
            >>> hooks2 = Hooks()
            >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
            >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
            >>> hooks1 += hooks2
            >>> hooks1.emit_completion_arguments()  # Prints both "Hook 1" and "Hook 2"
        """
        if not isinstance(other, Hooks):
            return NotImplemented

        # Add handlers from other to self
        for hook_name, handlers in other._handlers.items():
            self._handlers[hook_name].extend(handlers.copy())

        return self

    @classmethod
    def combine(cls, *hooks_instances: Hooks) -> Hooks:
        """
        Combine multiple Hooks instances into a new one.

        This class method creates a new Hooks instance that contains all handlers
        from all provided instances. Handlers are combined in the order of the
        provided instances.

        Args:
            *hooks_instances: Variable number of Hooks instances to combine.

        Returns:
            A new Hooks instance containing all handlers from all instances.

        Example:
            >>> hooks1 = Hooks()
            >>> hooks2 = Hooks()
            >>> hooks3 = Hooks()
            >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
            >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
            >>> hooks3.on("completion:kwargs", lambda **kw: print("Hook 3"))
            >>> combined = Hooks.combine(hooks1, hooks2, hooks3)
            >>> combined.emit_completion_arguments()  # Prints all three hooks
        """
        combined = cls()

        for hooks_instance in hooks_instances:
            if not isinstance(hooks_instance, cls):
                raise TypeError(f"Expected Hooks instance, got {type(hooks_instance)}")
            combined += hooks_instance

        return combined

    def copy(self) -> Hooks:
        """
        Create a deep copy of this Hooks instance.

        Returns:
            A new Hooks instance with all the same handlers.

        Example:
            >>> original = Hooks()
            >>> original.on("completion:kwargs", lambda **kw: print("Hook"))
            >>> copy = original.copy()
            >>> copy.emit_completion_arguments()  # Prints "Hook"
        """
        new_hooks = Hooks()
        for hook_name, handlers in self._handlers.items():
            new_hooks._handlers[hook_name].extend(handlers.copy())
        return new_hooks

__add__(other)

Combine two Hooks instances into a new one.

This creates a new Hooks instance that contains all handlers from both the current instance and the other instance. Handlers are combined by appending the other's handlers after the current instance's handlers.

Parameters:

Name Type Description Default
other Hooks

Another Hooks instance to combine with this one.

required

Returns:

Type Description
Hooks

A new Hooks instance containing all handlers from both instances.

Example

hooks1 = Hooks() hooks2 = Hooks() hooks1.on("completion:kwargs", lambda **kw: print("Hook 1")) hooks2.on("completion:kwargs", lambda **kw: print("Hook 2")) combined = hooks1 + hooks2 combined.emit_completion_arguments() # Prints both "Hook 1" and "Hook 2"

Source code in instructor/v2/core/hooks.py
def __add__(self, other: Hooks) -> Hooks:
    """
    Combine two Hooks instances into a new one.

    This creates a new Hooks instance that contains all handlers from both
    the current instance and the other instance. Handlers are combined by
    appending the other's handlers after the current instance's handlers.

    Args:
        other: Another Hooks instance to combine with this one.

    Returns:
        A new Hooks instance containing all handlers from both instances.

    Example:
        >>> hooks1 = Hooks()
        >>> hooks2 = Hooks()
        >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
        >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
        >>> combined = hooks1 + hooks2
        >>> combined.emit_completion_arguments()  # Prints both "Hook 1" and "Hook 2"
    """
    if not isinstance(other, Hooks):
        return NotImplemented

    combined = Hooks()

    # Copy handlers from self
    for hook_name, handlers in self._handlers.items():
        combined._handlers[hook_name].extend(handlers.copy())

    # Add handlers from other
    for hook_name, handlers in other._handlers.items():
        combined._handlers[hook_name].extend(handlers.copy())

    return combined

__iadd__(other)

Add another Hooks instance to this one in-place.

This modifies the current instance by adding all handlers from the other instance. The other instance's handlers are appended after the current instance's handlers for each event type.

Parameters:

Name Type Description Default
other Hooks

Another Hooks instance to add to this one.

required

Returns:

Type Description
Hooks

This Hooks instance (for method chaining).

Example

hooks1 = Hooks() hooks2 = Hooks() hooks1.on("completion:kwargs", lambda **kw: print("Hook 1")) hooks2.on("completion:kwargs", lambda **kw: print("Hook 2")) hooks1 += hooks2 hooks1.emit_completion_arguments() # Prints both "Hook 1" and "Hook 2"

Source code in instructor/v2/core/hooks.py
def __iadd__(self, other: Hooks) -> Hooks:
    """
    Add another Hooks instance to this one in-place.

    This modifies the current instance by adding all handlers from the other
    instance. The other instance's handlers are appended after the current
    instance's handlers for each event type.

    Args:
        other: Another Hooks instance to add to this one.

    Returns:
        This Hooks instance (for method chaining).

    Example:
        >>> hooks1 = Hooks()
        >>> hooks2 = Hooks()
        >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
        >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
        >>> hooks1 += hooks2
        >>> hooks1.emit_completion_arguments()  # Prints both "Hook 1" and "Hook 2"
    """
    if not isinstance(other, Hooks):
        return NotImplemented

    # Add handlers from other to self
    for hook_name, handlers in other._handlers.items():
        self._handlers[hook_name].extend(handlers.copy())

    return self

__init__()

Initialize the hooks container.

Source code in instructor/v2/core/hooks.py
def __init__(self) -> None:
    """Initialize the hooks container."""
    self._handlers: defaultdict[HookName, list[HandlerType]] = defaultdict(list)

clear(hook_name=None)

Clear handlers for a specific event or all events.

Parameters:

Name Type Description Default
hook_name HookNameType | None

The name of the event to clear handlers for. If None, all handlers are cleared.

None
Source code in instructor/v2/core/hooks.py
def clear(
    self,
    hook_name: HookNameType | None = None,
) -> None:
    """
    Clear handlers for a specific event or all events.

    Args:
        hook_name: The name of the event to clear handlers for.
                  If None, all handlers are cleared.
    """
    if hook_name is not None:
        hook_name = self.get_hook_name(hook_name)
        self._handlers.pop(hook_name, None)
    else:
        self._handlers.clear()

combine(*hooks_instances) classmethod

Combine multiple Hooks instances into a new one.

This class method creates a new Hooks instance that contains all handlers from all provided instances. Handlers are combined in the order of the provided instances.

Parameters:

Name Type Description Default
*hooks_instances Hooks

Variable number of Hooks instances to combine.

()

Returns:

Type Description
Hooks

A new Hooks instance containing all handlers from all instances.

Example

hooks1 = Hooks() hooks2 = Hooks() hooks3 = Hooks() hooks1.on("completion:kwargs", lambda **kw: print("Hook 1")) hooks2.on("completion:kwargs", lambda **kw: print("Hook 2")) hooks3.on("completion:kwargs", lambda **kw: print("Hook 3")) combined = Hooks.combine(hooks1, hooks2, hooks3) combined.emit_completion_arguments() # Prints all three hooks

Source code in instructor/v2/core/hooks.py
@classmethod
def combine(cls, *hooks_instances: Hooks) -> Hooks:
    """
    Combine multiple Hooks instances into a new one.

    This class method creates a new Hooks instance that contains all handlers
    from all provided instances. Handlers are combined in the order of the
    provided instances.

    Args:
        *hooks_instances: Variable number of Hooks instances to combine.

    Returns:
        A new Hooks instance containing all handlers from all instances.

    Example:
        >>> hooks1 = Hooks()
        >>> hooks2 = Hooks()
        >>> hooks3 = Hooks()
        >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
        >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
        >>> hooks3.on("completion:kwargs", lambda **kw: print("Hook 3"))
        >>> combined = Hooks.combine(hooks1, hooks2, hooks3)
        >>> combined.emit_completion_arguments()  # Prints all three hooks
    """
    combined = cls()

    for hooks_instance in hooks_instances:
        if not isinstance(hooks_instance, cls):
            raise TypeError(f"Expected Hooks instance, got {type(hooks_instance)}")
        combined += hooks_instance

    return combined

copy()

Create a deep copy of this Hooks instance.

Returns:

Type Description
Hooks

A new Hooks instance with all the same handlers.

Example

original = Hooks() original.on("completion:kwargs", lambda **kw: print("Hook")) copy = original.copy() copy.emit_completion_arguments() # Prints "Hook"

Source code in instructor/v2/core/hooks.py
def copy(self) -> Hooks:
    """
    Create a deep copy of this Hooks instance.

    Returns:
        A new Hooks instance with all the same handlers.

    Example:
        >>> original = Hooks()
        >>> original.on("completion:kwargs", lambda **kw: print("Hook"))
        >>> copy = original.copy()
        >>> copy.emit_completion_arguments()  # Prints "Hook"
    """
    new_hooks = Hooks()
    for hook_name, handlers in self._handlers.items():
        new_hooks._handlers[hook_name].extend(handlers.copy())
    return new_hooks

emit(hook_name, *args, **kwargs)

Generic method to emit events for any hook type.

Parameters:

Name Type Description Default
hook_name HookName

The hook to emit

required
*args Any

Positional arguments to pass to handlers

()
**kwargs Any

Keyword arguments to pass to handlers

{}
Source code in instructor/v2/core/hooks.py
def emit(self, hook_name: HookName, *args: Any, **kwargs: Any) -> None:
    """
    Generic method to emit events for any hook type.

    Args:
        hook_name: The hook to emit
        *args: Positional arguments to pass to handlers
        **kwargs: Keyword arguments to pass to handlers
    """
    for handler in self._handlers[hook_name]:
        try:
            if kwargs:
                try:
                    sig = inspect.signature(handler)
                    sig.bind(*args, **kwargs)
                except TypeError:
                    handler(*args)  # type: ignore
                    continue
            handler(*args, **kwargs)  # type: ignore
        except Exception:
            error_traceback = traceback.format_exc()
            warnings.warn(
                f"Error in {hook_name.value} handler:\n{error_traceback}",
                stacklevel=2,
            )

emit_completion_arguments(*args, **kwargs)

Emit a completion arguments event.

Parameters:

Name Type Description Default
*args Any

Positional arguments to pass to handlers

()
**kwargs Any

Keyword arguments to pass to handlers

{}
Source code in instructor/v2/core/hooks.py
def emit_completion_arguments(self, *args: Any, **kwargs: Any) -> None:
    """
    Emit a completion arguments event.

    Args:
        *args: Positional arguments to pass to handlers
        **kwargs: Keyword arguments to pass to handlers
    """
    self.emit(HookName.COMPLETION_KWARGS, *args, **kwargs)

emit_completion_error(error, **kwargs)

Emit a completion error event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
**kwargs Any

Optional metadata (attempt_number, max_attempts, is_last_attempt)

{}
Source code in instructor/v2/core/hooks.py
def emit_completion_error(self, error: Exception, **kwargs: Any) -> None:
    """
    Emit a completion error event.

    Args:
        error: The exception to pass to handlers
        **kwargs: Optional metadata (attempt_number, max_attempts, is_last_attempt)
    """
    self.emit(HookName.COMPLETION_ERROR, error, **kwargs)

emit_completion_last_attempt(error, **kwargs)

Emit a completion last attempt event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
**kwargs Any

Optional metadata (attempt_number, max_attempts, is_last_attempt)

{}
Source code in instructor/v2/core/hooks.py
def emit_completion_last_attempt(self, error: Exception, **kwargs: Any) -> None:
    """
    Emit a completion last attempt event.

    Args:
        error: The exception to pass to handlers
        **kwargs: Optional metadata (attempt_number, max_attempts, is_last_attempt)
    """
    self.emit(HookName.COMPLETION_LAST_ATTEMPT, error, **kwargs)

emit_completion_response(response)

Emit a completion response event.

Parameters:

Name Type Description Default
response Any

The completion response to pass to handlers

required
Source code in instructor/v2/core/hooks.py
def emit_completion_response(self, response: Any) -> None:
    """
    Emit a completion response event.

    Args:
        response: The completion response to pass to handlers
    """
    self.emit(HookName.COMPLETION_RESPONSE, response)

emit_parse_error(error)

Emit a parse error event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
Source code in instructor/v2/core/hooks.py
def emit_parse_error(self, error: Exception) -> None:
    """
    Emit a parse error event.

    Args:
        error: The exception to pass to handlers
    """
    self.emit(HookName.PARSE_ERROR, error)

get_hook_name(hook_name)

Convert a string hook name to its corresponding enum value.

Parameters:

Name Type Description Default
hook_name HookNameType

Either a HookName enum value or string representation.

required

Returns:

Type Description
HookName

The corresponding HookName enum value.

Raises:

Type Description
ValueError

If the string doesn't match any HookName enum value.

Source code in instructor/v2/core/hooks.py
def get_hook_name(self, hook_name: HookNameType) -> HookName:
    """
    Convert a string hook name to its corresponding enum value.

    Args:
        hook_name: Either a HookName enum value or string representation.

    Returns:
        The corresponding HookName enum value.

    Raises:
        ValueError: If the string doesn't match any HookName enum value.
    """
    if isinstance(hook_name, str):
        try:
            return HookName(hook_name)
        except ValueError as err:
            raise ValueError(f"Invalid hook name: {hook_name}") from err
    return hook_name

off(hook_name, handler)

Remove a specific handler from an event.

Parameters:

Name Type Description Default
hook_name HookNameType

The name of the hook.

required
handler HandlerType

The handler to remove.

required
Source code in instructor/v2/core/hooks.py
def off(
    self,
    hook_name: HookNameType,
    handler: HandlerType,
) -> None:
    """
    Remove a specific handler from an event.

    Args:
        hook_name: The name of the hook.
        handler: The handler to remove.
    """
    hook_name = self.get_hook_name(hook_name)
    if hook_name in self._handlers:
        if handler in self._handlers[hook_name]:
            self._handlers[hook_name].remove(handler)
            if not self._handlers[hook_name]:
                del self._handlers[hook_name]

on(hook_name, handler)

Register an event handler for a specific event.

This method allows you to attach a handler function to a specific event. When the event is emitted, all registered handlers for that event will be called.

Parameters:

Name Type Description Default
hook_name HookNameType

The event to listen for. This can be either a HookName enum value or a string representation of the event name.

required
handler HandlerType

The function to be called when the event is emitted.

required

Raises:

Type Description
ValueError

If the hook_name is not a valid HookName enum or string representation.

Example

def on_completion_kwargs(*args: Any, **kwargs: Any) -> None: ... print(f"Completion kwargs: {args}, {kwargs}") hooks = Hooks() hooks.on(HookName.COMPLETION_KWARGS, on_completion_kwargs) hooks.emit_completion_arguments(model="gpt-3.5-turbo", temperature=0.7) Completion kwargs: (), {'model': 'gpt-3.5-turbo', 'temperature': 0.7}

Source code in instructor/v2/core/hooks.py
def on(
    self,
    hook_name: HookNameType,
    handler: HandlerType,
) -> None:
    """
    Register an event handler for a specific event.

    This method allows you to attach a handler function to a specific event.
    When the event is emitted, all registered handlers for that event will be called.

    Args:
        hook_name: The event to listen for. This can be either a HookName enum
                   value or a string representation of the event name.
        handler: The function to be called when the event is emitted.

    Raises:
        ValueError: If the hook_name is not a valid HookName enum or string representation.

    Example:
        >>> def on_completion_kwargs(*args: Any, **kwargs: Any) -> None:
        ...     print(f"Completion kwargs: {args}, {kwargs}")
        >>> hooks = Hooks()
        >>> hooks.on(HookName.COMPLETION_KWARGS, on_completion_kwargs)
        >>> hooks.emit_completion_arguments(model="gpt-3.5-turbo", temperature=0.7)
        Completion kwargs: (), {'model': 'gpt-3.5-turbo', 'temperature': 0.7}
    """
    hook_name = self.get_hook_name(hook_name)
    self._handlers[hook_name].append(handler)

ParseErrorHandler

Bases: Protocol

Protocol for parse error handlers.

Source code in instructor/v2/core/hooks.py
class ParseErrorHandler(Protocol):
    """Protocol for parse error handlers."""

    def __call__(self, error: Exception) -> None: ...

Hooks class for handling and emitting events related to completion processes.

This class provides a mechanism to register event handlers and emit events for various stages of the completion process.

Source code in instructor/v2/core/hooks.py
class Hooks:
    """
    Hooks class for handling and emitting events related to completion processes.

    This class provides a mechanism to register event handlers and emit events
    for various stages of the completion process.
    """

    def __init__(self) -> None:
        """Initialize the hooks container."""
        self._handlers: defaultdict[HookName, list[HandlerType]] = defaultdict(list)

    def on(
        self,
        hook_name: HookNameType,
        handler: HandlerType,
    ) -> None:
        """
        Register an event handler for a specific event.

        This method allows you to attach a handler function to a specific event.
        When the event is emitted, all registered handlers for that event will be called.

        Args:
            hook_name: The event to listen for. This can be either a HookName enum
                       value or a string representation of the event name.
            handler: The function to be called when the event is emitted.

        Raises:
            ValueError: If the hook_name is not a valid HookName enum or string representation.

        Example:
            >>> def on_completion_kwargs(*args: Any, **kwargs: Any) -> None:
            ...     print(f"Completion kwargs: {args}, {kwargs}")
            >>> hooks = Hooks()
            >>> hooks.on(HookName.COMPLETION_KWARGS, on_completion_kwargs)
            >>> hooks.emit_completion_arguments(model="gpt-3.5-turbo", temperature=0.7)
            Completion kwargs: (), {'model': 'gpt-3.5-turbo', 'temperature': 0.7}
        """
        hook_name = self.get_hook_name(hook_name)
        self._handlers[hook_name].append(handler)

    def get_hook_name(self, hook_name: HookNameType) -> HookName:
        """
        Convert a string hook name to its corresponding enum value.

        Args:
            hook_name: Either a HookName enum value or string representation.

        Returns:
            The corresponding HookName enum value.

        Raises:
            ValueError: If the string doesn't match any HookName enum value.
        """
        if isinstance(hook_name, str):
            try:
                return HookName(hook_name)
            except ValueError as err:
                raise ValueError(f"Invalid hook name: {hook_name}") from err
        return hook_name

    def emit(self, hook_name: HookName, *args: Any, **kwargs: Any) -> None:
        """
        Generic method to emit events for any hook type.

        Args:
            hook_name: The hook to emit
            *args: Positional arguments to pass to handlers
            **kwargs: Keyword arguments to pass to handlers
        """
        for handler in self._handlers[hook_name]:
            try:
                if kwargs:
                    try:
                        sig = inspect.signature(handler)
                        sig.bind(*args, **kwargs)
                    except TypeError:
                        handler(*args)  # type: ignore
                        continue
                handler(*args, **kwargs)  # type: ignore
            except Exception:
                error_traceback = traceback.format_exc()
                warnings.warn(
                    f"Error in {hook_name.value} handler:\n{error_traceback}",
                    stacklevel=2,
                )

    def emit_completion_arguments(self, *args: Any, **kwargs: Any) -> None:
        """
        Emit a completion arguments event.

        Args:
            *args: Positional arguments to pass to handlers
            **kwargs: Keyword arguments to pass to handlers
        """
        self.emit(HookName.COMPLETION_KWARGS, *args, **kwargs)

    def emit_completion_response(self, response: Any) -> None:
        """
        Emit a completion response event.

        Args:
            response: The completion response to pass to handlers
        """
        self.emit(HookName.COMPLETION_RESPONSE, response)

    def emit_completion_error(self, error: Exception, **kwargs: Any) -> None:
        """
        Emit a completion error event.

        Args:
            error: The exception to pass to handlers
            **kwargs: Optional metadata (attempt_number, max_attempts, is_last_attempt)
        """
        self.emit(HookName.COMPLETION_ERROR, error, **kwargs)

    def emit_completion_last_attempt(self, error: Exception, **kwargs: Any) -> None:
        """
        Emit a completion last attempt event.

        Args:
            error: The exception to pass to handlers
            **kwargs: Optional metadata (attempt_number, max_attempts, is_last_attempt)
        """
        self.emit(HookName.COMPLETION_LAST_ATTEMPT, error, **kwargs)

    def emit_parse_error(self, error: Exception) -> None:
        """
        Emit a parse error event.

        Args:
            error: The exception to pass to handlers
        """
        self.emit(HookName.PARSE_ERROR, error)

    def off(
        self,
        hook_name: HookNameType,
        handler: HandlerType,
    ) -> None:
        """
        Remove a specific handler from an event.

        Args:
            hook_name: The name of the hook.
            handler: The handler to remove.
        """
        hook_name = self.get_hook_name(hook_name)
        if hook_name in self._handlers:
            if handler in self._handlers[hook_name]:
                self._handlers[hook_name].remove(handler)
                if not self._handlers[hook_name]:
                    del self._handlers[hook_name]

    def clear(
        self,
        hook_name: HookNameType | None = None,
    ) -> None:
        """
        Clear handlers for a specific event or all events.

        Args:
            hook_name: The name of the event to clear handlers for.
                      If None, all handlers are cleared.
        """
        if hook_name is not None:
            hook_name = self.get_hook_name(hook_name)
            self._handlers.pop(hook_name, None)
        else:
            self._handlers.clear()

    def __add__(self, other: Hooks) -> Hooks:
        """
        Combine two Hooks instances into a new one.

        This creates a new Hooks instance that contains all handlers from both
        the current instance and the other instance. Handlers are combined by
        appending the other's handlers after the current instance's handlers.

        Args:
            other: Another Hooks instance to combine with this one.

        Returns:
            A new Hooks instance containing all handlers from both instances.

        Example:
            >>> hooks1 = Hooks()
            >>> hooks2 = Hooks()
            >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
            >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
            >>> combined = hooks1 + hooks2
            >>> combined.emit_completion_arguments()  # Prints both "Hook 1" and "Hook 2"
        """
        if not isinstance(other, Hooks):
            return NotImplemented

        combined = Hooks()

        # Copy handlers from self
        for hook_name, handlers in self._handlers.items():
            combined._handlers[hook_name].extend(handlers.copy())

        # Add handlers from other
        for hook_name, handlers in other._handlers.items():
            combined._handlers[hook_name].extend(handlers.copy())

        return combined

    def __iadd__(self, other: Hooks) -> Hooks:
        """
        Add another Hooks instance to this one in-place.

        This modifies the current instance by adding all handlers from the other
        instance. The other instance's handlers are appended after the current
        instance's handlers for each event type.

        Args:
            other: Another Hooks instance to add to this one.

        Returns:
            This Hooks instance (for method chaining).

        Example:
            >>> hooks1 = Hooks()
            >>> hooks2 = Hooks()
            >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
            >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
            >>> hooks1 += hooks2
            >>> hooks1.emit_completion_arguments()  # Prints both "Hook 1" and "Hook 2"
        """
        if not isinstance(other, Hooks):
            return NotImplemented

        # Add handlers from other to self
        for hook_name, handlers in other._handlers.items():
            self._handlers[hook_name].extend(handlers.copy())

        return self

    @classmethod
    def combine(cls, *hooks_instances: Hooks) -> Hooks:
        """
        Combine multiple Hooks instances into a new one.

        This class method creates a new Hooks instance that contains all handlers
        from all provided instances. Handlers are combined in the order of the
        provided instances.

        Args:
            *hooks_instances: Variable number of Hooks instances to combine.

        Returns:
            A new Hooks instance containing all handlers from all instances.

        Example:
            >>> hooks1 = Hooks()
            >>> hooks2 = Hooks()
            >>> hooks3 = Hooks()
            >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
            >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
            >>> hooks3.on("completion:kwargs", lambda **kw: print("Hook 3"))
            >>> combined = Hooks.combine(hooks1, hooks2, hooks3)
            >>> combined.emit_completion_arguments()  # Prints all three hooks
        """
        combined = cls()

        for hooks_instance in hooks_instances:
            if not isinstance(hooks_instance, cls):
                raise TypeError(f"Expected Hooks instance, got {type(hooks_instance)}")
            combined += hooks_instance

        return combined

    def copy(self) -> Hooks:
        """
        Create a deep copy of this Hooks instance.

        Returns:
            A new Hooks instance with all the same handlers.

        Example:
            >>> original = Hooks()
            >>> original.on("completion:kwargs", lambda **kw: print("Hook"))
            >>> copy = original.copy()
            >>> copy.emit_completion_arguments()  # Prints "Hook"
        """
        new_hooks = Hooks()
        for hook_name, handlers in self._handlers.items():
            new_hooks._handlers[hook_name].extend(handlers.copy())
        return new_hooks

__add__(other)

Combine two Hooks instances into a new one.

This creates a new Hooks instance that contains all handlers from both the current instance and the other instance. Handlers are combined by appending the other's handlers after the current instance's handlers.

Parameters:

Name Type Description Default
other Hooks

Another Hooks instance to combine with this one.

required

Returns:

Type Description
Hooks

A new Hooks instance containing all handlers from both instances.

Example

hooks1 = Hooks() hooks2 = Hooks() hooks1.on("completion:kwargs", lambda **kw: print("Hook 1")) hooks2.on("completion:kwargs", lambda **kw: print("Hook 2")) combined = hooks1 + hooks2 combined.emit_completion_arguments() # Prints both "Hook 1" and "Hook 2"

Source code in instructor/v2/core/hooks.py
def __add__(self, other: Hooks) -> Hooks:
    """
    Combine two Hooks instances into a new one.

    This creates a new Hooks instance that contains all handlers from both
    the current instance and the other instance. Handlers are combined by
    appending the other's handlers after the current instance's handlers.

    Args:
        other: Another Hooks instance to combine with this one.

    Returns:
        A new Hooks instance containing all handlers from both instances.

    Example:
        >>> hooks1 = Hooks()
        >>> hooks2 = Hooks()
        >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
        >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
        >>> combined = hooks1 + hooks2
        >>> combined.emit_completion_arguments()  # Prints both "Hook 1" and "Hook 2"
    """
    if not isinstance(other, Hooks):
        return NotImplemented

    combined = Hooks()

    # Copy handlers from self
    for hook_name, handlers in self._handlers.items():
        combined._handlers[hook_name].extend(handlers.copy())

    # Add handlers from other
    for hook_name, handlers in other._handlers.items():
        combined._handlers[hook_name].extend(handlers.copy())

    return combined

__iadd__(other)

Add another Hooks instance to this one in-place.

This modifies the current instance by adding all handlers from the other instance. The other instance's handlers are appended after the current instance's handlers for each event type.

Parameters:

Name Type Description Default
other Hooks

Another Hooks instance to add to this one.

required

Returns:

Type Description
Hooks

This Hooks instance (for method chaining).

Example

hooks1 = Hooks() hooks2 = Hooks() hooks1.on("completion:kwargs", lambda **kw: print("Hook 1")) hooks2.on("completion:kwargs", lambda **kw: print("Hook 2")) hooks1 += hooks2 hooks1.emit_completion_arguments() # Prints both "Hook 1" and "Hook 2"

Source code in instructor/v2/core/hooks.py
def __iadd__(self, other: Hooks) -> Hooks:
    """
    Add another Hooks instance to this one in-place.

    This modifies the current instance by adding all handlers from the other
    instance. The other instance's handlers are appended after the current
    instance's handlers for each event type.

    Args:
        other: Another Hooks instance to add to this one.

    Returns:
        This Hooks instance (for method chaining).

    Example:
        >>> hooks1 = Hooks()
        >>> hooks2 = Hooks()
        >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
        >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
        >>> hooks1 += hooks2
        >>> hooks1.emit_completion_arguments()  # Prints both "Hook 1" and "Hook 2"
    """
    if not isinstance(other, Hooks):
        return NotImplemented

    # Add handlers from other to self
    for hook_name, handlers in other._handlers.items():
        self._handlers[hook_name].extend(handlers.copy())

    return self

__init__()

Initialize the hooks container.

Source code in instructor/v2/core/hooks.py
def __init__(self) -> None:
    """Initialize the hooks container."""
    self._handlers: defaultdict[HookName, list[HandlerType]] = defaultdict(list)

clear(hook_name=None)

Clear handlers for a specific event or all events.

Parameters:

Name Type Description Default
hook_name HookNameType | None

The name of the event to clear handlers for. If None, all handlers are cleared.

None
Source code in instructor/v2/core/hooks.py
def clear(
    self,
    hook_name: HookNameType | None = None,
) -> None:
    """
    Clear handlers for a specific event or all events.

    Args:
        hook_name: The name of the event to clear handlers for.
                  If None, all handlers are cleared.
    """
    if hook_name is not None:
        hook_name = self.get_hook_name(hook_name)
        self._handlers.pop(hook_name, None)
    else:
        self._handlers.clear()

combine(*hooks_instances) classmethod

Combine multiple Hooks instances into a new one.

This class method creates a new Hooks instance that contains all handlers from all provided instances. Handlers are combined in the order of the provided instances.

Parameters:

Name Type Description Default
*hooks_instances Hooks

Variable number of Hooks instances to combine.

()

Returns:

Type Description
Hooks

A new Hooks instance containing all handlers from all instances.

Example

hooks1 = Hooks() hooks2 = Hooks() hooks3 = Hooks() hooks1.on("completion:kwargs", lambda **kw: print("Hook 1")) hooks2.on("completion:kwargs", lambda **kw: print("Hook 2")) hooks3.on("completion:kwargs", lambda **kw: print("Hook 3")) combined = Hooks.combine(hooks1, hooks2, hooks3) combined.emit_completion_arguments() # Prints all three hooks

Source code in instructor/v2/core/hooks.py
@classmethod
def combine(cls, *hooks_instances: Hooks) -> Hooks:
    """
    Combine multiple Hooks instances into a new one.

    This class method creates a new Hooks instance that contains all handlers
    from all provided instances. Handlers are combined in the order of the
    provided instances.

    Args:
        *hooks_instances: Variable number of Hooks instances to combine.

    Returns:
        A new Hooks instance containing all handlers from all instances.

    Example:
        >>> hooks1 = Hooks()
        >>> hooks2 = Hooks()
        >>> hooks3 = Hooks()
        >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
        >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
        >>> hooks3.on("completion:kwargs", lambda **kw: print("Hook 3"))
        >>> combined = Hooks.combine(hooks1, hooks2, hooks3)
        >>> combined.emit_completion_arguments()  # Prints all three hooks
    """
    combined = cls()

    for hooks_instance in hooks_instances:
        if not isinstance(hooks_instance, cls):
            raise TypeError(f"Expected Hooks instance, got {type(hooks_instance)}")
        combined += hooks_instance

    return combined

copy()

Create a deep copy of this Hooks instance.

Returns:

Type Description
Hooks

A new Hooks instance with all the same handlers.

Example

original = Hooks() original.on("completion:kwargs", lambda **kw: print("Hook")) copy = original.copy() copy.emit_completion_arguments() # Prints "Hook"

Source code in instructor/v2/core/hooks.py
def copy(self) -> Hooks:
    """
    Create a deep copy of this Hooks instance.

    Returns:
        A new Hooks instance with all the same handlers.

    Example:
        >>> original = Hooks()
        >>> original.on("completion:kwargs", lambda **kw: print("Hook"))
        >>> copy = original.copy()
        >>> copy.emit_completion_arguments()  # Prints "Hook"
    """
    new_hooks = Hooks()
    for hook_name, handlers in self._handlers.items():
        new_hooks._handlers[hook_name].extend(handlers.copy())
    return new_hooks

emit(hook_name, *args, **kwargs)

Generic method to emit events for any hook type.

Parameters:

Name Type Description Default
hook_name HookName

The hook to emit

required
*args Any

Positional arguments to pass to handlers

()
**kwargs Any

Keyword arguments to pass to handlers

{}
Source code in instructor/v2/core/hooks.py
def emit(self, hook_name: HookName, *args: Any, **kwargs: Any) -> None:
    """
    Generic method to emit events for any hook type.

    Args:
        hook_name: The hook to emit
        *args: Positional arguments to pass to handlers
        **kwargs: Keyword arguments to pass to handlers
    """
    for handler in self._handlers[hook_name]:
        try:
            if kwargs:
                try:
                    sig = inspect.signature(handler)
                    sig.bind(*args, **kwargs)
                except TypeError:
                    handler(*args)  # type: ignore
                    continue
            handler(*args, **kwargs)  # type: ignore
        except Exception:
            error_traceback = traceback.format_exc()
            warnings.warn(
                f"Error in {hook_name.value} handler:\n{error_traceback}",
                stacklevel=2,
            )

emit_completion_arguments(*args, **kwargs)

Emit a completion arguments event.

Parameters:

Name Type Description Default
*args Any

Positional arguments to pass to handlers

()
**kwargs Any

Keyword arguments to pass to handlers

{}
Source code in instructor/v2/core/hooks.py
def emit_completion_arguments(self, *args: Any, **kwargs: Any) -> None:
    """
    Emit a completion arguments event.

    Args:
        *args: Positional arguments to pass to handlers
        **kwargs: Keyword arguments to pass to handlers
    """
    self.emit(HookName.COMPLETION_KWARGS, *args, **kwargs)

emit_completion_error(error, **kwargs)

Emit a completion error event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
**kwargs Any

Optional metadata (attempt_number, max_attempts, is_last_attempt)

{}
Source code in instructor/v2/core/hooks.py
def emit_completion_error(self, error: Exception, **kwargs: Any) -> None:
    """
    Emit a completion error event.

    Args:
        error: The exception to pass to handlers
        **kwargs: Optional metadata (attempt_number, max_attempts, is_last_attempt)
    """
    self.emit(HookName.COMPLETION_ERROR, error, **kwargs)

emit_completion_last_attempt(error, **kwargs)

Emit a completion last attempt event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
**kwargs Any

Optional metadata (attempt_number, max_attempts, is_last_attempt)

{}
Source code in instructor/v2/core/hooks.py
def emit_completion_last_attempt(self, error: Exception, **kwargs: Any) -> None:
    """
    Emit a completion last attempt event.

    Args:
        error: The exception to pass to handlers
        **kwargs: Optional metadata (attempt_number, max_attempts, is_last_attempt)
    """
    self.emit(HookName.COMPLETION_LAST_ATTEMPT, error, **kwargs)

emit_completion_response(response)

Emit a completion response event.

Parameters:

Name Type Description Default
response Any

The completion response to pass to handlers

required
Source code in instructor/v2/core/hooks.py
def emit_completion_response(self, response: Any) -> None:
    """
    Emit a completion response event.

    Args:
        response: The completion response to pass to handlers
    """
    self.emit(HookName.COMPLETION_RESPONSE, response)

emit_parse_error(error)

Emit a parse error event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
Source code in instructor/v2/core/hooks.py
def emit_parse_error(self, error: Exception) -> None:
    """
    Emit a parse error event.

    Args:
        error: The exception to pass to handlers
    """
    self.emit(HookName.PARSE_ERROR, error)

get_hook_name(hook_name)

Convert a string hook name to its corresponding enum value.

Parameters:

Name Type Description Default
hook_name HookNameType

Either a HookName enum value or string representation.

required

Returns:

Type Description
HookName

The corresponding HookName enum value.

Raises:

Type Description
ValueError

If the string doesn't match any HookName enum value.

Source code in instructor/v2/core/hooks.py
def get_hook_name(self, hook_name: HookNameType) -> HookName:
    """
    Convert a string hook name to its corresponding enum value.

    Args:
        hook_name: Either a HookName enum value or string representation.

    Returns:
        The corresponding HookName enum value.

    Raises:
        ValueError: If the string doesn't match any HookName enum value.
    """
    if isinstance(hook_name, str):
        try:
            return HookName(hook_name)
        except ValueError as err:
            raise ValueError(f"Invalid hook name: {hook_name}") from err
    return hook_name

off(hook_name, handler)

Remove a specific handler from an event.

Parameters:

Name Type Description Default
hook_name HookNameType

The name of the hook.

required
handler HandlerType

The handler to remove.

required
Source code in instructor/v2/core/hooks.py
def off(
    self,
    hook_name: HookNameType,
    handler: HandlerType,
) -> None:
    """
    Remove a specific handler from an event.

    Args:
        hook_name: The name of the hook.
        handler: The handler to remove.
    """
    hook_name = self.get_hook_name(hook_name)
    if hook_name in self._handlers:
        if handler in self._handlers[hook_name]:
            self._handlers[hook_name].remove(handler)
            if not self._handlers[hook_name]:
                del self._handlers[hook_name]

on(hook_name, handler)

Register an event handler for a specific event.

This method allows you to attach a handler function to a specific event. When the event is emitted, all registered handlers for that event will be called.

Parameters:

Name Type Description Default
hook_name HookNameType

The event to listen for. This can be either a HookName enum value or a string representation of the event name.

required
handler HandlerType

The function to be called when the event is emitted.

required

Raises:

Type Description
ValueError

If the hook_name is not a valid HookName enum or string representation.

Example

def on_completion_kwargs(*args: Any, **kwargs: Any) -> None: ... print(f"Completion kwargs: {args}, {kwargs}") hooks = Hooks() hooks.on(HookName.COMPLETION_KWARGS, on_completion_kwargs) hooks.emit_completion_arguments(model="gpt-3.5-turbo", temperature=0.7) Completion kwargs: (), {'model': 'gpt-3.5-turbo', 'temperature': 0.7}

Source code in instructor/v2/core/hooks.py
def on(
    self,
    hook_name: HookNameType,
    handler: HandlerType,
) -> None:
    """
    Register an event handler for a specific event.

    This method allows you to attach a handler function to a specific event.
    When the event is emitted, all registered handlers for that event will be called.

    Args:
        hook_name: The event to listen for. This can be either a HookName enum
                   value or a string representation of the event name.
        handler: The function to be called when the event is emitted.

    Raises:
        ValueError: If the hook_name is not a valid HookName enum or string representation.

    Example:
        >>> def on_completion_kwargs(*args: Any, **kwargs: Any) -> None:
        ...     print(f"Completion kwargs: {args}, {kwargs}")
        >>> hooks = Hooks()
        >>> hooks.on(HookName.COMPLETION_KWARGS, on_completion_kwargs)
        >>> hooks.emit_completion_arguments(model="gpt-3.5-turbo", temperature=0.7)
        Completion kwargs: (), {'model': 'gpt-3.5-turbo', 'temperature': 0.7}
    """
    hook_name = self.get_hook_name(hook_name)
    self._handlers[hook_name].append(handler)

Bases: Enum

Source code in instructor/v2/core/hooks.py
class HookName(Enum):
    COMPLETION_KWARGS = "completion:kwargs"
    COMPLETION_RESPONSE = "completion:response"
    COMPLETION_ERROR = "completion:error"
    COMPLETION_LAST_ATTEMPT = "completion:last_attempt"
    PARSE_ERROR = "parse:error"

Patch Functions

Decorators for patching LLM client methods.

Compatibility exports for v2-owned patch helpers.

Hooks

Hooks class for handling and emitting events related to completion processes.

This class provides a mechanism to register event handlers and emit events for various stages of the completion process.

Source code in instructor/v2/core/hooks.py
class Hooks:
    """
    Hooks class for handling and emitting events related to completion processes.

    This class provides a mechanism to register event handlers and emit events
    for various stages of the completion process.
    """

    def __init__(self) -> None:
        """Initialize the hooks container."""
        self._handlers: defaultdict[HookName, list[HandlerType]] = defaultdict(list)

    def on(
        self,
        hook_name: HookNameType,
        handler: HandlerType,
    ) -> None:
        """
        Register an event handler for a specific event.

        This method allows you to attach a handler function to a specific event.
        When the event is emitted, all registered handlers for that event will be called.

        Args:
            hook_name: The event to listen for. This can be either a HookName enum
                       value or a string representation of the event name.
            handler: The function to be called when the event is emitted.

        Raises:
            ValueError: If the hook_name is not a valid HookName enum or string representation.

        Example:
            >>> def on_completion_kwargs(*args: Any, **kwargs: Any) -> None:
            ...     print(f"Completion kwargs: {args}, {kwargs}")
            >>> hooks = Hooks()
            >>> hooks.on(HookName.COMPLETION_KWARGS, on_completion_kwargs)
            >>> hooks.emit_completion_arguments(model="gpt-3.5-turbo", temperature=0.7)
            Completion kwargs: (), {'model': 'gpt-3.5-turbo', 'temperature': 0.7}
        """
        hook_name = self.get_hook_name(hook_name)
        self._handlers[hook_name].append(handler)

    def get_hook_name(self, hook_name: HookNameType) -> HookName:
        """
        Convert a string hook name to its corresponding enum value.

        Args:
            hook_name: Either a HookName enum value or string representation.

        Returns:
            The corresponding HookName enum value.

        Raises:
            ValueError: If the string doesn't match any HookName enum value.
        """
        if isinstance(hook_name, str):
            try:
                return HookName(hook_name)
            except ValueError as err:
                raise ValueError(f"Invalid hook name: {hook_name}") from err
        return hook_name

    def emit(self, hook_name: HookName, *args: Any, **kwargs: Any) -> None:
        """
        Generic method to emit events for any hook type.

        Args:
            hook_name: The hook to emit
            *args: Positional arguments to pass to handlers
            **kwargs: Keyword arguments to pass to handlers
        """
        for handler in self._handlers[hook_name]:
            try:
                if kwargs:
                    try:
                        sig = inspect.signature(handler)
                        sig.bind(*args, **kwargs)
                    except TypeError:
                        handler(*args)  # type: ignore
                        continue
                handler(*args, **kwargs)  # type: ignore
            except Exception:
                error_traceback = traceback.format_exc()
                warnings.warn(
                    f"Error in {hook_name.value} handler:\n{error_traceback}",
                    stacklevel=2,
                )

    def emit_completion_arguments(self, *args: Any, **kwargs: Any) -> None:
        """
        Emit a completion arguments event.

        Args:
            *args: Positional arguments to pass to handlers
            **kwargs: Keyword arguments to pass to handlers
        """
        self.emit(HookName.COMPLETION_KWARGS, *args, **kwargs)

    def emit_completion_response(self, response: Any) -> None:
        """
        Emit a completion response event.

        Args:
            response: The completion response to pass to handlers
        """
        self.emit(HookName.COMPLETION_RESPONSE, response)

    def emit_completion_error(self, error: Exception, **kwargs: Any) -> None:
        """
        Emit a completion error event.

        Args:
            error: The exception to pass to handlers
            **kwargs: Optional metadata (attempt_number, max_attempts, is_last_attempt)
        """
        self.emit(HookName.COMPLETION_ERROR, error, **kwargs)

    def emit_completion_last_attempt(self, error: Exception, **kwargs: Any) -> None:
        """
        Emit a completion last attempt event.

        Args:
            error: The exception to pass to handlers
            **kwargs: Optional metadata (attempt_number, max_attempts, is_last_attempt)
        """
        self.emit(HookName.COMPLETION_LAST_ATTEMPT, error, **kwargs)

    def emit_parse_error(self, error: Exception) -> None:
        """
        Emit a parse error event.

        Args:
            error: The exception to pass to handlers
        """
        self.emit(HookName.PARSE_ERROR, error)

    def off(
        self,
        hook_name: HookNameType,
        handler: HandlerType,
    ) -> None:
        """
        Remove a specific handler from an event.

        Args:
            hook_name: The name of the hook.
            handler: The handler to remove.
        """
        hook_name = self.get_hook_name(hook_name)
        if hook_name in self._handlers:
            if handler in self._handlers[hook_name]:
                self._handlers[hook_name].remove(handler)
                if not self._handlers[hook_name]:
                    del self._handlers[hook_name]

    def clear(
        self,
        hook_name: HookNameType | None = None,
    ) -> None:
        """
        Clear handlers for a specific event or all events.

        Args:
            hook_name: The name of the event to clear handlers for.
                      If None, all handlers are cleared.
        """
        if hook_name is not None:
            hook_name = self.get_hook_name(hook_name)
            self._handlers.pop(hook_name, None)
        else:
            self._handlers.clear()

    def __add__(self, other: Hooks) -> Hooks:
        """
        Combine two Hooks instances into a new one.

        This creates a new Hooks instance that contains all handlers from both
        the current instance and the other instance. Handlers are combined by
        appending the other's handlers after the current instance's handlers.

        Args:
            other: Another Hooks instance to combine with this one.

        Returns:
            A new Hooks instance containing all handlers from both instances.

        Example:
            >>> hooks1 = Hooks()
            >>> hooks2 = Hooks()
            >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
            >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
            >>> combined = hooks1 + hooks2
            >>> combined.emit_completion_arguments()  # Prints both "Hook 1" and "Hook 2"
        """
        if not isinstance(other, Hooks):
            return NotImplemented

        combined = Hooks()

        # Copy handlers from self
        for hook_name, handlers in self._handlers.items():
            combined._handlers[hook_name].extend(handlers.copy())

        # Add handlers from other
        for hook_name, handlers in other._handlers.items():
            combined._handlers[hook_name].extend(handlers.copy())

        return combined

    def __iadd__(self, other: Hooks) -> Hooks:
        """
        Add another Hooks instance to this one in-place.

        This modifies the current instance by adding all handlers from the other
        instance. The other instance's handlers are appended after the current
        instance's handlers for each event type.

        Args:
            other: Another Hooks instance to add to this one.

        Returns:
            This Hooks instance (for method chaining).

        Example:
            >>> hooks1 = Hooks()
            >>> hooks2 = Hooks()
            >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
            >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
            >>> hooks1 += hooks2
            >>> hooks1.emit_completion_arguments()  # Prints both "Hook 1" and "Hook 2"
        """
        if not isinstance(other, Hooks):
            return NotImplemented

        # Add handlers from other to self
        for hook_name, handlers in other._handlers.items():
            self._handlers[hook_name].extend(handlers.copy())

        return self

    @classmethod
    def combine(cls, *hooks_instances: Hooks) -> Hooks:
        """
        Combine multiple Hooks instances into a new one.

        This class method creates a new Hooks instance that contains all handlers
        from all provided instances. Handlers are combined in the order of the
        provided instances.

        Args:
            *hooks_instances: Variable number of Hooks instances to combine.

        Returns:
            A new Hooks instance containing all handlers from all instances.

        Example:
            >>> hooks1 = Hooks()
            >>> hooks2 = Hooks()
            >>> hooks3 = Hooks()
            >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
            >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
            >>> hooks3.on("completion:kwargs", lambda **kw: print("Hook 3"))
            >>> combined = Hooks.combine(hooks1, hooks2, hooks3)
            >>> combined.emit_completion_arguments()  # Prints all three hooks
        """
        combined = cls()

        for hooks_instance in hooks_instances:
            if not isinstance(hooks_instance, cls):
                raise TypeError(f"Expected Hooks instance, got {type(hooks_instance)}")
            combined += hooks_instance

        return combined

    def copy(self) -> Hooks:
        """
        Create a deep copy of this Hooks instance.

        Returns:
            A new Hooks instance with all the same handlers.

        Example:
            >>> original = Hooks()
            >>> original.on("completion:kwargs", lambda **kw: print("Hook"))
            >>> copy = original.copy()
            >>> copy.emit_completion_arguments()  # Prints "Hook"
        """
        new_hooks = Hooks()
        for hook_name, handlers in self._handlers.items():
            new_hooks._handlers[hook_name].extend(handlers.copy())
        return new_hooks

__add__(other)

Combine two Hooks instances into a new one.

This creates a new Hooks instance that contains all handlers from both the current instance and the other instance. Handlers are combined by appending the other's handlers after the current instance's handlers.

Parameters:

Name Type Description Default
other Hooks

Another Hooks instance to combine with this one.

required

Returns:

Type Description
Hooks

A new Hooks instance containing all handlers from both instances.

Example

hooks1 = Hooks() hooks2 = Hooks() hooks1.on("completion:kwargs", lambda **kw: print("Hook 1")) hooks2.on("completion:kwargs", lambda **kw: print("Hook 2")) combined = hooks1 + hooks2 combined.emit_completion_arguments() # Prints both "Hook 1" and "Hook 2"

Source code in instructor/v2/core/hooks.py
def __add__(self, other: Hooks) -> Hooks:
    """
    Combine two Hooks instances into a new one.

    This creates a new Hooks instance that contains all handlers from both
    the current instance and the other instance. Handlers are combined by
    appending the other's handlers after the current instance's handlers.

    Args:
        other: Another Hooks instance to combine with this one.

    Returns:
        A new Hooks instance containing all handlers from both instances.

    Example:
        >>> hooks1 = Hooks()
        >>> hooks2 = Hooks()
        >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
        >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
        >>> combined = hooks1 + hooks2
        >>> combined.emit_completion_arguments()  # Prints both "Hook 1" and "Hook 2"
    """
    if not isinstance(other, Hooks):
        return NotImplemented

    combined = Hooks()

    # Copy handlers from self
    for hook_name, handlers in self._handlers.items():
        combined._handlers[hook_name].extend(handlers.copy())

    # Add handlers from other
    for hook_name, handlers in other._handlers.items():
        combined._handlers[hook_name].extend(handlers.copy())

    return combined

__iadd__(other)

Add another Hooks instance to this one in-place.

This modifies the current instance by adding all handlers from the other instance. The other instance's handlers are appended after the current instance's handlers for each event type.

Parameters:

Name Type Description Default
other Hooks

Another Hooks instance to add to this one.

required

Returns:

Type Description
Hooks

This Hooks instance (for method chaining).

Example

hooks1 = Hooks() hooks2 = Hooks() hooks1.on("completion:kwargs", lambda **kw: print("Hook 1")) hooks2.on("completion:kwargs", lambda **kw: print("Hook 2")) hooks1 += hooks2 hooks1.emit_completion_arguments() # Prints both "Hook 1" and "Hook 2"

Source code in instructor/v2/core/hooks.py
def __iadd__(self, other: Hooks) -> Hooks:
    """
    Add another Hooks instance to this one in-place.

    This modifies the current instance by adding all handlers from the other
    instance. The other instance's handlers are appended after the current
    instance's handlers for each event type.

    Args:
        other: Another Hooks instance to add to this one.

    Returns:
        This Hooks instance (for method chaining).

    Example:
        >>> hooks1 = Hooks()
        >>> hooks2 = Hooks()
        >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
        >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
        >>> hooks1 += hooks2
        >>> hooks1.emit_completion_arguments()  # Prints both "Hook 1" and "Hook 2"
    """
    if not isinstance(other, Hooks):
        return NotImplemented

    # Add handlers from other to self
    for hook_name, handlers in other._handlers.items():
        self._handlers[hook_name].extend(handlers.copy())

    return self

__init__()

Initialize the hooks container.

Source code in instructor/v2/core/hooks.py
def __init__(self) -> None:
    """Initialize the hooks container."""
    self._handlers: defaultdict[HookName, list[HandlerType]] = defaultdict(list)

clear(hook_name=None)

Clear handlers for a specific event or all events.

Parameters:

Name Type Description Default
hook_name HookNameType | None

The name of the event to clear handlers for. If None, all handlers are cleared.

None
Source code in instructor/v2/core/hooks.py
def clear(
    self,
    hook_name: HookNameType | None = None,
) -> None:
    """
    Clear handlers for a specific event or all events.

    Args:
        hook_name: The name of the event to clear handlers for.
                  If None, all handlers are cleared.
    """
    if hook_name is not None:
        hook_name = self.get_hook_name(hook_name)
        self._handlers.pop(hook_name, None)
    else:
        self._handlers.clear()

combine(*hooks_instances) classmethod

Combine multiple Hooks instances into a new one.

This class method creates a new Hooks instance that contains all handlers from all provided instances. Handlers are combined in the order of the provided instances.

Parameters:

Name Type Description Default
*hooks_instances Hooks

Variable number of Hooks instances to combine.

()

Returns:

Type Description
Hooks

A new Hooks instance containing all handlers from all instances.

Example

hooks1 = Hooks() hooks2 = Hooks() hooks3 = Hooks() hooks1.on("completion:kwargs", lambda **kw: print("Hook 1")) hooks2.on("completion:kwargs", lambda **kw: print("Hook 2")) hooks3.on("completion:kwargs", lambda **kw: print("Hook 3")) combined = Hooks.combine(hooks1, hooks2, hooks3) combined.emit_completion_arguments() # Prints all three hooks

Source code in instructor/v2/core/hooks.py
@classmethod
def combine(cls, *hooks_instances: Hooks) -> Hooks:
    """
    Combine multiple Hooks instances into a new one.

    This class method creates a new Hooks instance that contains all handlers
    from all provided instances. Handlers are combined in the order of the
    provided instances.

    Args:
        *hooks_instances: Variable number of Hooks instances to combine.

    Returns:
        A new Hooks instance containing all handlers from all instances.

    Example:
        >>> hooks1 = Hooks()
        >>> hooks2 = Hooks()
        >>> hooks3 = Hooks()
        >>> hooks1.on("completion:kwargs", lambda **kw: print("Hook 1"))
        >>> hooks2.on("completion:kwargs", lambda **kw: print("Hook 2"))
        >>> hooks3.on("completion:kwargs", lambda **kw: print("Hook 3"))
        >>> combined = Hooks.combine(hooks1, hooks2, hooks3)
        >>> combined.emit_completion_arguments()  # Prints all three hooks
    """
    combined = cls()

    for hooks_instance in hooks_instances:
        if not isinstance(hooks_instance, cls):
            raise TypeError(f"Expected Hooks instance, got {type(hooks_instance)}")
        combined += hooks_instance

    return combined

copy()

Create a deep copy of this Hooks instance.

Returns:

Type Description
Hooks

A new Hooks instance with all the same handlers.

Example

original = Hooks() original.on("completion:kwargs", lambda **kw: print("Hook")) copy = original.copy() copy.emit_completion_arguments() # Prints "Hook"

Source code in instructor/v2/core/hooks.py
def copy(self) -> Hooks:
    """
    Create a deep copy of this Hooks instance.

    Returns:
        A new Hooks instance with all the same handlers.

    Example:
        >>> original = Hooks()
        >>> original.on("completion:kwargs", lambda **kw: print("Hook"))
        >>> copy = original.copy()
        >>> copy.emit_completion_arguments()  # Prints "Hook"
    """
    new_hooks = Hooks()
    for hook_name, handlers in self._handlers.items():
        new_hooks._handlers[hook_name].extend(handlers.copy())
    return new_hooks

emit(hook_name, *args, **kwargs)

Generic method to emit events for any hook type.

Parameters:

Name Type Description Default
hook_name HookName

The hook to emit

required
*args Any

Positional arguments to pass to handlers

()
**kwargs Any

Keyword arguments to pass to handlers

{}
Source code in instructor/v2/core/hooks.py
def emit(self, hook_name: HookName, *args: Any, **kwargs: Any) -> None:
    """
    Generic method to emit events for any hook type.

    Args:
        hook_name: The hook to emit
        *args: Positional arguments to pass to handlers
        **kwargs: Keyword arguments to pass to handlers
    """
    for handler in self._handlers[hook_name]:
        try:
            if kwargs:
                try:
                    sig = inspect.signature(handler)
                    sig.bind(*args, **kwargs)
                except TypeError:
                    handler(*args)  # type: ignore
                    continue
            handler(*args, **kwargs)  # type: ignore
        except Exception:
            error_traceback = traceback.format_exc()
            warnings.warn(
                f"Error in {hook_name.value} handler:\n{error_traceback}",
                stacklevel=2,
            )

emit_completion_arguments(*args, **kwargs)

Emit a completion arguments event.

Parameters:

Name Type Description Default
*args Any

Positional arguments to pass to handlers

()
**kwargs Any

Keyword arguments to pass to handlers

{}
Source code in instructor/v2/core/hooks.py
def emit_completion_arguments(self, *args: Any, **kwargs: Any) -> None:
    """
    Emit a completion arguments event.

    Args:
        *args: Positional arguments to pass to handlers
        **kwargs: Keyword arguments to pass to handlers
    """
    self.emit(HookName.COMPLETION_KWARGS, *args, **kwargs)

emit_completion_error(error, **kwargs)

Emit a completion error event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
**kwargs Any

Optional metadata (attempt_number, max_attempts, is_last_attempt)

{}
Source code in instructor/v2/core/hooks.py
def emit_completion_error(self, error: Exception, **kwargs: Any) -> None:
    """
    Emit a completion error event.

    Args:
        error: The exception to pass to handlers
        **kwargs: Optional metadata (attempt_number, max_attempts, is_last_attempt)
    """
    self.emit(HookName.COMPLETION_ERROR, error, **kwargs)

emit_completion_last_attempt(error, **kwargs)

Emit a completion last attempt event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
**kwargs Any

Optional metadata (attempt_number, max_attempts, is_last_attempt)

{}
Source code in instructor/v2/core/hooks.py
def emit_completion_last_attempt(self, error: Exception, **kwargs: Any) -> None:
    """
    Emit a completion last attempt event.

    Args:
        error: The exception to pass to handlers
        **kwargs: Optional metadata (attempt_number, max_attempts, is_last_attempt)
    """
    self.emit(HookName.COMPLETION_LAST_ATTEMPT, error, **kwargs)

emit_completion_response(response)

Emit a completion response event.

Parameters:

Name Type Description Default
response Any

The completion response to pass to handlers

required
Source code in instructor/v2/core/hooks.py
def emit_completion_response(self, response: Any) -> None:
    """
    Emit a completion response event.

    Args:
        response: The completion response to pass to handlers
    """
    self.emit(HookName.COMPLETION_RESPONSE, response)

emit_parse_error(error)

Emit a parse error event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
Source code in instructor/v2/core/hooks.py
def emit_parse_error(self, error: Exception) -> None:
    """
    Emit a parse error event.

    Args:
        error: The exception to pass to handlers
    """
    self.emit(HookName.PARSE_ERROR, error)

get_hook_name(hook_name)

Convert a string hook name to its corresponding enum value.

Parameters:

Name Type Description Default
hook_name HookNameType

Either a HookName enum value or string representation.

required

Returns:

Type Description
HookName

The corresponding HookName enum value.

Raises:

Type Description
ValueError

If the string doesn't match any HookName enum value.

Source code in instructor/v2/core/hooks.py
def get_hook_name(self, hook_name: HookNameType) -> HookName:
    """
    Convert a string hook name to its corresponding enum value.

    Args:
        hook_name: Either a HookName enum value or string representation.

    Returns:
        The corresponding HookName enum value.

    Raises:
        ValueError: If the string doesn't match any HookName enum value.
    """
    if isinstance(hook_name, str):
        try:
            return HookName(hook_name)
        except ValueError as err:
            raise ValueError(f"Invalid hook name: {hook_name}") from err
    return hook_name

off(hook_name, handler)

Remove a specific handler from an event.

Parameters:

Name Type Description Default
hook_name HookNameType

The name of the hook.

required
handler HandlerType

The handler to remove.

required
Source code in instructor/v2/core/hooks.py
def off(
    self,
    hook_name: HookNameType,
    handler: HandlerType,
) -> None:
    """
    Remove a specific handler from an event.

    Args:
        hook_name: The name of the hook.
        handler: The handler to remove.
    """
    hook_name = self.get_hook_name(hook_name)
    if hook_name in self._handlers:
        if handler in self._handlers[hook_name]:
            self._handlers[hook_name].remove(handler)
            if not self._handlers[hook_name]:
                del self._handlers[hook_name]

on(hook_name, handler)

Register an event handler for a specific event.

This method allows you to attach a handler function to a specific event. When the event is emitted, all registered handlers for that event will be called.

Parameters:

Name Type Description Default
hook_name HookNameType

The event to listen for. This can be either a HookName enum value or a string representation of the event name.

required
handler HandlerType

The function to be called when the event is emitted.

required

Raises:

Type Description
ValueError

If the hook_name is not a valid HookName enum or string representation.

Example

def on_completion_kwargs(*args: Any, **kwargs: Any) -> None: ... print(f"Completion kwargs: {args}, {kwargs}") hooks = Hooks() hooks.on(HookName.COMPLETION_KWARGS, on_completion_kwargs) hooks.emit_completion_arguments(model="gpt-3.5-turbo", temperature=0.7) Completion kwargs: (), {'model': 'gpt-3.5-turbo', 'temperature': 0.7}

Source code in instructor/v2/core/hooks.py
def on(
    self,
    hook_name: HookNameType,
    handler: HandlerType,
) -> None:
    """
    Register an event handler for a specific event.

    This method allows you to attach a handler function to a specific event.
    When the event is emitted, all registered handlers for that event will be called.

    Args:
        hook_name: The event to listen for. This can be either a HookName enum
                   value or a string representation of the event name.
        handler: The function to be called when the event is emitted.

    Raises:
        ValueError: If the hook_name is not a valid HookName enum or string representation.

    Example:
        >>> def on_completion_kwargs(*args: Any, **kwargs: Any) -> None:
        ...     print(f"Completion kwargs: {args}, {kwargs}")
        >>> hooks = Hooks()
        >>> hooks.on(HookName.COMPLETION_KWARGS, on_completion_kwargs)
        >>> hooks.emit_completion_arguments(model="gpt-3.5-turbo", temperature=0.7)
        Completion kwargs: (), {'model': 'gpt-3.5-turbo', 'temperature': 0.7}
    """
    hook_name = self.get_hook_name(hook_name)
    self._handlers[hook_name].append(handler)

Mode

Bases: Enum

Mode enumeration for patching LLM API clients.

Each mode determines how the library formats and structures requests to different provider APIs and how it processes their responses.

Source code in instructor/v2/core/mode.py
class Mode(enum.Enum):
    """
    Mode enumeration for patching LLM API clients.

    Each mode determines how the library formats and structures requests
    to different provider APIs and how it processes their responses.
    """

    # OpenAI modes
    FUNCTIONS = "function_call"  # Deprecated
    PARALLEL_TOOLS = "parallel_tool_call"
    TOOLS = "tool_call"
    TOOLS_STRICT = "tools_strict"
    JSON = "json_mode"
    JSON_O1 = "json_o1"
    MD_JSON = "markdown_json_mode"
    JSON_SCHEMA = "json_schema_mode"

    # Add new modes to support responses api
    RESPONSES_TOOLS = "responses_tools"
    RESPONSES_TOOLS_WITH_INBUILT_TOOLS = "responses_tools_with_inbuilt_tools"

    # XAI modes
    XAI_JSON = "xai_json"
    XAI_TOOLS = "xai_tools"

    # Anthropic modes
    ANTHROPIC_TOOLS = "anthropic_tools"
    ANTHROPIC_REASONING_TOOLS = "anthropic_reasoning_tools"
    ANTHROPIC_JSON = "anthropic_json"
    ANTHROPIC_PARALLEL_TOOLS = "anthropic_parallel_tools"

    # Mistral modes
    MISTRAL_TOOLS = "mistral_tools"
    MISTRAL_STRUCTURED_OUTPUTS = "mistral_structured_outputs"

    # Vertex AI & Google modes
    VERTEXAI_TOOLS = "vertexai_tools"
    VERTEXAI_JSON = "vertexai_json"
    VERTEXAI_PARALLEL_TOOLS = "vertexai_parallel_tools"
    GEMINI_JSON = "gemini_json"
    GEMINI_TOOLS = "gemini_tools"
    GENAI_TOOLS = "genai_tools"
    GENAI_JSON = "genai_json"
    GENAI_STRUCTURED_OUTPUTS = (
        "genai_structured_outputs"  # Backwards compatibility alias
    )

    # Cohere modes
    COHERE_TOOLS = "cohere_tools"
    COHERE_JSON_SCHEMA = "json_object"

    # Cerebras modes
    CEREBRAS_TOOLS = "cerebras_tools"
    CEREBRAS_JSON = "cerebras_json"

    # Fireworks modes
    FIREWORKS_TOOLS = "fireworks_tools"
    FIREWORKS_JSON = "fireworks_json"

    # Other providers
    WRITER_TOOLS = "writer_tools"
    WRITER_JSON = "writer_json"
    BEDROCK_TOOLS = "bedrock_tools"
    BEDROCK_JSON = "bedrock_json"
    PERPLEXITY_JSON = "perplexity_json"
    OPENROUTER_STRUCTURED_OUTPUTS = "openrouter_structured_outputs"

    # Classification helpers
    @classmethod
    def tool_modes(cls) -> set["Mode"]:
        """Returns a set of all tool-based modes."""
        return {
            cls.FUNCTIONS,
            cls.PARALLEL_TOOLS,
            cls.TOOLS,
            cls.TOOLS_STRICT,
            cls.ANTHROPIC_TOOLS,
            cls.ANTHROPIC_REASONING_TOOLS,
            cls.ANTHROPIC_PARALLEL_TOOLS,
            cls.MISTRAL_TOOLS,
            cls.VERTEXAI_TOOLS,
            cls.VERTEXAI_PARALLEL_TOOLS,
            cls.GEMINI_TOOLS,
            cls.COHERE_TOOLS,
            cls.CEREBRAS_TOOLS,
            cls.FIREWORKS_TOOLS,
            cls.WRITER_TOOLS,
            cls.BEDROCK_TOOLS,
            cls.OPENROUTER_STRUCTURED_OUTPUTS,
            cls.MISTRAL_STRUCTURED_OUTPUTS,
            cls.XAI_TOOLS,
            cls.GENAI_TOOLS,
            cls.RESPONSES_TOOLS,
            cls.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
        }

    @classmethod
    def json_modes(cls) -> set["Mode"]:
        """Returns a set of all JSON-based modes."""
        return {
            cls.JSON,
            cls.JSON_O1,
            cls.MD_JSON,
            cls.JSON_SCHEMA,
            cls.ANTHROPIC_JSON,
            cls.VERTEXAI_JSON,
            cls.GEMINI_JSON,
            cls.COHERE_JSON_SCHEMA,
            cls.CEREBRAS_JSON,
            cls.FIREWORKS_JSON,
            cls.WRITER_JSON,
            cls.BEDROCK_JSON,
            cls.PERPLEXITY_JSON,
            cls.OPENROUTER_STRUCTURED_OUTPUTS,
            cls.MISTRAL_STRUCTURED_OUTPUTS,
            cls.XAI_JSON,
        }

    @classmethod
    def parallel_modes(cls) -> set["Mode"]:
        """Return canonical and compatibility aliases for parallel tool modes."""
        return {
            cls.PARALLEL_TOOLS,
            cls.ANTHROPIC_PARALLEL_TOOLS,
            cls.VERTEXAI_PARALLEL_TOOLS,
        }

    @classmethod
    def warn_mode_functions_deprecation(cls):
        """
        Warn about FUNCTIONS mode deprecation.

        Shows the warning only once per session to avoid spamming logs
        with the same message.
        """
        global _functions_deprecation_shown
        if not _functions_deprecation_shown:
            warnings.warn(
                "The FUNCTIONS mode is deprecated and will be removed in future versions",
                DeprecationWarning,
                stacklevel=2,
            )
            _functions_deprecation_shown = True

    @classmethod
    def warn_anthropic_reasoning_tools_deprecation(cls):
        """
        Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

        ANTHROPIC_TOOLS now supports extended thinking/reasoning via the
        'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'}
        instead of Mode.ANTHROPIC_REASONING_TOOLS.

        Shows the warning only once per session to avoid spamming logs
        with the same message.
        """
        global _reasoning_tools_deprecation_shown
        if not _reasoning_tools_deprecation_shown:
            warnings.warn(
                "Mode.ANTHROPIC_REASONING_TOOLS is deprecated. "
                "Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled', 'budget_tokens': ...} instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            _reasoning_tools_deprecation_shown = True

    @classmethod
    def warn_deprecated_mode(cls, mode: "Mode") -> None:
        """Warn about provider-specific mode deprecation.

        Uses a single warning per mode per process to reduce noise.
        """
        if mode not in DEPRECATED_TO_CORE:
            return
        if mode in _deprecated_modes_warned:
            return
        _deprecated_modes_warned.add(mode)
        replacement = DEPRECATED_TO_CORE[mode]
        warnings.warn(
            f"Mode.{mode.name} is deprecated and will be removed in v3.0. "
            f"Use Mode.{replacement.name} instead. "
            "The provider is determined by the client (from_openai, from_anthropic, etc.), "
            "not by the mode.",
            DeprecationWarning,
            stacklevel=3,
        )

json_modes() classmethod

Returns a set of all JSON-based modes.

Source code in instructor/v2/core/mode.py
@classmethod
def json_modes(cls) -> set["Mode"]:
    """Returns a set of all JSON-based modes."""
    return {
        cls.JSON,
        cls.JSON_O1,
        cls.MD_JSON,
        cls.JSON_SCHEMA,
        cls.ANTHROPIC_JSON,
        cls.VERTEXAI_JSON,
        cls.GEMINI_JSON,
        cls.COHERE_JSON_SCHEMA,
        cls.CEREBRAS_JSON,
        cls.FIREWORKS_JSON,
        cls.WRITER_JSON,
        cls.BEDROCK_JSON,
        cls.PERPLEXITY_JSON,
        cls.OPENROUTER_STRUCTURED_OUTPUTS,
        cls.MISTRAL_STRUCTURED_OUTPUTS,
        cls.XAI_JSON,
    }

parallel_modes() classmethod

Return canonical and compatibility aliases for parallel tool modes.

Source code in instructor/v2/core/mode.py
@classmethod
def parallel_modes(cls) -> set["Mode"]:
    """Return canonical and compatibility aliases for parallel tool modes."""
    return {
        cls.PARALLEL_TOOLS,
        cls.ANTHROPIC_PARALLEL_TOOLS,
        cls.VERTEXAI_PARALLEL_TOOLS,
    }

tool_modes() classmethod

Returns a set of all tool-based modes.

Source code in instructor/v2/core/mode.py
@classmethod
def tool_modes(cls) -> set["Mode"]:
    """Returns a set of all tool-based modes."""
    return {
        cls.FUNCTIONS,
        cls.PARALLEL_TOOLS,
        cls.TOOLS,
        cls.TOOLS_STRICT,
        cls.ANTHROPIC_TOOLS,
        cls.ANTHROPIC_REASONING_TOOLS,
        cls.ANTHROPIC_PARALLEL_TOOLS,
        cls.MISTRAL_TOOLS,
        cls.VERTEXAI_TOOLS,
        cls.VERTEXAI_PARALLEL_TOOLS,
        cls.GEMINI_TOOLS,
        cls.COHERE_TOOLS,
        cls.CEREBRAS_TOOLS,
        cls.FIREWORKS_TOOLS,
        cls.WRITER_TOOLS,
        cls.BEDROCK_TOOLS,
        cls.OPENROUTER_STRUCTURED_OUTPUTS,
        cls.MISTRAL_STRUCTURED_OUTPUTS,
        cls.XAI_TOOLS,
        cls.GENAI_TOOLS,
        cls.RESPONSES_TOOLS,
        cls.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
    }

warn_anthropic_reasoning_tools_deprecation() classmethod

Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

ANTHROPIC_TOOLS now supports extended thinking/reasoning via the 'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'} instead of Mode.ANTHROPIC_REASONING_TOOLS.

Shows the warning only once per session to avoid spamming logs with the same message.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_anthropic_reasoning_tools_deprecation(cls):
    """
    Warn about ANTHROPIC_REASONING_TOOLS mode deprecation.

    ANTHROPIC_TOOLS now supports extended thinking/reasoning via the
    'thinking' parameter. Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled'}
    instead of Mode.ANTHROPIC_REASONING_TOOLS.

    Shows the warning only once per session to avoid spamming logs
    with the same message.
    """
    global _reasoning_tools_deprecation_shown
    if not _reasoning_tools_deprecation_shown:
        warnings.warn(
            "Mode.ANTHROPIC_REASONING_TOOLS is deprecated. "
            "Use Mode.ANTHROPIC_TOOLS with thinking={'type': 'enabled', 'budget_tokens': ...} instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        _reasoning_tools_deprecation_shown = True

warn_deprecated_mode(mode) classmethod

Warn about provider-specific mode deprecation.

Uses a single warning per mode per process to reduce noise.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_deprecated_mode(cls, mode: "Mode") -> None:
    """Warn about provider-specific mode deprecation.

    Uses a single warning per mode per process to reduce noise.
    """
    if mode not in DEPRECATED_TO_CORE:
        return
    if mode in _deprecated_modes_warned:
        return
    _deprecated_modes_warned.add(mode)
    replacement = DEPRECATED_TO_CORE[mode]
    warnings.warn(
        f"Mode.{mode.name} is deprecated and will be removed in v3.0. "
        f"Use Mode.{replacement.name} instead. "
        "The provider is determined by the client (from_openai, from_anthropic, etc.), "
        "not by the mode.",
        DeprecationWarning,
        stacklevel=3,
    )

warn_mode_functions_deprecation() classmethod

Warn about FUNCTIONS mode deprecation.

Shows the warning only once per session to avoid spamming logs with the same message.

Source code in instructor/v2/core/mode.py
@classmethod
def warn_mode_functions_deprecation(cls):
    """
    Warn about FUNCTIONS mode deprecation.

    Shows the warning only once per session to avoid spamming logs
    with the same message.
    """
    global _functions_deprecation_shown
    if not _functions_deprecation_shown:
        warnings.warn(
            "The FUNCTIONS mode is deprecated and will be removed in future versions",
            DeprecationWarning,
            stacklevel=2,
        )
        _functions_deprecation_shown = True

Provider

Bases: Enum

Supported provider identifiers.

Source code in instructor/v2/core/providers.py
class Provider(Enum):
    """Supported provider identifiers."""

    OPENAI = "openai"
    AZURE_OPENAI = "azure_openai"
    VERTEXAI = "vertexai"
    ANTHROPIC = "anthropic"
    ANYSCALE = "anyscale"
    TOGETHER = "together"
    GROQ = "groq"
    MISTRAL = "mistral"
    COHERE = "cohere"
    GEMINI = "gemini"
    GENAI = "genai"
    DATABRICKS = "databricks"
    CEREBRAS = "cerebras"
    DEEPSEEK = "deepseek"
    FIREWORKS = "fireworks"
    WRITER = "writer"
    XAI = "xai"
    OLLAMA = "ollama"
    LITELLM = "litellm"
    GOOGLE = "google"
    GENERATIVE_AI = "generative-ai"
    UNKNOWN = "unknown"
    BEDROCK = "bedrock"
    PERPLEXITY = "perplexity"
    OPENROUTER = "openrouter"

RegistryValidationMixin

Mixin providing registry validation helper methods.

Source code in instructor/v2/core/exceptions.py
class RegistryValidationMixin:
    """Mixin providing registry validation helper methods."""

    @staticmethod
    def validate_mode_registration(provider: Provider, mode: Mode) -> None:
        """Validate that a mode is registered for a provider.

        Args:
            provider: Provider enum value
            mode: Mode enum value

        Raises:
            RegistryError: If mode is not registered for provider
        """
        from instructor.v2.core.registry import mode_registry

        if not mode_registry.is_registered(provider, mode):
            available = mode_registry.list_modes()
            raise RegistryError(
                f"Mode {mode} is not registered for provider {provider}. "
                f"Available modes: {available}"
            )

    @staticmethod
    def validate_context_parameters(
        context: dict[str, Any] | None,
        validation_context: dict[str, Any] | None,
    ) -> dict[str, Any] | None:
        """Validate and merge context parameters.

        Args:
            context: New-style context parameter
            validation_context: Deprecated validation_context parameter

        Returns:
            Merged context dict or None

        Raises:
            ValidationContextError: If both parameters are provided
        """
        if context is not None and validation_context is not None:
            raise ValidationContextError(
                "Cannot provide both 'context' and 'validation_context'. "
                "Use 'context' instead."
            )

        if validation_context is not None and context is None:
            import warnings

            warnings.warn(
                "'validation_context' is deprecated. Use 'context' instead.",
                DeprecationWarning,
                stacklevel=3,
            )
            return validation_context

        return context

validate_context_parameters(context, validation_context) staticmethod

Validate and merge context parameters.

Parameters:

Name Type Description Default
context dict[str, Any] | None

New-style context parameter

required
validation_context dict[str, Any] | None

Deprecated validation_context parameter

required

Returns:

Type Description
dict[str, Any] | None

Merged context dict or None

Raises:

Type Description
ValidationContextError

If both parameters are provided

Source code in instructor/v2/core/exceptions.py
@staticmethod
def validate_context_parameters(
    context: dict[str, Any] | None,
    validation_context: dict[str, Any] | None,
) -> dict[str, Any] | None:
    """Validate and merge context parameters.

    Args:
        context: New-style context parameter
        validation_context: Deprecated validation_context parameter

    Returns:
        Merged context dict or None

    Raises:
        ValidationContextError: If both parameters are provided
    """
    if context is not None and validation_context is not None:
        raise ValidationContextError(
            "Cannot provide both 'context' and 'validation_context'. "
            "Use 'context' instead."
        )

    if validation_context is not None and context is None:
        import warnings

        warnings.warn(
            "'validation_context' is deprecated. Use 'context' instead.",
            DeprecationWarning,
            stacklevel=3,
        )
        return validation_context

    return context

validate_mode_registration(provider, mode) staticmethod

Validate that a mode is registered for a provider.

Parameters:

Name Type Description Default
provider Provider

Provider enum value

required
mode Mode

Mode enum value

required

Raises:

Type Description
RegistryError

If mode is not registered for provider

Source code in instructor/v2/core/exceptions.py
@staticmethod
def validate_mode_registration(provider: Provider, mode: Mode) -> None:
    """Validate that a mode is registered for a provider.

    Args:
        provider: Provider enum value
        mode: Mode enum value

    Raises:
        RegistryError: If mode is not registered for provider
    """
    from instructor.v2.core.registry import mode_registry

    if not mode_registry.is_registered(provider, mode):
        available = mode_registry.list_modes()
        raise RegistryError(
            f"Mode {mode} is not registered for provider {provider}. "
            f"Available modes: {available}"
        )

apatch(client, mode=Mode.TOOLS, provider=Provider.OPENAI)

Deprecated alias for :func:patch.

Source code in instructor/v2/core/patch.py
def apatch(
    client: AsyncOpenAI,
    mode: Mode = Mode.TOOLS,
    provider: Provider = Provider.OPENAI,
) -> AsyncOpenAI:
    """Deprecated alias for :func:`patch`."""
    warnings.warn(
        "apatch is deprecated, use patch instead",
        DeprecationWarning,
        stacklevel=2,
    )
    return patch(client, mode=mode, provider=provider)

handle_templating(kwargs, mode, provider=None, context=None)

Handle templating for messages using the provided context.

This function processes messages, applying Jinja2 templating to their content using the provided context. It supports various message formats including OpenAI, Anthropic, Cohere, VertexAI, and Gemini.

Parameters:

Name Type Description Default
kwargs Dict[str, Any]

Keyword arguments being passed to the create method.

required
context Dict[str, Any] | None

Variables to use in templating. Defaults to None.

None

Returns:

Type Description
dict[str, Any]

Dict[str, Any]: The processed kwargs with templated content.

Raises:

Type Description
ValueError

If no recognized message format is found in kwargs.

Source code in instructor/v2/core/templating.py
def handle_templating(
    kwargs: dict[str, Any],
    mode: Mode,  # noqa: ARG001
    provider: Provider | dict[str, Any] | None = None,
    context: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """
    Handle templating for messages using the provided context.

    This function processes messages, applying Jinja2 templating to their content
    using the provided context. It supports various message formats including
    OpenAI, Anthropic, Cohere, VertexAI, and Gemini.

    Args:
        kwargs (Dict[str, Any]): Keyword arguments being passed to the create method.
        context (Dict[str, Any] | None, optional): Variables to use in templating. Defaults to None.

    Returns:
        Dict[str, Any]: The processed kwargs with templated content.

    Raises:
        ValueError: If no recognized message format is found in kwargs.
    """
    if context is None and isinstance(provider, dict):
        context = provider
        provider = None

    if not context:
        return kwargs

    if provider is None:
        provider = provider_from_mode(mode, Provider.OPENAI)

    new_kwargs = kwargs.copy()

    # Handle Cohere's message field
    if "message" in new_kwargs:
        new_kwargs["message"] = apply_template(new_kwargs["message"], context)
        new_kwargs["chat_history"] = [
            process_message(message, context, provider)
            for message in new_kwargs["chat_history"]
        ]

        return new_kwargs

    if isinstance(new_kwargs, list):
        messages = new_kwargs
        if not messages:
            return new_kwargs
    elif isinstance(new_kwargs, dict):
        messages = new_kwargs.get("messages") or new_kwargs.get("contents")

    if not messages:
        return new_kwargs

    if "messages" in new_kwargs:
        new_kwargs["messages"] = [
            process_message(message, context, provider) for message in messages
        ]

    elif "contents" in new_kwargs:
        new_kwargs["contents"] = [
            process_message(content, context, provider)
            for content in new_kwargs["contents"]
        ]

    return new_kwargs

is_async(func)

Return whether a callable is async, following wrapped callables.

Source code in instructor/v2/core/utils.py
def is_async(func: Callable[..., Any]) -> bool:
    """Return whether a callable is async, following wrapped callables."""
    is_coroutine = inspect.iscoroutinefunction(func)
    while hasattr(func, "__wrapped__"):
        func = func.__wrapped__  # type: ignore[attr-defined]
        is_coroutine = is_coroutine or inspect.iscoroutinefunction(func)
    return is_coroutine

patch(client=None, create=None, mode=Mode.TOOLS, provider=Provider.OPENAI)

patch(
    client: OpenAI,
    mode: Mode = Mode.TOOLS,
    provider: Provider = Provider.OPENAI,
) -> OpenAI
patch(
    client: AsyncOpenAI,
    mode: Mode = Mode.TOOLS,
    provider: Provider = Provider.OPENAI,
) -> AsyncOpenAI
patch(
    create: Callable[..., T_Retval],
    mode: Mode = Mode.TOOLS,
    provider: Provider = Provider.OPENAI,
) -> InstructorChatCompletionCreate
patch(
    create: Awaitable[T_Retval],
    mode: Mode = Mode.TOOLS,
    provider: Provider = Provider.OPENAI,
) -> InstructorChatCompletionCreate

Patch chat-completion create methods with v2 registry handlers.

Source code in instructor/v2/core/patch.py
def patch(  # type: ignore
    client: OpenAI | AsyncOpenAI | None = None,
    create: Callable[..., T_Retval] | None = None,
    mode: Mode = Mode.TOOLS,
    provider: Provider = Provider.OPENAI,
) -> OpenAI | AsyncOpenAI:
    """Patch chat-completion create methods with v2 registry handlers."""
    logger.debug(f"Patching `client.chat.completions.create` with {mode=}")

    if create is not None:
        func = create
    elif client is not None:
        func = client.chat.completions.create
    else:
        raise ValueError("Either client or create must be provided")

    new_create = patch_v2(func=func, provider=provider, mode=mode)

    if client is not None:
        client.chat.completions.create = new_create  # type: ignore[attr-defined]
        return client
    return new_create  # type: ignore[return-value]

patch_v2(func, provider, mode, default_model=None)

Patch a function to use v2 registry for structured outputs.

Parameters:

Name Type Description Default
func Callable[..., Any]

Function to patch (e.g., client.messages.create)

required
provider Provider

Provider enum value

required
mode Mode

Mode enum value

required
default_model str | None

Default model to inject if not provided in request

None

Returns:

Type Description
Callable[..., T_Model]

Patched function that supports response_model parameter

Raises:

Type Description
RegistryError

If mode is not registered for provider

Source code in instructor/v2/core/patch.py
def patch_v2(
    func: Callable[..., Any],
    provider: Provider,
    mode: Mode,
    default_model: str | None = None,
) -> Callable[..., T_Model]:
    """Patch a function to use v2 registry for structured outputs.

    Args:
        func: Function to patch (e.g., client.messages.create)
        provider: Provider enum value
        mode: Mode enum value
        default_model: Default model to inject if not provided in request

    Returns:
        Patched function that supports response_model parameter

    Raises:
        RegistryError: If mode is not registered for provider
    """
    logger.debug(f"Patching with v2 registry: {provider=}, {mode=}, {default_model=}")

    # Validate mode registration
    RegistryValidationMixin.validate_mode_registration(provider, mode)

    func_is_async = is_async(func)

    if func_is_async:
        return _create_async_wrapper(func, provider, mode, default_model)  # type: ignore[return-value]
    else:
        return _create_sync_wrapper(func, provider, mode, default_model)  # type: ignore[return-value]

retry_async_v2(func, response_model, provider, mode, context, max_retries, args, kwargs, strict, hooks=None) async

Async retry logic using v2 registry handlers.

Parameters:

Name Type Description Default
func Callable[..., Awaitable[Any]]

Async API function to call

required
response_model type[T_Model] | None

Pydantic model to extract

required
provider Provider

Provider enum

required
mode Mode

Mode enum

required
context dict[str, Any] | None

Validation context

required
max_retries int | AsyncRetrying

Max retry attempts or AsyncRetrying instance

required
args tuple[Any, ...]

Positional args for func

required
kwargs dict[str, Any]

Keyword args for func

required
strict bool

Strict validation mode

required
hooks Hooks | None

Optional hooks

None

Returns:

Type Description
T_Model

Validated Pydantic model instance

Raises:

Type Description
InstructorRetryException

If max retries exceeded

Source code in instructor/v2/core/retry.py
async def retry_async_v2(
    func: Callable[..., Awaitable[Any]],
    response_model: type[T_Model] | None,
    provider: Provider,
    mode: Mode,
    context: dict[str, Any] | None,
    max_retries: int | AsyncRetrying,
    args: tuple[Any, ...],
    kwargs: dict[str, Any],
    strict: bool,
    hooks: Hooks | None = None,
) -> T_Model:
    """Async retry logic using v2 registry handlers.

    Args:
        func: Async API function to call
        response_model: Pydantic model to extract
        provider: Provider enum
        mode: Mode enum
        context: Validation context
        max_retries: Max retry attempts or AsyncRetrying instance
        args: Positional args for func
        kwargs: Keyword args for func
        strict: Strict validation mode
        hooks: Optional hooks

    Returns:
        Validated Pydantic model instance

    Raises:
        InstructorRetryException: If max retries exceeded
    """
    if response_model is None:
        # No structured output, just call the API
        return await func(*args, **kwargs)

    # Validate and get handlers from registry
    RegistryValidationMixin.validate_mode_registration(provider, mode)
    handlers = mode_registry.get_handlers(provider, mode)

    # Setup retrying
    if isinstance(max_retries, int):
        stop_condition = stop_after_attempt(max(max_retries, 1))
        timeout = kwargs.get("timeout")
        if isinstance(timeout, (int, float)):
            stop_condition = stop_condition | stop_after_delay(timeout)
        max_retries_instance = AsyncRetrying(
            stop=stop_condition,
            retry=retry_if_exception_type(ValidationError),
            reraise=True,
        )
    else:
        max_retries_instance = max_retries

    failed_attempts: list[FailedAttempt] = []
    last_exception: Exception | None = None
    total_usage = _initialize_usage(provider)

    try:
        async for attempt in max_retries_instance:
            with attempt:
                # Call API
                if hooks:
                    hooks.emit_completion_arguments(**kwargs)

                try:
                    response = await func(*args, **kwargs)
                except IncompleteOutputException:
                    raise
                except Exception as e:
                    logger.error(
                        f"API call failed on attempt "
                        f"{attempt.retry_state.attempt_number}: {e}"
                    )
                    raise

                if hooks:
                    hooks.emit_completion_response(response)

                update_total_usage(response=response, total_usage=total_usage)

                # Parse response using registry
                try:
                    stream = kwargs.get("stream", False)
                    parsed = handlers.response_parser(
                        response=response,
                        response_model=response_model,
                        validation_context=context,
                        strict=strict,
                        stream=stream,
                        is_async=True,
                    )
                    logger.debug(
                        f"Successfully parsed response on attempt "
                        f"{attempt.retry_state.attempt_number}"
                    )
                    return _finalize_parsed_response(parsed, response)  # type: ignore

                except IncompleteOutputException:
                    raise
                except ValidationError as e:
                    attempt_number = attempt.retry_state.attempt_number
                    logger.debug(f"Validation error on attempt {attempt_number}: {e}")
                    failed_attempts.append(
                        FailedAttempt(
                            attempt_number=attempt_number,
                            exception=e,
                            completion=response,
                        )
                    )
                    last_exception = e

                    if hooks:
                        hooks.emit_parse_error(e)

                    # Prepare reask using registry
                    kwargs = handlers.reask_handler(
                        kwargs=kwargs,
                        response=response,
                        exception=e,
                    )

                    # Will retry with modified kwargs
                    raise

    except IncompleteOutputException:
        raise
    except Exception as e:
        # Max retries exceeded or non-validation error occurred
        if last_exception is None:
            last_exception = e

        logger.error(
            f"Max retries exceeded. Total attempts: {len(failed_attempts)}, "
            f"Last error: {last_exception}"
        )

        raise InstructorRetryException(
            str(last_exception),
            last_completion=failed_attempts[-1].completion if failed_attempts else None,
            n_attempts=len(failed_attempts),
            total_usage=total_usage,
            messages=extract_messages(kwargs),
            create_kwargs=kwargs,
            failed_attempts=failed_attempts,
        ) from last_exception

    # Should never reach here
    logger.error("Unexpected code path in retry_async_v2")
    raise InstructorRetryException(
        str(last_exception) if last_exception else "Unknown error",
        last_completion=failed_attempts[-1].completion if failed_attempts else None,
        n_attempts=len(failed_attempts),
        total_usage=total_usage,
        messages=extract_messages(kwargs),
        create_kwargs=kwargs,
        failed_attempts=failed_attempts,
    )

retry_sync_v2(func, response_model, provider, mode, context, max_retries, args, kwargs, strict, hooks=None)

Sync retry logic using v2 registry handlers.

Parameters:

Name Type Description Default
func Callable[..., Any]

API function to call

required
response_model type[T_Model] | None

Pydantic model to extract

required
provider Provider

Provider enum

required
mode Mode

Mode enum

required
context dict[str, Any] | None

Validation context

required
max_retries int | Retrying

Max retry attempts or Retrying instance

required
args tuple[Any, ...]

Positional args for func

required
kwargs dict[str, Any]

Keyword args for func

required
strict bool

Strict validation mode

required
hooks Hooks | None

Optional hooks

None

Returns:

Type Description
T_Model

Validated Pydantic model instance

Raises:

Type Description
InstructorRetryException

If max retries exceeded

Source code in instructor/v2/core/retry.py
def retry_sync_v2(
    func: Callable[..., Any],
    response_model: type[T_Model] | None,
    provider: Provider,
    mode: Mode,
    context: dict[str, Any] | None,
    max_retries: int | Retrying,
    args: tuple[Any, ...],
    kwargs: dict[str, Any],
    strict: bool,
    hooks: Hooks | None = None,
) -> T_Model:
    """Sync retry logic using v2 registry handlers.

    Args:
        func: API function to call
        response_model: Pydantic model to extract
        provider: Provider enum
        mode: Mode enum
        context: Validation context
        max_retries: Max retry attempts or Retrying instance
        args: Positional args for func
        kwargs: Keyword args for func
        strict: Strict validation mode
        hooks: Optional hooks

    Returns:
        Validated Pydantic model instance

    Raises:
        InstructorRetryException: If max retries exceeded
    """
    if response_model is None:
        # No structured output, just call the API
        return func(*args, **kwargs)

    # Validate and get handlers from registry
    RegistryValidationMixin.validate_mode_registration(provider, mode)
    handlers = mode_registry.get_handlers(provider, mode)

    # Setup retrying
    if isinstance(max_retries, int):
        stop_condition = stop_after_attempt(max(max_retries, 1))
        timeout = kwargs.get("timeout")
        if isinstance(timeout, (int, float)):
            stop_condition = stop_condition | stop_after_delay(timeout)
        max_retries_instance = Retrying(
            stop=stop_condition,
            retry=retry_if_exception_type(ValidationError),
            reraise=True,
        )
    else:
        max_retries_instance = max_retries

    failed_attempts: list[FailedAttempt] = []
    last_exception: Exception | None = None
    total_usage = _initialize_usage(provider)

    try:
        for attempt in max_retries_instance:
            with attempt:
                # Call API
                if hooks:
                    hooks.emit_completion_arguments(**kwargs)

                try:
                    response = func(*args, **kwargs)
                except IncompleteOutputException:
                    raise
                except Exception as e:
                    logger.error(
                        f"API call failed on attempt "
                        f"{attempt.retry_state.attempt_number}: {e}"
                    )
                    raise

                if hooks:
                    hooks.emit_completion_response(response)

                update_total_usage(response=response, total_usage=total_usage)

                # Parse response using registry
                try:
                    stream = kwargs.get("stream", False)
                    parsed = handlers.response_parser(
                        response=response,
                        response_model=response_model,
                        validation_context=context,
                        strict=strict,
                        stream=stream,
                        is_async=False,
                    )
                    logger.debug(
                        f"Successfully parsed response on attempt "
                        f"{attempt.retry_state.attempt_number}"
                    )
                    return _finalize_parsed_response(parsed, response)  # type: ignore

                except IncompleteOutputException:
                    raise
                except ValidationError as e:
                    attempt_number = attempt.retry_state.attempt_number
                    logger.debug(f"Validation error on attempt {attempt_number}: {e}")
                    failed_attempts.append(
                        FailedAttempt(
                            attempt_number=attempt_number,
                            exception=e,
                            completion=response,
                        )
                    )
                    last_exception = e

                    if hooks:
                        hooks.emit_parse_error(e)

                    # Prepare reask using registry
                    kwargs = handlers.reask_handler(
                        kwargs=kwargs,
                        response=response,
                        exception=e,
                    )

                    # Will retry with modified kwargs
                    raise

    except IncompleteOutputException:
        raise
    except Exception as e:
        # Max retries exceeded or non-validation error occurred
        if last_exception is None:
            last_exception = e

        logger.error(
            f"Max retries exceeded. Total attempts: {len(failed_attempts)}, "
            f"Last error: {last_exception}"
        )

        raise InstructorRetryException(
            str(last_exception),
            last_completion=failed_attempts[-1].completion if failed_attempts else None,
            n_attempts=len(failed_attempts),
            total_usage=total_usage,
            messages=extract_messages(kwargs),
            create_kwargs=kwargs,
            failed_attempts=failed_attempts,
        ) from last_exception

    # Should never reach here
    logger.error("Unexpected code path in retry_sync_v2")
    raise InstructorRetryException(
        str(last_exception) if last_exception else "Unknown error",
        last_completion=failed_attempts[-1].completion if failed_attempts else None,
        n_attempts=len(failed_attempts),
        total_usage=total_usage,
        messages=extract_messages(kwargs),
        create_kwargs=kwargs,
        failed_attempts=failed_attempts,
    )

Deprecated alias for :func:patch.

Source code in instructor/v2/core/patch.py
def apatch(
    client: AsyncOpenAI,
    mode: Mode = Mode.TOOLS,
    provider: Provider = Provider.OPENAI,
) -> AsyncOpenAI:
    """Deprecated alias for :func:`patch`."""
    warnings.warn(
        "apatch is deprecated, use patch instead",
        DeprecationWarning,
        stacklevel=2,
    )
    return patch(client, mode=mode, provider=provider)