Skip to content

Commit c09a104

Browse files
zeyuyuyuzeyuDouweM
authored
Add Gemini 3 Pro support to OpenRouterModel (#3548)
Co-authored-by: zeyu <zeyu@ip-172-19-0-1.ap-southeast-1.compute.internal> Co-authored-by: Douwe Maan <douwe@pydantic.dev>
1 parent de0aaa8 commit c09a104

File tree

2 files changed

+29
-17
lines changed

2 files changed

+29
-17
lines changed

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)

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)