Skip to content

Commit b5243d0

Browse files
committed
restore docs and bump anthropic sdk and simplify model - add new test with cassette
1 parent 04a9b3b commit b5243d0

File tree

9 files changed

+361
-272
lines changed

9 files changed

+361
-272
lines changed

docs/models/anthropic.md

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -144,32 +144,3 @@ async def main():
144144
print(f'Cache write tokens: {usage.cache_write_tokens}')
145145
print(f'Cache read tokens: {usage.cache_read_tokens}')
146146
```
147-
148-
## Structured outputs & strict tool calls
149-
150-
Anthropic offers [Structured Outputs](https://docs.claude.com/en/docs/build-with-claude/structured-outputs), which force the model to emit JSON matching a supplied schema and to validate tool inputs before they reach your code. Pydantic AI enables this automatically:
151-
152-
- When you use [`NativeOutput`][pydantic_ai.output.NativeOutput] (or set `output_mode='native'`), the agent sends Anthropic the JSON schema as an `output_format` payload and adds the required `structured-outputs-2025-11-13` beta header. The model's response will be validated just like on other providers, and `result.output` will contain your typed object.
153-
- Tool definitions that are strict-compatible are sent with `strict: true`, which enables Anthropic's parameter validation. You can opt out for a specific tool by passing `strict=False` to `Tool(...)` or an agent decorator.
154-
155-
```python {test="skip"}
156-
from pydantic import BaseModel
157-
158-
from pydantic_ai import Agent, NativeOutput
159-
from pydantic_ai.models.anthropic import AnthropicModel
160-
161-
162-
class Contact(BaseModel):
163-
name: str
164-
email: str
165-
166-
167-
model = AnthropicModel('claude-sonnet-4-5')
168-
agent = Agent(model, output_type=NativeOutput(Contact))
169-
result = agent.run_sync('Extract the contact info for Ada Lovelace.')
170-
print(result.output)
171-
#> Contact(name='Ada Lovelace', email='[email protected]')
172-
```
173-
174-
!!! note
175-
Pydantic AI automatically sets the beta headers, so you do not need to modify `model_settings.extra_headers`. If you also need to send custom `extra_body` fields, avoid supplying your own `output_format` as it will be generated for you.

docs/output.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ _(This example is complete, it can be run "as is")_
308308

309309
#### Native Output
310310

311-
Native Output mode uses a model's native "Structured Outputs" feature (aka "JSON Schema response format"), where the model is forced to only output text matching the provided JSON schema. Note that this is not supported by all models, and sometimes comes with restrictions. For example, Anthropic requires enabling their Structured Outputs beta (Pydantic AI handles the required headers automatically), while Gemini cannot use tools at the same time as structured output, and attempting to do so will result in an error.
311+
Native Output mode uses a model's native "Structured Outputs" feature (aka "JSON Schema response format"), where the model is forced to only output text matching the provided JSON schema. Note that this is not supported by all models, and sometimes comes with restrictions. For example, Gemini cannot use tools at the same time as structured output, and attempting to do so will result in an error.
312312

313313
To use this mode, you can wrap the output type(s) in the [`NativeOutput`][pydantic_ai.output.NativeOutput] marker class that also lets you specify a `name` and `description` if the name and docstring of the type or function are not sufficient.
314314

pydantic_ai_slim/pydantic_ai/models/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@
5757
Literal[
5858
'anthropic:claude-3-5-haiku-20241022',
5959
'anthropic:claude-3-5-haiku-latest',
60-
'anthropic:claude-3-5-sonnet-20240620',
61-
'anthropic:claude-3-5-sonnet-20241022',
62-
'anthropic:claude-3-5-sonnet-latest',
6360
'anthropic:claude-3-7-sonnet-20250219',
6461
'anthropic:claude-3-7-sonnet-latest',
6562
'anthropic:claude-3-haiku-20240307',

pydantic_ai_slim/pydantic_ai/models/anthropic.py

Lines changed: 13 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@
5353
'refusal': 'content_filter',
5454
}
5555

56-
# TODO: remove once anthropic moves it out of beta
57-
_STRUCTURED_OUTPUTS_BETA = 'structured-outputs-2025-11-13'
58-
5956

6057
try:
6158
from anthropic import NOT_GIVEN, APIStatusError, AsyncStream, omit as OMIT
@@ -73,6 +70,7 @@
7370
BetaContentBlockParam,
7471
BetaImageBlockParam,
7572
BetaInputJSONDelta,
73+
BetaJSONOutputFormatParam,
7674
BetaMCPToolResultBlock,
7775
BetaMCPToolUseBlock,
7876
BetaMCPToolUseBlockParam,
@@ -313,7 +311,6 @@ async def _messages_create(
313311
tools, strict_tools_requested = self._get_tools(model_request_parameters, model_settings)
314312
tools, mcp_servers, beta_features = self._add_builtin_tools(tools, model_request_parameters)
315313
output_format = self._build_output_format(model_request_parameters)
316-
structured_output_beta_required = strict_tools_requested or bool(output_format)
317314

318315
tool_choice: BetaToolChoiceParam | None
319316

@@ -330,21 +327,14 @@ async def _messages_create(
330327

331328
system_prompt, anthropic_messages = await self._map_message(messages, model_request_parameters, model_settings)
332329

330+
# Build betas list for SDK
331+
betas: list[str] = list(beta_features)
332+
if strict_tools_requested or output_format:
333+
betas.append('structured-outputs-2025-11-13')
334+
333335
try:
334336
extra_headers = model_settings.get('extra_headers', {})
335337
extra_headers.setdefault('User-Agent', get_user_agent())
336-
if beta_features or structured_output_beta_required:
337-
new_features = list(beta_features)
338-
if structured_output_beta_required:
339-
new_features.append(_STRUCTURED_OUTPUTS_BETA)
340-
extra_headers['anthropic-beta'] = self._format_beta_header(
341-
extra_headers.get('anthropic-beta'),
342-
new_features,
343-
)
344-
345-
extra_body = cast(dict[str, Any] | None, model_settings.get('extra_body'))
346-
if output_format is not None:
347-
extra_body = self._merge_output_format_extra_body(extra_body, output_format)
348338

349339
return await self.client.beta.messages.create(
350340
max_tokens=model_settings.get('max_tokens', 4096),
@@ -354,6 +344,8 @@ async def _messages_create(
354344
tools=tools or OMIT,
355345
tool_choice=tool_choice or OMIT,
356346
mcp_servers=mcp_servers or OMIT,
347+
output_format=output_format or OMIT,
348+
betas=betas or OMIT,
357349
stream=stream,
358350
thinking=model_settings.get('anthropic_thinking', OMIT),
359351
stop_sequences=model_settings.get('stop_sequences', OMIT),
@@ -362,7 +354,7 @@ async def _messages_create(
362354
timeout=model_settings.get('timeout', NOT_GIVEN),
363355
metadata=model_settings.get('anthropic_metadata', OMIT),
364356
extra_headers=extra_headers,
365-
extra_body=extra_body,
357+
extra_body=model_settings.get('extra_body'),
366358
)
367359
except APIStatusError as e:
368360
if (status_code := e.status_code) >= 400:
@@ -780,45 +772,18 @@ def _map_tool_definition(f: ToolDefinition) -> BetaToolParam:
780772
'description': f.description or '',
781773
'input_schema': f.parameters_json_schema,
782774
}
783-
if f.strict is not None:
784-
tool_param['strict'] = f.strict # type: ignore[assignment]
775+
if f.strict:
776+
tool_param['strict'] = f.strict
785777
return tool_param
786778

787779
@staticmethod
788-
def _build_output_format(model_request_parameters: ModelRequestParameters) -> dict[str, Any] | None:
780+
def _build_output_format(model_request_parameters: ModelRequestParameters) -> BetaJSONOutputFormatParam | None:
789781
if model_request_parameters.output_mode != 'native':
790782
return None
791783
output_object = model_request_parameters.output_object
792-
if output_object is None:
793-
return None
784+
assert output_object is not None
794785
return {'type': 'json_schema', 'schema': output_object.json_schema}
795786

796-
@staticmethod
797-
def _merge_output_format_extra_body(
798-
existing: dict[str, Any] | None, output_format: dict[str, Any]
799-
) -> dict[str, Any]:
800-
merged = dict(existing or {})
801-
if 'output_format' in merged:
802-
raise UserError(
803-
'`model_settings.extra_body` cannot define `output_format` when using native structured output.'
804-
)
805-
merged['output_format'] = output_format
806-
return merged
807-
808-
@staticmethod
809-
def _format_beta_header(existing: str | None, new_features: list[str]) -> str:
810-
values: list[str] = []
811-
if existing:
812-
values.extend(value.strip() for value in existing.split(',') if value.strip())
813-
values.extend(new_features)
814-
ordered: list[str] = []
815-
seen: set[str] = set()
816-
for value in values:
817-
if value not in seen:
818-
ordered.append(value)
819-
seen.add(value)
820-
return ','.join(ordered)
821-
822787

823788
def _map_usage(
824789
message: BetaMessage | BetaRawMessageStartEvent | BetaRawMessageDeltaEvent,

pydantic_ai_slim/pydantic_ai/profiles/anthropic.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,28 @@
11
from __future__ import annotations as _annotations
22

3-
import importlib
43
from collections.abc import Callable
54
from dataclasses import dataclass
6-
from typing import Any, cast
5+
from typing import Any
76

87
from .._json_schema import JsonSchema, JsonSchemaTransformer
98
from . import ModelProfile
109

1110
TransformSchemaFunc = Callable[[Any], JsonSchema]
1211

13-
try: # pragma: no cover
14-
_anthropic_module = importlib.import_module('anthropic')
15-
except Exception:
16-
_anthropic_transform_schema: TransformSchemaFunc | None = None
17-
else:
18-
_anthropic_transform_schema = cast(TransformSchemaFunc | None, getattr(_anthropic_module, 'transform_schema', None))
19-
2012

2113
@dataclass(init=False)
2214
class AnthropicJsonSchemaTransformer(JsonSchemaTransformer):
2315
"""Transforms schemas to the subset supported by Anthropic structured outputs."""
2416

2517
def walk(self) -> JsonSchema:
18+
from anthropic import transform_schema
19+
2620
schema = super().walk()
27-
helper = _anthropic_transform_schema
28-
if helper is None:
29-
return schema
30-
try: # pragma: no branch
31-
# helper may raise if schema already transformed
32-
transformed = helper(schema)
33-
except Exception:
34-
return schema
35-
if isinstance(transformed, dict):
36-
return transformed
37-
return schema
21+
transformed = transform_schema(schema)
22+
return transformed
3823

3924
def transform(self, schema: JsonSchema) -> JsonSchema:
25+
# for consistency with other transformers (openai,google)
4026
schema.pop('title', None)
4127
schema.pop('$schema', None)
4228
return schema

pydantic_ai_slim/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ openai = ["openai>=1.107.2"]
7171
cohere = ["cohere>=5.18.0; platform_system != 'Emscripten'"]
7272
vertexai = ["google-auth>=2.36.0", "requests>=2.32.2"]
7373
google = ["google-genai>=1.50.1"]
74-
anthropic = ["anthropic>=0.70.0"]
74+
anthropic = ["anthropic>=0.74.0"]
7575
groq = ["groq>=0.25.0"]
7676
mistral = ["mistralai>=1.9.10"]
7777
bedrock = ["boto3>=1.40.14"]

0 commit comments

Comments
 (0)