Skip to content

Commit 243b1af

Browse files
committed
declare transformer in profiles/anthropic.py to encapsulate dependency
1 parent 31cb403 commit 243b1af

File tree

2 files changed

+62
-59
lines changed

2 files changed

+62
-59
lines changed
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from __future__ import annotations as _annotations
22

3-
from pydantic_ai.providers.anthropic import AnthropicJsonSchemaTransformer
3+
from pydantic_ai._json_schema import JsonSchemaTransformer
44

55
from . import ModelProfile
66

77

8-
def anthropic_model_profile(model_name: str) -> ModelProfile | None:
8+
def anthropic_model_profile(
9+
model_name: str,
10+
anthropic_json_schema_transformer: type[JsonSchemaTransformer] | None = None,
11+
) -> ModelProfile | None:
912
"""Get the model profile for an Anthropic model."""
1013
models_that_support_json_schema_output = ('claude-sonnet-4-5', 'claude-opus-4-1', 'claude-opus-4-5')
1114
"""These models support both structured outputs and strict tool calling."""
@@ -15,6 +18,6 @@ def anthropic_model_profile(model_name: str) -> ModelProfile | None:
1518
supports_json_schema_output = model_name.startswith(models_that_support_json_schema_output)
1619
return ModelProfile(
1720
thinking_tags=('<thinking>', '</thinking>'),
18-
supports_json_schema_output=supports_json_schema_output,
19-
json_schema_transformer=AnthropicJsonSchemaTransformer,
21+
supports_json_schema_output=supports_json_schema_output and anthropic_json_schema_transformer is not None,
22+
json_schema_transformer=anthropic_json_schema_transformer,
2023
)

pydantic_ai_slim/pydantic_ai/providers/anthropic.py

Lines changed: 55 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,60 @@
2525
AsyncAnthropicClient: TypeAlias = AsyncAnthropic | AsyncAnthropicBedrock | AsyncAnthropicVertex
2626

2727

28+
@dataclass(init=False)
29+
class AnthropicJsonSchemaTransformer(JsonSchemaTransformer):
30+
"""Transforms schemas to the subset supported by Anthropic structured outputs.
31+
32+
Transformation is applied when:
33+
- `NativeOutput` is used as the `output_type` of the Agent
34+
- `strict=True` is set on the `Tool`
35+
36+
The behavior of this transformer differs from the OpenAI one in that it sets `Tool.strict=False` by default when not explicitly set to True.
37+
38+
Example:
39+
```python
40+
from pydantic_ai import Agent
41+
42+
agent = Agent('anthropic:claude-sonnet-4-5')
43+
44+
@agent.tool_plain # -> defaults to strict=False
45+
def my_tool(x: str) -> dict[str, int]:
46+
...
47+
```
48+
49+
Anthropic's SDK `transform_schema()` automatically:
50+
- Adds `additionalProperties: false` to all objects (required by API)
51+
- Removes unsupported constraints (minLength, pattern, etc.)
52+
- Moves removed constraints to description field
53+
- Removes title and $schema fields
54+
"""
55+
56+
def walk(self) -> JsonSchema:
57+
from anthropic import transform_schema
58+
59+
schema = super().walk()
60+
61+
# The caller (pydantic_ai.models._customize_tool_def or _customize_output_object) coalesces
62+
# - output_object.strict = self.is_strict_compatible
63+
# - tool_def.strict = self.is_strict_compatible
64+
# the reason we don't default to `strict=True` is that the transformation could be lossy
65+
# so in order to change the behavior (default to True), we need to come up with logic that will check for lossiness
66+
# https://github.com/pydantic/pydantic-ai/issues/3541
67+
self.is_strict_compatible = self.strict is True # not compatible when strict is False/None
68+
69+
if self.strict is True:
70+
from anthropic import transform_schema
71+
72+
return transform_schema(schema)
73+
else:
74+
return schema
75+
76+
def transform(self, schema: JsonSchema) -> JsonSchema:
77+
schema.pop('title', None)
78+
schema.pop('$schema', None)
79+
return schema
80+
81+
2882
class AnthropicProvider(Provider[AsyncAnthropicClient]):
2983
"""Provider for Anthropic API."""
3084

@@ -41,7 +95,7 @@ def client(self) -> AsyncAnthropicClient:
4195
return self._client
4296

4397
def model_profile(self, model_name: str) -> ModelProfile | None:
44-
return anthropic_model_profile(model_name)
98+
return anthropic_model_profile(model_name, AnthropicJsonSchemaTransformer)
4599

46100
@overload
47101
def __init__(self, *, anthropic_client: AsyncAnthropicClient | None = None) -> None: ...
@@ -85,57 +139,3 @@ def __init__(
85139
else:
86140
http_client = cached_async_http_client(provider='anthropic')
87141
self._client = AsyncAnthropic(api_key=api_key, base_url=base_url, http_client=http_client)
88-
89-
90-
@dataclass(init=False)
91-
class AnthropicJsonSchemaTransformer(JsonSchemaTransformer):
92-
"""Transforms schemas to the subset supported by Anthropic structured outputs.
93-
94-
Transformation is applied when:
95-
- `NativeOutput` is used as the `output_type` of the Agent
96-
- `strict=True` is set on the `Tool`
97-
98-
The behavior of this transformer differs from the OpenAI one in that it sets `Tool.strict=False` by default when not explicitly set to True.
99-
100-
Example:
101-
```python
102-
from pydantic_ai import Agent
103-
104-
agent = Agent('anthropic:claude-sonnet-4-5')
105-
106-
@agent.tool_plain # -> defaults to strict=False
107-
def my_tool(x: str) -> dict[str, int]:
108-
...
109-
```
110-
111-
Anthropic's SDK `transform_schema()` automatically:
112-
- Adds `additionalProperties: false` to all objects (required by API)
113-
- Removes unsupported constraints (minLength, pattern, etc.)
114-
- Moves removed constraints to description field
115-
- Removes title and $schema fields
116-
"""
117-
118-
def walk(self) -> JsonSchema:
119-
from anthropic import transform_schema
120-
121-
schema = super().walk()
122-
123-
# The caller (pydantic_ai.models._customize_tool_def or _customize_output_object) coalesces
124-
# - output_object.strict = self.is_strict_compatible
125-
# - tool_def.strict = self.is_strict_compatible
126-
# the reason we don't default to `strict=True` is that the transformation could be lossy
127-
# so in order to change the behavior (default to True), we need to come up with logic that will check for lossiness
128-
# https://github.com/pydantic/pydantic-ai/issues/3541
129-
self.is_strict_compatible = self.strict is True # not compatible when strict is False/None
130-
131-
if self.strict is True:
132-
from anthropic import transform_schema
133-
134-
return transform_schema(schema)
135-
else:
136-
return schema
137-
138-
def transform(self, schema: JsonSchema) -> JsonSchema:
139-
schema.pop('title', None)
140-
schema.pop('$schema', None)
141-
return schema

0 commit comments

Comments
 (0)