Skip to content

Commit f005a8f

Browse files
committed
Adding Anthropic support and test coverage
1 parent 2606315 commit f005a8f

File tree

4 files changed

+502
-12
lines changed

4 files changed

+502
-12
lines changed

pydantic_ai_slim/pydantic_ai/models/anthropic.py

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
BetaContentBlockParam,
6969
BetaImageBlockParam,
7070
BetaInputJSONDelta,
71+
BetaMCPToolResultBlock,
72+
BetaMCPToolUseBlock,
7173
BetaMemoryTool20250818Param,
7274
BetaMessage,
7375
BetaMessageParam,
@@ -267,7 +269,7 @@ async def _messages_create(
267269
# standalone function to make it easier to override
268270
tools = self._get_tools(model_request_parameters)
269271
tools, beta_features = self._add_builtin_tools(tools, model_request_parameters)
270-
mcp_servers = self._get_mcp_servers(model_request_parameters)
272+
mcp_servers, beta_features = self._get_mcp_servers(model_request_parameters, beta_features)
271273

272274
tool_choice: BetaToolChoiceParam | None
273275

@@ -323,6 +325,8 @@ def _process_response(self, response: BetaMessage) -> ModelResponse:
323325
"""Process a non-streamed response, and prepare a message to return."""
324326
items: list[ModelResponsePart] = []
325327
for item in response.content:
328+
from anthropic.types.beta import BetaMCPToolUseBlock
329+
326330
if isinstance(item, BetaTextBlock):
327331
items.append(TextPart(content=item.text))
328332
elif isinstance(item, BetaServerToolUseBlock):
@@ -337,6 +341,10 @@ def _process_response(self, response: BetaMessage) -> ModelResponse:
337341
)
338342
elif isinstance(item, BetaThinkingBlock):
339343
items.append(ThinkingPart(content=item.thinking, signature=item.signature, provider_name=self.system))
344+
elif isinstance(item, BetaMCPToolUseBlock):
345+
items.append(_map_mcp_server_use_block(item, self.system))
346+
elif isinstance(item, BetaMCPToolResultBlock):
347+
items.append(_map_mcp_server_result_block(item, self.system))
340348
else:
341349
assert isinstance(item, BetaToolUseBlock), f'unexpected item type {type(item)}'
342350
items.append(
@@ -421,18 +429,14 @@ def _add_builtin_tools(
421429
return tools, beta_features
422430

423431
def _get_mcp_servers(
424-
self, model_request_parameters: ModelRequestParameters
425-
) -> list[BetaRequestMCPServerURLDefinitionParam]:
432+
self, model_request_parameters: ModelRequestParameters, beta_features: list[str]
433+
) -> tuple[list[BetaRequestMCPServerURLDefinitionParam], list[str]]:
426434
mcp_servers: list[BetaRequestMCPServerURLDefinitionParam] = []
427435
for tool in model_request_parameters.builtin_tools:
428436
if isinstance(tool, MCPServerTool):
429-
tool_configuration = (
430-
BetaRequestMCPServerToolConfigurationParam(
431-
enabled=True,
432-
allowed_tools=tool.allowed_tools,
433-
)
434-
if tool.allowed_tools
435-
else None
437+
tool_configuration = BetaRequestMCPServerToolConfigurationParam(
438+
enabled=True,
439+
allowed_tools=tool.allowed_tools,
436440
)
437441
mcp_servers.append(
438442
BetaRequestMCPServerURLDefinitionParam(
@@ -443,7 +447,8 @@ def _get_mcp_servers(
443447
tool_configuration=tool_configuration,
444448
)
445449
)
446-
return mcp_servers
450+
beta_features.append('mcp-client-2025-04-04')
451+
return mcp_servers, beta_features
447452

448453
async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[BetaMessageParam]]: # noqa: C901
449454
"""Just maps a `pydantic_ai.Message` to a `anthropic.types.MessageParam`."""
@@ -743,6 +748,16 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
743748
vendor_part_id=event.index,
744749
part=_map_code_execution_tool_result_block(current_block, self.provider_name),
745750
)
751+
elif isinstance(current_block, BetaMCPToolUseBlock):
752+
yield self._parts_manager.handle_part(
753+
vendor_part_id=event.index,
754+
part=_map_mcp_server_use_block(current_block, self.provider_name),
755+
)
756+
elif isinstance(current_block, BetaMCPToolResultBlock):
757+
yield self._parts_manager.handle_part(
758+
vendor_part_id=event.index,
759+
part=_map_mcp_server_result_block(current_block, self.provider_name),
760+
)
746761

747762
elif isinstance(event, BetaRawContentBlockDeltaEvent):
748763
if isinstance(event.delta, BetaTextDelta):
@@ -848,3 +863,21 @@ def _map_code_execution_tool_result_block(
848863
content=code_execution_tool_result_content_ta.dump_python(item.content, mode='json'),
849864
tool_call_id=item.tool_use_id,
850865
)
866+
867+
868+
def _map_mcp_server_use_block(item: BetaMCPToolUseBlock, provider_name: str) -> BuiltinToolCallPart:
869+
return BuiltinToolCallPart(
870+
provider_name=provider_name,
871+
tool_name=MCPServerTool.kind,
872+
args=cast(dict[str, Any], item.input) or None,
873+
tool_call_id=item.id,
874+
)
875+
876+
877+
def _map_mcp_server_result_block(item: BetaMCPToolResultBlock, provider_name: str) -> BuiltinToolReturnPart:
878+
return BuiltinToolReturnPart(
879+
provider_name=provider_name,
880+
tool_name=CodeExecutionTool.kind,
881+
content=item.content,
882+
tool_call_id=item.tool_use_id,
883+
)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
interactions:
2+
- request:
3+
headers:
4+
accept:
5+
- application/json
6+
accept-encoding:
7+
- gzip, deflate
8+
connection:
9+
- keep-alive
10+
content-length:
11+
- '406'
12+
content-type:
13+
- application/json
14+
host:
15+
- api.anthropic.com
16+
method: POST
17+
parsed_body:
18+
max_tokens: 4096
19+
mcp_servers:
20+
- authorization_token: ''
21+
name: test-server
22+
tool_configuration:
23+
allowed_tools:
24+
- get_my_games
25+
enabled: true
26+
type: url
27+
url: https://example.com/mcp
28+
messages:
29+
- content:
30+
- text: What games do I have?
31+
type: text
32+
role: user
33+
model: claude-sonnet-4-0
34+
stream: false
35+
thinking:
36+
budget_tokens: 3000
37+
type: enabled
38+
uri: https://api.anthropic.com/v1/messages?beta=true
39+
response:
40+
headers:
41+
connection:
42+
- keep-alive
43+
content-length:
44+
- '2260'
45+
content-type:
46+
- application/json
47+
strict-transport-security:
48+
- max-age=31536000; includeSubDomains; preload
49+
transfer-encoding:
50+
- chunked
51+
parsed_body:
52+
content:
53+
- signature: EuwDCkYICBgCKkCNx3nUT7+tZhbzb0XgVYmWnCsR6L6sNy1kFWTGK6zyRD4Q8mMkrHJDXlxBw8C0p9NdyjmxgdPSbGoK0RzdrnFcEgzkzU9fCjHfzcnpbH8aDLMRKiFFZQsGs+oA9SIwMeS/AQFnsCJYn0ktKRRJe1lgXQugxIcXy4wufUx7rnLFBlkGv2nFbJCQ6m8nPM9JKtMCpptItrWIURJ1H4OSKrkULa3/x6u223x592H0EV48cBtB4zRFvK8y6M3JvMH0TXwsGw5dGw0j4oFWJZVi6RDGCzM4A0B6FExufgdqulZEPXwzOmRdT9+6YwhP934ECVkXXrnjkEOQBYR6WoYLJafsnAt6IrFjdpRqQx3GyHxnlGn5Yikmnp3jvOX/rUwBq4qaGV3GYv7K6er4eM+7E5+wBDeLCloauX1Sng+4+hrW9voWGVW/Y27YHtQ13/abLViZDJLS+hSYQzQQMI7lMkV+Ht44e3Vh2UioUVFqwmFkl0N3fn0WO+p1aDoktlN2sLRkg6ybP1S95spT+XZI21KHuASCHZ0pZKUlml1tb4BVxDdGjGC31NrBczoV/TlpyWZCrzMeoOx81vAiUAKHwQD+JdY7Xyw/UCPOSu7OzEkywKHEwcSW5qgAZHVSAZFQu+iE6uG9GAE=
54+
thinking: The user is asking what games they have. I have a function available called "test-server_get_my_games" that
55+
returns a list of the user's favorite games. This seems like the appropriate function to call to answer their question.
56+
The function doesn't require any parameters according to the schema, so I can call it directly.
57+
type: thinking
58+
- text: I'll check what games you have by looking up your game list.
59+
type: text
60+
- id: mcptoolu_01TwwBu8ALMaffJS5riJmE8r
61+
input: {}
62+
name: get_my_games
63+
server_name: test-server
64+
type: mcp_tool_use
65+
- content:
66+
- text: '["Lies of P","Bloodborne","Sekiro"]'
67+
type: text
68+
is_error: false
69+
tool_use_id: mcptoolu_01TwwBu8ALMaffJS5riJmE8r
70+
type: mcp_tool_result
71+
- text: |-
72+
Based on your game library, you have these games:
73+
74+
1. **Lies of P** - A dark fantasy soulslike game inspired by Pinocchio
75+
2. **Bloodborne** - FromSoftware's gothic horror action RPG
76+
3. **Sekiro** - FromSoftware's ninja-themed action-adventure game
77+
78+
It looks like you have a great collection of challenging action games, particularly from the soulslike genre!
79+
type: text
80+
id: msg_01T1PQHnUHVDevzQSm91GmVP
81+
model: claude-sonnet-4-20250514
82+
role: assistant
83+
stop_reason: end_turn
84+
stop_sequence: null
85+
type: message
86+
usage:
87+
cache_creation:
88+
ephemeral_1h_input_tokens: 0
89+
ephemeral_5m_input_tokens: 0
90+
cache_creation_input_tokens: 0
91+
cache_read_input_tokens: 0
92+
input_tokens: 965
93+
output_tokens: 234
94+
server_tool_use:
95+
web_fetch_requests: 0
96+
web_search_requests: 0
97+
service_tier: standard
98+
status:
99+
code: 200
100+
message: OK
101+
version: 1
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
interactions:
2+
- request:
3+
headers:
4+
accept:
5+
- application/json
6+
accept-encoding:
7+
- gzip, deflate
8+
connection:
9+
- keep-alive
10+
content-length:
11+
- '393'
12+
content-type:
13+
- application/json
14+
host:
15+
- api.anthropic.com
16+
method: POST
17+
parsed_body:
18+
max_tokens: 4096
19+
mcp_servers:
20+
- authorization_token: ''
21+
name: test-server
22+
tool_configuration:
23+
allowed_tools: null
24+
enabled: true
25+
type: url
26+
url: https://example.com/mcp
27+
messages:
28+
- content:
29+
- text: What games do I have?
30+
type: text
31+
role: user
32+
model: claude-sonnet-4-5
33+
stream: true
34+
thinking:
35+
budget_tokens: 3000
36+
type: enabled
37+
uri: https://api.anthropic.com/v1/messages?beta=true
38+
response:
39+
body:
40+
string: |+
41+
event: message_start
42+
data: {"type":"message_start","message":{"model":"claude-sonnet-4-5-20250929","id":"msg_012dGSyGXMqFnc6yNfYPq9Qe","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":585,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":8,"service_tier":"standard"}} }
43+
44+
event: content_block_start
45+
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""} }
46+
47+
event: content_block_delta
48+
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"The user is asking about their games"} }
49+
50+
event: ping
51+
data: {"type": "ping"}
52+
53+
event: content_block_delta
54+
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":". I have a function called `test-server_"} }
55+
56+
event: content_block_delta
57+
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"get_my_games` that returns"} }
58+
59+
event: content_block_delta
60+
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" a list of the user's favorite games. This function doesn't require any parameters,"} }
61+
62+
event: content_block_delta
63+
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" so I can call it directly."} }
64+
65+
event: content_block_delta
66+
data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"Ev4CCkYICBgCKkC2CV8NXGK/AuQ4/m1/1qE91t9kn26YXfJqV4GLu+pQ5arslF66ul0lb2tjDaq6jEoScCgXONCbjVTTGXRgL31OEgz6Hf2+sxQphfOM0NsaDAGUIjrX8JtDLUHYCSIwDiCjhcg4JzBLobWmlquAlva+JlrB3yUlyWBCOe3z0GBxqs9MGtaEsmROBfy9w5bVKuUBPWfEKKcSIj57MWaQKquRHrl+EaaTVI2/dS3fU0opvd7HTbJ647RvWIKatcw1u3Q9WlNuGhts2TzwpqvzliIkheOjbv3URnfdW2vjka5WN6sntwoqdMrmmJTMD33PLXs2PZlnFUJ2BAS0lglihGbrOrIE1gRsTjZVrhXI8ZpJWPMYJ07FdtLwd+v0F3gf32z282QuQdNlaVliNjdcV+IvfJJL8BCF7coL+fKahuu8GCLVCjwmitJEIgC3ykzuVxCGT7U6hCuEM3Oe7UcLlay9n89O2jmW/FbQIuUpNmVFrrmM3Mn1GBgB"} }
67+
68+
event: content_block_stop
69+
data: {"type":"content_block_stop","index":0 }
70+
71+
event: content_block_start
72+
data: {"type":"content_block_start","index":1,"content_block":{"type":"mcp_tool_use","id":"mcptoolu_01WaYLgtwyroxT1CQWGFfTTy","name":"get_my_games","input":{},"server_name":"test-server"} }
73+
74+
event: content_block_delta
75+
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} }
76+
77+
event: content_block_stop
78+
data: {"type":"content_block_stop","index":1 }
79+
80+
event: content_block_start
81+
data: {"type":"content_block_start","index":2,"content_block":{"type":"mcp_tool_result","tool_use_id":"mcptoolu_01WaYLgtwyroxT1CQWGFfTTy","is_error":false,"content":[{"type":"text","text":"[\"Lies of P\",\"Bloodborne\",\"Sekiro\"]"}]} }
82+
83+
event: content_block_stop
84+
data: {"type":"content_block_stop","index":2 }
85+
86+
event: content_block_start
87+
data: {"type":"content_block_start","index":3,"content_block":{"type":"text","text":""} }
88+
89+
event: content_block_delta
90+
data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"You"} }
91+
92+
event: content_block_delta
93+
data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" have the following games:\n1. Lies"} }
94+
95+
event: content_block_delta
96+
data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" of P\n2. Bloodborne"} }
97+
98+
event: content_block_delta
99+
data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"\n3. Sekiro\n\nThese"} }
100+
101+
event: content_block_delta
102+
data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" are all excellent action games known for their challenging combat"} }
103+
104+
event: content_block_delta
105+
data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"! It looks like you have a strong preference"} }
106+
107+
event: content_block_delta
108+
data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" for souls-like and FromSoftware titles"} }
109+
110+
event: content_block_delta
111+
data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"."} }
112+
113+
event: content_block_stop
114+
data: {"type":"content_block_stop","index":3 }
115+
116+
event: message_delta
117+
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":1297,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":161,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0}} }
118+
119+
event: message_stop
120+
data: {"type":"message_stop" }
121+
122+
headers:
123+
cache-control:
124+
- no-cache
125+
connection:
126+
- keep-alive
127+
content-type:
128+
- text/event-stream; charset=utf-8
129+
strict-transport-security:
130+
- max-age=31536000; includeSubDomains; preload
131+
transfer-encoding:
132+
- chunked
133+
status:
134+
code: 200
135+
message: OK
136+
version: 1
137+
...

0 commit comments

Comments
 (0)