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.

Source code in instructor/core/client.py
class Instructor:
    client: Any | None
    create_fn: Callable[..., Any]
    mode: instructor.Mode
    default_model: str | None = None
    provider: Provider
    hooks: Hooks

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

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

        if mode in {
            instructor.Mode.RESPONSES_TOOLS,
            instructor.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,
        validation_context: dict[str, Any] | None = None,
        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,
        validation_context: dict[str, Any] | None = None,
        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,
        validation_context: dict[str, Any] | None = None,
        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,
        validation_context: dict[str, Any] | None = None,
        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,
        validation_context: dict[str, Any] | None = None,
        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,
            validation_context=validation_context,
            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,
        validation_context: dict[str, Any] | None = None,
        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,
        validation_context: dict[str, Any] | None = None,  # Deprecate in 2.0
        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,
        validation_context: dict[str, Any] | None = None,  # Deprecate in 2.0
        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 = instructor.Partial[response_model]  # type: ignore
        return self.create_fn(
            messages=messages,
            response_model=response_model,
            max_retries=max_retries,
            validation_context=validation_context,
            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,
        validation_context: dict[str, Any] | None = None,  # Deprecate in 2.0
        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,
        validation_context: dict[str, Any] | None = None,  # Deprecate in 2.0
        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,
        validation_context: dict[str, Any] | None = None,  # Deprecate in 2.0
        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,
            validation_context=validation_context,
            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,
        validation_context: dict[str, Any] | None = None,  # Deprecate in 2.0
        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,
        validation_context: dict[str, Any] | None = None,  # Deprecate in 2.0
        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,
        validation_context: dict[str, Any] | None = None,  # Deprecate in 2.0
        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,
            validation_context=validation_context,
            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/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

Source code in instructor/core/client.py
class AsyncInstructor(Instructor):
    client: Any | None
    create_fn: Callable[..., Any]
    mode: instructor.Mode
    default_model: str | None = None
    provider: Provider
    hooks: Hooks

    def __init__(
        self,
        client: Any | None,
        create: Callable[..., Any],
        mode: instructor.Mode = instructor.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 {
            instructor.Mode.RESPONSES_TOOLS,
            instructor.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,
        validation_context: dict[str, Any] | None = None,  # Deprecate in 2.0
        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 {
                instructor.Mode.PARALLEL_TOOLS,
                instructor.Mode.VERTEXAI_PARALLEL_TOOLS,
                instructor.Mode.ANTHROPIC_PARALLEL_TOOLS,
            }
        ):
            return self.create_iterable(
                messages=messages,
                response_model=get_args(response_model)[0],
                max_retries=max_retries,
                validation_context=validation_context,
                context=context,
                strict=strict,
                hooks=hooks,  # Pass the per-call hooks to create_iterable
                **kwargs,
            )

        return await self.create_fn(
            response_model=response_model,
            validation_context=validation_context,
            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,
        validation_context: dict[str, Any] | None = None,  # Deprecate in 2.0
        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=instructor.Partial[response_model],  # type: ignore
            validation_context=validation_context,
            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,
        validation_context: dict[str, Any] | None = None,  # Deprecate in 2.0
        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],
            validation_context=validation_context,
            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,
        validation_context: dict[str, Any] | None = None,  # Deprecate in 2.0
        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,
            validation_context=validation_context,
            context=context,
            max_retries=max_retries,
            messages=messages,
            strict=strict,
            hooks=combined_hooks,
            **kwargs,
        )
        return response, response._raw_response
Source code in instructor/core/client.py
class Response:
    def __init__(
        self,
        client: Instructor,
    ):
        self.client = client

    def create(
        self,
        input: str | list[ChatCompletionMessageParam],
        response_model: type[T] | None = None,
        max_retries: int | Retrying = 3,
        validation_context: dict[str, Any] | None = None,
        context: dict[str, Any] | None = None,
        strict: bool = True,
        **kwargs,
    ) -> T | Any:
        if isinstance(input, str):
            input = [
                {
                    "role": "user",
                    "content": input,
                }
            ]

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

    def create_with_completion(
        self,
        input: str | list[ChatCompletionMessageParam],
        response_model: type[T],
        max_retries: int | Retrying = 3,
        **kwargs,
    ) -> tuple[T, Any]:
        if isinstance(input, str):
            input = [
                {
                    "role": "user",
                    "content": input,
                }
            ]

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

    def create_iterable(
        self,
        input: str | list[ChatCompletionMessageParam],
        response_model: type[T],
        max_retries: int | Retrying = 3,
        **kwargs,
    ) -> Generator[T, None, None]:
        if isinstance(input, str):
            input = [
                {
                    "role": "user",
                    "content": input,
                }
            ]

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

    def create_partial(
        self,
        input: str | list[ChatCompletionMessageParam],
        response_model: type[T],
        max_retries: int | Retrying = 3,
        **kwargs,
    ) -> Generator[T, None, None]:
        if isinstance(input, str):
            input = [
                {
                    "role": "user",
                    "content": input,
                }
            ]

        return self.client.create_partial(
            messages=input,
            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/auto_client.py
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 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
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
def from_provider(
    model: Union[str, KnownModelName],  # noqa: UP007
    async_client: bool = False,
    cache: BaseCache | None = None,
    mode: Union[instructor.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 .core.exceptions 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,
            )

    if provider == "openai":
        try:
            import openai
            import httpx
            from instructor import from_openai  # type: ignore[attr-defined]
            from openai import DEFAULT_MAX_RETRIES, NotGiven, Timeout, not_given
            from collections.abc import Mapping
            from typing import cast

            # Extract base_url and other OpenAI client parameters from kwargs
            base_url = kwargs.pop("base_url", None)
            organization = cast(str | None, kwargs.pop("organization", None))

            timeout_raw = kwargs.pop("timeout", not_given)
            timeout: float | Timeout | None | NotGiven
            timeout = (
                not_given
                if timeout_raw is not_given
                else cast(float | Timeout | None, timeout_raw)
            )

            max_retries_raw = kwargs.pop("max_retries", None)
            max_retries = (
                DEFAULT_MAX_RETRIES
                if max_retries_raw is None
                else int(cast(int, max_retries_raw))
            )

            default_headers = cast(
                Mapping[str, str] | None, kwargs.pop("default_headers", None)
            )
            default_query = cast(
                Mapping[str, object] | None, kwargs.pop("default_query", None)
            )
            http_client_raw = kwargs.pop("http_client", None)
            strict_response_validation = bool(
                kwargs.pop("_strict_response_validation", False)
            )

            if async_client:
                http_client = cast(httpx.AsyncClient | None, http_client_raw)
                client = openai.AsyncOpenAI(
                    api_key=api_key,
                    base_url=base_url,
                    organization=organization,
                    timeout=timeout,
                    max_retries=max_retries,
                    default_headers=default_headers,
                    default_query=default_query,
                    http_client=http_client,
                    _strict_response_validation=strict_response_validation,
                )
            else:
                http_client = cast(httpx.Client | None, http_client_raw)
                client = openai.OpenAI(
                    api_key=api_key,
                    base_url=base_url,
                    organization=organization,
                    timeout=timeout,
                    max_retries=max_retries,
                    default_headers=default_headers,
                    default_query=default_query,
                    http_client=http_client,
                    _strict_response_validation=strict_response_validation,
                )

            result = from_openai(
                client,
                model=model_name,
                mode=mode if mode else instructor.Mode.TOOLS,
                **kwargs,
            )
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The openai package is required to use the OpenAI provider. "
                "Install it with `pip install openai`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "azure_openai":
        try:
            import os
            from openai import AzureOpenAI, AsyncAzureOpenAI
            from instructor import from_openai  # type: ignore[attr-defined]

            # Get required Azure OpenAI configuration from environment
            api_key = api_key or os.environ.get("AZURE_OPENAI_API_KEY")
            azure_endpoint = kwargs.pop(
                "azure_endpoint", os.environ.get("AZURE_OPENAI_ENDPOINT")
            )
            api_version = kwargs.pop("api_version", "2024-02-01")

            if not api_key:
                from .core.exceptions import ConfigurationError

                raise ConfigurationError(
                    "AZURE_OPENAI_API_KEY is not set. "
                    "Set it with `export AZURE_OPENAI_API_KEY=<your-api-key>` or pass it as kwarg api_key=<your-api-key>"
                )

            if not azure_endpoint:
                from .core.exceptions import ConfigurationError

                raise ConfigurationError(
                    "AZURE_OPENAI_ENDPOINT is not set. "
                    "Set it with `export AZURE_OPENAI_ENDPOINT=<your-endpoint>` or pass it as kwarg azure_endpoint=<your-endpoint>"
                )

            client = (
                AsyncAzureOpenAI(
                    api_key=api_key,
                    api_version=api_version,
                    azure_endpoint=azure_endpoint,
                )
                if async_client
                else AzureOpenAI(
                    api_key=api_key,
                    api_version=api_version,
                    azure_endpoint=azure_endpoint,
                )
            )
            result = from_openai(
                client,
                model=model_name,
                mode=mode if mode else instructor.Mode.TOOLS,
                **kwargs,
            )
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The openai package is required to use the Azure OpenAI provider. "
                "Install it with `pip install openai`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "databricks":
        try:
            import os
            import openai
            from instructor import from_openai  # type: ignore[attr-defined]

            api_key = (
                api_key
                or os.environ.get("DATABRICKS_TOKEN")
                or os.environ.get("DATABRICKS_API_KEY")
            )
            if not api_key:
                from .core.exceptions import ConfigurationError

                raise ConfigurationError(
                    "DATABRICKS_TOKEN is not set. "
                    "Set it with `export DATABRICKS_TOKEN=<your-token>` or `export DATABRICKS_API_KEY=<your-token>` "
                    "or pass it as kwarg `api_key=<your-token>`."
                )

            base_url = kwargs.pop("base_url", None)
            if base_url is None:
                base_url = (
                    os.environ.get("DATABRICKS_BASE_URL")
                    or os.environ.get("DATABRICKS_HOST")
                    or os.environ.get("DATABRICKS_WORKSPACE_URL")
                )

            if not base_url:
                from .core.exceptions import ConfigurationError

                raise ConfigurationError(
                    "DATABRICKS_HOST is not set. "
                    "Set it with `export DATABRICKS_HOST=<your-workspace-url>` or `export DATABRICKS_WORKSPACE_URL=<your-workspace-url>` "
                    "or pass `base_url=<your-workspace-url>`."
                )

            base_url = str(base_url).rstrip("/")
            if not base_url.endswith("/serving-endpoints"):
                base_url = f"{base_url}/serving-endpoints"

            openai_client_kwargs = {}
            for key in (
                "organization",
                "timeout",
                "max_retries",
                "default_headers",
                "http_client",
                "app_info",
            ):
                if key in kwargs:
                    openai_client_kwargs[key] = kwargs.pop(key)

            client = (
                openai.AsyncOpenAI(
                    api_key=api_key, base_url=base_url, **openai_client_kwargs
                )
                if async_client
                else openai.OpenAI(
                    api_key=api_key, base_url=base_url, **openai_client_kwargs
                )
            )
            result = from_openai(
                client,
                model=model_name,
                mode=mode if mode else instructor.Mode.TOOLS,
                **kwargs,
            )
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The openai package is required to use the Databricks provider. "
                "Install it with `pip install openai`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise
    elif provider == "anthropic":
        try:
            import anthropic
            from instructor import from_anthropic  # type: ignore[attr-defined]  # type: ignore[attr-defined]

            client = (
                anthropic.AsyncAnthropic(api_key=api_key)
                if async_client
                else anthropic.Anthropic(api_key=api_key)
            )
            max_tokens = kwargs.pop("max_tokens", 4096)
            result = from_anthropic(
                client,
                model=model_name,
                mode=mode if mode else instructor.Mode.ANTHROPIC_TOOLS,
                max_tokens=max_tokens,
                **kwargs,
            )
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The anthropic package is required to use the Anthropic provider. "
                "Install it with `pip install anthropic`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "google":
        # Import google-genai package - catch ImportError only for actual imports
        try:
            import google.genai as genai
            from instructor import from_genai  # type: ignore[attr-defined]
        except ImportError as e:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The google-genai package is required to use the Google provider. "
                "Install it with `pip install google-genai`."
            ) from e

        try:
            import os

            # Remove vertexai from kwargs if present to avoid passing it twice
            vertexai_flag = kwargs.pop("vertexai", False)

            # Get API key from kwargs or environment
            api_key = api_key or os.environ.get("GOOGLE_API_KEY")

            # Extract client-specific parameters
            client_kwargs = {}
            for key in [
                "debug_config",
                "http_options",
                "credentials",
                "project",
                "location",
            ]:
                if key in kwargs:
                    client_kwargs[key] = kwargs.pop(key)

            client = genai.Client(
                vertexai=vertexai_flag,
                api_key=api_key,
                **client_kwargs,
            )  # type: ignore
            if async_client:
                result = from_genai(
                    client,
                    use_async=True,
                    model=model_name,
                    mode=mode if mode else instructor.Mode.GENAI_TOOLS,
                    **kwargs,
                )  # type: ignore
            else:
                result = from_genai(
                    client,
                    model=model_name,
                    mode=mode if mode else instructor.Mode.GENAI_TOOLS,
                    **kwargs,
                )  # type: ignore
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "mistral":
        try:
            from mistralai import Mistral
            from instructor import from_mistral  # type: ignore[attr-defined]
            import os

            api_key = api_key or os.environ.get("MISTRAL_API_KEY")

            if api_key:
                client = Mistral(api_key=api_key)
            else:
                raise ValueError(
                    "MISTRAL_API_KEY is not set. "
                    "Set it with `export MISTRAL_API_KEY=<your-api-key>`."
                )

            if async_client:
                result = from_mistral(
                    client, model=model_name, use_async=True, **kwargs
                )
            else:
                result = from_mistral(client, model=model_name, **kwargs)
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The mistralai package is required to use the Mistral provider. "
                "Install it with `pip install mistralai`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "cohere":
        try:
            import cohere
            from instructor import from_cohere  # type: ignore[attr-defined]

            client = (
                cohere.AsyncClientV2(api_key=api_key)
                if async_client
                else cohere.ClientV2(api_key=api_key)
            )
            result = from_cohere(client, model=model_name, **kwargs)
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The cohere package is required to use the Cohere provider. "
                "Install it with `pip install cohere`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "perplexity":
        try:
            import openai
            from instructor import from_perplexity  # type: ignore[attr-defined]
            import os

            api_key = api_key or os.environ.get("PERPLEXITY_API_KEY")
            if not api_key:
                raise ValueError(
                    "PERPLEXITY_API_KEY is not set. "
                    "Set it with `export PERPLEXITY_API_KEY=<your-api-key>` or pass it as a kwarg api_key=<your-api-key>"
                )

            client = (
                openai.AsyncOpenAI(
                    api_key=api_key, base_url="https://api.perplexity.ai"
                )
                if async_client
                else openai.OpenAI(
                    api_key=api_key, base_url="https://api.perplexity.ai"
                )
            )
            result = from_perplexity(client, model=model_name, **kwargs)
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The openai package is required to use the Perplexity provider. "
                "Install it with `pip install openai`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "groq":
        try:
            import groq
            from instructor import from_groq  # type: ignore[attr-defined]

            client = (
                groq.AsyncGroq(api_key=api_key)
                if async_client
                else groq.Groq(api_key=api_key)
            )
            result = from_groq(client, model=model_name, **kwargs)
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The groq package is required to use the Groq provider. "
                "Install it with `pip install groq`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "writer":
        try:
            from writerai import AsyncWriter, Writer
            from instructor import from_writer  # type: ignore[attr-defined]

            client = (
                AsyncWriter(api_key=api_key)
                if async_client
                else Writer(api_key=api_key)
            )
            result = from_writer(client, model=model_name, **kwargs)
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The writerai package is required to use the Writer provider. "
                "Install it with `pip install writer-sdk`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "bedrock":
        try:
            import os
            import boto3
            from instructor import from_bedrock  # type: ignore[attr-defined]

            # Get AWS configuration from environment or kwargs
            if "region" in kwargs:
                region = kwargs.pop("region")
            else:
                logger.debug(
                    "AWS_DEFAULT_REGION is not set. Using default region us-east-1"
                )
                region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")

            # Extract AWS-specific parameters
            # Dictionary to collect AWS credentials and session parameters for boto3 client
            aws_kwargs = {}
            for key in [
                "aws_access_key_id",
                "aws_secret_access_key",
                "aws_session_token",
            ]:
                if key in kwargs:
                    aws_kwargs[key] = kwargs.pop(key)
                elif key.upper() in os.environ:
                    logger.debug(f"Using {key.upper()} from environment variable")
                    aws_kwargs[key] = os.environ[key.upper()]

            # Add region to client configuration
            aws_kwargs["region_name"] = region

            # Create bedrock-runtime client
            client = boto3.client("bedrock-runtime", **aws_kwargs)

            # Determine default mode based on model
            if mode is None:
                # Anthropic models (Claude) support tools, others use JSON
                if model_name and (
                    "anthropic" in model_name.lower() or "claude" in model_name.lower()
                ):
                    default_mode = instructor.Mode.BEDROCK_TOOLS
                else:
                    default_mode = instructor.Mode.BEDROCK_JSON
            else:
                default_mode = mode

            result = from_bedrock(
                client,
                mode=default_mode,
                async_client=async_client,
                _async=async_client,  # for backward compatibility
                **kwargs,
            )
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The boto3 package is required to use the AWS Bedrock provider. "
                "Install it with `pip install boto3`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "cerebras":
        try:
            from cerebras.cloud.sdk import AsyncCerebras, Cerebras
            from instructor import from_cerebras  # type: ignore[attr-defined]

            client = (
                AsyncCerebras(api_key=api_key)
                if async_client
                else Cerebras(api_key=api_key)
            )
            result = from_cerebras(client, model=model_name, **kwargs)
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The cerebras package is required to use the Cerebras provider. "
                "Install it with `pip install cerebras`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "fireworks":
        try:
            from fireworks.client import AsyncFireworks, Fireworks
            from instructor import from_fireworks  # type: ignore[attr-defined]

            client = (
                AsyncFireworks(api_key=api_key)
                if async_client
                else Fireworks(api_key=api_key)
            )
            result = from_fireworks(client, model=model_name, **kwargs)
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The fireworks-ai package is required to use the Fireworks provider. "
                "Install it with `pip install fireworks-ai`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "vertexai":
        warnings.warn(
            "The 'vertexai' provider is deprecated. Use 'google' provider with vertexai=True instead. "
            "Example: instructor.from_provider('google/gemini-pro', vertexai=True)",
            DeprecationWarning,
            stacklevel=2,
        )
        # Import google-genai package - catch ImportError only for actual imports
        try:
            import google.genai as genai  # type: ignore
            from instructor import from_genai  # type: ignore[attr-defined]
        except ImportError as e:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The google-genai package is required to use the VertexAI provider. "
                "Install it with `pip install google-genai`."
            ) from e

        try:
            import os

            # Get project and location from kwargs or environment
            project = kwargs.pop("project", os.environ.get("GOOGLE_CLOUD_PROJECT"))
            location = kwargs.pop(
                "location", os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1")
            )

            if not project:
                raise ValueError(
                    "Project ID is required for Vertex AI. "
                    "Set it with `export GOOGLE_CLOUD_PROJECT=<your-project-id>` "
                    "or pass it as kwarg project=<your-project-id>"
                )

            client = genai.Client(
                vertexai=True,
                project=project,
                location=location,
                **kwargs,
            )  # type: ignore
            kwargs["model"] = model_name  # Pass model as part of kwargs
            if async_client:
                result = from_genai(
                    client,
                    use_async=True,
                    mode=mode if mode else instructor.Mode.GENAI_TOOLS,
                    **kwargs,
                )  # type: ignore
            else:
                result = from_genai(
                    client, mode=mode if mode else instructor.Mode.GENAI_TOOLS, **kwargs
                )  # type: ignore
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "generative-ai":
        warnings.warn(
            "The 'generative-ai' provider is deprecated. Use 'google' provider instead. "
            "Example: instructor.from_provider('google/gemini-pro')",
            DeprecationWarning,
            stacklevel=2,
        )
        # Import google-genai package - catch ImportError only for actual imports
        try:
            from google import genai
            from instructor import from_genai  # type: ignore[attr-defined]
        except ImportError as e:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The google-genai package is required to use the Google GenAI provider. "
                "Install it with `pip install google-genai`."
            ) from e

        try:
            import os

            # Get API key from kwargs or environment
            api_key = api_key or os.environ.get("GOOGLE_API_KEY")

            client = genai.Client(vertexai=False, api_key=api_key)
            if async_client:
                result = from_genai(
                    client,
                    use_async=True,
                    model=model_name,
                    mode=mode if mode else instructor.Mode.GENAI_TOOLS,
                    **kwargs,
                )  # type: ignore
            else:
                result = from_genai(
                    client,
                    model=model_name,
                    mode=mode if mode else instructor.Mode.GENAI_TOOLS,
                    **kwargs,
                )  # type: ignore
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "ollama":
        try:
            import openai
            from instructor import from_openai  # type: ignore[attr-defined]

            # Get base_url from kwargs or use default
            base_url = kwargs.pop("base_url", "http://localhost:11434/v1")
            api_key = kwargs.pop("api_key", "ollama")  # required but unused

            client = (
                openai.AsyncOpenAI(base_url=base_url, api_key=api_key)
                if async_client
                else openai.OpenAI(base_url=base_url, api_key=api_key)
            )

            # Models that support function calling (tools mode)
            tool_capable_models = {
                "llama3.1",
                "llama3.2",
                "llama4",
                "mistral-nemo",
                "firefunction-v2",
                "command-a",
                "command-r",
                "command-r-plus",
                "command-r7b",
                "qwen2.5",
                "qwen2.5-coder",
                "qwen3",
                "devstral",
            }

            # Check if model supports tools by looking at model name
            supports_tools = any(
                capable_model in model_name.lower()
                for capable_model in tool_capable_models
            )

            default_mode = (
                instructor.Mode.TOOLS if supports_tools else instructor.Mode.JSON
            )

            result = from_openai(
                client,
                model=model_name,
                mode=mode if mode else default_mode,
                **kwargs,
            )
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The openai package is required to use the Ollama provider. "
                "Install it with `pip install openai`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "deepseek":
        try:
            import openai
            from instructor import from_openai  # type: ignore[attr-defined]
            import os

            # Get API key from kwargs or environment
            api_key = api_key or os.environ.get("DEEPSEEK_API_KEY")

            if not api_key:
                from .core.exceptions import ConfigurationError

                raise ConfigurationError(
                    "DEEPSEEK_API_KEY is not set. "
                    "Set it with `export DEEPSEEK_API_KEY=<your-api-key>` or pass it as kwarg api_key=<your-api-key>"
                )

            # DeepSeek uses OpenAI-compatible API
            base_url = kwargs.pop("base_url", "https://api.deepseek.com")

            client = (
                openai.AsyncOpenAI(api_key=api_key, base_url=base_url)
                if async_client
                else openai.OpenAI(api_key=api_key, base_url=base_url)
            )

            result = from_openai(
                client,
                model=model_name,
                mode=mode if mode else instructor.Mode.TOOLS,
                **kwargs,
            )
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The openai package is required to use the DeepSeek provider. "
                "Install it with `pip install openai`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "xai":
        try:
            from xai_sdk.sync.client import Client as SyncClient
            from xai_sdk.aio.client import Client as AsyncClient
            from instructor import from_xai  # type: ignore[attr-defined]

            client = (
                AsyncClient(api_key=api_key)
                if async_client
                else SyncClient(api_key=api_key)
            )
            result = from_xai(
                client,
                mode=mode if mode else instructor.Mode.XAI_JSON,
                model=model_name,
                **kwargs,
            )
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The xAI provider needs the optional dependency `xai-sdk`. "
                'Install it with `uv pip install "instructor[xai]"` (or `pip install "instructor[xai]"`). '
                "Note: xai-sdk requires Python 3.10+."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "openrouter":
        try:
            import openai
            from instructor import from_openai  # type: ignore[attr-defined]
            import os

            # Get API key from kwargs or environment
            api_key = api_key or os.environ.get("OPENROUTER_API_KEY")

            if not api_key:
                from .core.exceptions import ConfigurationError

                raise ConfigurationError(
                    "OPENROUTER_API_KEY is not set. "
                    "Set it with `export OPENROUTER_API_KEY=<your-api-key>` or pass it as kwarg api_key=<your-api-key>"
                )

            # OpenRouter uses OpenAI-compatible API
            base_url = kwargs.pop("base_url", "https://openrouter.ai/api/v1")

            client = (
                openai.AsyncOpenAI(api_key=api_key, base_url=base_url)
                if async_client
                else openai.OpenAI(api_key=api_key, base_url=base_url)
            )

            result = from_openai(
                client,
                model=model_name,
                mode=mode if mode else instructor.Mode.TOOLS,
                **kwargs,
            )
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The openai package is required to use the OpenRouter provider. "
                "Install it with `pip install openai`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    elif provider == "litellm":
        try:
            from litellm import completion, acompletion
            from instructor import from_litellm

            completion_func = acompletion if async_client else completion
            result = from_litellm(
                completion_func,
                mode=mode if mode else instructor.Mode.TOOLS,
                **kwargs,
            )
            logger.info(
                "Client initialized",
                extra={**provider_info, "status": "success"},
            )
            return result
        except ImportError:
            from .core.exceptions import ConfigurationError

            raise ConfigurationError(
                "The litellm package is required to use the LiteLLM provider. "
                "Install it with `pip install litellm`."
            ) from None
        except Exception as e:
            logger.error(
                "Error initializing %s client: %s",
                provider,
                e,
                exc_info=True,
                extra={**provider_info, "status": "error"},
            )
            raise

    else:
        from .core.exceptions 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}"
        )
Source code in instructor/core/client.py
def from_openai(
    client: openai.OpenAI | openai.AsyncOpenAI,
    mode: instructor.Mode = instructor.Mode.TOOLS,
    **kwargs: Any,
) -> Instructor | AsyncInstructor:
    if hasattr(client, "base_url"):
        provider = get_provider(str(client.base_url))
    else:
        provider = Provider.OPENAI

    if not isinstance(client, (openai.OpenAI, openai.AsyncOpenAI)):
        import warnings

        warnings.warn(
            "Client should be an instance of openai.OpenAI or openai.AsyncOpenAI. Unexpected behavior may occur with other client types.",
            stacklevel=2,
        )

    if provider in {Provider.OPENROUTER}:
        assert mode in {
            instructor.Mode.TOOLS,
            instructor.Mode.OPENROUTER_STRUCTURED_OUTPUTS,
            instructor.Mode.JSON,
        }

    if provider in {Provider.ANYSCALE, Provider.TOGETHER}:
        assert mode in {
            instructor.Mode.TOOLS,
            instructor.Mode.JSON,
            instructor.Mode.JSON_SCHEMA,
            instructor.Mode.MD_JSON,
        }

    if provider in {Provider.OPENAI, Provider.DATABRICKS}:
        assert mode in {
            instructor.Mode.TOOLS,
            instructor.Mode.JSON,
            instructor.Mode.FUNCTIONS,
            instructor.Mode.PARALLEL_TOOLS,
            instructor.Mode.MD_JSON,
            instructor.Mode.TOOLS_STRICT,
            instructor.Mode.JSON_O1,
            instructor.Mode.RESPONSES_TOOLS,
            instructor.Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
        }

    if isinstance(client, openai.OpenAI):
        return Instructor(
            client=client,
            create=instructor.patch(
                create=(
                    client.chat.completions.create
                    if mode
                    not in {
                        instructor.Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
                        instructor.Mode.RESPONSES_TOOLS,
                    }
                    else partial(map_chat_completion_to_response, client=client)
                ),
                mode=mode,
            ),
            mode=mode,
            provider=provider,
            **kwargs,
        )

    if isinstance(client, openai.AsyncOpenAI):
        return AsyncInstructor(
            client=client,
            create=instructor.patch(
                create=(
                    client.chat.completions.create
                    if mode
                    not in {
                        instructor.Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
                        instructor.Mode.RESPONSES_TOOLS,
                    }
                    else partial(async_map_chat_completion_to_response, client=client)
                ),
                mode=mode,
            ),
            mode=mode,
            provider=provider,
            **kwargs,
        )
Source code in instructor/core/client.py
def from_litellm(
    completion: Callable[..., Any] | Callable[..., Awaitable[Any]],
    mode: instructor.Mode = instructor.Mode.TOOLS,
    **kwargs: Any,
) -> Instructor | AsyncInstructor:
    is_async = inspect.iscoroutinefunction(completion)

    if not is_async:
        return Instructor(
            client=None,
            create=instructor.patch(create=completion, mode=mode),
            mode=mode,
            **kwargs,
        )
    else:
        return AsyncInstructor(
            client=None,
            create=instructor.patch(create=completion, mode=mode),
            mode=mode,
            **kwargs,
        )

DSL Components

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

Backwards compatibility module for instructor.dsl.validators.

This module provides lazy imports to avoid circular import issues.

__getattr__(name)

Lazy import to avoid circular dependencies.

Source code in instructor/dsl/validators.py
def __getattr__(name: str):
    """Lazy import to avoid circular dependencies."""
    from ..processing import validators as processing_validators
    from .. import validation

    # Try processing.validators first
    if hasattr(processing_validators, name):
        return getattr(processing_validators, name)

    # Then try validation module
    if hasattr(validation, name):
        return getattr(validation, name)

    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

IterableBase

Source code in instructor/dsl/iterable.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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
class IterableBase:
    task_type: ClassVar[Optional[type[BaseModel]]] = None

    @classmethod
    def from_streaming_response(
        cls, completion: Iterable[Any], mode: Mode, **kwargs: Any
    ) -> Generator[BaseModel, None, None]:  # noqa: ARG003
        json_chunks = cls.extract_json(completion, mode)

        if mode in {Mode.MD_JSON, Mode.GEMINI_TOOLS}:
            json_chunks = extract_json_from_stream(json_chunks)

        if mode in {Mode.VERTEXAI_TOOLS, Mode.MISTRAL_TOOLS}:
            response = next(json_chunks)
            if not response:
                return

            json_response = json.loads(response)
            if not json_response["tasks"]:
                return

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

        yield from cls.tasks_from_chunks(json_chunks, **kwargs)

    @classmethod
    async def from_streaming_response_async(
        cls, completion: AsyncGenerator[Any, None], mode: Mode, **kwargs: Any
    ) -> AsyncGenerator[BaseModel, None]:
        json_chunks = cls.extract_json_async(completion, mode)

        if mode == Mode.MD_JSON:
            json_chunks = extract_json_from_stream_async(json_chunks)

        if mode in {Mode.MISTRAL_TOOLS, Mode.VERTEXAI_TOOLS}:
            async for item in cls.tasks_from_mistral_chunks(json_chunks, **kwargs):
                yield item
        else:
            async for item in cls.tasks_from_chunks_async(json_chunks, **kwargs):
                yield item

    @classmethod
    async def tasks_from_mistral_chunks(
        cls, json_chunks: AsyncGenerator[str, None], **kwargs: Any
    ) -> AsyncGenerator[BaseModel, None]:
        """Process streaming chunks from Mistral and VertexAI.

        Handles the specific JSON format used by these providers when streaming."""

        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_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], mode: Mode
    ) -> Generator[str, None, None]:
        json_started = False
        for chunk in completion:
            try:
                if mode in {Mode.COHERE_TOOLS, Mode.COHERE_JSON_SCHEMA}:
                    event_type = getattr(chunk, "event_type", None)
                    if event_type == "text-generation":
                        if text := getattr(chunk, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (text.find("{"), text.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    elif event_type == "tool-calls-chunk":
                        delta = getattr(chunk, "tool_call_delta", None)
                        args = getattr(delta, "parameters", None) or getattr(
                            delta, "text", None
                        )
                        if args:
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (args.find("{"), args.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                args = args[json_start:]
                            yield args
                        elif text := getattr(chunk, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (text.find("{"), text.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    elif event_type == "tool-calls-generation":
                        tool_calls = getattr(chunk, "tool_calls", None)
                        if tool_calls:
                            args = json.dumps(tool_calls[0].parameters)
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (args.find("{"), args.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                args = args[json_start:]
                            yield args
                        elif text := getattr(chunk, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (text.find("{"), text.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    else:
                        chunk_type = getattr(chunk, "type", None)
                        if chunk_type == "content-delta":
                            delta = getattr(chunk, "delta", None)
                            message = getattr(delta, "message", None)
                            content = getattr(message, "content", None)
                            if text := getattr(content, "text", None):
                                if not json_started:
                                    json_start = min(
                                        (
                                            pos
                                            for pos in (
                                                text.find("{"),
                                                text.find("["),
                                            )
                                            if pos != -1
                                        ),
                                        default=-1,
                                    )
                                    if json_start == -1:
                                        continue
                                    json_started = True
                                    text = text[json_start:]
                                yield text
                        elif chunk_type == "tool-call-delta":
                            delta = getattr(chunk, "delta", None)
                            message = getattr(delta, "message", None)
                            tool_calls = getattr(message, "tool_calls", None)
                            function = getattr(tool_calls, "function", None)
                            if args := getattr(function, "arguments", None):
                                if not json_started:
                                    json_start = min(
                                        (
                                            pos
                                            for pos in (
                                                args.find("{"),
                                                args.find("["),
                                            )
                                            if pos != -1
                                        ),
                                        default=-1,
                                    )
                                    if json_start == -1:
                                        continue
                                    json_started = True
                                    args = args[json_start:]
                                yield args
                if mode == Mode.ANTHROPIC_JSON:
                    if json_chunk := chunk.delta.text:
                        yield json_chunk
                if mode == Mode.ANTHROPIC_TOOLS:
                    yield chunk.delta.partial_json
                if mode == Mode.GEMINI_JSON:
                    yield chunk.text
                if mode == Mode.VERTEXAI_JSON:
                    yield chunk.candidates[0].content.parts[0].text
                if mode == Mode.VERTEXAI_TOOLS:
                    yield json.dumps(
                        chunk.candidates[0].content.parts[0].function_call.args
                    )
                if mode == Mode.MISTRAL_STRUCTURED_OUTPUTS:
                    yield chunk.data.choices[0].delta.content
                if mode == Mode.MISTRAL_TOOLS:
                    if not chunk.data.choices[0].delta.tool_calls:
                        continue
                    yield chunk.data.choices[0].delta.tool_calls[0].function.arguments

                if mode in {Mode.GENAI_TOOLS}:
                    yield json.dumps(
                        chunk.candidates[0].content.parts[0].function_call.args
                    )
                if mode in {Mode.GENAI_STRUCTURED_OUTPUTS}:
                    yield chunk.candidates[0].content.parts[0].text

                if mode in {Mode.GEMINI_TOOLS}:
                    resp = chunk.candidates[0].content.parts[0].function_call
                    resp_dict = type(resp).to_dict(resp)  # type:ignore

                    if "args" in resp_dict:
                        yield json.dumps(resp_dict["args"])

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

                    if isinstance(chunk, ResponseFunctionCallArgumentsDeltaEvent):
                        yield chunk.delta
                elif chunk.choices:
                    if mode == Mode.FUNCTIONS:
                        Mode.warn_mode_functions_deprecation()
                        if json_chunk := chunk.choices[0].delta.function_call.arguments:
                            yield json_chunk
                    elif mode in {
                        Mode.JSON,
                        Mode.MD_JSON,
                        Mode.JSON_SCHEMA,
                        Mode.CEREBRAS_JSON,
                        Mode.FIREWORKS_JSON,
                        Mode.PERPLEXITY_JSON,
                        Mode.WRITER_JSON,
                    }:
                        if json_chunk := chunk.choices[0].delta.content:
                            yield json_chunk
                    elif mode in {
                        Mode.TOOLS,
                        Mode.TOOLS_STRICT,
                        Mode.FIREWORKS_TOOLS,
                        Mode.WRITER_TOOLS,
                    }:
                        if json_chunk := chunk.choices[0].delta.tool_calls:
                            if json_chunk[0].function.arguments is not None:
                                yield json_chunk[0].function.arguments
                    else:
                        raise NotImplementedError(
                            f"Mode {mode} is not supported for MultiTask streaming"
                        )
            except AttributeError:
                pass

    @staticmethod
    async def extract_json_async(
        completion: AsyncGenerator[Any, None], mode: Mode
    ) -> AsyncGenerator[str, None]:
        json_started = False
        async for chunk in completion:
            try:
                if mode in {Mode.COHERE_TOOLS, Mode.COHERE_JSON_SCHEMA}:
                    event_type = getattr(chunk, "event_type", None)
                    if event_type == "text-generation":
                        if text := getattr(chunk, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (text.find("{"), text.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    elif event_type == "tool-calls-chunk":
                        delta = getattr(chunk, "tool_call_delta", None)
                        args = getattr(delta, "parameters", None) or getattr(
                            delta, "text", None
                        )
                        if args:
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (args.find("{"), args.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                args = args[json_start:]
                            yield args
                        elif text := getattr(chunk, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (text.find("{"), text.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    elif event_type == "tool-calls-generation":
                        tool_calls = getattr(chunk, "tool_calls", None)
                        if tool_calls:
                            args = json.dumps(tool_calls[0].parameters)
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (args.find("{"), args.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                args = args[json_start:]
                            yield args
                        elif text := getattr(chunk, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (text.find("{"), text.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    else:
                        chunk_type = getattr(chunk, "type", None)
                        if chunk_type == "content-delta":
                            delta = getattr(chunk, "delta", None)
                            message = getattr(delta, "message", None)
                            content = getattr(message, "content", None)
                            if text := getattr(content, "text", None):
                                if not json_started:
                                    json_start = min(
                                        (
                                            pos
                                            for pos in (
                                                text.find("{"),
                                                text.find("["),
                                            )
                                            if pos != -1
                                        ),
                                        default=-1,
                                    )
                                    if json_start == -1:
                                        continue
                                    json_started = True
                                    text = text[json_start:]
                                yield text
                        elif chunk_type == "tool-call-delta":
                            delta = getattr(chunk, "delta", None)
                            message = getattr(delta, "message", None)
                            tool_calls = getattr(message, "tool_calls", None)
                            function = getattr(tool_calls, "function", None)
                            if args := getattr(function, "arguments", None):
                                if not json_started:
                                    json_start = min(
                                        (
                                            pos
                                            for pos in (
                                                args.find("{"),
                                                args.find("["),
                                            )
                                            if pos != -1
                                        ),
                                        default=-1,
                                    )
                                    if json_start == -1:
                                        continue
                                    json_started = True
                                    args = args[json_start:]
                                yield args
                if mode == Mode.ANTHROPIC_JSON:
                    if json_chunk := chunk.delta.text:
                        yield json_chunk
                if mode == Mode.ANTHROPIC_TOOLS:
                    yield chunk.delta.partial_json
                if mode == Mode.VERTEXAI_JSON:
                    yield chunk.candidates[0].content.parts[0].text
                if mode == Mode.VERTEXAI_TOOLS:
                    yield json.dumps(
                        chunk.candidates[0].content.parts[0].function_call.args
                    )
                if mode == Mode.MISTRAL_STRUCTURED_OUTPUTS:
                    yield chunk.data.choices[0].delta.content
                if mode == Mode.MISTRAL_TOOLS:
                    if not chunk.data.choices[0].delta.tool_calls:
                        continue
                    yield chunk.data.choices[0].delta.tool_calls[0].function.arguments
                if mode == Mode.GENAI_STRUCTURED_OUTPUTS:
                    yield chunk.text
                if mode in {Mode.GENAI_TOOLS}:
                    yield json.dumps(
                        chunk.candidates[0].content.parts[0].function_call.args
                    )
                if mode in {
                    Mode.RESPONSES_TOOLS,
                    Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
                }:
                    from openai.types.responses import (
                        ResponseFunctionCallArgumentsDeltaEvent,
                    )

                    if isinstance(chunk, ResponseFunctionCallArgumentsDeltaEvent):
                        yield chunk.delta
                elif chunk.choices:
                    if mode == Mode.FUNCTIONS:
                        Mode.warn_mode_functions_deprecation()
                        if json_chunk := chunk.choices[0].delta.function_call.arguments:
                            yield json_chunk
                    elif mode in {
                        Mode.JSON,
                        Mode.MD_JSON,
                        Mode.JSON_SCHEMA,
                        Mode.CEREBRAS_JSON,
                        Mode.FIREWORKS_JSON,
                        Mode.PERPLEXITY_JSON,
                        Mode.WRITER_JSON,
                    }:
                        if json_chunk := chunk.choices[0].delta.content:
                            yield json_chunk
                    elif mode in {
                        Mode.TOOLS,
                        Mode.TOOLS_STRICT,
                        Mode.FIREWORKS_TOOLS,
                        Mode.WRITER_TOOLS,
                    }:
                        if json_chunk := chunk.choices[0].delta.tool_calls:
                            if json_chunk[0].function.arguments is not None:
                                yield json_chunk[0].function.arguments
                    else:
                        raise NotImplementedError(
                            f"Mode {mode} is not supported for MultiTask streaming"
                        )
            except AttributeError:
                pass

    @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_mistral_chunks(json_chunks, **kwargs) async classmethod

Process streaming chunks from Mistral and VertexAI.

Handles the specific JSON format used by these providers when streaming.

Source code in instructor/dsl/iterable.py
@classmethod
async def tasks_from_mistral_chunks(
    cls, json_chunks: AsyncGenerator[str, None], **kwargs: Any
) -> AsyncGenerator[BaseModel, None]:
    """Process streaming chunks from Mistral and VertexAI.

    Handles the specific JSON format used by these providers when streaming."""

    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

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/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
                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/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
            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/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/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/dsl/partial.py
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
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
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], mode: Mode, **kwargs: Any
    ) -> Generator[T_Model, None, None]:
        json_chunks = cls.extract_json(completion, mode)

        if mode in {Mode.MD_JSON, Mode.GEMINI_TOOLS}:
            json_chunks = extract_json_from_stream(json_chunks)

        if mode == Mode.WRITER_TOOLS:
            yield from cls.writer_model_from_chunks(json_chunks, **kwargs)
        else:
            yield from cls.model_from_chunks(json_chunks, **kwargs)

    @classmethod
    async def from_streaming_response_async(
        cls, completion: AsyncGenerator[Any, None], mode: Mode, **kwargs: Any
    ) -> AsyncGenerator[T_Model, None]:
        json_chunks = cls.extract_json_async(completion, mode)

        if mode == Mode.MD_JSON:
            json_chunks = extract_json_from_stream_async(json_chunks)

        if mode == Mode.WRITER_TOOLS:
            async for item in cls.writer_model_from_chunks_async(json_chunks, **kwargs):
                yield item
        else:
            async for item in cls.model_from_chunks_async(json_chunks, **kwargs):
                yield item

    @classmethod
    def writer_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:
            # Writer mode special handling: chunk might be complete JSON replacing accumulated
            if (
                len(chunk) > len(potential_object)
                and chunk.startswith("{")
                and chunk.endswith("}")
            ):
                potential_object = chunk
            else:
                potential_object += 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 writer_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:
            # Writer mode special handling: chunk might be complete JSON replacing accumulated
            if (
                len(chunk) > len(potential_object)
                and chunk.startswith("{")
                and chunk.endswith("}")
            ):
                potential_object = chunk
            else:
                potential_object += 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
    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], mode: Mode
    ) -> Generator[str, None, None]:
        """Extract JSON chunks from various LLM provider streaming responses.

        Each provider has a different structure for streaming responses that needs
        specific handling to extract the relevant JSON data."""
        json_started = False
        for chunk in completion:
            try:
                if mode in {Mode.COHERE_TOOLS, Mode.COHERE_JSON_SCHEMA}:
                    event_type = getattr(chunk, "event_type", None)
                    if event_type == "text-generation":
                        if text := getattr(chunk, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (text.find("{"), text.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    elif event_type == "tool-calls-chunk":
                        delta = getattr(chunk, "tool_call_delta", None)
                        args = getattr(delta, "parameters", None) or getattr(
                            delta, "text", None
                        )
                        if args:
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (args.find("{"), args.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                args = args[json_start:]
                            yield args
                        elif text := getattr(chunk, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (text.find("{"), text.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    elif event_type == "tool-calls-generation":
                        tool_calls = getattr(chunk, "tool_calls", None)
                        if tool_calls:
                            args = json.dumps(tool_calls[0].parameters)
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (args.find("{"), args.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                args = args[json_start:]
                            yield args
                        elif text := getattr(chunk, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (text.find("{"), text.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    else:
                        chunk_type = getattr(chunk, "type", None)
                        if chunk_type == "content-delta":
                            delta = getattr(chunk, "delta", None)
                            message = getattr(delta, "message", None)
                            content = getattr(message, "content", None)
                            if text := getattr(content, "text", None):
                                if not json_started:
                                    json_start = min(
                                        (
                                            pos
                                            for pos in (
                                                text.find("{"),
                                                text.find("["),
                                            )
                                            if pos != -1
                                        ),
                                        default=-1,
                                    )
                                    if json_start == -1:
                                        continue
                                    json_started = True
                                    text = text[json_start:]
                                yield text
                        elif chunk_type == "tool-call-delta":
                            delta = getattr(chunk, "delta", None)
                            message = getattr(delta, "message", None)
                            tool_calls = getattr(message, "tool_calls", None)
                            function = getattr(tool_calls, "function", None)
                            if args := getattr(function, "arguments", None):
                                if not json_started:
                                    json_start = min(
                                        (
                                            pos
                                            for pos in (
                                                args.find("{"),
                                                args.find("["),
                                            )
                                            if pos != -1
                                        ),
                                        default=-1,
                                    )
                                    if json_start == -1:
                                        continue
                                    json_started = True
                                    args = args[json_start:]
                                yield args
                if mode == Mode.MISTRAL_STRUCTURED_OUTPUTS:
                    yield chunk.data.choices[0].delta.content
                if mode == Mode.MISTRAL_TOOLS:
                    if not chunk.data.choices[0].delta.tool_calls:
                        continue
                    yield chunk.data.choices[0].delta.tool_calls[0].function.arguments
                if mode == Mode.ANTHROPIC_JSON:
                    if json_chunk := chunk.delta.text:
                        yield json_chunk
                if mode == Mode.ANTHROPIC_TOOLS:
                    yield chunk.delta.partial_json
                if mode == Mode.VERTEXAI_JSON:
                    yield chunk.candidates[0].content.parts[0].text
                if mode == Mode.VERTEXAI_TOOLS:
                    yield json.dumps(
                        chunk.candidates[0].content.parts[0].function_call.args
                    )

                if mode == Mode.GENAI_STRUCTURED_OUTPUTS:
                    try:
                        yield chunk.text
                    except ValueError as e:
                        if "valid `Part`" in str(e):
                            # Skip chunk with invalid Part (e.g., due to finish_reason=1 token limit)
                            continue
                        raise
                if mode == Mode.GENAI_TOOLS:
                    fc = chunk.candidates[0].content.parts[0].function_call.args
                    yield json.dumps(fc)
                if mode == Mode.GEMINI_JSON:
                    try:
                        yield chunk.text
                    except ValueError as e:
                        if "valid `Part`" in str(e):
                            # Skip chunk with invalid Part (e.g., due to finish_reason=1 token limit)
                            continue
                        raise
                if mode == Mode.GEMINI_TOOLS:
                    resp = chunk.candidates[0].content.parts[0].function_call
                    resp_dict = type(resp).to_dict(resp)  # type:ignore
                    if "args" in resp_dict:
                        yield json.dumps(resp_dict["args"])
                elif mode in {
                    Mode.RESPONSES_TOOLS,
                    Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
                }:
                    from openai.types.responses import (
                        ResponseFunctionCallArgumentsDeltaEvent,
                    )

                    if isinstance(chunk, ResponseFunctionCallArgumentsDeltaEvent):
                        yield chunk.delta

                elif chunk.choices:
                    if mode == Mode.FUNCTIONS:
                        Mode.warn_mode_functions_deprecation()
                        if json_chunk := chunk.choices[0].delta.function_call.arguments:
                            yield json_chunk
                    elif mode in {
                        Mode.JSON,
                        Mode.MD_JSON,
                        Mode.JSON_SCHEMA,
                        Mode.CEREBRAS_JSON,
                        Mode.FIREWORKS_JSON,
                        Mode.PERPLEXITY_JSON,
                        Mode.WRITER_JSON,
                    }:
                        if json_chunk := chunk.choices[0].delta.content:
                            yield json_chunk
                    elif mode in {
                        Mode.TOOLS,
                        Mode.TOOLS_STRICT,
                        Mode.FIREWORKS_TOOLS,
                        Mode.WRITER_TOOLS,
                    }:
                        if json_chunk := chunk.choices[0].delta.tool_calls:
                            if json_chunk[0].function.arguments:
                                yield json_chunk[0].function.arguments
                    else:
                        raise NotImplementedError(
                            f"Mode {mode} is not supported for MultiTask streaming"
                        )
            except AttributeError:
                pass

    @staticmethod
    async def extract_json_async(
        completion: AsyncGenerator[Any, None], mode: Mode
    ) -> AsyncGenerator[str, None]:
        json_started = False
        async for chunk in completion:
            try:
                if mode in {Mode.COHERE_TOOLS, Mode.COHERE_JSON_SCHEMA}:
                    event_type = getattr(chunk, "event_type", None)
                    if event_type == "text-generation":
                        if text := getattr(chunk, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (text.find("{"), text.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    elif event_type == "tool-calls-chunk":
                        delta = getattr(chunk, "tool_call_delta", None)
                        args = getattr(delta, "parameters", None) or getattr(
                            delta, "text", None
                        )
                        if args:
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (args.find("{"), args.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                args = args[json_start:]
                            yield args
                        elif text := getattr(chunk, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (text.find("{"), text.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    elif event_type == "tool-calls-generation":
                        tool_calls = getattr(chunk, "tool_calls", None)
                        if tool_calls:
                            args = json.dumps(tool_calls[0].parameters)
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (args.find("{"), args.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                args = args[json_start:]
                            yield args
                        elif text := getattr(chunk, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (text.find("{"), text.find("["))
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    else:
                        chunk_type = getattr(chunk, "type", None)
                        if chunk_type == "content-delta":
                            delta = getattr(chunk, "delta", None)
                            message = getattr(delta, "message", None)
                            content = getattr(message, "content", None)
                            if text := getattr(content, "text", None):
                                if not json_started:
                                    json_start = min(
                                        (
                                            pos
                                            for pos in (
                                                text.find("{"),
                                                text.find("["),
                                            )
                                            if pos != -1
                                        ),
                                        default=-1,
                                    )
                                    if json_start == -1:
                                        continue
                                    json_started = True
                                    text = text[json_start:]
                                yield text
                        elif chunk_type == "tool-call-delta":
                            delta = getattr(chunk, "delta", None)
                            message = getattr(delta, "message", None)
                            tool_calls = getattr(message, "tool_calls", None)
                            function = getattr(tool_calls, "function", None)
                            if args := getattr(function, "arguments", None):
                                if not json_started:
                                    json_start = min(
                                        (
                                            pos
                                            for pos in (
                                                args.find("{"),
                                                args.find("["),
                                            )
                                            if pos != -1
                                        ),
                                        default=-1,
                                    )
                                    if json_start == -1:
                                        continue
                                    json_started = True
                                    args = args[json_start:]
                                yield args
                if mode == Mode.ANTHROPIC_JSON:
                    if json_chunk := chunk.delta.text:
                        yield json_chunk
                if mode == Mode.ANTHROPIC_TOOLS:
                    yield chunk.delta.partial_json
                if mode == Mode.MISTRAL_STRUCTURED_OUTPUTS:
                    yield chunk.data.choices[0].delta.content
                if mode == Mode.MISTRAL_TOOLS:
                    if not chunk.data.choices[0].delta.tool_calls:
                        continue
                    yield chunk.data.choices[0].delta.tool_calls[0].function.arguments
                if mode == Mode.VERTEXAI_JSON:
                    yield chunk.candidates[0].content.parts[0].text
                if mode == Mode.VERTEXAI_TOOLS:
                    yield json.dumps(
                        chunk.candidates[0].content.parts[0].function_call.args
                    )
                if mode == Mode.GENAI_STRUCTURED_OUTPUTS:
                    try:
                        yield chunk.text
                    except ValueError as e:
                        if "valid `Part`" in str(e):
                            # Skip chunk with invalid Part (e.g., due to finish_reason=1 token limit)
                            continue
                        raise
                if mode == Mode.GENAI_TOOLS:
                    fc = chunk.candidates[0].content.parts[0].function_call.args
                    yield json.dumps(fc)
                if mode == Mode.GEMINI_JSON:
                    try:
                        yield chunk.text
                    except ValueError as e:
                        if "valid `Part`" in str(e):
                            # Skip chunk with invalid Part (e.g., due to finish_reason=1 token limit)
                            continue
                        raise
                if mode == Mode.GEMINI_TOOLS:
                    resp = chunk.candidates[0].content.parts[0].function_call
                    resp_dict = type(resp).to_dict(resp)  # type:ignore
                    if "args" in resp_dict:
                        yield json.dumps(resp_dict["args"])

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

                    if isinstance(chunk, ResponseFunctionCallArgumentsDeltaEvent):
                        yield chunk.delta
                elif chunk.choices:
                    if mode == Mode.FUNCTIONS:
                        Mode.warn_mode_functions_deprecation()
                        if json_chunk := chunk.choices[0].delta.function_call.arguments:
                            yield json_chunk
                    elif mode in {
                        Mode.JSON,
                        Mode.MD_JSON,
                        Mode.JSON_SCHEMA,
                        Mode.CEREBRAS_JSON,
                        Mode.FIREWORKS_JSON,
                        Mode.PERPLEXITY_JSON,
                        Mode.WRITER_JSON,
                    }:
                        if json_chunk := chunk.choices[0].delta.content:
                            yield json_chunk
                    elif mode in {
                        Mode.TOOLS,
                        Mode.TOOLS_STRICT,
                        Mode.FIREWORKS_TOOLS,
                        Mode.WRITER_TOOLS,
                    }:
                        if json_chunk := chunk.choices[0].delta.tool_calls:
                            if json_chunk[0].function.arguments:
                                yield json_chunk[0].function.arguments
                    else:
                        raise NotImplementedError(
                            f"Mode {mode} is not supported for MultiTask streaming"
                        )
            except AttributeError:
                pass

extract_json(completion, mode) staticmethod

Extract JSON chunks from various LLM provider streaming responses.

Each provider has a different structure for streaming responses that needs specific handling to extract the relevant JSON data.

Source code in instructor/dsl/partial.py
@staticmethod
def extract_json(
    completion: Iterable[Any], mode: Mode
) -> Generator[str, None, None]:
    """Extract JSON chunks from various LLM provider streaming responses.

    Each provider has a different structure for streaming responses that needs
    specific handling to extract the relevant JSON data."""
    json_started = False
    for chunk in completion:
        try:
            if mode in {Mode.COHERE_TOOLS, Mode.COHERE_JSON_SCHEMA}:
                event_type = getattr(chunk, "event_type", None)
                if event_type == "text-generation":
                    if text := getattr(chunk, "text", None):
                        if not json_started:
                            json_start = min(
                                (
                                    pos
                                    for pos in (text.find("{"), text.find("["))
                                    if pos != -1
                                ),
                                default=-1,
                            )
                            if json_start == -1:
                                continue
                            json_started = True
                            text = text[json_start:]
                        yield text
                elif event_type == "tool-calls-chunk":
                    delta = getattr(chunk, "tool_call_delta", None)
                    args = getattr(delta, "parameters", None) or getattr(
                        delta, "text", None
                    )
                    if args:
                        if not json_started:
                            json_start = min(
                                (
                                    pos
                                    for pos in (args.find("{"), args.find("["))
                                    if pos != -1
                                ),
                                default=-1,
                            )
                            if json_start == -1:
                                continue
                            json_started = True
                            args = args[json_start:]
                        yield args
                    elif text := getattr(chunk, "text", None):
                        if not json_started:
                            json_start = min(
                                (
                                    pos
                                    for pos in (text.find("{"), text.find("["))
                                    if pos != -1
                                ),
                                default=-1,
                            )
                            if json_start == -1:
                                continue
                            json_started = True
                            text = text[json_start:]
                        yield text
                elif event_type == "tool-calls-generation":
                    tool_calls = getattr(chunk, "tool_calls", None)
                    if tool_calls:
                        args = json.dumps(tool_calls[0].parameters)
                        if not json_started:
                            json_start = min(
                                (
                                    pos
                                    for pos in (args.find("{"), args.find("["))
                                    if pos != -1
                                ),
                                default=-1,
                            )
                            if json_start == -1:
                                continue
                            json_started = True
                            args = args[json_start:]
                        yield args
                    elif text := getattr(chunk, "text", None):
                        if not json_started:
                            json_start = min(
                                (
                                    pos
                                    for pos in (text.find("{"), text.find("["))
                                    if pos != -1
                                ),
                                default=-1,
                            )
                            if json_start == -1:
                                continue
                            json_started = True
                            text = text[json_start:]
                        yield text
                else:
                    chunk_type = getattr(chunk, "type", None)
                    if chunk_type == "content-delta":
                        delta = getattr(chunk, "delta", None)
                        message = getattr(delta, "message", None)
                        content = getattr(message, "content", None)
                        if text := getattr(content, "text", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (
                                            text.find("{"),
                                            text.find("["),
                                        )
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                text = text[json_start:]
                            yield text
                    elif chunk_type == "tool-call-delta":
                        delta = getattr(chunk, "delta", None)
                        message = getattr(delta, "message", None)
                        tool_calls = getattr(message, "tool_calls", None)
                        function = getattr(tool_calls, "function", None)
                        if args := getattr(function, "arguments", None):
                            if not json_started:
                                json_start = min(
                                    (
                                        pos
                                        for pos in (
                                            args.find("{"),
                                            args.find("["),
                                        )
                                        if pos != -1
                                    ),
                                    default=-1,
                                )
                                if json_start == -1:
                                    continue
                                json_started = True
                                args = args[json_start:]
                            yield args
            if mode == Mode.MISTRAL_STRUCTURED_OUTPUTS:
                yield chunk.data.choices[0].delta.content
            if mode == Mode.MISTRAL_TOOLS:
                if not chunk.data.choices[0].delta.tool_calls:
                    continue
                yield chunk.data.choices[0].delta.tool_calls[0].function.arguments
            if mode == Mode.ANTHROPIC_JSON:
                if json_chunk := chunk.delta.text:
                    yield json_chunk
            if mode == Mode.ANTHROPIC_TOOLS:
                yield chunk.delta.partial_json
            if mode == Mode.VERTEXAI_JSON:
                yield chunk.candidates[0].content.parts[0].text
            if mode == Mode.VERTEXAI_TOOLS:
                yield json.dumps(
                    chunk.candidates[0].content.parts[0].function_call.args
                )

            if mode == Mode.GENAI_STRUCTURED_OUTPUTS:
                try:
                    yield chunk.text
                except ValueError as e:
                    if "valid `Part`" in str(e):
                        # Skip chunk with invalid Part (e.g., due to finish_reason=1 token limit)
                        continue
                    raise
            if mode == Mode.GENAI_TOOLS:
                fc = chunk.candidates[0].content.parts[0].function_call.args
                yield json.dumps(fc)
            if mode == Mode.GEMINI_JSON:
                try:
                    yield chunk.text
                except ValueError as e:
                    if "valid `Part`" in str(e):
                        # Skip chunk with invalid Part (e.g., due to finish_reason=1 token limit)
                        continue
                    raise
            if mode == Mode.GEMINI_TOOLS:
                resp = chunk.candidates[0].content.parts[0].function_call
                resp_dict = type(resp).to_dict(resp)  # type:ignore
                if "args" in resp_dict:
                    yield json.dumps(resp_dict["args"])
            elif mode in {
                Mode.RESPONSES_TOOLS,
                Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
            }:
                from openai.types.responses import (
                    ResponseFunctionCallArgumentsDeltaEvent,
                )

                if isinstance(chunk, ResponseFunctionCallArgumentsDeltaEvent):
                    yield chunk.delta

            elif chunk.choices:
                if mode == Mode.FUNCTIONS:
                    Mode.warn_mode_functions_deprecation()
                    if json_chunk := chunk.choices[0].delta.function_call.arguments:
                        yield json_chunk
                elif mode in {
                    Mode.JSON,
                    Mode.MD_JSON,
                    Mode.JSON_SCHEMA,
                    Mode.CEREBRAS_JSON,
                    Mode.FIREWORKS_JSON,
                    Mode.PERPLEXITY_JSON,
                    Mode.WRITER_JSON,
                }:
                    if json_chunk := chunk.choices[0].delta.content:
                        yield json_chunk
                elif mode in {
                    Mode.TOOLS,
                    Mode.TOOLS_STRICT,
                    Mode.FIREWORKS_TOOLS,
                    Mode.WRITER_TOOLS,
                }:
                    if json_chunk := chunk.choices[0].delta.tool_calls:
                        if json_chunk[0].function.arguments:
                            yield json_chunk[0].function.arguments
                else:
                    raise NotImplementedError(
                        f"Mode {mode} is not supported for MultiTask streaming"
                    )
        except AttributeError:
            pass

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/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/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,
        )

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/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

    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, **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)

MaybeBase

Bases: BaseModel, Generic[T]

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

Source code in instructor/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/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",
            ),
        ),
    )

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/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/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.

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/core/exceptions.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

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/core/exceptions.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/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_STRUCTURED_OUTPUTS = "genai_structured_outputs"

    # 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 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

json_modes() classmethod

Returns a set of all JSON-based modes.

Source code in instructor/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,
    }

tool_modes() classmethod

Returns a set of all tool-based modes.

Source code in instructor/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_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/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

OpenAISchema

Bases: BaseModel

Source code in instructor/processing/function_calls.py
113
114
115
116
117
118
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
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
class OpenAISchema(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
        """
        return generate_openai_schema(cls)

    @classproperty
    def anthropic_schema(cls) -> dict[str, Any]:
        # Generate the Anthropic schema based on the OpenAI schema to avoid redundant schema generation
        return generate_anthropic_schema(cls)

    @classproperty
    def gemini_schema(cls) -> Any:
        # This is kept for backward compatibility but deprecated
        return generate_gemini_schema(cls)

    @classmethod
    def from_response(
        cls,
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
        mode: Mode = Mode.TOOLS,
    ) -> BaseModel:
        """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 openai completion mode

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

        if mode == Mode.ANTHROPIC_TOOLS:
            return cls.parse_anthropic_tools(completion, validation_context, strict)

        if mode == Mode.ANTHROPIC_TOOLS or mode == Mode.ANTHROPIC_REASONING_TOOLS:
            return cls.parse_anthropic_tools(completion, validation_context, strict)

        if mode == Mode.ANTHROPIC_JSON:
            return cls.parse_anthropic_json(completion, validation_context, strict)

        if mode == Mode.BEDROCK_JSON:
            return cls.parse_bedrock_json(completion, validation_context, strict)

        if mode == Mode.BEDROCK_TOOLS:
            return cls.parse_bedrock_tools(completion, validation_context, strict)

        if mode in {Mode.VERTEXAI_TOOLS, Mode.GEMINI_TOOLS}:
            return cls.parse_vertexai_tools(completion, validation_context)

        if mode == Mode.VERTEXAI_JSON:
            return cls.parse_vertexai_json(completion, validation_context, strict)

        if mode == Mode.COHERE_TOOLS:
            return cls.parse_cohere_tools(completion, validation_context, strict)

        if mode == Mode.GEMINI_JSON:
            return cls.parse_gemini_json(completion, validation_context, strict)

        if mode == Mode.GENAI_STRUCTURED_OUTPUTS:
            return cls.parse_genai_structured_outputs(
                completion, validation_context, strict
            )

        if mode == Mode.GEMINI_TOOLS:
            return cls.parse_gemini_tools(completion, validation_context, strict)

        if mode == Mode.GENAI_TOOLS:
            return cls.parse_genai_tools(completion, validation_context, strict)

        if mode == Mode.COHERE_JSON_SCHEMA:
            return cls.parse_cohere_json_schema(completion, validation_context, strict)

        if mode == Mode.WRITER_TOOLS:
            return cls.parse_writer_tools(completion, validation_context, strict)

        if mode == Mode.WRITER_JSON:
            return cls.parse_writer_json(completion, validation_context, strict)

        if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
            return cls.parse_responses_tools(
                completion,
                validation_context,
                strict,
            )

        if not completion.choices:
            # This helps catch errors from OpenRouter
            if hasattr(completion, "error"):
                raise ResponseParsingError(
                    f"LLM provider returned error: {completion.error}",
                    mode=str(mode),
                    raw_response=completion,
                )

            raise ResponseParsingError(
                "No completion choices found in LLM response",
                mode=str(mode),
                raw_response=completion,
            )

        if completion.choices[0].finish_reason == "length":
            raise IncompleteOutputException(last_completion=completion)

        if mode == Mode.FUNCTIONS:
            Mode.warn_mode_functions_deprecation()
            return cls.parse_functions(completion, validation_context, strict)

        if mode == Mode.MISTRAL_STRUCTURED_OUTPUTS:
            return cls.parse_mistral_structured_outputs(
                completion, validation_context, strict
            )

        if mode in {
            Mode.TOOLS,
            Mode.MISTRAL_TOOLS,
            Mode.TOOLS_STRICT,
            Mode.CEREBRAS_TOOLS,
            Mode.FIREWORKS_TOOLS,
        }:
            return cls.parse_tools(completion, validation_context, strict)

        if mode in {
            Mode.JSON,
            Mode.JSON_SCHEMA,
            Mode.MD_JSON,
            Mode.JSON_O1,
            Mode.CEREBRAS_JSON,
            Mode.FIREWORKS_JSON,
            Mode.PERPLEXITY_JSON,
            Mode.OPENROUTER_STRUCTURED_OUTPUTS,
        }:
            return cls.parse_json(completion, validation_context, strict)

        raise ConfigurationError(
            f"Invalid or unsupported mode: {mode}. This mode may not be implemented for response parsing."
        )

    @classmethod
    def parse_genai_structured_outputs(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        return cls.model_validate_json(
            completion.text, context=validation_context, strict=strict
        )

    @classmethod
    def parse_genai_tools(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        from google.genai import types

        assert isinstance(completion, types.GenerateContentResponse)
        assert len(completion.candidates) == 1

        # Filter out thought parts (parts with thought: true)
        parts = completion.candidates[0].content.parts
        non_thought_parts = [
            part for part in parts if not (hasattr(part, "thought") and part.thought)
        ]

        assert len(non_thought_parts) == 1, (
            f"Instructor does not support multiple function calls, use List[Model] instead"
        )
        function_call = non_thought_parts[0].function_call
        assert function_call is not None, (
            f"Please return your response as a function call with the schema {cls.openai_schema} and the name {cls.openai_schema['name']}"
        )

        assert function_call.name == cls.openai_schema["name"]
        return cls.model_validate(
            obj=function_call.args, context=validation_context, strict=strict
        )

    @classmethod
    def parse_cohere_json_schema(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ):
        # Handle both V1 and V2 response structures
        if hasattr(completion, "text"):
            # V1 format: direct text access
            text = completion.text
        elif hasattr(completion, "message") and hasattr(completion.message, "content"):
            # V2 format: nested structure (message.content[].text)
            # V2 responses may have multiple content items (thinking, text, etc.)
            content_items = completion.message.content
            if content_items and len(content_items) > 0:
                # Find the text content item (skip thinking/other types)
                # TODO handle these other content types
                text = None
                for item in content_items:
                    if (
                        hasattr(item, "type")
                        and item.type == "text"
                        and hasattr(item, "text")
                    ):
                        text = item.text
                        break

                if text is None:
                    raise ResponseParsingError(
                        "Cohere V2 response has no text content item",
                        mode="COHERE_JSON_SCHEMA",
                        raw_response=completion,
                    )
            else:
                raise ResponseParsingError(
                    "Cohere V2 response has no content",
                    mode="COHERE_JSON_SCHEMA",
                    raw_response=completion,
                )
        else:
            raise ResponseParsingError(
                f"Unsupported Cohere response format. Expected 'text' (V1) or "
                f"'message.content[].text' (V2), got: {type(completion)}",
                mode="COHERE_JSON_SCHEMA",
                raw_response=completion,
            )

        return cls.model_validate_json(text, context=validation_context, strict=strict)

    @classmethod
    def parse_anthropic_tools(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        from anthropic.types import Message

        if isinstance(completion, Message) and completion.stop_reason == "max_tokens":
            raise IncompleteOutputException(last_completion=completion)

        # Anthropic returns arguments as a dict, dump to json for model validation below
        tool_calls = [
            json.dumps(c.input) for c in completion.content if c.type == "tool_use"
        ]  # TODO update with anthropic specific types

        tool_calls_validator = TypeAdapter(
            Annotated[list[Any], Field(min_length=1, max_length=1)]
        )
        tool_call = tool_calls_validator.validate_python(tool_calls)[0]

        return cls.model_validate_json(
            tool_call, context=validation_context, strict=strict
        )

    @classmethod
    def parse_anthropic_json(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        from anthropic.types import Message

        last_block = None

        if hasattr(completion, "choices"):
            completion = completion.choices[0]
            if completion.finish_reason == "length":
                raise IncompleteOutputException(last_completion=completion)
            text = completion.message.content
        else:
            assert isinstance(completion, Message)
            if completion.stop_reason == "max_tokens":
                raise IncompleteOutputException(last_completion=completion)
            # Find the last text block in the completion
            # this is because the completion is a list of blocks
            # and the last block is the one that contains the text ideally
            # this could happen due to things like multiple tool calls
            # read: https://docs.anthropic.com/en/docs/build-with-claude/tool-use/web-search-tool#response
            text_blocks = [c for c in completion.content if c.type == "text"]
            last_block = text_blocks[-1]
            text = last_block.text

        extra_text = extract_json_from_codeblock(text)

        if strict:
            model = cls.model_validate_json(
                extra_text, context=validation_context, strict=True
            )
        else:
            # Allow control characters to pass through by using the non-strict JSON parser.
            parsed = json.loads(extra_text, strict=False)
            # Pydantic non-strict: https://docs.pydantic.dev/latest/concepts/strict_mode/
            model = cls.model_validate(parsed, context=validation_context, strict=False)

        return model

    @classmethod
    def parse_bedrock_json(
        cls: type[BaseModel],
        completion: Any,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        if isinstance(completion, dict):
            # OpenAI will send the first content to be 'reasoningText', and then 'text'
            content = completion["output"]["message"]["content"]
            text_content = next((c for c in content if "text" in c), None)
            if not text_content:
                raise ResponseParsingError(
                    "Unexpected format. No text content found in Bedrock response.",
                    mode="BEDROCK_JSON",
                    raw_response=completion,
                )
            text = text_content["text"]
            match = re.search(r"```?json(.*?)```?", text, re.DOTALL)
            if match:
                text = match.group(1).strip()

            text = re.sub(r"```?json|\\n", "", text).strip()
        else:
            text = completion.text
        return cls.model_validate_json(text, context=validation_context, strict=strict)

    @classmethod
    def parse_bedrock_tools(
        cls: type[BaseModel],
        completion: Any,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        if isinstance(completion, dict):
            # Extract the tool use from Bedrock response
            message = completion.get("output", {}).get("message", {})
            content = message.get("content", [])

            # Find the tool use content block
            for content_block in content:
                if "toolUse" in content_block:
                    tool_use = content_block["toolUse"]
                    assert tool_use.get("name") == cls.__name__, (
                        f"Tool name mismatch: expected {cls.__name__}, got {tool_use.get('name')}"
                    )
                    return cls.model_validate(
                        tool_use.get("input", {}),
                        context=validation_context,
                        strict=strict,
                    )

            raise ResponseParsingError(
                "No tool use found in Bedrock response",
                mode="BEDROCK_TOOLS",
                raw_response=completion,
            )
        else:
            # Fallback for other response formats
            return cls.model_validate_json(
                completion.text, context=validation_context, strict=strict
            )

    @classmethod
    def parse_gemini_json(
        cls: type[BaseModel],
        completion: Any,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        try:
            text = completion.text
        except ValueError:
            logger.debug(
                f"Error response: {completion.result.candidates[0].finish_reason}\n\n{completion.result.candidates[0].safety_ratings}"
            )

        try:
            extra_text = extract_json_from_codeblock(text)  # type: ignore
        except UnboundLocalError:
            raise ResponseParsingError(
                "Unable to extract JSON from completion text. The response may have been blocked or empty.",
                mode="GEMINI_JSON",
                raw_response=completion,
            ) from None

        if strict:
            return cls.model_validate_json(
                extra_text, context=validation_context, strict=True
            )
        else:
            # Allow control characters.
            parsed = json.loads(extra_text, strict=False)
            # Pydantic non-strict: https://docs.pydantic.dev/latest/concepts/strict_mode/
            return cls.model_validate(parsed, context=validation_context, strict=False)

    @classmethod
    def parse_vertexai_tools(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
    ) -> BaseModel:
        tool_call = completion.candidates[0].content.parts[0].function_call.args  # type: ignore
        model = {}
        for field in tool_call:  # type: ignore
            model[field] = tool_call[field]
        # We enable strict=False because the conversion from protobuf -> dict often results in types like ints being cast to floats, as a result in order for model.validate to work we need to disable strict mode.
        return cls.model_validate(model, context=validation_context, strict=False)

    @classmethod
    def parse_vertexai_json(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        return cls.model_validate_json(
            completion.text, context=validation_context, strict=strict
        )

    @classmethod
    def parse_cohere_tools(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        """
        Parse Cohere tools response.

        Supports:
        - V1 native tool calls: completion.tool_calls[0].parameters
        - V2 native tool calls: completion.message.tool_calls[0].function.arguments (JSON string)
        - V1 text-based: completion.text (prompt-based approach)
        - V2 text-based: completion.message.content[].text (prompt-based approach)
        """
        # First, check for native Cohere tool calls (V1 and V2)
        # V1: completion.tool_calls with tc.parameters (dict)
        if hasattr(completion, "tool_calls") and completion.tool_calls:
            # V1 tool call format
            tool_call = completion.tool_calls[0]
            # Parameters in V1 are already a dict
            return cls.model_validate(
                tool_call.parameters, context=validation_context, strict=strict
            )

        # V2: completion.message.tool_calls with tc.function.arguments (JSON string)
        if (
            hasattr(completion, "message")
            and hasattr(completion.message, "tool_calls")
            and completion.message.tool_calls
        ):
            # V2 tool call format
            tool_call = completion.message.tool_calls[0]
            # Arguments in V2 are a JSON string
            import json

            arguments = json.loads(tool_call.function.arguments)
            return cls.model_validate(
                arguments, context=validation_context, strict=strict
            )

        # Fallback to text-based extraction (current prompt-based approach)
        # Handle both V1 and V2 text response structures
        if hasattr(completion, "text"):
            # V1 format: direct text access
            text = completion.text
        elif hasattr(completion, "message") and hasattr(completion.message, "content"):
            # V2 format: nested structure (message.content[].text)
            # V2 responses may have multiple content items (thinking, text, etc.)
            content_items = completion.message.content
            if content_items and len(content_items) > 0:
                # Find the text content item (skip thinking/other types)
                text = None
                for item in content_items:
                    if (
                        hasattr(item, "type")
                        and item.type == "text"
                        and hasattr(item, "text")
                    ):
                        text = item.text
                        break

                if text is None:
                    raise ResponseParsingError(
                        "Cohere V2 response has no text content item",
                        mode="COHERE_TOOLS",
                        raw_response=completion,
                    )
            else:
                raise ResponseParsingError(
                    "Cohere V2 response has no content",
                    mode="COHERE_TOOLS",
                    raw_response=completion,
                )
        else:
            raise ResponseParsingError(
                f"Unsupported Cohere response format. Expected tool_calls or text content. "
                f"Got: {type(completion)}",
                mode="COHERE_TOOLS",
                raw_response=completion,
            )

        # Extract JSON from text (for prompt-based approach)
        extra_text = extract_json_from_codeblock(text)
        return cls.model_validate_json(
            extra_text, context=validation_context, strict=strict
        )

    @classmethod
    def parse_writer_tools(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        message = completion.choices[0].message
        tool_calls = message.tool_calls if message.tool_calls else "{}"
        assert len(tool_calls) == 1, (
            "Instructor does not support multiple tool calls, use List[Model] instead"
        )
        assert tool_calls[0].function.name == cls.openai_schema["name"], (
            "Tool name does not match"
        )
        loaded_args = json.loads(tool_calls[0].function.arguments)
        return cls.model_validate_json(
            json.dumps(loaded_args) if isinstance(loaded_args, dict) else loaded_args,
            context=validation_context,
            strict=strict,
        )

    @classmethod
    def parse_writer_json(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        _handle_incomplete_output(completion)

        message = completion.choices[0].message.content or ""
        json_content = extract_json_from_codeblock(message)

        if strict:
            return cls.model_validate_json(
                json_content, context=validation_context, strict=True
            )
        else:
            parsed = json.loads(json_content, strict=False)
            return cls.model_validate(parsed, context=validation_context, strict=False)

    @classmethod
    def parse_functions(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        message = completion.choices[0].message
        assert (
            message.function_call.name == cls.openai_schema["name"]  # type: ignore[index]
        ), "Function name does not match"
        return cls.model_validate_json(
            message.function_call.arguments,  # type: ignore[attr-defined]
            context=validation_context,
            strict=strict,
        )

    @classmethod
    def parse_responses_tools(
        cls: type[BaseModel],
        completion: Any,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        from openai.types.responses import ResponseFunctionToolCall

        tool_call_message = None
        for message in completion.output:
            if isinstance(message, ResponseFunctionToolCall):
                if message.name == cls.openai_schema["name"]:
                    tool_call_message = message
                    break
        if not tool_call_message:
            raise ResponseParsingError(
                f"Required tool call '{cls.openai_schema['name']}' not found in response",
                mode="RESPONSES_TOOLS",
                raw_response=completion,
            )

        return cls.model_validate_json(
            tool_call_message.arguments,  # type: ignore[attr-defined]
            context=validation_context,
            strict=strict,
        )

    @classmethod
    def parse_tools(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        message = completion.choices[0].message
        # this field seems to be missing when using instructor with some other tools (e.g. litellm)
        # trying to fix this by adding a check

        if hasattr(message, "refusal"):
            assert message.refusal is None, (
                f"Unable to generate a response due to {message.refusal}"
            )
        assert len(message.tool_calls or []) == 1, (
            f"Instructor does not support multiple tool calls, use List[Model] instead"
        )
        tool_call = message.tool_calls[0]  # type: ignore
        assert (
            tool_call.function.name == cls.openai_schema["name"]  # type: ignore[index]
        ), "Tool name does not match"
        return cls.model_validate_json(
            tool_call.function.arguments,  # type: ignore
            context=validation_context,
            strict=strict,
        )

    @classmethod
    def parse_mistral_structured_outputs(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        if not completion.choices or len(completion.choices) > 1:
            raise ConfigurationError(
                "Instructor does not support multiple tool calls in MISTRAL_STRUCTURED_OUTPUTS mode. "
                "Use list[Model] instead to handle multiple items."
            )

        message = completion.choices[0].message

        return cls.model_validate_json(
            message.content, context=validation_context, strict=strict
        )

    @classmethod
    def parse_json(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        """Parse JSON mode responses using the optimized extraction and validation."""
        # Check for incomplete output
        _handle_incomplete_output(completion)

        # Extract text from the response
        message = _extract_text_content(completion)
        if not message:
            # Fallback for OpenAI format if _extract_text_content doesn't handle it
            message = completion.choices[0].message.content or ""

        # Extract JSON from the text
        json_content = extract_json_from_codeblock(message)

        # Validate the model from the JSON
        return _validate_model_from_json(cls, json_content, validation_context, strict)

from_response(completion, validation_context=None, strict=None, mode=Mode.TOOLS) 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 openai completion mode

TOOLS

Returns:

Name Type Description
cls OpenAISchema

An instance of the class

Source code in instructor/processing/function_calls.py
@classmethod
def from_response(
    cls,
    completion: ChatCompletion,
    validation_context: Optional[dict[str, Any]] = None,
    strict: Optional[bool] = None,
    mode: Mode = Mode.TOOLS,
) -> BaseModel:
    """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 openai completion mode

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

    if mode == Mode.ANTHROPIC_TOOLS:
        return cls.parse_anthropic_tools(completion, validation_context, strict)

    if mode == Mode.ANTHROPIC_TOOLS or mode == Mode.ANTHROPIC_REASONING_TOOLS:
        return cls.parse_anthropic_tools(completion, validation_context, strict)

    if mode == Mode.ANTHROPIC_JSON:
        return cls.parse_anthropic_json(completion, validation_context, strict)

    if mode == Mode.BEDROCK_JSON:
        return cls.parse_bedrock_json(completion, validation_context, strict)

    if mode == Mode.BEDROCK_TOOLS:
        return cls.parse_bedrock_tools(completion, validation_context, strict)

    if mode in {Mode.VERTEXAI_TOOLS, Mode.GEMINI_TOOLS}:
        return cls.parse_vertexai_tools(completion, validation_context)

    if mode == Mode.VERTEXAI_JSON:
        return cls.parse_vertexai_json(completion, validation_context, strict)

    if mode == Mode.COHERE_TOOLS:
        return cls.parse_cohere_tools(completion, validation_context, strict)

    if mode == Mode.GEMINI_JSON:
        return cls.parse_gemini_json(completion, validation_context, strict)

    if mode == Mode.GENAI_STRUCTURED_OUTPUTS:
        return cls.parse_genai_structured_outputs(
            completion, validation_context, strict
        )

    if mode == Mode.GEMINI_TOOLS:
        return cls.parse_gemini_tools(completion, validation_context, strict)

    if mode == Mode.GENAI_TOOLS:
        return cls.parse_genai_tools(completion, validation_context, strict)

    if mode == Mode.COHERE_JSON_SCHEMA:
        return cls.parse_cohere_json_schema(completion, validation_context, strict)

    if mode == Mode.WRITER_TOOLS:
        return cls.parse_writer_tools(completion, validation_context, strict)

    if mode == Mode.WRITER_JSON:
        return cls.parse_writer_json(completion, validation_context, strict)

    if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
        return cls.parse_responses_tools(
            completion,
            validation_context,
            strict,
        )

    if not completion.choices:
        # This helps catch errors from OpenRouter
        if hasattr(completion, "error"):
            raise ResponseParsingError(
                f"LLM provider returned error: {completion.error}",
                mode=str(mode),
                raw_response=completion,
            )

        raise ResponseParsingError(
            "No completion choices found in LLM response",
            mode=str(mode),
            raw_response=completion,
        )

    if completion.choices[0].finish_reason == "length":
        raise IncompleteOutputException(last_completion=completion)

    if mode == Mode.FUNCTIONS:
        Mode.warn_mode_functions_deprecation()
        return cls.parse_functions(completion, validation_context, strict)

    if mode == Mode.MISTRAL_STRUCTURED_OUTPUTS:
        return cls.parse_mistral_structured_outputs(
            completion, validation_context, strict
        )

    if mode in {
        Mode.TOOLS,
        Mode.MISTRAL_TOOLS,
        Mode.TOOLS_STRICT,
        Mode.CEREBRAS_TOOLS,
        Mode.FIREWORKS_TOOLS,
    }:
        return cls.parse_tools(completion, validation_context, strict)

    if mode in {
        Mode.JSON,
        Mode.JSON_SCHEMA,
        Mode.MD_JSON,
        Mode.JSON_O1,
        Mode.CEREBRAS_JSON,
        Mode.FIREWORKS_JSON,
        Mode.PERPLEXITY_JSON,
        Mode.OPENROUTER_STRUCTURED_OUTPUTS,
    }:
        return cls.parse_json(completion, validation_context, strict)

    raise ConfigurationError(
        f"Invalid or unsupported mode: {mode}. This mode may not be implemented for response parsing."
    )

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/processing/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
    """
    return generate_openai_schema(cls)

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

Parse Cohere tools response.

Supports: - V1 native tool calls: completion.tool_calls[0].parameters - V2 native tool calls: completion.message.tool_calls[0].function.arguments (JSON string) - V1 text-based: completion.text (prompt-based approach) - V2 text-based: completion.message.content[].text (prompt-based approach)

Source code in instructor/processing/function_calls.py
@classmethod
def parse_cohere_tools(
    cls: type[BaseModel],
    completion: ChatCompletion,
    validation_context: Optional[dict[str, Any]] = None,
    strict: Optional[bool] = None,
) -> BaseModel:
    """
    Parse Cohere tools response.

    Supports:
    - V1 native tool calls: completion.tool_calls[0].parameters
    - V2 native tool calls: completion.message.tool_calls[0].function.arguments (JSON string)
    - V1 text-based: completion.text (prompt-based approach)
    - V2 text-based: completion.message.content[].text (prompt-based approach)
    """
    # First, check for native Cohere tool calls (V1 and V2)
    # V1: completion.tool_calls with tc.parameters (dict)
    if hasattr(completion, "tool_calls") and completion.tool_calls:
        # V1 tool call format
        tool_call = completion.tool_calls[0]
        # Parameters in V1 are already a dict
        return cls.model_validate(
            tool_call.parameters, context=validation_context, strict=strict
        )

    # V2: completion.message.tool_calls with tc.function.arguments (JSON string)
    if (
        hasattr(completion, "message")
        and hasattr(completion.message, "tool_calls")
        and completion.message.tool_calls
    ):
        # V2 tool call format
        tool_call = completion.message.tool_calls[0]
        # Arguments in V2 are a JSON string
        import json

        arguments = json.loads(tool_call.function.arguments)
        return cls.model_validate(
            arguments, context=validation_context, strict=strict
        )

    # Fallback to text-based extraction (current prompt-based approach)
    # Handle both V1 and V2 text response structures
    if hasattr(completion, "text"):
        # V1 format: direct text access
        text = completion.text
    elif hasattr(completion, "message") and hasattr(completion.message, "content"):
        # V2 format: nested structure (message.content[].text)
        # V2 responses may have multiple content items (thinking, text, etc.)
        content_items = completion.message.content
        if content_items and len(content_items) > 0:
            # Find the text content item (skip thinking/other types)
            text = None
            for item in content_items:
                if (
                    hasattr(item, "type")
                    and item.type == "text"
                    and hasattr(item, "text")
                ):
                    text = item.text
                    break

            if text is None:
                raise ResponseParsingError(
                    "Cohere V2 response has no text content item",
                    mode="COHERE_TOOLS",
                    raw_response=completion,
                )
        else:
            raise ResponseParsingError(
                "Cohere V2 response has no content",
                mode="COHERE_TOOLS",
                raw_response=completion,
            )
    else:
        raise ResponseParsingError(
            f"Unsupported Cohere response format. Expected tool_calls or text content. "
            f"Got: {type(completion)}",
            mode="COHERE_TOOLS",
            raw_response=completion,
        )

    # Extract JSON from text (for prompt-based approach)
    extra_text = extract_json_from_codeblock(text)
    return cls.model_validate_json(
        extra_text, context=validation_context, strict=strict
    )

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

Parse JSON mode responses using the optimized extraction and validation.

Source code in instructor/processing/function_calls.py
@classmethod
def parse_json(
    cls: type[BaseModel],
    completion: ChatCompletion,
    validation_context: Optional[dict[str, Any]] = None,
    strict: Optional[bool] = None,
) -> BaseModel:
    """Parse JSON mode responses using the optimized extraction and validation."""
    # Check for incomplete output
    _handle_incomplete_output(completion)

    # Extract text from the response
    message = _extract_text_content(completion)
    if not message:
        # Fallback for OpenAI format if _extract_text_content doesn't handle it
        message = completion.choices[0].message.content or ""

    # Extract JSON from the text
    json_content = extract_json_from_codeblock(message)

    # Validate the model from the JSON
    return _validate_model_from_json(cls, json_content, validation_context, strict)

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/core/exceptions.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)

classproperty

Bases: Generic[R_co]

Descriptor for class-level properties.

Examples:

>>> from instructor.utils import classproperty
>>> class MyClass:
...     @classproperty
...     def my_property(cls):
...         return cls
>>> assert MyClass.my_property
Source code in instructor/utils/core.py
class classproperty(Generic[R_co]):
    """Descriptor for class-level properties.

    Examples:
        >>> from instructor.utils import classproperty

        >>> class MyClass:
        ...     @classproperty
        ...     def my_property(cls):
        ...         return cls

        >>> assert MyClass.my_property
    """

    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)

extract_json_from_codeblock(content)

Extract JSON from a string that may contain extra text.

The function looks for the first '{' and the last '}' in the string and returns the content between them, inclusive. If no braces are found, the original string is returned.

Parameters:

Name Type Description Default
content str

The string that may contain JSON

required

Returns:

Type Description
str

The extracted JSON string

Source code in instructor/utils/core.py
def extract_json_from_codeblock(content: str) -> str:
    """
    Extract JSON from a string that may contain extra text.

    The function looks for the first '{' and the last '}' in the string and
    returns the content between them, inclusive. If no braces are found,
    the original string is returned.

    Args:
        content: The string that may contain JSON

    Returns:
        The extracted JSON string
    """

    first_brace = content.find("{")
    last_brace = content.rfind("}")
    if first_brace != -1 and last_brace != -1:
        json_content = content[first_brace : last_brace + 1]
    else:
        json_content = content  # Return as is if no JSON-like content found

    return json_content

generate_anthropic_schema(model) cached

Generate Anthropic tool schema from a Pydantic model.

Parameters:

Name Type Description Default
model type[BaseModel]

A Pydantic BaseModel subclass

required

Returns:

Type Description
dict[str, Any]

A dictionary in the format of Anthropic's tool schema

Source code in instructor/processing/schema.py
@functools.lru_cache(maxsize=256)
def generate_anthropic_schema(model: type[BaseModel]) -> dict[str, Any]:
    """
    Generate Anthropic tool schema from a Pydantic model.

    Args:
        model: A Pydantic BaseModel subclass

    Returns:
        A dictionary in the format of Anthropic's tool schema
    """
    # Generate the Anthropic schema based on the OpenAI schema to avoid redundant schema generation
    openai_schema = generate_openai_schema(model)
    return {
        "name": openai_schema["name"],
        "description": openai_schema["description"],
        "input_schema": model.model_json_schema(),
    }

generate_gemini_schema(model) cached

Generate Gemini function schema from a Pydantic model.

Parameters:

Name Type Description Default
model type[BaseModel]

A Pydantic BaseModel subclass

required

Returns:

Type Description
Any

A Gemini FunctionDeclaration object

Note

This function is deprecated. The google-generativeai library is being replaced by google-genai.

Source code in instructor/processing/schema.py
@functools.lru_cache(maxsize=256)
def generate_gemini_schema(model: type[BaseModel]) -> Any:
    """
    Generate Gemini function schema from a Pydantic model.

    Args:
        model: A Pydantic BaseModel subclass

    Returns:
        A Gemini FunctionDeclaration object

    Note:
        This function is deprecated. The google-generativeai library is being replaced by google-genai.
    """
    # This is kept for backward compatibility but deprecated
    warnings.warn(
        "generate_gemini_schema is deprecated. The google-generativeai library is being replaced by google-genai.",
        DeprecationWarning,
        stacklevel=2,
    )

    try:
        import importlib

        genai_types = cast(Any, importlib.import_module("google.generativeai.types"))

        # Use OpenAI schema
        openai_schema = generate_openai_schema(model)

        # Transform to Gemini format
        function = genai_types.FunctionDeclaration(
            name=openai_schema["name"],
            description=openai_schema["description"],
            parameters=map_to_gemini_function_schema(openai_schema["parameters"]),
        )

        return function
    except ImportError as e:
        raise ImportError(
            "google-generativeai is deprecated. Please install google-genai instead: pip install google-genai"
        ) from e

generate_openai_schema(model) cached

Generate OpenAI function schema from a Pydantic model.

Parameters:

Name Type Description Default
model type[BaseModel]

A Pydantic BaseModel subclass

required

Returns:

Type Description
dict[str, Any]

A dictionary in the format of OpenAI's function schema

Note

The model's docstring will be used for the function description. Parameter descriptions from the docstring will enrich field descriptions.

Source code in instructor/processing/schema.py
@functools.lru_cache(maxsize=256)
def generate_openai_schema(model: type[BaseModel]) -> dict[str, Any]:
    """
    Generate OpenAI function schema from a Pydantic model.

    Args:
        model: A Pydantic BaseModel subclass

    Returns:
        A dictionary in the format of OpenAI's function schema

    Note:
        The model's docstring will be used for the function description.
        Parameter descriptions from the docstring will enrich field descriptions.
    """
    schema = model.model_json_schema()
    docstring = parse(model.__doc__ or "")
    parameters = {k: v for k, v in schema.items() if k not in ("title", "description")}

    # Enrich parameter descriptions from docstring
    for param in docstring.params:
        if (name := param.arg_name) in parameters["properties"] and (
            description := param.description
        ):
            if "description" not in parameters["properties"][name]:
                parameters["properties"][name]["description"] = description

    parameters["required"] = sorted(
        k for k, v in parameters["properties"].items() if "default" not in v
    )

    if "description" not in schema:
        if docstring.short_description:
            schema["description"] = docstring.short_description
        else:
            schema["description"] = (
                f"Correctly extracted `{model.__name__}` with all "
                f"the required parameters with correct types"
            )

    return {
        "name": schema["title"],
        "description": schema["description"],
        "parameters": parameters,
    }

openai_schema(cls)

Wrap a Pydantic model class to add OpenAISchema functionality.

Source code in instructor/processing/function_calls.py
def openai_schema(cls: type[BaseModel]) -> OpenAISchema:
    """
    Wrap a Pydantic model class to add OpenAISchema functionality.
    """
    if not issubclass(cls, BaseModel):
        raise ConfigurationError(
            f"response_model must be a Pydantic BaseModel subclass, got {type(cls).__name__}"
        )

    # Create the wrapped model
    schema = wraps(cls, updated=())(
        create_model(
            cls.__name__ if hasattr(cls, "__name__") else str(cls),
            __base__=(cls, OpenAISchema),
        )
    )

    return cast(OpenAISchema, schema)

Bases: BaseModel

Source code in instructor/processing/function_calls.py
113
114
115
116
117
118
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
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
class OpenAISchema(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
        """
        return generate_openai_schema(cls)

    @classproperty
    def anthropic_schema(cls) -> dict[str, Any]:
        # Generate the Anthropic schema based on the OpenAI schema to avoid redundant schema generation
        return generate_anthropic_schema(cls)

    @classproperty
    def gemini_schema(cls) -> Any:
        # This is kept for backward compatibility but deprecated
        return generate_gemini_schema(cls)

    @classmethod
    def from_response(
        cls,
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
        mode: Mode = Mode.TOOLS,
    ) -> BaseModel:
        """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 openai completion mode

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

        if mode == Mode.ANTHROPIC_TOOLS:
            return cls.parse_anthropic_tools(completion, validation_context, strict)

        if mode == Mode.ANTHROPIC_TOOLS or mode == Mode.ANTHROPIC_REASONING_TOOLS:
            return cls.parse_anthropic_tools(completion, validation_context, strict)

        if mode == Mode.ANTHROPIC_JSON:
            return cls.parse_anthropic_json(completion, validation_context, strict)

        if mode == Mode.BEDROCK_JSON:
            return cls.parse_bedrock_json(completion, validation_context, strict)

        if mode == Mode.BEDROCK_TOOLS:
            return cls.parse_bedrock_tools(completion, validation_context, strict)

        if mode in {Mode.VERTEXAI_TOOLS, Mode.GEMINI_TOOLS}:
            return cls.parse_vertexai_tools(completion, validation_context)

        if mode == Mode.VERTEXAI_JSON:
            return cls.parse_vertexai_json(completion, validation_context, strict)

        if mode == Mode.COHERE_TOOLS:
            return cls.parse_cohere_tools(completion, validation_context, strict)

        if mode == Mode.GEMINI_JSON:
            return cls.parse_gemini_json(completion, validation_context, strict)

        if mode == Mode.GENAI_STRUCTURED_OUTPUTS:
            return cls.parse_genai_structured_outputs(
                completion, validation_context, strict
            )

        if mode == Mode.GEMINI_TOOLS:
            return cls.parse_gemini_tools(completion, validation_context, strict)

        if mode == Mode.GENAI_TOOLS:
            return cls.parse_genai_tools(completion, validation_context, strict)

        if mode == Mode.COHERE_JSON_SCHEMA:
            return cls.parse_cohere_json_schema(completion, validation_context, strict)

        if mode == Mode.WRITER_TOOLS:
            return cls.parse_writer_tools(completion, validation_context, strict)

        if mode == Mode.WRITER_JSON:
            return cls.parse_writer_json(completion, validation_context, strict)

        if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
            return cls.parse_responses_tools(
                completion,
                validation_context,
                strict,
            )

        if not completion.choices:
            # This helps catch errors from OpenRouter
            if hasattr(completion, "error"):
                raise ResponseParsingError(
                    f"LLM provider returned error: {completion.error}",
                    mode=str(mode),
                    raw_response=completion,
                )

            raise ResponseParsingError(
                "No completion choices found in LLM response",
                mode=str(mode),
                raw_response=completion,
            )

        if completion.choices[0].finish_reason == "length":
            raise IncompleteOutputException(last_completion=completion)

        if mode == Mode.FUNCTIONS:
            Mode.warn_mode_functions_deprecation()
            return cls.parse_functions(completion, validation_context, strict)

        if mode == Mode.MISTRAL_STRUCTURED_OUTPUTS:
            return cls.parse_mistral_structured_outputs(
                completion, validation_context, strict
            )

        if mode in {
            Mode.TOOLS,
            Mode.MISTRAL_TOOLS,
            Mode.TOOLS_STRICT,
            Mode.CEREBRAS_TOOLS,
            Mode.FIREWORKS_TOOLS,
        }:
            return cls.parse_tools(completion, validation_context, strict)

        if mode in {
            Mode.JSON,
            Mode.JSON_SCHEMA,
            Mode.MD_JSON,
            Mode.JSON_O1,
            Mode.CEREBRAS_JSON,
            Mode.FIREWORKS_JSON,
            Mode.PERPLEXITY_JSON,
            Mode.OPENROUTER_STRUCTURED_OUTPUTS,
        }:
            return cls.parse_json(completion, validation_context, strict)

        raise ConfigurationError(
            f"Invalid or unsupported mode: {mode}. This mode may not be implemented for response parsing."
        )

    @classmethod
    def parse_genai_structured_outputs(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        return cls.model_validate_json(
            completion.text, context=validation_context, strict=strict
        )

    @classmethod
    def parse_genai_tools(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        from google.genai import types

        assert isinstance(completion, types.GenerateContentResponse)
        assert len(completion.candidates) == 1

        # Filter out thought parts (parts with thought: true)
        parts = completion.candidates[0].content.parts
        non_thought_parts = [
            part for part in parts if not (hasattr(part, "thought") and part.thought)
        ]

        assert len(non_thought_parts) == 1, (
            f"Instructor does not support multiple function calls, use List[Model] instead"
        )
        function_call = non_thought_parts[0].function_call
        assert function_call is not None, (
            f"Please return your response as a function call with the schema {cls.openai_schema} and the name {cls.openai_schema['name']}"
        )

        assert function_call.name == cls.openai_schema["name"]
        return cls.model_validate(
            obj=function_call.args, context=validation_context, strict=strict
        )

    @classmethod
    def parse_cohere_json_schema(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ):
        # Handle both V1 and V2 response structures
        if hasattr(completion, "text"):
            # V1 format: direct text access
            text = completion.text
        elif hasattr(completion, "message") and hasattr(completion.message, "content"):
            # V2 format: nested structure (message.content[].text)
            # V2 responses may have multiple content items (thinking, text, etc.)
            content_items = completion.message.content
            if content_items and len(content_items) > 0:
                # Find the text content item (skip thinking/other types)
                # TODO handle these other content types
                text = None
                for item in content_items:
                    if (
                        hasattr(item, "type")
                        and item.type == "text"
                        and hasattr(item, "text")
                    ):
                        text = item.text
                        break

                if text is None:
                    raise ResponseParsingError(
                        "Cohere V2 response has no text content item",
                        mode="COHERE_JSON_SCHEMA",
                        raw_response=completion,
                    )
            else:
                raise ResponseParsingError(
                    "Cohere V2 response has no content",
                    mode="COHERE_JSON_SCHEMA",
                    raw_response=completion,
                )
        else:
            raise ResponseParsingError(
                f"Unsupported Cohere response format. Expected 'text' (V1) or "
                f"'message.content[].text' (V2), got: {type(completion)}",
                mode="COHERE_JSON_SCHEMA",
                raw_response=completion,
            )

        return cls.model_validate_json(text, context=validation_context, strict=strict)

    @classmethod
    def parse_anthropic_tools(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        from anthropic.types import Message

        if isinstance(completion, Message) and completion.stop_reason == "max_tokens":
            raise IncompleteOutputException(last_completion=completion)

        # Anthropic returns arguments as a dict, dump to json for model validation below
        tool_calls = [
            json.dumps(c.input) for c in completion.content if c.type == "tool_use"
        ]  # TODO update with anthropic specific types

        tool_calls_validator = TypeAdapter(
            Annotated[list[Any], Field(min_length=1, max_length=1)]
        )
        tool_call = tool_calls_validator.validate_python(tool_calls)[0]

        return cls.model_validate_json(
            tool_call, context=validation_context, strict=strict
        )

    @classmethod
    def parse_anthropic_json(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        from anthropic.types import Message

        last_block = None

        if hasattr(completion, "choices"):
            completion = completion.choices[0]
            if completion.finish_reason == "length":
                raise IncompleteOutputException(last_completion=completion)
            text = completion.message.content
        else:
            assert isinstance(completion, Message)
            if completion.stop_reason == "max_tokens":
                raise IncompleteOutputException(last_completion=completion)
            # Find the last text block in the completion
            # this is because the completion is a list of blocks
            # and the last block is the one that contains the text ideally
            # this could happen due to things like multiple tool calls
            # read: https://docs.anthropic.com/en/docs/build-with-claude/tool-use/web-search-tool#response
            text_blocks = [c for c in completion.content if c.type == "text"]
            last_block = text_blocks[-1]
            text = last_block.text

        extra_text = extract_json_from_codeblock(text)

        if strict:
            model = cls.model_validate_json(
                extra_text, context=validation_context, strict=True
            )
        else:
            # Allow control characters to pass through by using the non-strict JSON parser.
            parsed = json.loads(extra_text, strict=False)
            # Pydantic non-strict: https://docs.pydantic.dev/latest/concepts/strict_mode/
            model = cls.model_validate(parsed, context=validation_context, strict=False)

        return model

    @classmethod
    def parse_bedrock_json(
        cls: type[BaseModel],
        completion: Any,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        if isinstance(completion, dict):
            # OpenAI will send the first content to be 'reasoningText', and then 'text'
            content = completion["output"]["message"]["content"]
            text_content = next((c for c in content if "text" in c), None)
            if not text_content:
                raise ResponseParsingError(
                    "Unexpected format. No text content found in Bedrock response.",
                    mode="BEDROCK_JSON",
                    raw_response=completion,
                )
            text = text_content["text"]
            match = re.search(r"```?json(.*?)```?", text, re.DOTALL)
            if match:
                text = match.group(1).strip()

            text = re.sub(r"```?json|\\n", "", text).strip()
        else:
            text = completion.text
        return cls.model_validate_json(text, context=validation_context, strict=strict)

    @classmethod
    def parse_bedrock_tools(
        cls: type[BaseModel],
        completion: Any,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        if isinstance(completion, dict):
            # Extract the tool use from Bedrock response
            message = completion.get("output", {}).get("message", {})
            content = message.get("content", [])

            # Find the tool use content block
            for content_block in content:
                if "toolUse" in content_block:
                    tool_use = content_block["toolUse"]
                    assert tool_use.get("name") == cls.__name__, (
                        f"Tool name mismatch: expected {cls.__name__}, got {tool_use.get('name')}"
                    )
                    return cls.model_validate(
                        tool_use.get("input", {}),
                        context=validation_context,
                        strict=strict,
                    )

            raise ResponseParsingError(
                "No tool use found in Bedrock response",
                mode="BEDROCK_TOOLS",
                raw_response=completion,
            )
        else:
            # Fallback for other response formats
            return cls.model_validate_json(
                completion.text, context=validation_context, strict=strict
            )

    @classmethod
    def parse_gemini_json(
        cls: type[BaseModel],
        completion: Any,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        try:
            text = completion.text
        except ValueError:
            logger.debug(
                f"Error response: {completion.result.candidates[0].finish_reason}\n\n{completion.result.candidates[0].safety_ratings}"
            )

        try:
            extra_text = extract_json_from_codeblock(text)  # type: ignore
        except UnboundLocalError:
            raise ResponseParsingError(
                "Unable to extract JSON from completion text. The response may have been blocked or empty.",
                mode="GEMINI_JSON",
                raw_response=completion,
            ) from None

        if strict:
            return cls.model_validate_json(
                extra_text, context=validation_context, strict=True
            )
        else:
            # Allow control characters.
            parsed = json.loads(extra_text, strict=False)
            # Pydantic non-strict: https://docs.pydantic.dev/latest/concepts/strict_mode/
            return cls.model_validate(parsed, context=validation_context, strict=False)

    @classmethod
    def parse_vertexai_tools(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
    ) -> BaseModel:
        tool_call = completion.candidates[0].content.parts[0].function_call.args  # type: ignore
        model = {}
        for field in tool_call:  # type: ignore
            model[field] = tool_call[field]
        # We enable strict=False because the conversion from protobuf -> dict often results in types like ints being cast to floats, as a result in order for model.validate to work we need to disable strict mode.
        return cls.model_validate(model, context=validation_context, strict=False)

    @classmethod
    def parse_vertexai_json(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        return cls.model_validate_json(
            completion.text, context=validation_context, strict=strict
        )

    @classmethod
    def parse_cohere_tools(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        """
        Parse Cohere tools response.

        Supports:
        - V1 native tool calls: completion.tool_calls[0].parameters
        - V2 native tool calls: completion.message.tool_calls[0].function.arguments (JSON string)
        - V1 text-based: completion.text (prompt-based approach)
        - V2 text-based: completion.message.content[].text (prompt-based approach)
        """
        # First, check for native Cohere tool calls (V1 and V2)
        # V1: completion.tool_calls with tc.parameters (dict)
        if hasattr(completion, "tool_calls") and completion.tool_calls:
            # V1 tool call format
            tool_call = completion.tool_calls[0]
            # Parameters in V1 are already a dict
            return cls.model_validate(
                tool_call.parameters, context=validation_context, strict=strict
            )

        # V2: completion.message.tool_calls with tc.function.arguments (JSON string)
        if (
            hasattr(completion, "message")
            and hasattr(completion.message, "tool_calls")
            and completion.message.tool_calls
        ):
            # V2 tool call format
            tool_call = completion.message.tool_calls[0]
            # Arguments in V2 are a JSON string
            import json

            arguments = json.loads(tool_call.function.arguments)
            return cls.model_validate(
                arguments, context=validation_context, strict=strict
            )

        # Fallback to text-based extraction (current prompt-based approach)
        # Handle both V1 and V2 text response structures
        if hasattr(completion, "text"):
            # V1 format: direct text access
            text = completion.text
        elif hasattr(completion, "message") and hasattr(completion.message, "content"):
            # V2 format: nested structure (message.content[].text)
            # V2 responses may have multiple content items (thinking, text, etc.)
            content_items = completion.message.content
            if content_items and len(content_items) > 0:
                # Find the text content item (skip thinking/other types)
                text = None
                for item in content_items:
                    if (
                        hasattr(item, "type")
                        and item.type == "text"
                        and hasattr(item, "text")
                    ):
                        text = item.text
                        break

                if text is None:
                    raise ResponseParsingError(
                        "Cohere V2 response has no text content item",
                        mode="COHERE_TOOLS",
                        raw_response=completion,
                    )
            else:
                raise ResponseParsingError(
                    "Cohere V2 response has no content",
                    mode="COHERE_TOOLS",
                    raw_response=completion,
                )
        else:
            raise ResponseParsingError(
                f"Unsupported Cohere response format. Expected tool_calls or text content. "
                f"Got: {type(completion)}",
                mode="COHERE_TOOLS",
                raw_response=completion,
            )

        # Extract JSON from text (for prompt-based approach)
        extra_text = extract_json_from_codeblock(text)
        return cls.model_validate_json(
            extra_text, context=validation_context, strict=strict
        )

    @classmethod
    def parse_writer_tools(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        message = completion.choices[0].message
        tool_calls = message.tool_calls if message.tool_calls else "{}"
        assert len(tool_calls) == 1, (
            "Instructor does not support multiple tool calls, use List[Model] instead"
        )
        assert tool_calls[0].function.name == cls.openai_schema["name"], (
            "Tool name does not match"
        )
        loaded_args = json.loads(tool_calls[0].function.arguments)
        return cls.model_validate_json(
            json.dumps(loaded_args) if isinstance(loaded_args, dict) else loaded_args,
            context=validation_context,
            strict=strict,
        )

    @classmethod
    def parse_writer_json(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        _handle_incomplete_output(completion)

        message = completion.choices[0].message.content or ""
        json_content = extract_json_from_codeblock(message)

        if strict:
            return cls.model_validate_json(
                json_content, context=validation_context, strict=True
            )
        else:
            parsed = json.loads(json_content, strict=False)
            return cls.model_validate(parsed, context=validation_context, strict=False)

    @classmethod
    def parse_functions(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        message = completion.choices[0].message
        assert (
            message.function_call.name == cls.openai_schema["name"]  # type: ignore[index]
        ), "Function name does not match"
        return cls.model_validate_json(
            message.function_call.arguments,  # type: ignore[attr-defined]
            context=validation_context,
            strict=strict,
        )

    @classmethod
    def parse_responses_tools(
        cls: type[BaseModel],
        completion: Any,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        from openai.types.responses import ResponseFunctionToolCall

        tool_call_message = None
        for message in completion.output:
            if isinstance(message, ResponseFunctionToolCall):
                if message.name == cls.openai_schema["name"]:
                    tool_call_message = message
                    break
        if not tool_call_message:
            raise ResponseParsingError(
                f"Required tool call '{cls.openai_schema['name']}' not found in response",
                mode="RESPONSES_TOOLS",
                raw_response=completion,
            )

        return cls.model_validate_json(
            tool_call_message.arguments,  # type: ignore[attr-defined]
            context=validation_context,
            strict=strict,
        )

    @classmethod
    def parse_tools(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        message = completion.choices[0].message
        # this field seems to be missing when using instructor with some other tools (e.g. litellm)
        # trying to fix this by adding a check

        if hasattr(message, "refusal"):
            assert message.refusal is None, (
                f"Unable to generate a response due to {message.refusal}"
            )
        assert len(message.tool_calls or []) == 1, (
            f"Instructor does not support multiple tool calls, use List[Model] instead"
        )
        tool_call = message.tool_calls[0]  # type: ignore
        assert (
            tool_call.function.name == cls.openai_schema["name"]  # type: ignore[index]
        ), "Tool name does not match"
        return cls.model_validate_json(
            tool_call.function.arguments,  # type: ignore
            context=validation_context,
            strict=strict,
        )

    @classmethod
    def parse_mistral_structured_outputs(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        if not completion.choices or len(completion.choices) > 1:
            raise ConfigurationError(
                "Instructor does not support multiple tool calls in MISTRAL_STRUCTURED_OUTPUTS mode. "
                "Use list[Model] instead to handle multiple items."
            )

        message = completion.choices[0].message

        return cls.model_validate_json(
            message.content, context=validation_context, strict=strict
        )

    @classmethod
    def parse_json(
        cls: type[BaseModel],
        completion: ChatCompletion,
        validation_context: Optional[dict[str, Any]] = None,
        strict: Optional[bool] = None,
    ) -> BaseModel:
        """Parse JSON mode responses using the optimized extraction and validation."""
        # Check for incomplete output
        _handle_incomplete_output(completion)

        # Extract text from the response
        message = _extract_text_content(completion)
        if not message:
            # Fallback for OpenAI format if _extract_text_content doesn't handle it
            message = completion.choices[0].message.content or ""

        # Extract JSON from the text
        json_content = extract_json_from_codeblock(message)

        # Validate the model from the JSON
        return _validate_model_from_json(cls, json_content, validation_context, strict)

from_response(completion, validation_context=None, strict=None, mode=Mode.TOOLS) 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 openai completion mode

TOOLS

Returns:

Name Type Description
cls OpenAISchema

An instance of the class

Source code in instructor/processing/function_calls.py
@classmethod
def from_response(
    cls,
    completion: ChatCompletion,
    validation_context: Optional[dict[str, Any]] = None,
    strict: Optional[bool] = None,
    mode: Mode = Mode.TOOLS,
) -> BaseModel:
    """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 openai completion mode

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

    if mode == Mode.ANTHROPIC_TOOLS:
        return cls.parse_anthropic_tools(completion, validation_context, strict)

    if mode == Mode.ANTHROPIC_TOOLS or mode == Mode.ANTHROPIC_REASONING_TOOLS:
        return cls.parse_anthropic_tools(completion, validation_context, strict)

    if mode == Mode.ANTHROPIC_JSON:
        return cls.parse_anthropic_json(completion, validation_context, strict)

    if mode == Mode.BEDROCK_JSON:
        return cls.parse_bedrock_json(completion, validation_context, strict)

    if mode == Mode.BEDROCK_TOOLS:
        return cls.parse_bedrock_tools(completion, validation_context, strict)

    if mode in {Mode.VERTEXAI_TOOLS, Mode.GEMINI_TOOLS}:
        return cls.parse_vertexai_tools(completion, validation_context)

    if mode == Mode.VERTEXAI_JSON:
        return cls.parse_vertexai_json(completion, validation_context, strict)

    if mode == Mode.COHERE_TOOLS:
        return cls.parse_cohere_tools(completion, validation_context, strict)

    if mode == Mode.GEMINI_JSON:
        return cls.parse_gemini_json(completion, validation_context, strict)

    if mode == Mode.GENAI_STRUCTURED_OUTPUTS:
        return cls.parse_genai_structured_outputs(
            completion, validation_context, strict
        )

    if mode == Mode.GEMINI_TOOLS:
        return cls.parse_gemini_tools(completion, validation_context, strict)

    if mode == Mode.GENAI_TOOLS:
        return cls.parse_genai_tools(completion, validation_context, strict)

    if mode == Mode.COHERE_JSON_SCHEMA:
        return cls.parse_cohere_json_schema(completion, validation_context, strict)

    if mode == Mode.WRITER_TOOLS:
        return cls.parse_writer_tools(completion, validation_context, strict)

    if mode == Mode.WRITER_JSON:
        return cls.parse_writer_json(completion, validation_context, strict)

    if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
        return cls.parse_responses_tools(
            completion,
            validation_context,
            strict,
        )

    if not completion.choices:
        # This helps catch errors from OpenRouter
        if hasattr(completion, "error"):
            raise ResponseParsingError(
                f"LLM provider returned error: {completion.error}",
                mode=str(mode),
                raw_response=completion,
            )

        raise ResponseParsingError(
            "No completion choices found in LLM response",
            mode=str(mode),
            raw_response=completion,
        )

    if completion.choices[0].finish_reason == "length":
        raise IncompleteOutputException(last_completion=completion)

    if mode == Mode.FUNCTIONS:
        Mode.warn_mode_functions_deprecation()
        return cls.parse_functions(completion, validation_context, strict)

    if mode == Mode.MISTRAL_STRUCTURED_OUTPUTS:
        return cls.parse_mistral_structured_outputs(
            completion, validation_context, strict
        )

    if mode in {
        Mode.TOOLS,
        Mode.MISTRAL_TOOLS,
        Mode.TOOLS_STRICT,
        Mode.CEREBRAS_TOOLS,
        Mode.FIREWORKS_TOOLS,
    }:
        return cls.parse_tools(completion, validation_context, strict)

    if mode in {
        Mode.JSON,
        Mode.JSON_SCHEMA,
        Mode.MD_JSON,
        Mode.JSON_O1,
        Mode.CEREBRAS_JSON,
        Mode.FIREWORKS_JSON,
        Mode.PERPLEXITY_JSON,
        Mode.OPENROUTER_STRUCTURED_OUTPUTS,
    }:
        return cls.parse_json(completion, validation_context, strict)

    raise ConfigurationError(
        f"Invalid or unsupported mode: {mode}. This mode may not be implemented for response parsing."
    )

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/processing/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
    """
    return generate_openai_schema(cls)

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

Parse Cohere tools response.

Supports: - V1 native tool calls: completion.tool_calls[0].parameters - V2 native tool calls: completion.message.tool_calls[0].function.arguments (JSON string) - V1 text-based: completion.text (prompt-based approach) - V2 text-based: completion.message.content[].text (prompt-based approach)

Source code in instructor/processing/function_calls.py
@classmethod
def parse_cohere_tools(
    cls: type[BaseModel],
    completion: ChatCompletion,
    validation_context: Optional[dict[str, Any]] = None,
    strict: Optional[bool] = None,
) -> BaseModel:
    """
    Parse Cohere tools response.

    Supports:
    - V1 native tool calls: completion.tool_calls[0].parameters
    - V2 native tool calls: completion.message.tool_calls[0].function.arguments (JSON string)
    - V1 text-based: completion.text (prompt-based approach)
    - V2 text-based: completion.message.content[].text (prompt-based approach)
    """
    # First, check for native Cohere tool calls (V1 and V2)
    # V1: completion.tool_calls with tc.parameters (dict)
    if hasattr(completion, "tool_calls") and completion.tool_calls:
        # V1 tool call format
        tool_call = completion.tool_calls[0]
        # Parameters in V1 are already a dict
        return cls.model_validate(
            tool_call.parameters, context=validation_context, strict=strict
        )

    # V2: completion.message.tool_calls with tc.function.arguments (JSON string)
    if (
        hasattr(completion, "message")
        and hasattr(completion.message, "tool_calls")
        and completion.message.tool_calls
    ):
        # V2 tool call format
        tool_call = completion.message.tool_calls[0]
        # Arguments in V2 are a JSON string
        import json

        arguments = json.loads(tool_call.function.arguments)
        return cls.model_validate(
            arguments, context=validation_context, strict=strict
        )

    # Fallback to text-based extraction (current prompt-based approach)
    # Handle both V1 and V2 text response structures
    if hasattr(completion, "text"):
        # V1 format: direct text access
        text = completion.text
    elif hasattr(completion, "message") and hasattr(completion.message, "content"):
        # V2 format: nested structure (message.content[].text)
        # V2 responses may have multiple content items (thinking, text, etc.)
        content_items = completion.message.content
        if content_items and len(content_items) > 0:
            # Find the text content item (skip thinking/other types)
            text = None
            for item in content_items:
                if (
                    hasattr(item, "type")
                    and item.type == "text"
                    and hasattr(item, "text")
                ):
                    text = item.text
                    break

            if text is None:
                raise ResponseParsingError(
                    "Cohere V2 response has no text content item",
                    mode="COHERE_TOOLS",
                    raw_response=completion,
                )
        else:
            raise ResponseParsingError(
                "Cohere V2 response has no content",
                mode="COHERE_TOOLS",
                raw_response=completion,
            )
    else:
        raise ResponseParsingError(
            f"Unsupported Cohere response format. Expected tool_calls or text content. "
            f"Got: {type(completion)}",
            mode="COHERE_TOOLS",
            raw_response=completion,
        )

    # Extract JSON from text (for prompt-based approach)
    extra_text = extract_json_from_codeblock(text)
    return cls.model_validate_json(
        extra_text, context=validation_context, strict=strict
    )

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

Parse JSON mode responses using the optimized extraction and validation.

Source code in instructor/processing/function_calls.py
@classmethod
def parse_json(
    cls: type[BaseModel],
    completion: ChatCompletion,
    validation_context: Optional[dict[str, Any]] = None,
    strict: Optional[bool] = None,
) -> BaseModel:
    """Parse JSON mode responses using the optimized extraction and validation."""
    # Check for incomplete output
    _handle_incomplete_output(completion)

    # Extract text from the response
    message = _extract_text_content(completion)
    if not message:
        # Fallback for OpenAI format if _extract_text_content doesn't handle it
        message = completion.choices[0].message.content or ""

    # Extract JSON from the text
    json_content = extract_json_from_codeblock(message)

    # Validate the model from the JSON
    return _validate_model_from_json(cls, json_content, validation_context, strict)

Wrap a Pydantic model class to add OpenAISchema functionality.

Source code in instructor/processing/function_calls.py
def openai_schema(cls: type[BaseModel]) -> OpenAISchema:
    """
    Wrap a Pydantic model class to add OpenAISchema functionality.
    """
    if not issubclass(cls, BaseModel):
        raise ConfigurationError(
            f"response_model must be a Pydantic BaseModel subclass, got {type(cls).__name__}"
        )

    # Create the wrapped model
    schema = wraps(cls, updated=())(
        create_model(
            cls.__name__ if hasattr(cls, "__name__") else str(cls),
            __base__=(cls, OpenAISchema),
        )
    )

    return cast(OpenAISchema, schema)

Generate OpenAI function schema from a Pydantic model.

Parameters:

Name Type Description Default
model type[BaseModel]

A Pydantic BaseModel subclass

required

Returns:

Type Description
dict[str, Any]

A dictionary in the format of OpenAI's function schema

Note

The model's docstring will be used for the function description. Parameter descriptions from the docstring will enrich field descriptions.

Source code in instructor/processing/schema.py
@functools.lru_cache(maxsize=256)
def generate_openai_schema(model: type[BaseModel]) -> dict[str, Any]:
    """
    Generate OpenAI function schema from a Pydantic model.

    Args:
        model: A Pydantic BaseModel subclass

    Returns:
        A dictionary in the format of OpenAI's function schema

    Note:
        The model's docstring will be used for the function description.
        Parameter descriptions from the docstring will enrich field descriptions.
    """
    schema = model.model_json_schema()
    docstring = parse(model.__doc__ or "")
    parameters = {k: v for k, v in schema.items() if k not in ("title", "description")}

    # Enrich parameter descriptions from docstring
    for param in docstring.params:
        if (name := param.arg_name) in parameters["properties"] and (
            description := param.description
        ):
            if "description" not in parameters["properties"][name]:
                parameters["properties"][name]["description"] = description

    parameters["required"] = sorted(
        k for k, v in parameters["properties"].items() if "default" not in v
    )

    if "description" not in schema:
        if docstring.short_description:
            schema["description"] = docstring.short_description
        else:
            schema["description"] = (
                f"Correctly extracted `{model.__name__}` with all "
                f"the required parameters with correct types"
            )

    return {
        "name": schema["title"],
        "description": schema["description"],
        "parameters": parameters,
    }

Generate Anthropic tool schema from a Pydantic model.

Parameters:

Name Type Description Default
model type[BaseModel]

A Pydantic BaseModel subclass

required

Returns:

Type Description
dict[str, Any]

A dictionary in the format of Anthropic's tool schema

Source code in instructor/processing/schema.py
@functools.lru_cache(maxsize=256)
def generate_anthropic_schema(model: type[BaseModel]) -> dict[str, Any]:
    """
    Generate Anthropic tool schema from a Pydantic model.

    Args:
        model: A Pydantic BaseModel subclass

    Returns:
        A dictionary in the format of Anthropic's tool schema
    """
    # Generate the Anthropic schema based on the OpenAI schema to avoid redundant schema generation
    openai_schema = generate_openai_schema(model)
    return {
        "name": openai_schema["name"],
        "description": openai_schema["description"],
        "input_schema": model.model_json_schema(),
    }

Generate Gemini function schema from a Pydantic model.

Parameters:

Name Type Description Default
model type[BaseModel]

A Pydantic BaseModel subclass

required

Returns:

Type Description
Any

A Gemini FunctionDeclaration object

Note

This function is deprecated. The google-generativeai library is being replaced by google-genai.

Source code in instructor/processing/schema.py
@functools.lru_cache(maxsize=256)
def generate_gemini_schema(model: type[BaseModel]) -> Any:
    """
    Generate Gemini function schema from a Pydantic model.

    Args:
        model: A Pydantic BaseModel subclass

    Returns:
        A Gemini FunctionDeclaration object

    Note:
        This function is deprecated. The google-generativeai library is being replaced by google-genai.
    """
    # This is kept for backward compatibility but deprecated
    warnings.warn(
        "generate_gemini_schema is deprecated. The google-generativeai library is being replaced by google-genai.",
        DeprecationWarning,
        stacklevel=2,
    )

    try:
        import importlib

        genai_types = cast(Any, importlib.import_module("google.generativeai.types"))

        # Use OpenAI schema
        openai_schema = generate_openai_schema(model)

        # Transform to Gemini format
        function = genai_types.FunctionDeclaration(
            name=openai_schema["name"],
            description=openai_schema["description"],
            parameters=map_to_gemini_function_schema(openai_schema["parameters"]),
        )

        return function
    except ImportError as e:
        raise ImportError(
            "google-generativeai is deprecated. Please install google-genai instead: pip install google-genai"
        ) from e

Validation

Validation utilities for LLM outputs and async validation support.

Validation components for instructor.

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.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/core/exceptions.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.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: OpenAISchema

Validate if an attribute is correct and if not, return a new value with an error message

Source code in instructor/processing/validators.py
class Validator(OpenAISchema):
    """
    Validate if an attribute is correct and if not,
    return a new value with an error message
    """

    is_valid: bool = Field(
        default=True,
        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",
    )

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

Create a validator that uses the LLM to validate an attribute

Usage

from instructor import llm_validator
from pydantic import BaseModel, Field, field_validator

class User(BaseModel):
    name: str = Annotated[str, llm_validator("The name must be a full name all lowercase")
    age: int = Field(description="The age of the person")

try:
    user = User(name="Jason Liu", age=20)
except ValidationError as e:
    print(e)
1 validation error for User
name
    The name is valid but not all lowercase (type=value_error.llm_validator)

Note that there, the error message is written by the LLM, and the error type is value_error.llm_validator.

Parameters:

Name Type Description Default
statement str

The statement to validate

required
model str

The LLM to use for validation (default: "gpt-4o-mini")

'gpt-3.5-turbo'
temperature float

The temperature to use for the LLM (default: 0)

0
client OpenAI

The OpenAI client to use (default: None)

required
Source code in instructor/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 the LLM to validate an attribute

    ## Usage

    ```python
    from instructor import llm_validator
    from pydantic import BaseModel, Field, field_validator

    class User(BaseModel):
        name: str = Annotated[str, llm_validator("The name must be a full name all lowercase")
        age: int = Field(description="The age of the person")

    try:
        user = User(name="Jason Liu", age=20)
    except ValidationError as e:
        print(e)
    ```

    ```
    1 validation error for User
    name
        The name is valid but not all lowercase (type=value_error.llm_validator)
    ```

    Note that there, the error message is written by the LLM, and the error type is `value_error.llm_validator`.

    Parameters:
        statement (str): The statement to validate
        model (str): The LLM to use for validation (default: "gpt-4o-mini")
        temperature (float): The temperature to use for the LLM (default: 0)
        client (OpenAI): The OpenAI client to use (default: None)
    """

    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 the response is  not valid, return the reason, this could be used in
        # the future to generate a better response, via reasking mechanism.
        assert resp.is_valid, resp.reason

        if allow_override and not resp.is_valid and resp.fixed_value is not None:
            # If the value is not valid, but we allow override, return the fixed value
            return resp.fixed_value
        return v

    return llm

openai_moderation(client)

Validates a message using OpenAI moderation model.

Should only be used for monitoring inputs and outputs of OpenAI APIs Other use cases are disallowed as per: https://platform.openai.com/docs/guides/moderation/overview

Example:

from instructor import OpenAIModeration

class Response(BaseModel):
    message: Annotated[str, AfterValidator(OpenAIModeration(openai_client=client))]

Response(message="I hate you")

 ValidationError: 1 validation error for Response
 message
Value error, `I hate you.` was flagged for ['harassment'] [type=value_error, input_value='I hate you.', input_type=str]

client (OpenAI): The OpenAI client to use, must be sync (default: None)

Source code in instructor/validation/llm_validators.py
def openai_moderation(client: OpenAI) -> Callable[[str], str]:
    """
    Validates a message using OpenAI moderation model.

    Should only be used for monitoring inputs and outputs of OpenAI APIs
    Other use cases are disallowed as per:
    https://platform.openai.com/docs/guides/moderation/overview

    Example:
    ```python
    from instructor import OpenAIModeration

    class Response(BaseModel):
        message: Annotated[str, AfterValidator(OpenAIModeration(openai_client=client))]

    Response(message="I hate you")
    ```

    ```
     ValidationError: 1 validation error for Response
     message
    Value error, `I hate you.` was flagged for ['harassment'] [type=value_error, input_value='I hate you.', input_type=str]
    ```

    client (OpenAI): The OpenAI client to use, must be sync (default: None)
    """

    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 the LLM to validate an attribute

Usage

from instructor import llm_validator
from pydantic import BaseModel, Field, field_validator

class User(BaseModel):
    name: str = Annotated[str, llm_validator("The name must be a full name all lowercase")
    age: int = Field(description="The age of the person")

try:
    user = User(name="Jason Liu", age=20)
except ValidationError as e:
    print(e)
1 validation error for User
name
    The name is valid but not all lowercase (type=value_error.llm_validator)

Note that there, the error message is written by the LLM, and the error type is value_error.llm_validator.

Parameters:

Name Type Description Default
statement str

The statement to validate

required
model str

The LLM to use for validation (default: "gpt-4o-mini")

'gpt-3.5-turbo'
temperature float

The temperature to use for the LLM (default: 0)

0
client OpenAI

The OpenAI client to use (default: None)

required
Source code in instructor/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 the LLM to validate an attribute

    ## Usage

    ```python
    from instructor import llm_validator
    from pydantic import BaseModel, Field, field_validator

    class User(BaseModel):
        name: str = Annotated[str, llm_validator("The name must be a full name all lowercase")
        age: int = Field(description="The age of the person")

    try:
        user = User(name="Jason Liu", age=20)
    except ValidationError as e:
        print(e)
    ```

    ```
    1 validation error for User
    name
        The name is valid but not all lowercase (type=value_error.llm_validator)
    ```

    Note that there, the error message is written by the LLM, and the error type is `value_error.llm_validator`.

    Parameters:
        statement (str): The statement to validate
        model (str): The LLM to use for validation (default: "gpt-4o-mini")
        temperature (float): The temperature to use for the LLM (default: 0)
        client (OpenAI): The OpenAI client to use (default: None)
    """

    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 the response is  not valid, return the reason, this could be used in
        # the future to generate a better response, via reasking mechanism.
        assert resp.is_valid, resp.reason

        if allow_override and not resp.is_valid and resp.fixed_value is not None:
            # If the value is not valid, but we allow override, return the fixed value
            return resp.fixed_value
        return v

    return llm

Validates a message using OpenAI moderation model.

Should only be used for monitoring inputs and outputs of OpenAI APIs Other use cases are disallowed as per: https://platform.openai.com/docs/guides/moderation/overview

Example:

from instructor import OpenAIModeration

class Response(BaseModel):
    message: Annotated[str, AfterValidator(OpenAIModeration(openai_client=client))]

Response(message="I hate you")

 ValidationError: 1 validation error for Response
 message
Value error, `I hate you.` was flagged for ['harassment'] [type=value_error, input_value='I hate you.', input_type=str]

client (OpenAI): The OpenAI client to use, must be sync (default: None)

Source code in instructor/validation/llm_validators.py
def openai_moderation(client: OpenAI) -> Callable[[str], str]:
    """
    Validates a message using OpenAI moderation model.

    Should only be used for monitoring inputs and outputs of OpenAI APIs
    Other use cases are disallowed as per:
    https://platform.openai.com/docs/guides/moderation/overview

    Example:
    ```python
    from instructor import OpenAIModeration

    class Response(BaseModel):
        message: Annotated[str, AfterValidator(OpenAIModeration(openai_client=client))]

    Response(message="I hate you")
    ```

    ```
     ValidationError: 1 validation error for Response
     message
    Value error, `I hate you.` was flagged for ['harassment'] [type=value_error, input_value='I hate you.', input_type=str]
    ```

    client (OpenAI): The OpenAI client to use, must be sync (default: None)
    """

    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[self.response_model](
                    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[self.response_model](
                    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[self.response_model](
                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[self.response_model](
                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 [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 [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 [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[self.response_model](
                    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[self.response_model](
                    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[self.response_model](
                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[self.response_model](
                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 = openai_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 = openai_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 = openai_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 = openai_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.

Audio

Bases: BaseModel

Represents an audio that can be loaded from a URL or file path.

Source code in instructor/processing/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]:
        """Convert the Audio instance to OpenAI's API format."""
        if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
            raise ValueError("OpenAI Responses doesn't support audio")

        return {
            "type": "input_audio",
            "input_audio": {"data": self.data, "format": "wav"},
        }

    def to_anthropic(self) -> dict[str, Any]:
        raise NotImplementedError("Anthropic is not supported yet")

    def to_genai(self):
        """
        Convert the Audio instance to Google GenAI's API format.
        """
        try:
            from google.genai import types
        except ImportError as err:
            raise ImportError(
                "google-genai package is required for GenAI integration. Install with: pip install google-genai"
            ) from err

        return types.Part.from_bytes(
            data=base64.b64decode(self.data),  # type: ignore
            mime_type=self.media_type,
        )

autodetect(source) classmethod

Attempt to autodetect an audio from a source string or Path.

Source code in instructor/processing/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/processing/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/processing/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/processing/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/processing/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)

to_genai()

Convert the Audio instance to Google GenAI's API format.

Source code in instructor/processing/multimodal.py
def to_genai(self):
    """
    Convert the Audio instance to Google GenAI's API format.
    """
    try:
        from google.genai import types
    except ImportError as err:
        raise ImportError(
            "google-genai package is required for GenAI integration. Install with: pip install google-genai"
        ) from err

    return types.Part.from_bytes(
        data=base64.b64decode(self.data),  # type: ignore
        mime_type=self.media_type,
    )

to_openai(mode)

Convert the Audio instance to OpenAI's API format.

Source code in instructor/processing/multimodal.py
def to_openai(self, mode: Mode) -> dict[str, Any]:
    """Convert the Audio instance to OpenAI's API format."""
    if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
        raise ValueError("OpenAI Responses doesn't support audio")

    return {
        "type": "input_audio",
        "input_audio": {"data": self.data, "format": "wav"},
    }

Image

Bases: BaseModel

Source code in instructor/processing/multimodal.py
class Image(BaseModel):
    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]:
        if (
            isinstance(self.source, str)
            and self.source.startswith(("http://", "https://"))
            and not self.data
        ):
            self.data = self.url_to_base64(self.source)

        return {
            "type": "image",
            "source": {
                "type": "base64",
                "media_type": self.media_type,
                "data": self.data,
            },
        }

    def to_openai(self, mode: Mode) -> dict[str, Any]:
        image_type = (
            "input_image"
            if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}
            else "image_url"
        )
        if (
            isinstance(self.source, str)
            and self.source.startswith(("http://", "https://"))
            and not self.is_base64(self.source)
        ):
            if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
                return {"type": "input_image", "image_url": self.source}
            else:
                return {"type": image_type, "image_url": {"url": self.source}}
        elif self.data or self.is_base64(str(self.source)):
            data = self.data or str(self.source).split(",", 1)[1]
            if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
                return {
                    "type": "input_image",
                    "image_url": f"data:{self.media_type};base64,{data}",
                }
            else:
                return {
                    "type": image_type,
                    "image_url": {"url": f"data:{self.media_type};base64,{data}"},
                }
        else:
            raise ValueError("Image data is missing for base64 encoding.")

    def to_genai(self):
        """
        Convert the Image instance to Google GenAI's API format.
        """
        try:
            from google.genai import types
        except ImportError as err:
            raise ImportError(
                "google-genai package is required for GenAI integration. Install with: pip install google-genai"
            ) from err

        # Google Cloud Storage
        if isinstance(self.source, str) and self.source.startswith("gs://"):
            return types.Part.from_bytes(
                data=self.data,  # type: ignore
                mime_type=self.media_type,
            )

        # URL
        if isinstance(self.source, str) and self.source.startswith(
            ("http://", "https://")
        ):
            return types.Part.from_bytes(
                data=requests.get(self.source).content,
                mime_type=self.media_type,
            )

        if self.data or self.is_base64(str(self.source)):
            data = self.data or str(self.source).split(",", 1)[1]
            return types.Part.from_bytes(
                data=base64.b64decode(data), mime_type=self.media_type
            )  # type: ignore

        else:
            raise ValueError("Image data is missing for base64 encoding.")

autodetect(source) classmethod

Attempt to autodetect an image from a source string or Path.

Source code in instructor/processing/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/processing/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/processing/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

to_genai()

Convert the Image instance to Google GenAI's API format.

Source code in instructor/processing/multimodal.py
def to_genai(self):
    """
    Convert the Image instance to Google GenAI's API format.
    """
    try:
        from google.genai import types
    except ImportError as err:
        raise ImportError(
            "google-genai package is required for GenAI integration. Install with: pip install google-genai"
        ) from err

    # Google Cloud Storage
    if isinstance(self.source, str) and self.source.startswith("gs://"):
        return types.Part.from_bytes(
            data=self.data,  # type: ignore
            mime_type=self.media_type,
        )

    # URL
    if isinstance(self.source, str) and self.source.startswith(
        ("http://", "https://")
    ):
        return types.Part.from_bytes(
            data=requests.get(self.source).content,
            mime_type=self.media_type,
        )

    if self.data or self.is_base64(str(self.source)):
        data = self.data or str(self.source).split(",", 1)[1]
        return types.Part.from_bytes(
            data=base64.b64decode(data), mime_type=self.media_type
        )  # type: ignore

    else:
        raise ValueError("Image data is missing for base64 encoding.")

url_to_base64(url) cached staticmethod

Cachable helper method for getting image url and encoding to base64.

Source code in instructor/processing/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/processing/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]:
        """Override Anthropic return with cache_control."""
        result = super().to_anthropic()
        if self.cache_control:
            result["cache_control"] = self.cache_control
        return result

to_anthropic()

Override Anthropic return with cache_control.

Source code in instructor/processing/multimodal.py
def to_anthropic(self) -> dict[str, Any]:
    """Override Anthropic return with cache_control."""
    result = super().to_anthropic()
    if self.cache_control:
        result["cache_control"] = self.cache_control
    return result

PDF

Bases: BaseModel

Source code in instructor/processing/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]:
        if (
            isinstance(self.source, str)
            and self.source.startswith(("http://", "https://"))
            and not self.data
        ):
            return {
                "type": "document_url",
                "document_url": self.source,
            }
        raise ValueError("Mistral only supports document URLs for now")

    def to_openai(self, mode: Mode) -> dict[str, Any]:
        """Convert to OpenAI's document format."""
        input_file_type = (
            "input_file"
            if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}
            else "file"
        )

        if (
            isinstance(self.source, str)
            and self.source.startswith(("http://", "https://"))
            and not self.data
        ):
            # Fetch the file from URL and convert to base64
            data = requests.get(self.source)
            data = base64.b64encode(data.content).decode("utf-8")
            if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
                return {
                    "type": input_file_type,
                    "filename": self.source,
                    "file_data": f"data:{self.media_type};base64,{data}",
                }
            else:
                return {
                    "type": input_file_type,
                    "file": {
                        "filename": self.source,
                        "file_data": f"data:{self.media_type};base64,{data}",
                    },
                }
        elif self.data or self.is_base64(str(self.source)):
            data = self.data or str(self.source).split(",", 1)[1]
            if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
                return {
                    "type": input_file_type,
                    "filename": (
                        self.source
                        if isinstance(self.source, str)
                        else str(self.source)
                    ),
                    "file_data": f"data:{self.media_type};base64,{data}",
                }
            else:
                return {
                    "type": input_file_type,
                    "file": {
                        "filename": (
                            self.source
                            if isinstance(self.source, str)
                            else str(self.source)
                        ),
                        "file_data": f"data:{self.media_type};base64,{data}",
                    },
                }
        else:
            raise ValueError("PDF data is missing for base64 encoding.")

    def to_anthropic(self) -> dict[str, Any]:
        """Convert to Anthropic's document format."""
        if (
            isinstance(self.source, str)
            and self.source.startswith(("http://", "https://"))
            and not self.data
        ):
            return {
                "type": "document",
                "source": {
                    "type": "url",
                    "url": self.source,
                },
            }
        else:
            if not self.data:
                self.data = requests.get(str(self.source)).content  # type: ignore
                self.data = base64.b64encode(self.data).decode("utf-8")  # type: ignore

            return {
                "type": "document",
                "source": {
                    "type": "base64",
                    "media_type": self.media_type,
                    "data": self.data,
                },
            }

    def to_genai(self):
        try:
            from google.genai import types
        except ImportError as err:
            raise ImportError(
                "google-genai package is required for GenAI integration. Install with: pip install google-genai"
            ) from err

        if (
            isinstance(self.source, str)
            and self.source.startswith(("http://", "https://"))
            and not self.data
        ):
            # Fetch the file from URL and convert to base64
            data = requests.get(self.source).content
            data = base64.b64encode(data).decode("utf-8")
            return types.Part.from_bytes(
                data=base64.b64decode(data),
                mime_type=self.media_type,
            )

        if self.data:
            return types.Part.from_bytes(
                data=base64.b64decode(self.data),
                mime_type=self.media_type,
            )

        raise ValueError("Unsupported PDF format")

    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 (URLs, paths, base64)
        if not self.data:
            # Need to fetch/load the data
            if isinstance(self.source, str) and self.source.startswith(
                ("http://", "https://")
            ):
                response = requests.get(self.source)
                response.raise_for_status()
                pdf_bytes = response.content
            elif isinstance(self.source, Path) or (
                isinstance(self.source, str) and Path(self.source).exists()
            ):
                pdf_bytes = Path(self.source).read_bytes()
            else:
                raise ValueError("PDF data is missing and source cannot be loaded")
        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/processing/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/processing/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/processing/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_anthropic()

Convert to Anthropic's document format.

Source code in instructor/processing/multimodal.py
def to_anthropic(self) -> dict[str, Any]:
    """Convert to Anthropic's document format."""
    if (
        isinstance(self.source, str)
        and self.source.startswith(("http://", "https://"))
        and not self.data
    ):
        return {
            "type": "document",
            "source": {
                "type": "url",
                "url": self.source,
            },
        }
    else:
        if not self.data:
            self.data = requests.get(str(self.source)).content  # type: ignore
            self.data = base64.b64encode(self.data).decode("utf-8")  # type: ignore

        return {
            "type": "document",
            "source": {
                "type": "base64",
                "media_type": self.media_type,
                "data": self.data,
            },
        }

to_bedrock(name=None)

Convert to Bedrock's document format.

Source code in instructor/processing/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 (URLs, paths, base64)
    if not self.data:
        # Need to fetch/load the data
        if isinstance(self.source, str) and self.source.startswith(
            ("http://", "https://")
        ):
            response = requests.get(self.source)
            response.raise_for_status()
            pdf_bytes = response.content
        elif isinstance(self.source, Path) or (
            isinstance(self.source, str) and Path(self.source).exists()
        ):
            pdf_bytes = Path(self.source).read_bytes()
        else:
            raise ValueError("PDF data is missing and source cannot be loaded")
    else:
        # Decode base64 data to bytes
        pdf_bytes = base64.b64decode(self.data)

    return {
        "document": {"format": "pdf", "name": name, "source": {"bytes": pdf_bytes}}
    }

to_openai(mode)

Convert to OpenAI's document format.

Source code in instructor/processing/multimodal.py
def to_openai(self, mode: Mode) -> dict[str, Any]:
    """Convert to OpenAI's document format."""
    input_file_type = (
        "input_file"
        if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}
        else "file"
    )

    if (
        isinstance(self.source, str)
        and self.source.startswith(("http://", "https://"))
        and not self.data
    ):
        # Fetch the file from URL and convert to base64
        data = requests.get(self.source)
        data = base64.b64encode(data.content).decode("utf-8")
        if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
            return {
                "type": input_file_type,
                "filename": self.source,
                "file_data": f"data:{self.media_type};base64,{data}",
            }
        else:
            return {
                "type": input_file_type,
                "file": {
                    "filename": self.source,
                    "file_data": f"data:{self.media_type};base64,{data}",
                },
            }
    elif self.data or self.is_base64(str(self.source)):
        data = self.data or str(self.source).split(",", 1)[1]
        if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
            return {
                "type": input_file_type,
                "filename": (
                    self.source
                    if isinstance(self.source, str)
                    else str(self.source)
                ),
                "file_data": f"data:{self.media_type};base64,{data}",
            }
        else:
            return {
                "type": input_file_type,
                "file": {
                    "filename": (
                        self.source
                        if isinstance(self.source, str)
                        else str(self.source)
                    ),
                    "file_data": f"data:{self.media_type};base64,{data}",
                },
            }
    else:
        raise ValueError("PDF data is missing for base64 encoding.")

PDFWithCacheControl

Bases: PDF

PDF with Anthropic prompt caching support.

Source code in instructor/processing/multimodal.py
class PDFWithCacheControl(PDF):
    """PDF with Anthropic prompt caching support."""

    def to_anthropic(self) -> dict[str, Any]:
        """Override Anthropic return with cache_control."""
        result = super().to_anthropic()
        result["cache_control"] = {"type": "ephemeral"}
        return result

to_anthropic()

Override Anthropic return with cache_control.

Source code in instructor/processing/multimodal.py
def to_anthropic(self) -> dict[str, Any]:
    """Override Anthropic return with cache_control."""
    result = super().to_anthropic()
    result["cache_control"] = {"type": "ephemeral"}
    return result

PDFWithGenaiFile

Bases: PDF

Source code in instructor/processing/multimodal.py
class PDFWithGenaiFile(PDF):
    @classmethod
    def from_new_genai_file(
        cls, file_path: str, retry_delay: int = 10, max_retries: int = 20
    ) -> PDFWithGenaiFile:
        """Create a new PDFWithGenaiFile from a file path."""
        from google.genai.types import FileState
        import time
        from google.genai import Client

        client = Client()
        file = client.files.upload(file=file_path)
        while file.state != FileState.ACTIVE:
            time.sleep(retry_delay)
            file = client.files.get(name=file.name)  # type: ignore
            if max_retries > 0:
                max_retries -= 1
            else:
                raise Exception(
                    "Max retries reached. File upload has been started but is still pending"
                )

        return cls(source=file.uri, media_type=file.mime_type, data=None)  # type: ignore

    @classmethod
    def from_existing_genai_file(cls, file_name: str) -> PDFWithGenaiFile:
        """Create a new PDFWithGenaiFile from a file URL."""
        from google.genai import types
        from google.genai.types import FileState
        from google.genai import Client

        client = Client()
        file = client.files.get(name=file_name)
        if file.source == types.FileSource.UPLOADED and file.state == FileState.ACTIVE:
            return cls(
                source=file.uri,  # type: ignore
                media_type=file.mime_type,  # type: ignore
                data=None,
            )
        else:
            raise ValueError("We only support uploaded PDFs for now")

    def to_genai(self):
        try:
            from google.genai import types
        except ImportError as err:
            raise ImportError(
                "google-genai package is required for GenAI integration. Install with: pip install google-genai"
            ) from err

        if (
            self.source
            and isinstance(self.source, str)
            and "https://generativelanguage.googleapis.com/v1beta/files/" in self.source
        ):
            return types.Part.from_uri(
                file_uri=self.source,
                mime_type=self.media_type,
            )

        return super().to_genai()

from_existing_genai_file(file_name) classmethod

Create a new PDFWithGenaiFile from a file URL.

Source code in instructor/processing/multimodal.py
@classmethod
def from_existing_genai_file(cls, file_name: str) -> PDFWithGenaiFile:
    """Create a new PDFWithGenaiFile from a file URL."""
    from google.genai import types
    from google.genai.types import FileState
    from google.genai import Client

    client = Client()
    file = client.files.get(name=file_name)
    if file.source == types.FileSource.UPLOADED and file.state == FileState.ACTIVE:
        return cls(
            source=file.uri,  # type: ignore
            media_type=file.mime_type,  # type: ignore
            data=None,
        )
    else:
        raise ValueError("We only support uploaded PDFs for now")

from_new_genai_file(file_path, retry_delay=10, max_retries=20) classmethod

Create a new PDFWithGenaiFile from a file path.

Source code in instructor/processing/multimodal.py
@classmethod
def from_new_genai_file(
    cls, file_path: str, retry_delay: int = 10, max_retries: int = 20
) -> PDFWithGenaiFile:
    """Create a new PDFWithGenaiFile from a file path."""
    from google.genai.types import FileState
    import time
    from google.genai import Client

    client = Client()
    file = client.files.upload(file=file_path)
    while file.state != FileState.ACTIVE:
        time.sleep(retry_delay)
        file = client.files.get(name=file.name)  # type: ignore
        if max_retries > 0:
            max_retries -= 1
        else:
            raise Exception(
                "Max retries reached. File upload has been started but is still pending"
            )

    return cls(source=file.uri, media_type=file.mime_type, data=None)  # type: ignore

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/processing/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/processing/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/processing/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
            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)

Convert Typed Contents to the appropriate format for Google GenAI.

Source code in instructor/processing/multimodal.py
def extract_genai_multimodal_content(
    contents: list[Any],
    autodetect_images: bool = True,
):
    """
    Convert Typed Contents to the appropriate format for Google GenAI.
    """
    from google.genai import types

    result: list[Union[types.Content, types.File]] = []  # noqa: UP007
    for content in contents:
        # Check for Files
        if isinstance(content, types.File):
            result.append(content)
            continue

        # We only want to do the conversion for the Image type
        if not isinstance(content, types.Content):
            raise ValueError(
                f"Unsupported content type: {type(content)}. This should only be used for the Google types"
            )
        # Cast to list of Parts
        content = cast(types.Content, content)
        converted_contents: list[types.Part] = []

        if not content.parts:
            raise ValueError("Content parts are empty")

        # Now we need to support a few cases
        for content_part in content.parts:
            if content_part.text and autodetect_images:
                converted_item = autodetect_media(content_part.text)

                if isinstance(converted_item, (Image, Audio, PDF)):
                    converted_contents.append(converted_item.to_genai())
                    continue

                converted_contents.append(content_part)
            else:
                converted_contents.append(content_part)

        result.append(types.Content(parts=converted_contents, role=content.role))

    return result

Bases: BaseModel

Source code in instructor/processing/multimodal.py
class Image(BaseModel):
    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]:
        if (
            isinstance(self.source, str)
            and self.source.startswith(("http://", "https://"))
            and not self.data
        ):
            self.data = self.url_to_base64(self.source)

        return {
            "type": "image",
            "source": {
                "type": "base64",
                "media_type": self.media_type,
                "data": self.data,
            },
        }

    def to_openai(self, mode: Mode) -> dict[str, Any]:
        image_type = (
            "input_image"
            if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}
            else "image_url"
        )
        if (
            isinstance(self.source, str)
            and self.source.startswith(("http://", "https://"))
            and not self.is_base64(self.source)
        ):
            if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
                return {"type": "input_image", "image_url": self.source}
            else:
                return {"type": image_type, "image_url": {"url": self.source}}
        elif self.data or self.is_base64(str(self.source)):
            data = self.data or str(self.source).split(",", 1)[1]
            if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
                return {
                    "type": "input_image",
                    "image_url": f"data:{self.media_type};base64,{data}",
                }
            else:
                return {
                    "type": image_type,
                    "image_url": {"url": f"data:{self.media_type};base64,{data}"},
                }
        else:
            raise ValueError("Image data is missing for base64 encoding.")

    def to_genai(self):
        """
        Convert the Image instance to Google GenAI's API format.
        """
        try:
            from google.genai import types
        except ImportError as err:
            raise ImportError(
                "google-genai package is required for GenAI integration. Install with: pip install google-genai"
            ) from err

        # Google Cloud Storage
        if isinstance(self.source, str) and self.source.startswith("gs://"):
            return types.Part.from_bytes(
                data=self.data,  # type: ignore
                mime_type=self.media_type,
            )

        # URL
        if isinstance(self.source, str) and self.source.startswith(
            ("http://", "https://")
        ):
            return types.Part.from_bytes(
                data=requests.get(self.source).content,
                mime_type=self.media_type,
            )

        if self.data or self.is_base64(str(self.source)):
            data = self.data or str(self.source).split(",", 1)[1]
            return types.Part.from_bytes(
                data=base64.b64decode(data), mime_type=self.media_type
            )  # type: ignore

        else:
            raise ValueError("Image data is missing for base64 encoding.")

autodetect(source) classmethod

Attempt to autodetect an image from a source string or Path.

Source code in instructor/processing/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/processing/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/processing/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

to_genai()

Convert the Image instance to Google GenAI's API format.

Source code in instructor/processing/multimodal.py
def to_genai(self):
    """
    Convert the Image instance to Google GenAI's API format.
    """
    try:
        from google.genai import types
    except ImportError as err:
        raise ImportError(
            "google-genai package is required for GenAI integration. Install with: pip install google-genai"
        ) from err

    # Google Cloud Storage
    if isinstance(self.source, str) and self.source.startswith("gs://"):
        return types.Part.from_bytes(
            data=self.data,  # type: ignore
            mime_type=self.media_type,
        )

    # URL
    if isinstance(self.source, str) and self.source.startswith(
        ("http://", "https://")
    ):
        return types.Part.from_bytes(
            data=requests.get(self.source).content,
            mime_type=self.media_type,
        )

    if self.data or self.is_base64(str(self.source)):
        data = self.data or str(self.source).split(",", 1)[1]
        return types.Part.from_bytes(
            data=base64.b64decode(data), mime_type=self.media_type
        )  # type: ignore

    else:
        raise ValueError("Image data is missing for base64 encoding.")

url_to_base64(url) cached staticmethod

Cachable helper method for getting image url and encoding to base64.

Source code in instructor/processing/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/processing/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]:
        """Convert the Audio instance to OpenAI's API format."""
        if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
            raise ValueError("OpenAI Responses doesn't support audio")

        return {
            "type": "input_audio",
            "input_audio": {"data": self.data, "format": "wav"},
        }

    def to_anthropic(self) -> dict[str, Any]:
        raise NotImplementedError("Anthropic is not supported yet")

    def to_genai(self):
        """
        Convert the Audio instance to Google GenAI's API format.
        """
        try:
            from google.genai import types
        except ImportError as err:
            raise ImportError(
                "google-genai package is required for GenAI integration. Install with: pip install google-genai"
            ) from err

        return types.Part.from_bytes(
            data=base64.b64decode(self.data),  # type: ignore
            mime_type=self.media_type,
        )

autodetect(source) classmethod

Attempt to autodetect an audio from a source string or Path.

Source code in instructor/processing/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/processing/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/processing/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/processing/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/processing/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)

to_genai()

Convert the Audio instance to Google GenAI's API format.

Source code in instructor/processing/multimodal.py
def to_genai(self):
    """
    Convert the Audio instance to Google GenAI's API format.
    """
    try:
        from google.genai import types
    except ImportError as err:
        raise ImportError(
            "google-genai package is required for GenAI integration. Install with: pip install google-genai"
        ) from err

    return types.Part.from_bytes(
        data=base64.b64decode(self.data),  # type: ignore
        mime_type=self.media_type,
    )

to_openai(mode)

Convert the Audio instance to OpenAI's API format.

Source code in instructor/processing/multimodal.py
def to_openai(self, mode: Mode) -> dict[str, Any]:
    """Convert the Audio instance to OpenAI's API format."""
    if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
        raise ValueError("OpenAI Responses doesn't support audio")

    return {
        "type": "input_audio",
        "input_audio": {"data": self.data, "format": "wav"},
    }

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/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_STRUCTURED_OUTPUTS = "genai_structured_outputs"

    # 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 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

json_modes() classmethod

Returns a set of all JSON-based modes.

Source code in instructor/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,
    }

tool_modes() classmethod

Returns a set of all tool-based modes.

Source code in instructor/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_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/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

Source code in instructor/utils/providers.py
class Provider(Enum):
    OPENAI = "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"
    UNKNOWN = "unknown"
    BEDROCK = "bedrock"
    PERPLEXITY = "perplexity"
    OPENROUTER = "openrouter"

Exceptions

Exception classes for error handling.

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.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/core/exceptions.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.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/core/exceptions.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/core/exceptions.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.core.exceptions 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/core/exceptions.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.core.exceptions 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/core/exceptions.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/core/exceptions.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/core/exceptions.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/core/exceptions.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/core/exceptions.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/core/exceptions.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/core/exceptions.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/core/exceptions.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/core/exceptions.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.

CompletionErrorHandler

Bases: Protocol

Protocol for completion error and last attempt handlers.

Source code in instructor/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/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/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/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:
                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) -> None:
        """
        Emit a completion error event.

        Args:
            error: The exception to pass to handlers
        """
        self.emit(HookName.COMPLETION_ERROR, error)

    def emit_completion_last_attempt(self, error: Exception) -> None:
        """
        Emit a completion last attempt event.

        Args:
            error: The exception to pass to handlers
        """
        self.emit(HookName.COMPLETION_LAST_ATTEMPT, error)

    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/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/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/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/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/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/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/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:
            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/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)

Emit a completion error event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
Source code in instructor/core/hooks.py
def emit_completion_error(self, error: Exception) -> None:
    """
    Emit a completion error event.

    Args:
        error: The exception to pass to handlers
    """
    self.emit(HookName.COMPLETION_ERROR, error)

emit_completion_last_attempt(error)

Emit a completion last attempt event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
Source code in instructor/core/hooks.py
def emit_completion_last_attempt(self, error: Exception) -> None:
    """
    Emit a completion last attempt event.

    Args:
        error: The exception to pass to handlers
    """
    self.emit(HookName.COMPLETION_LAST_ATTEMPT, error)

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/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/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/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/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/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/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/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:
                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) -> None:
        """
        Emit a completion error event.

        Args:
            error: The exception to pass to handlers
        """
        self.emit(HookName.COMPLETION_ERROR, error)

    def emit_completion_last_attempt(self, error: Exception) -> None:
        """
        Emit a completion last attempt event.

        Args:
            error: The exception to pass to handlers
        """
        self.emit(HookName.COMPLETION_LAST_ATTEMPT, error)

    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/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/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/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/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/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/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/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:
            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/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)

Emit a completion error event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
Source code in instructor/core/hooks.py
def emit_completion_error(self, error: Exception) -> None:
    """
    Emit a completion error event.

    Args:
        error: The exception to pass to handlers
    """
    self.emit(HookName.COMPLETION_ERROR, error)

emit_completion_last_attempt(error)

Emit a completion last attempt event.

Parameters:

Name Type Description Default
error Exception

The exception to pass to handlers

required
Source code in instructor/core/hooks.py
def emit_completion_last_attempt(self, error: Exception) -> None:
    """
    Emit a completion last attempt event.

    Args:
        error: The exception to pass to handlers
    """
    self.emit(HookName.COMPLETION_LAST_ATTEMPT, error)

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/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/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/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/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/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/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.

apatch(client, mode=Mode.TOOLS)

No longer necessary, use patch instead.

Patch the client.chat.completions.create method

Enables the following features:

  • response_model parameter to parse the response from OpenAI's API
  • max_retries parameter to retry the function if the response is not valid
  • validation_context parameter to validate the response using the pydantic model
  • strict parameter to use strict json parsing
Source code in instructor/core/patch.py
def apatch(client: AsyncOpenAI, mode: Mode = Mode.TOOLS) -> AsyncOpenAI:
    """
    No longer necessary, use `patch` instead.

    Patch the `client.chat.completions.create` method

    Enables the following features:

    - `response_model` parameter to parse the response from OpenAI's API
    - `max_retries` parameter to retry the function if the response is not valid
    - `validation_context` parameter to validate the response using the pydantic model
    - `strict` parameter to use strict json parsing
    """
    import warnings

    warnings.warn(
        "apatch is deprecated, use patch instead",
        DeprecationWarning,
        stacklevel=2,
    )
    return patch(client, mode=mode)

handle_context(context=None, validation_context=None)

Handle the context and validation_context parameters. If both are provided, raise an error. If validation_context is provided, issue a deprecation warning and use it as context. If neither is provided, return None.

Source code in instructor/core/patch.py
def handle_context(
    context: dict[str, Any] | None = None,
    validation_context: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
    """
    Handle the context and validation_context parameters.
    If both are provided, raise an error.
    If validation_context is provided, issue a deprecation warning and use it as context.
    If neither is provided, return None.
    """
    if context is not None and validation_context is not None:
        from .exceptions import ConfigurationError

        raise ConfigurationError(
            "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=2,
        )
        context = validation_context
    return context

patch(client=None, create=None, mode=Mode.TOOLS)

patch(client: OpenAI, mode: Mode = Mode.TOOLS) -> OpenAI
patch(client: AsyncOpenAI, mode: Mode = Mode.TOOLS) -> AsyncOpenAI
patch(create: Callable[T_ParamSpec, T_Retval], mode: Mode = Mode.TOOLS) -> InstructorChatCompletionCreate
patch(create: Awaitable[T_Retval], mode: Mode = Mode.TOOLS) -> InstructorChatCompletionCreate

Patch the client.chat.completions.create method

Enables the following features:

  • response_model parameter to parse the response from OpenAI's API
  • max_retries parameter to retry the function if the response is not valid
  • validation_context parameter to validate the response using the pydantic model
  • strict parameter to use strict json parsing
  • hooks parameter to hook into the completion process
Source code in instructor/core/patch.py
def patch(  # type: ignore
    client: OpenAI | AsyncOpenAI | None = None,
    create: Callable[T_ParamSpec, T_Retval] | None = None,
    mode: Mode = Mode.TOOLS,
) -> OpenAI | AsyncOpenAI:
    """
    Patch the `client.chat.completions.create` method

    Enables the following features:

    - `response_model` parameter to parse the response from OpenAI's API
    - `max_retries` parameter to retry the function if the response is not valid
    - `validation_context` parameter to validate the response using the pydantic model
    - `strict` parameter to use strict json parsing
    - `hooks` parameter to hook into the completion process
    """

    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")

    func_is_async = is_async(func)

    @wraps(func)  # type: ignore
    async def new_create_async(
        response_model: type[T_Model] | None = None,
        validation_context: dict[str, Any] | None = None,
        context: dict[str, Any] | None = None,
        max_retries: int | AsyncRetrying = 1,
        strict: bool = True,
        hooks: Hooks | None = None,
        *args: T_ParamSpec.args,
        **kwargs: T_ParamSpec.kwargs,
    ) -> T_Model:
        # -----------------------------
        # Cache handling (async path)
        # -----------------------------
        from ..cache import BaseCache, make_cache_key, load_cached_response

        cache: BaseCache | None = kwargs.pop("cache", None)  # type: ignore[assignment]
        cache_ttl_raw = kwargs.pop("cache_ttl", None)
        cache_ttl: int | None = (
            cache_ttl_raw if isinstance(cache_ttl_raw, int) else None
        )

        context = handle_context(context, validation_context)

        response_model, new_kwargs = handle_response_model(
            response_model=response_model, mode=mode, **kwargs
        )  # type: ignore
        new_kwargs = handle_templating(new_kwargs, mode=mode, context=context)

        # Attempt cache lookup **before** hitting retry layer
        if cache is not None and response_model is not None:
            key = make_cache_key(
                messages=new_kwargs.get("messages")
                or new_kwargs.get("contents")
                or new_kwargs.get("chat_history"),
                model=new_kwargs.get("model"),
                response_model=response_model,
                mode=mode.value if hasattr(mode, "value") else str(mode),
            )
            obj = load_cached_response(cache, key, response_model)
            if obj is not None:
                return obj  # type: ignore[return-value]

        response = await retry_async(
            func=func,  # type:ignore
            response_model=response_model,
            context=context,
            max_retries=max_retries,
            args=args,
            kwargs=new_kwargs,
            strict=strict,
            mode=mode,
            hooks=hooks,
        )

        # Store in cache *after* successful call
        if cache is not None and response_model is not None:
            try:
                from pydantic import BaseModel as _BM  # type: ignore[import-not-found]

                if isinstance(response, _BM):
                    # mypy: ignore-next-line
                    from ..cache import store_cached_response

                    store_cached_response(cache, key, response, ttl=cache_ttl)
            except ModuleNotFoundError:
                pass
        return response  # type: ignore

    @wraps(func)  # type: ignore
    def new_create_sync(
        response_model: type[T_Model] | None = None,
        validation_context: dict[str, Any] | None = None,
        context: dict[str, Any] | None = None,
        max_retries: int | Retrying = 1,
        strict: bool = True,
        hooks: Hooks | None = None,
        *args: T_ParamSpec.args,
        **kwargs: T_ParamSpec.kwargs,
    ) -> T_Model:
        # -----------------------------
        # Cache handling (sync path)
        # -----------------------------
        from ..cache import BaseCache, make_cache_key, load_cached_response

        cache: BaseCache | None = kwargs.pop("cache", None)  # type: ignore[assignment]
        cache_ttl_raw = kwargs.pop("cache_ttl", None)
        cache_ttl: int | None = (
            cache_ttl_raw if isinstance(cache_ttl_raw, int) else None
        )

        context = handle_context(context, validation_context)
        # print(f"instructor.patch: patched_function {func.__name__}")
        response_model, new_kwargs = handle_response_model(
            response_model=response_model, mode=mode, **kwargs
        )  # type: ignore

        new_kwargs = handle_templating(new_kwargs, mode=mode, context=context)

        # Attempt cache lookup
        if cache is not None and response_model is not None:
            key = make_cache_key(
                messages=new_kwargs.get("messages")
                or new_kwargs.get("contents")
                or new_kwargs.get("chat_history"),
                model=new_kwargs.get("model"),
                response_model=response_model,
                mode=mode.value if hasattr(mode, "value") else str(mode),
            )
            obj = load_cached_response(cache, key, response_model)
            if obj is not None:
                return obj  # type: ignore[return-value]

        response = retry_sync(
            func=func,  # type: ignore
            response_model=response_model,
            context=context,
            max_retries=max_retries,
            args=args,
            hooks=hooks,
            strict=strict,
            kwargs=new_kwargs,
            mode=mode,
        )

        # Save to cache
        if cache is not None and response_model is not None:
            try:
                from pydantic import BaseModel as _BM  # type: ignore[import-not-found]

                if isinstance(response, _BM):
                    # mypy: ignore-next-line
                    from ..cache import store_cached_response

                    store_cached_response(cache, key, response, ttl=cache_ttl)
            except ModuleNotFoundError:
                pass
        return response  # type: ignore

    new_create = new_create_async if func_is_async else new_create_sync

    if client is not None:
        client.chat.completions.create = new_create  # type: ignore
        return client
    else:
        return new_create  # type: ignore

Backwards compatibility module for instructor.patch.

This module provides lazy imports to maintain backwards compatibility.

__getattr__(name)

Lazy import to provide backward compatibility for patch imports.

Source code in instructor/patch.py
def __getattr__(name: str):
    """Lazy import to provide backward compatibility for patch imports."""
    warnings.warn(
        f"Importing from 'instructor.patch' is deprecated and will be removed in v2.0.0. "
        f"Please update your imports to use 'instructor.core.patch.{name}' instead:\n"
        "  from instructor.core.patch import patch, apatch",
        DeprecationWarning,
        stacklevel=2,
    )

    from .core import patch as core_patch

    # Try to get the attribute from the core.patch module
    if hasattr(core_patch, name):
        return getattr(core_patch, name)

    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

No longer necessary, use patch instead.

Patch the client.chat.completions.create method

Enables the following features:

  • response_model parameter to parse the response from OpenAI's API
  • max_retries parameter to retry the function if the response is not valid
  • validation_context parameter to validate the response using the pydantic model
  • strict parameter to use strict json parsing
Source code in instructor/core/patch.py
def apatch(client: AsyncOpenAI, mode: Mode = Mode.TOOLS) -> AsyncOpenAI:
    """
    No longer necessary, use `patch` instead.

    Patch the `client.chat.completions.create` method

    Enables the following features:

    - `response_model` parameter to parse the response from OpenAI's API
    - `max_retries` parameter to retry the function if the response is not valid
    - `validation_context` parameter to validate the response using the pydantic model
    - `strict` parameter to use strict json parsing
    """
    import warnings

    warnings.warn(
        "apatch is deprecated, use patch instead",
        DeprecationWarning,
        stacklevel=2,
    )
    return patch(client, mode=mode)