-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Initial Checks
- I confirm that I'm using the latest version of Pydantic AI
- I confirm that I searched for my issue in https://github.com/pydantic/pydantic-ai/issues before opening this issue
Description
Summary
Since #3357, when using Google/Gemini models via OpenRouter (openrouter:google/gemini-2.5-flash), the JSON schema sent to the API uses modern JSON Schema features ($defs, $ref, anyOf for nullable types) that Gemini models don't handle well unless the new parameters_json_schema/response_json_schema field is used, resulting in degraded tool call output.
Environment
- pydantic-ai version: post-1.19.0
- Model:
openrouter:google/gemini-2.5-flash
Expected behavior
The model should correctly populate the level field as an object with level_name and level_type, and spaces as a list of objects with space_name.
Actual behavior
The model returns degraded output:
levelbecomes a simple string ("ground") instead of an objectspacesbecomes a list of strings instead of objects
Root cause
PR #3357 simplified GoogleJsonSchemaTransformer by removing workarounds that are now handled natively by the Gemini API as of https://blog.google/technology/developers/gemini-api-structured-outputs/. However, when models are accessed via OpenRouter, these native features may not be properly supported through the compatibility layer.
Before (v1.19.0) - Schema sent to API:
{
"level": {
"type": "object",
"nullable": true,
"properties": { "level_name": {...}, "level_type": {...} }
}
}After (main) - Schema sent to API:
{
"level": {
"anyOf": [
{"$ref": "#/$defs/InsertLevelArg"},
{"type": "null"}
]
},
"$defs": {
"InsertLevelArg": {...}
}
}The new format with $defs, $ref, and anyOf causes Gemini via OpenRouter to misunderstand the schema structure, even though it works when using GoogleModel directly.
Suggested fix
For OpenRouter specifically, use a transformer that restores the v1.19.0 behavior:
prefer_inlined_defs=True- inline$defsinstead of using$refsimplify_nullable_unions=True- convertanyOf: [{...}, {type: null}]tonullable: true
This would be applied only to Google models accessed via OpenRouter, since direct Google API access may work correctly with the new schema format.
Related
- PR Support Gemini enhanced JSON Schema features #3357: Support Gemini enhanced JSON Schema features
Example Code
import asyncio
import logfire
from pydantic import BaseModel, Field
from enum import Enum
from pydantic_ai import Agent, RunContext
logfire.configure()
logfire.instrument_httpx(capture_all=True)
class LevelType(str, Enum):
ground = "ground"
basement = "basement"
class InsertLevelArg(BaseModel):
level_name: str = Field(description="Name of the level")
level_type: LevelType = Field(description="Type of level")
class SpaceArg(BaseModel):
space_name: str = Field(description="Name of the space")
class InsertLevelWithSpacesArgs(BaseModel):
level: InsertLevelArg | None = Field(default=None, description="Level definition (None for annexes)")
spaces: list[SpaceArg] = Field(description="List of spaces")
agent = Agent(
"openrouter:google/gemini-2.5-flash",
system_prompt="You help configure buildings.",
)
@agent.tool
def insert_level_with_spaces(ctx: RunContext, args: InsertLevelWithSpacesArgs) -> str:
"""Insert a level with its spaces."""
return f"Level created: {args.level}, spaces: {args.spaces}"
async def main():
result = await agent.run("Create a ground floor with an entrance and a garage.")
print(f"Result: {result.output}")
asyncio.run(main())Python, Pydantic AI & LLM client version
Python 3.12
pydantic-ai 1.25
gemini-flash-2.5 via openrouter (see MRE)