Skip to content

Commit 6da2126

Browse files
committed
- bump anthropic sdk to 0.75 (adjusted a couple of tests)
- add anthropic opus-4-5 - move beta headers test
1 parent 84cddbf commit 6da2126

File tree

8 files changed

+148
-142
lines changed

8 files changed

+148
-142
lines changed

pydantic_ai_slim/pydantic_ai/models/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
'anthropic:claude-opus-4-0',
7070
'anthropic:claude-opus-4-1-20250805',
7171
'anthropic:claude-opus-4-20250514',
72+
'anthropic:claude-opus-4-5',
73+
'anthropic:claude-opus-4-5-20251101',
7274
'anthropic:claude-sonnet-4-0',
7375
'anthropic:claude-sonnet-4-20250514',
7476
'anthropic:claude-sonnet-4-5',
@@ -97,6 +99,7 @@
9799
'bedrock:eu.anthropic.claude-haiku-4-5-20251001-v1:0',
98100
'bedrock:eu.anthropic.claude-sonnet-4-20250514-v1:0',
99101
'bedrock:eu.anthropic.claude-sonnet-4-5-20250929-v1:0',
102+
'bedrock:global.anthropic.claude-opus-4-5-20251101-v1:0',
100103
'bedrock:meta.llama3-1-405b-instruct-v1:0',
101104
'bedrock:meta.llama3-1-70b-instruct-v1:0',
102105
'bedrock:meta.llama3-1-8b-instruct-v1:0',

pydantic_ai_slim/pydantic_ai/models/anthropic.py

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
UserPromptPart,
3939
)
4040
from ..profiles import ModelProfileSpec
41-
from ..profiles.anthropic import models_that_support_json_schema_output
41+
from ..profiles.anthropic import ANTHROPIC_MODELS_THAT_SUPPORT_JSON_SCHEMA_OUTPUT
4242
from ..providers import Provider, infer_provider
4343
from ..providers.anthropic import AsyncAnthropicClient
4444
from ..settings import ModelSettings, merge_model_settings
@@ -311,14 +311,15 @@ def prepare_request(
311311
)
312312

313313
if model_request_parameters.output_mode == 'native':
314-
if model_request_parameters.output_object is None: # pragma: no cover
315-
raise UserError('Unreachable code: can never set `output_type=NativeOutput(None)`.')
314+
assert model_request_parameters.output_object is not None
316315
if model_request_parameters.output_object.strict is False:
317-
raise UserError('Cannot use `output_type=NativeOutput(...)` with `strict=False`.')
316+
raise UserError(
317+
'Setting `strict=False` on `output_type=NativeOutput(...)` is not allowed for Anthropic models.'
318+
)
318319
if not self.profile.supports_json_schema_output:
319320
raise UserError(
320321
f'Model {self.model_name} does not support native output. Use `output_type=PromptedOutput(...)` instead.'
321-
f'Models that support native output include: {models_that_support_json_schema_output}.'
322+
f'Models that support native output include: {ANTHROPIC_MODELS_THAT_SUPPORT_JSON_SCHEMA_OUTPUT}.'
322323
)
323324
model_request_parameters = replace(
324325
model_request_parameters, output_object=replace(model_request_parameters.output_object, strict=True)
@@ -365,11 +366,11 @@ async def _messages_create(
365366
system_prompt, anthropic_messages = await self._map_message(messages, model_request_parameters, model_settings)
366367

367368
output_format = self._native_output_format(model_request_parameters)
368-
betas_set = self._get_betas_set(tools, model_request_parameters)
369-
betas_set.update(builtin_tool_betas)
369+
betas = self._get_betas_set(tools, model_request_parameters)
370+
betas.update(builtin_tool_betas)
370371

371372
try:
372-
betas, extra_headers = self._prepare_betas_and_headers(betas_set, model_settings)
373+
betas, extra_headers = self._prepare_betas_and_headers(betas, model_settings)
373374

374375
return await self.client.beta.messages.create(
375376
max_tokens=model_settings.get('max_tokens', 4096),
@@ -380,7 +381,7 @@ async def _messages_create(
380381
tool_choice=tool_choice or OMIT,
381382
mcp_servers=mcp_servers or OMIT,
382383
output_format=output_format or OMIT,
383-
betas=betas or OMIT,
384+
betas=sorted(betas) or OMIT,
384385
stream=stream,
385386
thinking=model_settings.get('anthropic_thinking', OMIT),
386387
stop_sequences=model_settings.get('stop_sequences', OMIT),
@@ -416,11 +417,11 @@ async def _messages_count_tokens(
416417
system_prompt, anthropic_messages = await self._map_message(messages, model_request_parameters, model_settings)
417418

418419
output_format = self._native_output_format(model_request_parameters)
419-
betas_set = self._get_betas_set(tools, model_request_parameters)
420-
betas_set.update(builtin_tool_betas)
420+
betas = self._get_betas_set(tools, model_request_parameters)
421+
betas.update(builtin_tool_betas)
421422

422423
try:
423-
betas, extra_headers = self._prepare_betas_and_headers(betas_set, model_settings)
424+
betas, extra_headers = self._prepare_betas_and_headers(betas, model_settings)
424425

425426
return await self.client.beta.messages.count_tokens(
426427
system=system_prompt or OMIT,
@@ -429,7 +430,7 @@ async def _messages_count_tokens(
429430
tools=tools or OMIT,
430431
tool_choice=tool_choice or OMIT,
431432
mcp_servers=mcp_servers or OMIT,
432-
betas=betas or OMIT,
433+
betas=sorted(betas) or OMIT,
433434
output_format=output_format or OMIT,
434435
thinking=model_settings.get('anthropic_thinking', OMIT),
435436
timeout=model_settings.get('timeout', NOT_GIVEN),
@@ -578,7 +579,7 @@ def _add_builtin_tools(
578579
if 'memory' not in model_request_parameters.tool_defs:
579580
raise UserError("Built-in `MemoryTool` requires a 'memory' tool to be defined.")
580581
# Replace the memory tool definition with the built-in memory tool
581-
tools = [tool for tool in tools if tool['name'] != 'memory']
582+
tools = [tool for tool in tools if tool.get('name') != 'memory']
582583
tools.append(BetaMemoryTool20250818Param(name='memory', type='memory_20250818'))
583584
beta_features.add('context-management-2025-06-27')
584585
elif isinstance(tool, MCPServerTool) and tool.url:
@@ -625,26 +626,19 @@ def _infer_tool_choice(
625626

626627
def _prepare_betas_and_headers(
627628
self, betas: set[str], model_settings: AnthropicModelSettings
628-
) -> tuple[list[str], dict[str, str]]:
629+
) -> tuple[set[str], dict[str, str]]:
629630
"""Prepare beta features list and extra headers for API request.
630631
631-
Handles merging custom anthropic-beta header from extra_headers into betas set
632-
and ensuring User-Agent is set.
633-
634-
Args:
635-
betas: Set of beta feature strings (naturally deduplicated)
636-
model_settings: Model settings containing extra_headers
637-
638-
Returns:
639-
Tuple of (betas list, extra_headers dict)
632+
Handles merging custom `anthropic-beta` header from `extra_headers` into betas set
633+
and ensuring `User-Agent` is set.
640634
"""
641635
extra_headers = model_settings.get('extra_headers', {})
642636
extra_headers.setdefault('User-Agent', get_user_agent())
643637

644638
if beta_header := extra_headers.pop('anthropic-beta', None):
645639
betas.update({stripped_beta for beta in beta_header.split(',') if (stripped_beta := beta.strip())})
646640

647-
return sorted(betas), extra_headers
641+
return betas, extra_headers
648642

649643
async def _map_message( # noqa: C901
650644
self,
@@ -922,8 +916,7 @@ def _map_tool_definition(self, f: ToolDefinition) -> BetaToolParam:
922916
'description': f.description or '',
923917
'input_schema': f.parameters_json_schema,
924918
}
925-
if f.strict and self.profile.supports_json_schema_output: # pragma: no branch
926-
# NOTE we could warn the user that the model doesn't support strict mode to nudge them into one that does
919+
if f.strict and self.profile.supports_json_schema_output:
927920
tool_param['strict'] = f.strict
928921
return tool_param
929922

@@ -1143,6 +1136,9 @@ def _map_server_tool_use_block(item: BetaServerToolUseBlock, provider_name: str)
11431136
)
11441137
elif item.name in ('web_fetch', 'bash_code_execution', 'text_editor_code_execution'): # pragma: no cover
11451138
raise NotImplementedError(f'Anthropic built-in tool {item.name!r} is not currently supported.')
1139+
elif item.name in ('tool_search_tool_regex', 'tool_search_tool_bm25'): # pragma: no cover
1140+
# TODO 0.75
1141+
raise NotImplementedError(f'Anthropic built-in tool {item.name!r} is not currently supported.')
11461142
else:
11471143
assert_never(item.name)
11481144

pydantic_ai_slim/pydantic_ai/models/bedrock.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
105105
'anthropic.claude-opus-4-20250514-v1:0',
106106
'us.anthropic.claude-opus-4-20250514-v1:0',
107+
'global.anthropic.claude-opus-4-5-20251101-v1:0',
107108
'anthropic.claude-sonnet-4-20250514-v1:0',
108109
'us.anthropic.claude-sonnet-4-20250514-v1:0',
109110
'eu.anthropic.claude-sonnet-4-20250514-v1:0',
@@ -155,7 +156,7 @@
155156
'tool_use': 'tool_call',
156157
}
157158

158-
_AWS_BEDROCK_INFERENCE_GEO_PREFIXES: tuple[str, ...] = ('us.', 'eu.', 'apac.', 'jp.', 'au.', 'ca.')
159+
_AWS_BEDROCK_INFERENCE_GEO_PREFIXES: tuple[str, ...] = ('us.', 'eu.', 'apac.', 'jp.', 'au.', 'ca.', '.global')
159160
"""Geo prefixes for Bedrock inference profile IDs (e.g., 'eu.', 'us.').
160161
161162
Used to strip the geo prefix so we can pass a pure foundation model ID/ARN to CountTokens,

pydantic_ai_slim/pydantic_ai/profiles/anthropic.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
from .._json_schema import JsonSchema, JsonSchemaTransformer
66
from . import ModelProfile
77

8+
ANTHROPIC_MODELS_THAT_SUPPORT_JSON_SCHEMA_OUTPUT = ('claude-sonnet-4-5', 'claude-opus-4-1', 'claude-opus-4-5')
9+
"""These models support both structured outputs and strict tool calling."""
810
# TODO update when new models are released that support structured outputs
911
# https://docs.claude.com/en/docs/build-with-claude/structured-outputs#example-usage
10-
models_that_support_json_schema_output = ('claude-sonnet-4-5', 'claude-opus-4-1')
11-
"""These models support both structured outputs and strict tool calling."""
1212

1313

1414
def anthropic_model_profile(model_name: str) -> ModelProfile | None:
1515
"""Get the model profile for an Anthropic model."""
16-
supports_json_schema_output = model_name.startswith(models_that_support_json_schema_output)
16+
supports_json_schema_output = model_name.startswith(ANTHROPIC_MODELS_THAT_SUPPORT_JSON_SCHEMA_OUTPUT)
1717
return ModelProfile(
1818
thinking_tags=('<thinking>', '</thinking>'),
1919
supports_json_schema_output=supports_json_schema_output,
@@ -43,7 +43,9 @@ def walk(self) -> JsonSchema:
4343
# - tool_def.strict = self.is_strict_compatible
4444
# - output_object.strict = self.is_strict_compatible
4545
# we need to set it to False if we're not transforming, otherwise anthropic's API will reject the request
46-
self.is_strict_compatible = self.strict or False # default to False if None
46+
# if you want this behavior to change, please comment in this issue:
47+
# https://github.com/pydantic/pydantic-ai/issues/3541
48+
self.is_strict_compatible = self.strict is True # not compatible is strict is False/None
4749

4850
return transform_schema(schema) if self.strict is True else schema
4951

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.51.0"]
74-
anthropic = ["anthropic>=0.74.0"]
74+
anthropic = ["anthropic>=0.75.0"]
7575
groq = ["groq>=0.25.0"]
7676
openrouter = ["openai>=2.8.0"]
7777
mistral = ["mistralai>=1.9.10"]

tests/models/anthropic/test_output.py

Lines changed: 9 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77
1. Strict Tools - Model Support
88
2. Strict Tools - Schema Compatibility
99
3. Native Output - Model Support
10-
4. Auto Mode Selection
11-
5. Beta Header Management
12-
6. Comprehensive Parametrized Tests - All Combinations (24 test cases)
1310
"""
1411

1512
from __future__ import annotations as _annotations
@@ -33,7 +30,7 @@
3330
from anthropic import AsyncAnthropic, omit as OMIT
3431
from anthropic.types.beta import BetaMessage, BetaTextBlock, BetaUsage
3532

36-
from pydantic_ai.models.anthropic import AnthropicModel, AnthropicModelSettings
33+
from pydantic_ai.models.anthropic import AnthropicModel
3734
from pydantic_ai.providers.anthropic import AnthropicProvider
3835

3936
from ..test_anthropic import completion_message
@@ -53,7 +50,7 @@
5350
def test_strict_tools_supported_model_auto_enabled(
5451
allow_model_requests: None, weather_tool_responses: list[BetaMessage]
5552
):
56-
"""sonnet-4-5: strict=None + compatible schema → auto strict=True + beta header."""
53+
"""sonnet-4-5: strict=None + compatible schema → no strict field, no beta header."""
5754
mock_client = MockAnthropic.create_mock(weather_tool_responses)
5855
model = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(anthropic_client=mock_client))
5956
agent = Agent(model)
@@ -139,7 +136,7 @@ def get_weather(location: str) -> str:
139136

140137

141138
def test_strict_tools_incompatible_schema_not_auto_enabled(allow_model_requests: None):
142-
"""sonnet-4-5: strict=None + lossy schema → no strict field, no beta header."""
139+
"""sonnet-4-5: strict=None → no strict field, no beta header."""
143140
mock_client = MockAnthropic.create_mock(
144141
completion_message([BetaTextBlock(text='Sure', type='text')], BetaUsage(input_tokens=5, output_tokens=2))
145142
)
@@ -156,9 +153,9 @@ def constrained_tool(username: Annotated[str, Field(min_length=3)]) -> str: # p
156153
tools = completion_kwargs['tools']
157154
betas = completion_kwargs.get('betas')
158155

159-
# Lossy schema: strict is not auto-enabled, so no strict field
156+
# strict is not auto-enabled, so no strict field
160157
assert 'strict' not in tools[0]
161-
# Schema still has the constraint (not removed)
158+
# because the schema wasn't transformed, it keeps the pydantic constraint
162159
assert tools[0]['input_schema']['properties']['username']['minLength'] == 3
163160
assert betas is OMIT
164161

@@ -188,64 +185,6 @@ def test_native_output_supported_model(
188185
assert betas == snapshot(['structured-outputs-2025-11-13'])
189186

190187

191-
def test_native_output_unsupported_model_raises_error(
192-
allow_model_requests: None, city_location_schema: type[BaseModel]
193-
):
194-
"""sonnet-4-0: NativeOutput → raises UserError."""
195-
mock_client = MockAnthropic.create_mock(
196-
completion_message([BetaTextBlock(text='test', type='text')], BetaUsage(input_tokens=5, output_tokens=2))
197-
)
198-
model = AnthropicModel('claude-sonnet-4-0', provider=AnthropicProvider(anthropic_client=mock_client))
199-
agent = Agent(model, output_type=NativeOutput(city_location_schema))
200-
201-
with pytest.raises(UserError, match='Model claude-sonnet-4-0 does not support native output.'):
202-
agent.run_sync('What is the capital of France?')
203-
204-
205-
# =============================================================================
206-
# AUTO MODE Selection
207-
# =============================================================================
208-
209-
210-
def test_auto_mode_model_profile_check(allow_model_requests: None):
211-
"""Verify profile.supports_json_schema_output is set correctly."""
212-
mock_client = MockAnthropic.create_mock(
213-
completion_message([BetaTextBlock(text='test', type='text')], BetaUsage(input_tokens=5, output_tokens=2))
214-
)
215-
216-
sonnet_4_5 = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(anthropic_client=mock_client))
217-
assert sonnet_4_5.profile.supports_json_schema_output is True
218-
219-
sonnet_4_0 = AnthropicModel('claude-sonnet-4-0', provider=AnthropicProvider(anthropic_client=mock_client))
220-
assert sonnet_4_0.profile.supports_json_schema_output is False
221-
222-
223-
# =============================================================================
224-
# BETA HEADER Management
225-
# =============================================================================
226-
227-
228-
def test_beta_header_merge_custom_headers(
229-
allow_model_requests: None,
230-
mock_sonnet_4_5: tuple[AnthropicModel, AsyncAnthropic],
231-
city_location_schema: type[BaseModel],
232-
):
233-
"""Custom beta headers merge with structured-outputs beta."""
234-
model, mock_client = mock_sonnet_4_5
235-
236-
agent = Agent(
237-
model,
238-
output_type=NativeOutput(city_location_schema),
239-
model_settings=AnthropicModelSettings(extra_headers={'anthropic-beta': 'custom-feature-1, custom-feature-2'}),
240-
)
241-
agent.run_sync('What is the capital of France?')
242-
243-
completion_kwargs = get_mock_chat_completion_kwargs(mock_client)[-1]
244-
betas = completion_kwargs['betas']
245-
246-
assert betas == snapshot(['custom-feature-1', 'custom-feature-2', 'structured-outputs-2025-11-13'])
247-
248-
249188
# =============================================================================
250189
# COMPREHENSIVE INTEGRATION TESTS - All Combinations
251190
# =============================================================================
@@ -379,7 +318,10 @@ def test_no_tools_native_output_strict_false(
379318

380319
agent = Agent(model, output_type=NativeOutput(CityInfo, strict=False))
381320

382-
with pytest.raises(UserError, match='Cannot use `output_type=NativeOutput\\(\\.\\.\\.\\)` with `strict=False`'):
321+
with pytest.raises(
322+
UserError,
323+
match='Setting `strict=False` on `output_type=NativeOutput\\(\\.\\.\\.\\)` is not allowed for Anthropic models.',
324+
):
383325
agent.run_sync('Tell me about Rome')
384326

385327

0 commit comments

Comments
 (0)