-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Support native json output and strict tool calls for anthropic #3457
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dsfaccini
wants to merge
12
commits into
pydantic:main
Choose a base branch
from
dsfaccini:anthropic-native-json
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,069
−228
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
b26f93b
adds native json output and strict tool call support for the anthropi…
dsfaccini 04a9b3b
update tests to use new transformer
dsfaccini b5243d0
restore docs and bump anthropic sdk and simplify model - add new test…
dsfaccini 5cd89db
Merge upstream/main: Add count_tokens and TTL support
dsfaccini 4a51289
validate strict compatibility
dsfaccini 41598ec
Merge branch 'main' into anthropic-native-json
dsfaccini 1578993
add model-based support and add tests
dsfaccini 0b27ecf
update snapshots for coverage
dsfaccini eb6edc6
rerun anthropic tests against api
dsfaccini 2446e6f
updated respective cassette
dsfaccini c2aa94f
add tests
dsfaccini dea5f0f
check compatibility for strict tool defs
dsfaccini File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,144 @@ | ||
| from __future__ import annotations as _annotations | ||
|
|
||
| from dataclasses import dataclass | ||
|
|
||
| from .._json_schema import JsonSchema, JsonSchemaTransformer | ||
| from . import ModelProfile | ||
|
|
||
|
|
||
| def _schema_is_lossless(schema: JsonSchema) -> bool: # noqa: C901 | ||
| """Return True when `anthropic.transform_schema` won't need to drop constraints. | ||
|
|
||
| Anthropic's structured output API only supports a subset of JSON Schema features. | ||
| This function detects whether a schema uses only supported features, allowing us | ||
| to safely enable strict mode for guaranteed server-side validation. | ||
|
|
||
| Checks are performed based on https://docs.claude.com/en/docs/build-with-claude/structured-outputs#how-sdk-transformation-works | ||
|
|
||
| Args: | ||
| schema: JSON Schema dictionary (typically from BaseModel.model_json_schema()) | ||
|
|
||
| Returns: | ||
| True if schema is lossless (all constraints preserved), False if lossy | ||
|
|
||
| Examples: | ||
| Lossless schemas (constraints preserved): | ||
| >>> _schema_is_lossless({'type': 'string'}) | ||
| True | ||
| >>> _schema_is_lossless({'type': 'object', 'properties': {'name': {'type': 'string'}}}) | ||
| True | ||
|
|
||
| Lossy schemas (constraints dropped): | ||
| >>> _schema_is_lossless({'type': 'string', 'minLength': 5}) | ||
| False | ||
| >>> _schema_is_lossless({'type': 'array', 'items': {'type': 'string'}, 'minItems': 2}) | ||
| False | ||
|
|
||
| Note: | ||
| Some checks handle edge cases that rarely occur with Pydantic-generated schemas: | ||
| - oneOf: Pydantic generates anyOf for Union types | ||
| - Custom formats: Pydantic doesn't expose custom format generation in normal API | ||
| """ | ||
| from anthropic.lib._parse._transform import SupportedStringFormats | ||
|
|
||
| def _walk(node: JsonSchema) -> bool: # noqa: C901 | ||
| if not isinstance(node, dict): # pragma: no cover | ||
| return False | ||
|
|
||
| node = dict(node) | ||
|
|
||
| if '$ref' in node: | ||
| node.pop('$ref') | ||
| return not node | ||
|
|
||
| defs = node.pop('$defs', None) | ||
| if defs: | ||
| for value in defs.values(): | ||
| if not _walk(value): | ||
| return False | ||
|
|
||
| type_ = node.pop('type', None) | ||
| any_of = node.pop('anyOf', None) | ||
| one_of = node.pop('oneOf', None) | ||
| all_of = node.pop('allOf', None) | ||
|
|
||
| node.pop('description', None) | ||
| node.pop('title', None) | ||
|
|
||
| # every sub-schema in the list must itself be lossless -> `all(_walk(item) for item in any_of)` | ||
| # the wrapper object must not have any other unsupported fields -> `and not node` | ||
| if isinstance(any_of, list): | ||
| return all(_walk(item) for item in any_of) and not node # pyright: ignore[reportUnknownVariableType, reportUnknownArgumentType] | ||
| if isinstance(one_of, list): # pragma: no cover | ||
| # pydantic generates anyOf for Union types, leaving this here for JSON schemas that don't come from pydantic.BaseModel | ||
| return all(_walk(item) for item in one_of) and not node # pyright: ignore[reportUnknownVariableType, reportUnknownArgumentType] | ||
| if isinstance(all_of, list): | ||
| return all(_walk(item) for item in all_of) and not node # pyright: ignore[reportUnknownVariableType, reportUnknownArgumentType] | ||
|
|
||
| if type_ is None: | ||
| return False | ||
|
|
||
| if type_ == 'object': | ||
| properties = node.pop('properties', None) | ||
| if properties: | ||
| for value in properties.values(): | ||
| if not _walk(value): | ||
| return False | ||
| additional = node.pop('additionalProperties', None) | ||
| if additional not in (None, False): | ||
| return False | ||
| node.pop('required', None) | ||
| elif type_ == 'array': | ||
| items = node.pop('items', None) | ||
| if items and not _walk(items): | ||
| return False | ||
| min_items = node.pop('minItems', None) | ||
| if min_items not in (None, 0, 1): | ||
| return False | ||
| elif type_ == 'string': | ||
| format_ = node.pop('format', None) | ||
| if format_ is not None and format_ not in SupportedStringFormats: # pragma: no cover | ||
| return False | ||
| elif type_ in {'integer', 'number', 'boolean', 'null'}: | ||
| pass | ||
| else: | ||
| return False | ||
|
|
||
| return not node | ||
|
|
||
| return _walk(schema) | ||
|
|
||
|
|
||
| @dataclass(init=False) | ||
| class AnthropicJsonSchemaTransformer(JsonSchemaTransformer): | ||
| """Transforms schemas to the subset supported by Anthropic structured outputs.""" | ||
|
|
||
| def walk(self) -> JsonSchema: | ||
| from anthropic import transform_schema | ||
|
|
||
| schema = super().walk() | ||
| if self.is_strict_compatible and not _schema_is_lossless(schema): | ||
| # check compatibility before calling anthropic's transformer | ||
| # so we don't auto-enable strict when the SDK would drop constraints | ||
| self.is_strict_compatible = False | ||
| transformed = transform_schema(schema) | ||
| return transformed | ||
|
|
||
| def transform(self, schema: JsonSchema) -> JsonSchema: | ||
| # for consistency with other transformers (openai,google) | ||
| schema.pop('title', None) | ||
| schema.pop('$schema', None) | ||
| return schema | ||
|
|
||
|
|
||
| def anthropic_model_profile(model_name: str) -> ModelProfile | None: | ||
| """Get the model profile for an Anthropic model.""" | ||
| return ModelProfile(thinking_tags=('<thinking>', '</thinking>')) | ||
| models_that_support_json_schema_output = ('claude-sonnet-4-5', 'claude-opus-4-1') | ||
| # anthropic introduced support for both structured outputs and strict tool use | ||
| # https://docs.claude.com/en/docs/build-with-claude/structured-outputs#example-usage | ||
| supports_json_schema_output = model_name.startswith(models_that_support_json_schema_output) | ||
| return ModelProfile( | ||
| thinking_tags=('<thinking>', '</thinking>'), | ||
| supports_json_schema_output=supports_json_schema_output, | ||
| json_schema_transformer=AnthropicJsonSchemaTransformer, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.