8000 Add `OpenRouterModel` by DanKing1903 · Pull Request #1870 · pydantic/pydantic-ai · GitHub
[go: up one dir, main page]

Skip to content

Add OpenRouterModel #1870

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

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

DanKing1903
Copy link
@DanKing1903 DanKing1903 commented May 30, 2025

This PR implements an OpenRouterModel class as discussed in #1849. The OpenRouterModel is an extension of OpenAIModel that captures the provider metadata returned by OpenRouter in the vendor_details dictionary.

Additionally adds error handling for the scenario when OpenRouter API returns a 200 OK, but the response relays an error from the upstream LLM provider as described in #1746 and #527

Changes

  • Added OpenRouterModel that extends OpenAIModel to handle OpenRouter-specific response fields and error handling
  • Added tests for error handling and provider details in OpenRouter responses

Linked Issues

@DanKing1903 DanKing1903 force-pushed the feature/add-openrouter-model branch from 6f299fb to 1341160 Compare May 30, 2025 05:59
Copy link
Contributor
@DouweM DouweM left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DanKing1903 Thanks Dan! In addition to the comments I left, we'll also need to update the docs. Can you please have a look at that? Note that this may not make sense anymore to do this under the "OpenAI compatible" section as it's now its own model, not just a provider to use with OpenAIModel.

error: OpenRouterErrorResponse | None


class OpenRouterModel(OpenAIModel):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please override __init__ to automatically use the OpenRouterProvider as well when this model is used?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DouweM I have added an __init__ method with narrowed type annotations for provider that does nothing apart from calling super().__init__. Is this acceptable or would you like me to add something like:

        if isinstance(provider, str):
            if provider != "openrouter":
                error_msg = ...
                raise ValueError(error_msg)
            provider = OpenRouterProvider()

"""Extends OpenAIModel to capture extra metadata for Openrouter."""

def _process_response(self, response: chat.ChatCompletion) -> ModelResponse:
response = cast(OpenRouterChatCompletion, response)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this does what we want, because response won't actually be an OpenRouterChatCompletion. Would response = OpenRouterChatCompletion.modal_validate(response) work to actually create the new object? Then we should be able to read response.error without checking hasattr.

Copy link
Author
@DanKing1903 DanKing1903 Jun 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback. While testing your suggestion I ran into some issues. The problem is that when OpenRouter is relaying upstream errors, the response is not actually a valid ChatCompletion. The signature of the response as per their docs is:

type ErrorResponse = {
  error: {
    code: number;
    message: string;
    metadata?: Record<string, unknown>;
  };
};

So the code should look more like this:

class OpenRouterErrorDetails(BaseModel):
    code: int
    message: str
    metadata: dict[str, Any] | None

class OpenRouterErrorResponse(BaseModel):
    error: OpenRouterErrorDetails

class OpenRouterChatCompletion(chat.ChatCompletion):
    provider: str

class OpenRouterModel(OpenAIModel):
    def _process_response(self, response: Union[OpenRouterChatCompletion, OpenRouterErrorResponse]) -> ModelResponse:
        if error := getattr(response, 'error', None):
            raise ModelHTTPError(status_code=error['code'], model_name=self.model_name, body=error)
        else:
            model_response = super()._process_response(response=response)
        response = OpenRouterChatCompletion.model_validate(response.model_dump())
        ...

However this approach results in typechecking errors because I would be overriding the signature of _process_response:

PYRIGHT_PYTHON_IGNORE_WARNINGS=1 uv run pyright
/Users/danwork/Dev/pydantic-ai/pydantic_ai_slim/pydantic_ai/models/openrouter.py
  /Users/danwork/Dev/pydantic-ai/pydantic_ai_slim/pydantic_ai/models/openrouter.py:52:9 - error: Method "_process_response" overrides class "OpenAIModel" in an incompatible manner
    Parameter 2 type mismatch: base parameter is type "ChatCompletion", override parameter is type "OpenRouterChatCompletion | OpenRouterErrorResponse"
      Type "ChatCompletion" is not assignable to type "OpenRouterChatCompletion | OpenRouterErrorResponse"
        "ChatCompletion" is not assignable to "OpenRouterChatCompletion"
        "ChatCompletion" is not assignable to "OpenRouterErrorResponse" (reportIncompatibleMethodOverride)
  /Users/danwork/Dev/pydantic-ai/pydantic_ai_slim/pydantic_ai/models/openrouter.py:56:65 - error: Argument of type "OpenRouterChatCompletion | OpenRouterErrorResponse" cannot be assigned to parameter "response" of type "ChatCompletion" in function "_process_response"
    Type "OpenRouterChatCompletion | OpenRouterErrorResponse" is not assignable to type "ChatCompletion"
      "OpenRouterErrorResponse" is not assignable to "ChatCompletion" (reportArgumentType)
2 errors, 0 warnings, 0 informations 
make: *** [typecheck-pyright] Error 1

Would appreciate if you have any advice on how I could handle this! Thanks

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I think the solution is to move the error handling into _completions_create...

@Kludex
Copy link
Member
Kludex commented Jun 8, 2025

Can't we not create a new model and handle this in the OpenAIModel?

@DouweM
Copy link
Contributor
DouweM commented Jun 10, 2025

@Kludex I didn't want to clutter OpenAIModel with stuff that's specific to one particular OpenAI-compatible provider, but looking at how complex it's getting to subclass it, I guess just reading provider and error attrs wouldn't be so bad.

@DanKing1903 Can you please try reimplementing this on OpenAIModel directly? That should make it a bit easier to hit the test coverage as well :)

@piiq
Copy link
piiq commented Jun 20, 2025

Thanks for the PR. I wanted to highlight that OpenRouter’s API offers much more than basic OpenAI compatibility. The provider routing and fallback logic are key differentiators that I and likely many others rely on.

I understand that this PR primarily focuses on capturing provider metadata in responses, which is absolutely great. Would it be possible to also support OpenRouter-specific request parameters? Examples include:

  • Restricting routing to providers that fully support required features (tools, function calling, JSON mode, etc.)
  • Provider prioritization (latency vs throughput vs price)
  • Selective provider exclusion for specific calls
  • Provider fallback logic

See: https://openrouter.ai/docs/features/provider-routing for the full parameter list.

These are typically passed via extra_body (or extra_header depending on the param) to chat.completions.create in a vanilla OpenAI client.

I haven’t checked if this is already supported. If it is, disregard this comment. Otherwise, please consider enabling this for the OpenRouter provider/model. This would enable some important routing use cases.

@DouweM
Copy link
Contributor
DouweM commented Jun 20, 2025

@piiq Thanks for jumping in, I agree those are OpenRouter-specific features worth supporting that wouldn't make sense on OpenAIModel, so it's a good reason to stick with a separate OpenRouterModel.

@DanKing1903 Would you be up for seeing how those features could be implemented on the new OpenRouterModel?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
0