Skip to content

Commit 6066bd9

Browse files
authored
Merge branch 'main' into dmontagu/fix-evals-token-double-counting
2 parents 07978f4 + 1748efd commit 6066bd9

File tree

6 files changed

+322
-24
lines changed

6 files changed

+322
-24
lines changed

pydantic_ai_slim/pydantic_ai/mcp.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,15 +1040,16 @@ async def client_streams(
10401040
sse_read_timeout=self.read_timeout,
10411041
)
10421042

1043-
if self.http_client is not None: # pragma: no cover
1044-
1045-
def httpx_client_factory(
1043+
if self.http_client is not None:
1044+
# TODO: Clean up once https://github.com/modelcontextprotocol/python-sdk/pull/1177 lands.
1045+
@asynccontextmanager
1046+
async def httpx_client_factory(
10461047
headers: dict[str, str] | None = None,
10471048
timeout: httpx.Timeout | None = None,
10481049
auth: httpx.Auth | None = None,
1049-
) -> httpx.AsyncClient:
1050+
) -> AsyncIterator[httpx.AsyncClient]:
10501051
assert self.http_client is not None
1051-
return self.http_client
1052+
yield self.http_client
10521053

10531054
async with transport_client_partial(httpx_client_factory=httpx_client_factory) as (
10541055
read_stream,

pydantic_ai_slim/pydantic_ai/models/openai.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ async def _completions_create(
518518
extra_headers = model_settings.get('extra_headers', {})
519519
extra_headers.setdefault('User-Agent', get_user_agent())
520520
return await self.client.chat.completions.create(
521-
model=self._model_name,
521+
model=self.model_name,
522522
messages=openai_messages,
523523
parallel_tool_calls=model_settings.get('parallel_tool_calls', OMIT),
524524
tools=tools or OMIT,

pydantic_ai_slim/pydantic_ai/models/openrouter.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,11 @@ class _BaseReasoningDetail(BaseModel, frozen=True):
244244
"""Common fields shared across all reasoning detail types."""
245245

246246
id: str | None = None
247-
format: Literal['unknown', 'openai-responses-v1', 'anthropic-claude-v1', 'xai-responses-v1'] | None
247+
format: (
248+
Literal['unknown', 'openai-responses-v1', 'anthropic-claude-v1', 'xai-responses-v1', 'google-gemini-v1']
249+
| str
250+
| None
251+
)
248252
index: int | None
249253
type: Literal['reasoning.text', 'reasoning.summary', 'reasoning.encrypted']
250254

@@ -610,13 +614,21 @@ def _map_thinking_delta(self, choice: chat_completion_chunk.Choice) -> Iterable[
610614
assert isinstance(choice, _OpenRouterChunkChoice)
611615

612616
if reasoning_details := choice.delta.reasoning_details:
613-
for detail in reasoning_details:
617+
for i, detail in enumerate(reasoning_details):
614618
thinking_part = _from_reasoning_detail(detail)
619+
# Use unique vendor_part_id for each reasoning detail type to prevent
620+
# different detail types (e.g., reasoning.text, reasoning.encrypted)
621+
# from being incorrectly merged into a single ThinkingPart.
622+
# This is required for Gemini 3 Pro which returns multiple reasoning
623+
# detail types that must be preserved separately for thought_signature handling.
624+
vendor_id = f'reasoning_detail_{detail.type}_{i}'
615625
yield self._parts_manager.handle_thinking_delta(
616-
vendor_part_id='reasoning_detail',
626+
vendor_part_id=vendor_id,
617627
id=thinking_part.id,
618628
content=thinking_part.content,
629+
signature=thinking_part.signature,
619630
provider_name=self._provider_name,
631+
provider_details=thinking_part.provider_details,
620632
)
621633
else:
622634
return super()._map_thinking_delta(choice)
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
interactions:
2+
- request:
3+
headers:
4+
accept:
5+
- application/json, text/event-stream
6+
accept-encoding:
7+
- gzip, deflate
8+
connection:
9+
- keep-alive
10+
content-length:
11+
- '165'
12+
content-type:
13+
- application/json
14+
host:
15+
- mcp.deepwiki.com
16+
method: POST
17+
parsed_body:
18+
id: 0
19+
jsonrpc: '2.0'
20+
method: initialize
21+
params:
22+
capabilities:
23+
sampling: {}
24+
clientInfo:
25+
name: mcp
26+
version: 0.1.0
27+
protocolVersion: '2025-06-18'
28+
uri: https://mcp.deepwiki.com/mcp
29+
response:
30+
body:
31+
string: |+
32+
event: message
33+
data: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"DeepWiki","version":"0.0.1"}}}
34+
35+
event: ping
36+
data: ping
37+
38+
headers:
39+
access-control-allow-headers:
40+
- Content-Type, mcp-session-id, mcp-protocol-version
41+
access-control-allow-methods:
42+
- GET, POST, OPTIONS
43+
access-control-allow-origin:
44+
- '*'
45+
access-control-expose-headers:
46+
- mcp-session-id
47+
access-control-max-age:
48+
- '86400'
49+
alt-svc:
50+
- h3=":443"; ma=86400
51+
cache-control:
52+
- no-cache
53+
connection:
54+
- keep-alive
55+
content-type:
56+
- text/event-stream
57+
mcp-session-id:
58+
- 79634f20860e529d0253e303afbce2571a0543c155a087250648960da3c67059
59+
nel:
60+
- '{"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}'
61+
report-to:
62+
- '{"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=rMn85WgDL0%2B8DcP3GhSk08gOMLOCdd9ylzYlWV%2BPazzXbOlw7hBD5mlea3Yzxs%2BdtHeX7t1OSADQzo5YxQq55wD0eI0FTdoN0UBf2SM%2F7zsjJieuzN%2FodKifnGLW%2FgU5j%2F%2F%2FHA%3D%3D"}]}'
63+
transfer-encoding:
64+
- chunked
65+
vary:
66+
- accept-encoding
67+
status:
68+
code: 200
69+
message: OK
70+
- request:
71+
body: ''
72+
headers:
73+
accept:
74+
- application/json, text/event-stream, text/event-stream
75+
accept-encoding:
76+
- gzip, deflate
77+
cache-control:
78+
- no-store
79+
connection:
80+
- keep-alive
81+
content-type:
82+
- application/json
83+
host:
84+
- mcp.deepwiki.com
85+
mcp-protocol-version:
86+
- '2025-03-26'
87+
mcp-session-id:
88+
- 79634f20860e529d0253e303afbce2571a0543c155a087250648960da3c67059
89+
method: GET
90+
uri: https://mcp.deepwiki.com/mcp
91+
response:
92+
body:
93+
string: '{"jsonrpc":"2.0","error":{"code":-32000,"message":"Method not allowed"},"id":null}'
94+
headers:
95+
alt-svc:
96+
- h3=":443"; ma=86400
97+
connection:
98+
- keep-alive
99+
content-length:
100+
- '82'
101+
content-type:
102+
- text/plain;charset=UTF-8
103+
nel:
104+
- '{"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}'
105+
report-to:
106+
- '{"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=PJNTRlKrI8lL0aiZCc1uJTiDVehFnTLMMt8VdPxyqnbI%2F0JHNcJ7zP3QWIemrZXXzvU8Vd%2Bei6J8F5UuykGqFqdTH%2BXrjvH5bM9Is8%2F%2BWmpiJxekG6tEGDnwy9qFv8xegxo8jQ%3D%3D"}]}'
107+
vary:
108+
- accept-encoding
109+
status:
110+
code: 405
111+
message: Method Not Allowed
112+
- request:
113+
headers:
114+
accept:
115+
- application/json, text/event-stream
116+
accept-encoding:
117+
- gzip, deflate
118+
connection:
119+
- keep-alive
120+
content-length:
121+
- '54'
122+
content-type:
123+
- application/json
124+
host:
125+
- mcp.deepwiki.com
126+
mcp-protocol-version:
127+
- '2025-03-26'
128+
mcp-session-id:
129+
- 79634f20860e529d0253e303afbce2571a0543c155a087250648960da3c67059
130+
method: POST
131+
parsed_body:
132+
jsonrpc: '2.0'
133+
method: notifications/initialized
134+
uri: https://mcp.deepwiki.com/mcp
135+
response:
136+
body:
137+
string: ''
138+
headers:
139+
access-control-allow-headers:
140+
- Content-Type, mcp-session-id, mcp-protocol-version
141+
access-control-allow-methods:
142+
- GET, POST, OPTIONS
143+
access-control-allow-origin:
144+
- '*'
145+
access-control-expose-headers:
146+
- mcp-session-id
147+
access-control-max-age:
148+
- '86400'
149+
alt-svc:
150+
- h3=":443"; ma=86400
151+
connection:
152+
- keep-alive
153+
content-length:
154+
- '0'
155+
nel:
156+
- '{"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}'
157+
report-to:
158+
- '{"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=YOAoKhRcD%2FYSYhguoqnWMHg%2FDamN%2BTbORh3ouJR1UoKYxtW%2BOW%2BAHCtlYMucysWlEeSIhTC5vm5N999APwF5Rwu1Z8liG5LqCo4ixNr69BVK1ssPS4i24x%2FyVLYXc0HTo9dkIg%3D%3D"}]}'
159+
vary:
160+
- accept-encoding
161+
status:
162+
code: 202
163+
message: Accepted
164+
- request:
165+
headers:
166+
accept:
167+
- application/json, text/event-stream
168+
accept-encoding:
169+
- gzip, deflate
170+
connection:
171+
- keep-alive
172+
content-length:
173+
- '46'
174+
content-type:
175+
- application/json
176+
host:
177+
- mcp.deepwiki.com
178+
mcp-protocol-version:
179+
- '2025-03-26'
180+
mcp-session-id:
181+
- 79634f20860e529d0253e303afbce2571a0543c155a087250648960da3c67059
182+
method: POST
183+
parsed_body:
184+
id: 1
185+
jsonrpc: '2.0'
186+
method: tools/list
187+
uri: https://mcp.deepwiki.com/mcp
188+
response:
189+
body:
190+
string: |+
191+
event: message
192+
data: {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"read_wiki_structure","description":"Get a list of documentation topics for a GitHub repository","inputSchema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"}},"required":["repoName"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"read_wiki_contents","description":"View documentation about a GitHub repository","inputSchema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"}},"required":["repoName"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"ask_question","description":"Ask any question about a GitHub repository","inputSchema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"},"question":{"type":"string","description":"The question to ask about the repository"}},"required":["repoName","question"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}]}}
193+
194+
event: ping
195+
data: ping
196+
197+
headers:
198+
access-control-allow-headers:
199+
- Content-Type, mcp-session-id, mcp-protocol-version
200+
access-control-allow-methods:
201+
- GET, POST, OPTIONS
202+
access-control-allow-origin:
203+
- '*'
204+
access-control-expose-headers:
205+
- mcp-session-id
206+
access-control-max-age:
207+
- '86400'
208+
alt-svc:
209+
- h3=":443"; ma=86400
210+
cache-control:
211+
- no-cache
212+
connection:
213+
- keep-alive
214+
content-type:
215+
- text/event-stream
216+
mcp-session-id:
217+
- 79634f20860e529d0253e303afbce2571a0543c155a087250648960da3c67059
218+
nel:
219+
- '{"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}'
220+
report-to:
221+
- '{"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=rTAkw2F56hOqBG74w2PLRaPXDJf5XzFASzgxvvmTIrJZmebUbzATwXgvs3W6GMYCXnwMdcOUJcN05oKIJ6hq8XoPuu%2F97yD66drIfQOhQjT0HG2W4Kd1btBaRf0stfwOdus64w%3D%3D"}]}'
222+
transfer-encoding:
223+
- chunked
224+
vary:
225+
- accept-encoding
226+
status:
227+
code: 200
228+
message: OK
229+
- request:
230+
body: ''
231+
headers:
232+
accept:
233+
- application/json, text/event-stream
234+
accept-encoding:
235+
- gzip, deflate
236+
connection:
237+
- keep-alive
238+
content-type:
239+
- application/json
240+
host:
241+
- mcp.deepwiki.com
242+
mcp-protocol-version:
243+
- '2025-03-26'
244+
mcp-session-id:
245+
- 79634f20860e529d0253e303afbce2571a0543c155a087250648960da3c67059
246+
method: DELETE
247+
uri: https://mcp.deepwiki.com/mcp
248+
response:
249+
body:
250+
string: '{"jsonrpc":"2.0","error":{"code":-32000,"message":"Method not allowed"},"id":null}'
251+
headers:
252+
alt-svc:
253+
- h3=":443"; ma=86400
254+
connection:
255+
- keep-alive
256+
content-length:
257+
- '82'
258+
content-type:
259+
- text/plain;charset=UTF-8
260+
nel:
261+
- '{"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}'
262+
report-to:
263+
- '{"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=0AMi1Wp%2BN%2FfpGPKL%2BRDf5fdSr0BLZQRV0fR7plR%2FnrWUjtsC6slmDj71mtsoXeq%2B3YtYdqo0zDvuI59BSy9HcqeWAIau1X6Ng3C5K%2BzFGL1hIr4135Ivh%2BaiFo17vSsis7rEDQ%3D%3D"}]}'
264+
vary:
265+
- accept-encoding
266+
status:
267+
code: 405
268+
message: Method Not Allowed
269+
version: 1
270+
...

tests/models/test_openrouter.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -112,23 +112,23 @@ async def test_openrouter_stream_with_reasoning(allow_model_requests: None, open
112112

113113
thinking_event_start = chunks[0]
114114
assert isinstance(thinking_event_start, PartStartEvent)
115-
assert thinking_event_start.part == snapshot(
116-
ThinkingPart(
117-
content='',
118-
id='rs_0aa4f2c435e6d1dc0169082486816c8193a029b5fc4ef1764f',
119-
provider_name='openrouter',
120-
)
121-
)
115+
thinking_part = thinking_event_start.part
116+
assert isinstance(thinking_part, ThinkingPart)
117+
assert thinking_part.id == 'rs_0aa4f2c435e6d1dc0169082486816c8193a029b5fc4ef1764f'
118+
assert thinking_part.content == ''
119+
assert thinking_part.provider_name == 'openrouter'
120+
# After fix: signature and provider_details are now properly preserved
121+
assert thinking_part.signature is not None
122+
assert thinking_part.provider_details is not None
123+
assert thinking_part.provider_details['type'] == 'reasoning.encrypted'
124+
assert thinking_part.provider_details['format'] == 'openai-responses-v1'
122125

123126
thinking_event_end = chunks[1]
124127
assert isinstance(thinking_event_end, PartEndEvent)
125-
assert thinking_event_end.part == snapshot(
126-
ThinkingPart(
127-
content='',
128-
id='rs_0aa4f2c435e6d1dc0169082486816c8193a029b5fc4ef1764f',
129-
provider_name='openrouter',
130-
)
131-
)
128+
thinking_part_end = thinking_event_end.part
129+
assert isinstance(thinking_part_end, ThinkingPart)
130+
assert thinking_part_end.id == 'rs_0aa4f2c435e6d1dc0169082486816c8193a029b5fc4ef1764f'
131+
assert thinking_part_end.signature is not None
132132

133133

134134
async def test_openrouter_stream_error(allow_model_requests: None, openrouter_api_key: str) -> None:

0 commit comments

Comments
 (0)