11from __future__ import annotations as _annotations
22
33import os
4+ from dataclasses import replace
45from typing import overload
56
67import httpx
78from openai import AsyncOpenAI
89
910from pydantic_ai import ModelProfile
11+ from pydantic_ai ._json_schema import JsonSchema , JsonSchemaTransformer
1012from pydantic_ai .exceptions import UserError
1113from pydantic_ai .models import cached_async_http_client
1214from pydantic_ai .profiles .amazon import amazon_model_profile
3133 ) from _import_error
3234
3335
36+ class _OpenRouterGoogleJsonSchemaTransformer (JsonSchemaTransformer ):
37+ """Legacy Google JSON schema transformer for OpenRouter compatibility.
38+
39+ OpenRouter's compatibility layer doesn't fully support modern JSON Schema features
40+ like $defs/$ref and anyOf for nullable types. This transformer restores v1.19.0
41+ behavior by inlining definitions and simplifying nullable unions.
42+
43+ See: https://github.com/pydantic/pydantic-ai/issues/3617
44+ """
45+
46+ def __init__ (self , schema : JsonSchema , * , strict : bool | None = None ):
47+ super ().__init__ (schema , strict = strict , prefer_inlined_defs = True , simplify_nullable_unions = True )
48+
49+ def transform (self , schema : JsonSchema ) -> JsonSchema :
50+ # Remove properties not supported by Gemini
51+ schema .pop ('$schema' , None )
52+ schema .pop ('title' , None )
53+ schema .pop ('discriminator' , None )
54+ schema .pop ('examples' , None )
55+ schema .pop ('exclusiveMaximum' , None )
56+ schema .pop ('exclusiveMinimum' , None )
57+
58+ if (const := schema .pop ('const' , None )) is not None :
59+ schema ['enum' ] = [const ]
60+
61+ # Convert enums to string type (legacy Gemini requirement)
62+ if enum := schema .get ('enum' ):
63+ schema ['type' ] = 'string'
64+ schema ['enum' ] = [str (val ) for val in enum ]
65+
66+ # Convert oneOf to anyOf for discriminated unions
67+ if 'oneOf' in schema and 'type' not in schema :
68+ schema ['anyOf' ] = schema .pop ('oneOf' )
69+
70+ # Handle string format -> description
71+ type_ = schema .get ('type' )
72+ if type_ == 'string' and (fmt := schema .pop ('format' , None )):
73+ description = schema .get ('description' )
74+ if description :
75+ schema ['description' ] = f'{ description } (format: { fmt } )'
76+ else :
77+ schema ['description' ] = f'Format: { fmt } '
78+
79+ return schema
80+
81+
82+ def _openrouter_google_model_profile (model_name : str ) -> ModelProfile | None :
83+ """Get the model profile for a Google model accessed via OpenRouter.
84+
85+ Uses the legacy transformer to maintain compatibility with OpenRouter's
86+ translation layer, which doesn't fully support modern JSON Schema features.
87+ """
88+ profile = google_model_profile (model_name )
89+ if profile is None : # pragma: no cover
90+ return None
91+ return replace (profile , json_schema_transformer = _OpenRouterGoogleJsonSchemaTransformer )
92+
93+
3494class OpenRouterProvider (Provider [AsyncOpenAI ]):
3595 """Provider for OpenRouter API."""
3696
@@ -48,7 +108,7 @@ def client(self) -> AsyncOpenAI:
48108
49109 def model_profile (self , model_name : str ) -> ModelProfile | None :
50110 provider_to_profile = {
51- 'google' : google_model_profile ,
111+ 'google' : _openrouter_google_model_profile ,
52112 'openai' : openai_model_profile ,
53113 'anthropic' : anthropic_model_profile ,
54114 'mistralai' : mistral_model_profile ,
0 commit comments