Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions pydantic_ai_slim/pydantic_ai/providers/openrouter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
from openai import AsyncOpenAI

from pydantic_ai import ModelProfile
from pydantic_ai._json_schema import JsonSchema, JsonSchemaTransformer
from pydantic_ai.exceptions import UserError
from pydantic_ai.models import cached_async_http_client
from pydantic_ai.profiles.amazon import amazon_model_profile
from pydantic_ai.profiles.anthropic import anthropic_model_profile
from pydantic_ai.profiles.cohere import cohere_model_profile
from pydantic_ai.profiles.deepseek import deepseek_model_profile
from pydantic_ai.profiles.google import google_model_profile
from pydantic_ai.profiles.google import GoogleModelProfile
from pydantic_ai.profiles.grok import grok_model_profile
from pydantic_ai.profiles.meta import meta_model_profile
from pydantic_ai.profiles.mistral import mistral_model_profile
Expand All @@ -31,6 +32,70 @@
) from _import_error


class OpenRouterGoogleJsonSchemaTransformer(JsonSchemaTransformer):
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is public now, I don't think it should be :)

"""Legacy Google JSON schema transformer for OpenRouter compatibility.

OpenRouter's compatibility layer doesn't fully support modern JSON Schema features
like $defs/$ref and anyOf for nullable types. This transformer restores v1.19.0
behavior by inlining definitions and simplifying nullable unions.

See: https://github.com/pydantic/pydantic-ai/issues/3617
"""

def __init__(self, schema: JsonSchema, *, strict: bool | None = None):
super().__init__(schema, strict=strict, prefer_inlined_defs=True, simplify_nullable_unions=True)

def transform(self, schema: JsonSchema) -> JsonSchema:
# Remove properties not supported by Gemini
schema.pop('$schema', None)
schema.pop('title', None)
schema.pop('discriminator', None)
schema.pop('examples', None)
schema.pop('exclusiveMaximum', None)
schema.pop('exclusiveMinimum', None)

if (const := schema.pop('const', None)) is not None:
schema['enum'] = [const]

# Convert enums to string type (legacy Gemini requirement)
if enum := schema.get('enum'):
schema['type'] = 'string'
schema['enum'] = [str(val) for val in enum]

# Convert oneOf to anyOf for discriminated unions
if 'oneOf' in schema and 'type' not in schema:
schema['anyOf'] = schema.pop('oneOf')

# Handle string format -> description
type_ = schema.get('type')
if type_ == 'string' and (fmt := schema.pop('format', None)):
description = schema.get('description')
if description:
schema['description'] = f'{description} (format: {fmt})'
else:
schema['description'] = f'Format: {fmt}'

return schema


def openrouter_google_model_profile(model_name: str) -> ModelProfile | None:
"""Get the model profile for a Google model accessed via OpenRouter.

Uses the legacy transformer to maintain compatibility with OpenRouter's
translation layer, which doesn't fully support modern JSON Schema features.
"""
is_image_model = 'image' in model_name
is_3_or_newer = 'gemini-3' in model_name
return GoogleModelProfile(
Copy link
Collaborator

Choose a reason for hiding this comment

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

We shouldn't copy paste all of this! We should call the original google_model_profile, and then do the replace thing to swap out just the transformer

json_schema_transformer=OpenRouterGoogleJsonSchemaTransformer,
supports_image_output=is_image_model,
supports_json_schema_output=is_3_or_newer or not is_image_model,
supports_json_object_output=is_3_or_newer or not is_image_model,
supports_tools=not is_image_model,
google_supports_native_output_with_builtin_tools=is_3_or_newer,
)


class OpenRouterProvider(Provider[AsyncOpenAI]):
"""Provider for OpenRouter API."""

Expand All @@ -48,7 +113,7 @@ def client(self) -> AsyncOpenAI:

def model_profile(self, model_name: str) -> ModelProfile | None:
provider_to_profile = {
'google': google_model_profile,
'google': openrouter_google_model_profile,
'openai': openai_model_profile,
'anthropic': anthropic_model_profile,
'mistralai': mistral_model_profile,
Expand Down
Loading