-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Add OpenRouterModel as OpenAIChatModel subclass #3089
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ajac-zero Muchas gracias Anibal!
|
Buen día @DouweM, can you take a look when you get the chance? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gracias!
It'd be interesting to add support for the WebSearchTool built-in tool as well, shouldn't be too complicated I think: https://openrouter.ai/docs/features/web-search
|
|
||
| if signature := reasoning_details[0].get('signature', None): | ||
| thinking_part = cast(ThinkingPart, model_response.parts[0]) | ||
| thinking_part.signature = signature |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should definitely have an instance check here
| for message, openai_message in zip(messages, openai_messages): | ||
| if isinstance(message, ModelResponse): | ||
| provider_details = cast(dict[str, Any], message.provider_details) | ||
| if reasoning_details := provider_details.get('reasoning_details', None): # pragma: lax no cover |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we build this from the ThinkingParts? I don't want to store the entire reasoning_details verbatim on the ModelResponse, if we can parse it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could do that, but I'm not use how to handle the encrypted variant. Has something similar been done before for redacted reasoning tokens?
Here is what an encrypted reasoning_details looks like:
{
"type": "reasoning.encrypted",
"data": "eyJlbmNyeXB0ZWQiOiJ0cnVlIiwiY29udGVudCI6IltSRURBQ1RFRF0ifQ==",
"id": "reasoning-encrypted-1",
"format": "anthropic-claude-v1",
"index": 1
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, in that case, we should store empty text and put the encrypted data in signature. See here for an example:
pydantic-ai/pydantic_ai_slim/pydantic_ai/models/bedrock.py
Lines 515 to 524 in c5b1495
| elif isinstance(item, ThinkingPart): | |
| if ( | |
| item.provider_name == self.system | |
| and item.signature | |
| and BedrockModelProfile.from_profile(self.profile).bedrock_send_back_thinking_parts | |
| ): | |
| if item.id == 'redacted_content': | |
| reasoning_content: ReasoningContentBlockOutputTypeDef = { | |
| 'redactedContent': item.signature.encode('utf-8'), | |
| } |
Note that we should only send signatures or encrypted thoughts back if the provider name matches (as you can see there).
|
|
||
| model_response = super()._process_response(response=response) | ||
|
|
||
| provider_details: dict[str, Any] = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we respect the existing model_response.provider_details?
| provider_details['native_finish_reason'] = choice.native_finish_reason | ||
|
|
||
| if reasoning_details := choice.message.reasoning_details: | ||
| provider_details['reasoning_details'] = [detail.model_dump() for detail in reasoning_details] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reminder to remove this
|
|
||
| reasoning = reasoning_details[0] | ||
|
|
||
| assert isinstance(model_response.parts, list) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather built a new parts list like model_response.parts = [*new_parts, *model_response.parts]
| provider_name=native_response.provider, | ||
| ), | ||
| ) | ||
| elif isinstance(reasoning, ReasoningSummary): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should support ReasoningEncrypted as well, see here for an example:
pydantic-ai/pydantic_ai_slim/pydantic_ai/models/bedrock.py
Lines 303 to 311 in c5b1495
| if redacted_content := reasoning_content.get('redactedContent'): | |
| items.append( | |
| ThinkingPart( | |
| id='redacted_content', | |
| content='', | |
| signature=redacted_content.decode('utf-8'), | |
| provider_name=self.system, | |
| ) | |
| ) |
| for message, openai_message in zip(messages, openai_messages): | ||
| if isinstance(message, ModelResponse): | ||
| provider_details = cast(dict[str, Any], message.provider_details) | ||
| if reasoning_details := provider_details.get('reasoning_details', None): # pragma: lax no cover |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, in that case, we should store empty text and put the encrypted data in signature. See here for an example:
pydantic-ai/pydantic_ai_slim/pydantic_ai/models/bedrock.py
Lines 515 to 524 in c5b1495
| elif isinstance(item, ThinkingPart): | |
| if ( | |
| item.provider_name == self.system | |
| and item.signature | |
| and BedrockModelProfile.from_profile(self.profile).bedrock_send_back_thinking_parts | |
| ): | |
| if item.id == 'redacted_content': | |
| reasoning_content: ReasoningContentBlockOutputTypeDef = { | |
| 'redactedContent': item.signature.encode('utf-8'), | |
| } |
Note that we should only send signatures or encrypted thoughts back if the provider name matches (as you can see there).
|
@ajac-zero We can also remove this comment from openai.py: pydantic-ai/pydantic_ai_slim/pydantic_ai/models/openai.py Lines 558 to 560 in c5b1495
|
Co-authored-by: Douwe Maan <me@douwe.me>
Co-authored-by: Douwe Maan <me@douwe.me>
|
Hi, just found this useful pr and I think top_k and other missing model config should be added to align with the Request Schema documented here https://openrouter.ai/docs/api-reference/overview. |
Hi! This pull request takes a shot at implementing a dedicated
OpenRouterModelmodel. Closes #2936.The differentiator for this PR is that this implementation minimizes code duplication as much as possible by delegating the main logic to
OpenAIChatModel, such that the new model class serves as a convenience layer for OpenRouter specific features.The main thinking behind this solution is that as long as the OpenRouter API is still fully accessible via the
openaipackage, it would be inefficient to reimplement the internal logic using this same package again. We can instead use hooks to achieve the requested features.I would like to get some thoughts on this implementation before starting to update the docs.
Addressed issues
Provider metadata can now be accessed via the 'downstream_provider' key in ModelMessage.provider_details:
The new
OpenRouterModelSettingsallows for the reasoning parameter by OpenRouter, the thinking can then be accessed as aThinkingPartin the model response:errorresponse from OpenRouter as exception instead of validation failure #2323. Closes OpenRouter uses non-compatible finish reason #2844These are dependent on some downstream logic from OpenRouter or their own downstream providers (that a response of type 'error' will have a >= 400 status code), but for most cases I would say it works as one would expect:
OpenRouterModel#1870 (comment)Add some additional type support to set the provider routing options from OpenRouter: