33
44from openai import AsyncStream
55from openai .types import chat
6- from openai .types .chat . chat_completion import Choice
6+ from openai .types .chat import chat_completion , chat_completion_chunk
77from pydantic import AliasChoices , BaseModel , Field , TypeAdapter
88from typing_extensions import TypedDict , assert_never
99
@@ -346,7 +346,7 @@ class OpenRouterCompletionMessage(chat.ChatCompletionMessage):
346346 """The reasoning details associated with the message, if any."""
347347
348348
349- class OpenRouterChoice (Choice ):
349+ class OpenRouterChoice (chat_completion . Choice ):
350350 """Wraps OpenAI chat completion choice with OpenRouter specific attributes."""
351351
352352 native_finish_reason : str
@@ -375,14 +375,40 @@ class OpenRouterChatCompletion(chat.ChatCompletion):
375375 """OpenRouter specific error attribute."""
376376
377377
378+ class OpenRouterChoiceDelta (chat_completion_chunk .ChoiceDelta ):
379+ """Wrapped chat completion message with OpenRouter specific attributes."""
380+
381+ reasoning : str | None = None
382+ """The reasoning text associated with the message, if any."""
383+
384+ reasoning_details : list [OpenRouterReasoningDetail ] | None = None
385+ """The reasoning details associated with the message, if any."""
386+
387+
388+ class OpenRouterChunkChoice (chat_completion_chunk .Choice ):
389+ """Wraps OpenAI chat completion chunk choice with OpenRouter specific attributes."""
390+
391+ native_finish_reason : str | None
392+ """The provided finish reason by the downstream provider from OpenRouter."""
393+
394+ finish_reason : Literal ['stop' , 'length' , 'tool_calls' , 'content_filter' , 'error' ] | None # type: ignore[reportIncompatibleVariableOverride]
395+ """OpenRouter specific finish reasons for streaming chunks.
396+
397+ Notably, removes 'function_call' and adds 'error' finish reasons.
398+ """
399+
400+ delta : OpenRouterChoiceDelta # type: ignore[reportIncompatibleVariableOverride]
401+ """A wrapped chat completion delta with OpenRouter specific attributes."""
402+
403+
378404class OpenRouterChatCompletionChunk (chat .ChatCompletionChunk ):
379405 """Wraps OpenAI chat completion with OpenRouter specific attributes."""
380406
381407 provider : str
382408 """The downstream provider that was used by OpenRouter."""
383409
384- choices : list [OpenRouterChoice ] # type: ignore[reportIncompatibleVariableOverride]
385- """A list of chat completion choices modified with OpenRouter specific attributes."""
410+ choices : list [OpenRouterChunkChoice ] # type: ignore[reportIncompatibleVariableOverride]
411+ """A list of chat completion chunk choices modified with OpenRouter specific attributes."""
386412
387413 error : OpenRouterError | None = None
388414 """OpenRouter specific error attribute."""
@@ -428,6 +454,48 @@ class OpenRouterStreamedResponse(OpenAIStreamedResponse):
428454 def _map_usage (self , response : chat .ChatCompletionChunk ):
429455 return _map_usage (response , self ._provider_name , self ._provider_url , self ._model_name )
430456
457+ @override
458+ def _map_finish_reason ( # type: ignore[reportIncompatibleMethodOverride]
459+ self , key : Literal ['stop' , 'length' , 'tool_calls' , 'content_filter' , 'error' ]
460+ ) -> FinishReason | None :
461+ return _CHAT_FINISH_REASON_MAP .get (key )
462+
463+ @override
464+ def _handle_thinking_delta (self , chunk : OpenRouterChatCompletionChunk ): # type: ignore[reportIncompatibleMethodOverride]
465+ delta = chunk .choices [0 ].delta
466+ if reasoning_details := delta .reasoning_details :
467+ for detail in reasoning_details :
468+ thinking_part = OpenRouterThinkingPart .from_reasoning_detail (detail )
469+ yield self ._parts_manager .handle_thinking_delta (
470+ vendor_part_id = 'reasoning_detail' ,
471+ id = thinking_part .id ,
472+ content = thinking_part .content ,
473+ provider_name = self ._provider_name ,
474+ )
475+
476+ @override
477+ def _handle_provider_details (self , chunk : chat .ChatCompletionChunk ) -> dict [str , str ] | None :
478+ native_chunk = OpenRouterChatCompletionChunk .model_validate (chunk .model_dump ())
479+
480+ if provider_details := super ()._handle_provider_details (chunk ):
481+ if provider := native_chunk .provider :
482+ provider_details ['downstream_provider' ] = provider
483+
484+ if native_finish_reason := native_chunk .choices [0 ].native_finish_reason :
485+ provider_details ['native_finish_reason' ] = native_finish_reason
486+
487+ return provider_details
488+
489+ @override
490+ async def _validate_response (self ):
491+ async for chunk in self ._response :
492+ chunk = OpenRouterChatCompletionChunk .model_validate (chunk .model_dump ())
493+
494+ if error := chunk .error :
495+ raise ModelHTTPError (status_code = error .code , model_name = chunk .model , body = error .message )
496+
497+ yield chunk
498+
431499
432500def _openrouter_settings_to_openai_settings (model_settings : OpenRouterModelSettings ) -> OpenAIChatModelSettings :
433501 """Transforms a 'OpenRouterModelSettings' object into an 'OpenAIChatModelSettings' object.
@@ -475,6 +543,7 @@ def __init__(
475543 """
476544 super ().__init__ (model_name , provider = provider or OpenRouterProvider (), profile = profile , settings = settings )
477545
546+ @override
478547 def prepare_request (
479548 self ,
480549 model_settings : ModelSettings | None ,
@@ -485,13 +554,13 @@ def prepare_request(
485554 return new_settings , customized_parameters
486555
487556 @override
488- def _map_finish_reason (
557+ def _map_finish_reason ( # type: ignore[reportIncompatibleMethodOverride]
489558 self , key : Literal ['stop' , 'length' , 'tool_calls' , 'content_filter' , 'error' ]
490- ) -> FinishReason | None : # type: ignore[reportIncompatibleMethodOverride]
559+ ) -> FinishReason | None :
491560 return _CHAT_FINISH_REASON_MAP .get (key )
492561
493562 @override
494- def _process_reasoning (self , response : OpenRouterChatCompletion ) -> list [ThinkingPart ]:
563+ def _process_reasoning (self , response : OpenRouterChatCompletion ) -> list [ThinkingPart ]: # type: ignore[reportIncompatibleMethodOverride]
495564 message = response .choices [0 ].message
496565 items : list [ThinkingPart ] = []
497566
@@ -502,10 +571,7 @@ def _process_reasoning(self, response: OpenRouterChatCompletion) -> list[Thinkin
502571 return items
503572
504573 @override
505- def _process_provider_details (self , response : OpenRouterChatCompletion ) -> dict [str , Any ]:
506- if error := response .error :
507- raise ModelHTTPError (status_code = error .code , model_name = response .model , body = error .message )
508-
574+ def _process_provider_details (self , response : OpenRouterChatCompletion ) -> dict [str , Any ]: # type: ignore[reportIncompatibleMethodOverride]
509575 provider_details = super ()._process_provider_details (response )
510576
511577 provider_details ['downstream_provider' ] = response .provider
@@ -515,8 +581,14 @@ def _process_provider_details(self, response: OpenRouterChatCompletion) -> dict[
515581
516582 @override
517583 def _validate_completion (self , response : chat .ChatCompletion ) -> chat .ChatCompletion :
518- return OpenRouterChatCompletion .model_validate (response .model_dump ())
584+ response = OpenRouterChatCompletion .model_validate (response .model_dump ())
519585
586+ if error := response .error :
587+ raise ModelHTTPError (status_code = error .code , model_name = response .model , body = error .message )
588+
589+ return response
590+
591+ @override
520592 async def _process_streamed_response (
521593 self , response : AsyncStream [chat .ChatCompletionChunk ], model_request_parameters : ModelRequestParameters
522594 ) -> OpenRouterStreamedResponse :
@@ -538,6 +610,7 @@ async def _process_streamed_response(
538610 _provider_url = self ._provider .base_url ,
539611 )
540612
613+ @override
541614 def _map_model_response (self , message : ModelResponse ) -> chat .ChatCompletionMessageParam :
542615 texts : list [str ] = []
543616 tool_calls : list [chat .ChatCompletionMessageFunctionToolCallParam ] = []
0 commit comments