11from __future__ import annotations as _annotations
22
33from collections .abc import Iterable
4- from dataclasses import dataclass , field
5- from typing import Any , Literal , cast
4+ from dataclasses import asdict , dataclass , field
5+ from typing import Annotated , Any , Literal , cast
66
7- from pydantic import BaseModel
7+ from pydantic import AliasChoices , BaseModel , Field , TypeAdapter
88from typing_extensions import TypedDict , assert_never , override
99
1010from ..exceptions import ModelHTTPError , UnexpectedModelBehavior
@@ -246,95 +246,69 @@ class _BaseReasoningDetail(BaseModel, frozen=True):
246246 id : str | None = None
247247 format : Literal ['unknown' , 'openai-responses-v1' , 'anthropic-claude-v1' , 'xai-responses-v1' ] | None
248248 index : int | None
249- type : Literal ['reasoning.text' , 'reasoning.summary' , 'reasoning.encrypted' ]
250249
251250
252251class _ReasoningSummary (_BaseReasoningDetail , frozen = True ):
253252 """Represents a high-level summary of the reasoning process."""
254253
255254 type : Literal ['reasoning.summary' ]
256- summary : str
255+ summary : str = Field ( validation_alias = AliasChoices ( 'summary' , 'content' ))
257256
258257
259258class _ReasoningEncrypted (_BaseReasoningDetail , frozen = True ):
260259 """Represents encrypted reasoning data."""
261260
262261 type : Literal ['reasoning.encrypted' ]
263- data : str
262+ data : str = Field ( validation_alias = AliasChoices ( 'data' , 'signature' ))
264263
265264
266265class _ReasoningText (_BaseReasoningDetail , frozen = True ):
267266 """Represents raw text reasoning."""
268267
269268 type : Literal ['reasoning.text' ]
270- text : str
269+ text : str = Field ( validation_alias = AliasChoices ( 'text' , 'content' ))
271270 signature : str | None = None
272271
273272
274- _OpenRouterReasoningDetail = _ReasoningSummary | _ReasoningEncrypted | _ReasoningText
273+ _OpenRouterReasoningDetail = Annotated [
274+ _ReasoningSummary | _ReasoningEncrypted | _ReasoningText , Field (discriminator = 'type' )
275+ ]
276+ _openrouter_reasoning_detail_adapter : TypeAdapter [_OpenRouterReasoningDetail ] = TypeAdapter (_OpenRouterReasoningDetail )
275277
276278
277279def _from_reasoning_detail (reasoning : _OpenRouterReasoningDetail ) -> ThinkingPart :
278280 provider_name = 'openrouter'
279- reasoning_id = reasoning .model_dump_json (include = {'id' , 'format' , 'index' , 'type' })
281+ provider_details = reasoning .model_dump (include = {'format' , 'index' , 'type' })
280282 if isinstance (reasoning , _ReasoningText ):
281283 return ThinkingPart (
282- id = reasoning_id ,
284+ id = reasoning . id ,
283285 content = reasoning .text ,
284286 signature = reasoning .signature ,
285287 provider_name = provider_name ,
288+ provider_details = provider_details ,
286289 )
287290 elif isinstance (reasoning , _ReasoningSummary ):
288291 return ThinkingPart (
289- id = reasoning_id ,
290- content = reasoning .summary ,
291- provider_name = provider_name ,
292+ id = reasoning .id , content = reasoning .summary , provider_name = provider_name , provider_details = provider_details
292293 )
293294 elif isinstance (reasoning , _ReasoningEncrypted ):
294295 return ThinkingPart (
295- id = reasoning_id ,
296+ id = reasoning . id ,
296297 content = '' ,
297298 signature = reasoning .data ,
298299 provider_name = provider_name ,
300+ provider_details = provider_details ,
299301 )
300302 else :
301303 assert_never (reasoning )
302304
303305
304306def _into_reasoning_detail (thinking_part : ThinkingPart ) -> _OpenRouterReasoningDetail :
305- if thinking_part .id is None : # pragma: lax no cover
306- raise UnexpectedModelBehavior ('OpenRouter thinking part has no ID' )
307-
308- data = _BaseReasoningDetail .model_validate_json (thinking_part .id )
309-
310- if data .type == 'reasoning.text' :
311- return _ReasoningText (
312- type = data .type ,
313- id = data .id ,
314- format = data .format ,
315- index = data .index ,
316- text = thinking_part .content ,
317- signature = thinking_part .signature ,
318- )
319- elif data .type == 'reasoning.summary' :
320- return _ReasoningSummary (
321- type = data .type ,
322- id = data .id ,
323- format = data .format ,
324- index = data .index ,
325- summary = thinking_part .content ,
326- )
327- elif data .type == 'reasoning.encrypted' :
328- assert thinking_part .signature is not None
329- return _ReasoningEncrypted (
330- type = data .type ,
331- id = data .id ,
332- format = data .format ,
333- index = data .index ,
334- data = thinking_part .signature ,
335- )
336- else :
337- assert_never (data .type )
307+ if thinking_part .provider_details is None : # pragma: lax no cover
308+ raise UnexpectedModelBehavior ('OpenRouter thinking part has no provider_details' )
309+ thinking_part_dict = asdict (thinking_part )
310+ thinking_part_dict .update (thinking_part_dict .pop ('provider_details' ))
311+ return _openrouter_reasoning_detail_adapter .validate_python (thinking_part_dict )
338312
339313
340314class _OpenRouterCompletionMessage (chat .ChatCompletionMessage ):
@@ -363,7 +337,8 @@ class _OpenRouterChoice(chat_completion.Choice):
363337 """A wrapped chat completion message with OpenRouter specific attributes."""
364338
365339
366- class _OpenRouterCostDetails (BaseModel ):
340+ @dataclass
341+ class _OpenRouterCostDetails :
367342 """OpenRouter specific cost details."""
368343
369344 upstream_inference_cost : int | None = None
@@ -417,7 +392,7 @@ def _map_openrouter_provider_details(
417392 provider_details : dict [str , Any ] = {}
418393
419394 provider_details ['downstream_provider' ] = response .provider
420- provider_details ['native_finish_reason ' ] = response .choices [0 ].native_finish_reason
395+ provider_details ['finish_reason ' ] = response .choices [0 ].native_finish_reason
421396
422397 if usage := response .usage :
423398 if cost := usage .cost :
@@ -517,7 +492,7 @@ def _process_provider_details(self, response: chat.ChatCompletion) -> dict[str,
517492 return provider_details
518493
519494 @dataclass
520- class _MapModelResposeContext (OpenAIChatModel ._MapModelResposeContext ): # type: ignore[reportPrivateUsage]
495+ class _MapModelResponseContext (OpenAIChatModel ._MapModelResponseContext ): # type: ignore[reportPrivateUsage]
521496 reasoning_details : list [dict [str , Any ]] = field (default_factory = list )
522497
523498 def into_message_param (self ) -> chat .ChatCompletionAssistantMessageParam :
@@ -527,8 +502,8 @@ def into_message_param(self) -> chat.ChatCompletionAssistantMessageParam:
527502 return message_param
528503
529504 @override
530- def _map_response_thinking_part (self , ctx : OpenAIChatModel ._MapModelResposeContext , item : ThinkingPart ) -> None :
531- assert isinstance (ctx , self ._MapModelResposeContext )
505+ def _map_response_thinking_part (self , ctx : OpenAIChatModel ._MapModelResponseContext , item : ThinkingPart ) -> None :
506+ assert isinstance (ctx , self ._MapModelResponseContext )
532507 if item .provider_name == self .system :
533508 ctx .reasoning_details .append (_into_reasoning_detail (item ).model_dump ())
534509 elif content := item .content : # pragma: lax no cover
0 commit comments