Skip to content

OpenRouter + Google models produce degraded output due to schema format changes #3617

@dsfaccini

Description

@dsfaccini

Initial Checks

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:

  • level becomes a simple string ("ground") instead of an object
  • spaces becomes 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 $defs instead of using $ref
  • simplify_nullable_unions=True - convert anyOf: [{...}, {type: null}] to nullable: 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

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions