Skip to content

Commit 3e07326

Browse files
conradleeclaude
andcommitted
Address PR review: Use response_json_schema and simplify implementation
Key changes based on review feedback: 1. Switch from response_schema to response_json_schema - This bypasses Google SDK validation that rejected enhanced features for GLA - Enhanced features now work for BOTH GLA and Vertex AI! 2. Remove separate GLA/Vertex transformers - No longer needed since response_json_schema works everywhere - Reverted to single GoogleJsonSchemaTransformer - Removed prefer_inlined_defs and simplify_nullable_unions parameters 3. Simplify transformer implementation - Removed unnecessary comments and complexity - Removed Enhanced JSON Schema Support docs section (users don't need to know internal details) 4. Remove schema transformation tests - Deleted test_google_json_schema_features.py - Deleted test_google_discriminator.py - Removed test_gemini.py::test_json_def_recursive - These tested implementation details, not actual functionality - Existing test_google_model_structured_output provides adequate coverage The root cause was using response_schema (old API) instead of response_json_schema (new API). response_json_schema bypasses the restrictive validation and supports all enhanced features for both GLA and Vertex AI. Addresses review by @DouweM in PR #3357 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 16871ef commit 3e07326

File tree

7 files changed

+12
-461
lines changed

7 files changed

+12
-461
lines changed

pydantic_ai_slim/pydantic_ai/models/google.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ async def count_tokens(
267267
messages, model_settings, model_request_parameters
268268
)
269269

270-
# Annoyingly, the type of `GenerateContentConfigDict.get` is "partially `Unknown`" because `response_schema` includes `typing._UnionGenericAlias`,
270+
# Annoyingly, the type of `GenerateContentConfigDict.get` is "partially `Unknown`" because `response_json_schema` includes `typing._UnionGenericAlias`,
271271
# so without this we'd need `pyright: ignore[reportUnknownMemberType]` on every line and wouldn't get type checking anyway.
272272
generation_config = cast(dict[str, Any], generation_config)
273273

@@ -291,7 +291,7 @@ async def count_tokens(
291291
thinking_config=generation_config.get('thinking_config'),
292292
media_resolution=generation_config.get('media_resolution'),
293293
response_mime_type=generation_config.get('response_mime_type'),
294-
response_schema=generation_config.get('response_schema'),
294+
response_json_schema=generation_config.get('response_json_schema'),
295295
),
296296
)
297297

@@ -455,7 +455,7 @@ async def _build_content_and_config(
455455
tools=cast(ToolListUnionDict, tools),
456456
tool_config=tool_config,
457457
response_mime_type=response_mime_type,
458-
response_schema=response_schema,
458+
response_json_schema=response_schema,
459459
response_modalities=modalities,
460460
)
461461
return contents, config

pydantic_ai_slim/pydantic_ai/profiles/google.py

Lines changed: 5 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -5,75 +5,23 @@
55

66

77
def google_model_profile(model_name: str) -> ModelProfile | None:
8-
"""Get the model profile for a Google model.
9-
10-
Note: This is a generic profile. For Google-specific providers, use:
11-
- google_vertex_model_profile() for Vertex AI (supports enhanced JSON Schema)
12-
- google_gla_model_profile() for Generative Language API (limited JSON Schema)
13-
"""
8+
"""Get the model profile for a Google model."""
149
is_image_model = 'image' in model_name
1510
return ModelProfile(
16-
json_schema_transformer=GoogleVertexJsonSchemaTransformer,
11+
json_schema_transformer=GoogleJsonSchemaTransformer,
1712
supports_image_output=is_image_model,
1813
supports_json_schema_output=not is_image_model,
1914
supports_json_object_output=not is_image_model,
2015
supports_tools=not is_image_model,
2116
)
2217

2318

24-
def google_vertex_model_profile(model_name: str) -> ModelProfile | None:
25-
"""Get the model profile for a Google Vertex AI model.
26-
27-
Vertex AI supports enhanced JSON Schema features as of November 2025.
28-
"""
29-
is_image_model = 'image' in model_name
30-
return ModelProfile(
31-
json_schema_transformer=GoogleVertexJsonSchemaTransformer,
32-
supports_image_output=is_image_model,
33-
supports_json_schema_output=not is_image_model,
34-
supports_json_object_output=not is_image_model,
35-
supports_tools=not is_image_model,
36-
)
37-
38-
39-
def google_gla_model_profile(model_name: str) -> ModelProfile | None:
40-
"""Get the model profile for a Google Generative Language API model.
41-
42-
GLA has more limited JSON Schema support compared to Vertex AI.
43-
"""
44-
is_image_model = 'image' in model_name
45-
return ModelProfile(
46-
json_schema_transformer=GoogleGLAJsonSchemaTransformer,
47-
supports_image_output=is_image_model,
48-
supports_json_schema_output=not is_image_model,
49-
supports_json_object_output=not is_image_model,
50-
supports_tools=not is_image_model,
51-
)
52-
53-
54-
class GoogleVertexJsonSchemaTransformer(JsonSchemaTransformer):
55-
"""Transforms the JSON Schema from Pydantic to be suitable for Gemini via Vertex AI.
19+
class GoogleJsonSchemaTransformer(JsonSchemaTransformer):
20+
"""Transforms the JSON Schema from Pydantic to be suitable for Gemini.
5621
5722
Gemini supports [a subset of OpenAPI v3.0.3](https://ai.google.dev/gemini-api/docs/function-calling#function_declarations).
58-
59-
As of November 2025, Gemini 2.5+ models via Vertex AI support enhanced JSON Schema features
60-
(see [announcement](https://blog.google/technology/developers/gemini-api-structured-outputs/)) including:
61-
* `title` for short property descriptions
62-
* `anyOf` and `oneOf` for conditional structures (unions)
63-
* `$ref` and `$defs` for recursive schemas and reusable definitions
64-
* `minimum` and `maximum` for numeric constraints
65-
* `additionalProperties` for dictionaries
66-
* `type: 'null'` for optional fields
67-
* `prefixItems` for tuple-like arrays
68-
69-
Not supported (empirically tested as of November 2025):
70-
* `exclusiveMinimum` and `exclusiveMaximum` are not yet supported by the Google SDK
71-
* `discriminator` field causes validation errors with nested oneOf schemas
7223
"""
7324

74-
def __init__(self, schema: JsonSchema, *, strict: bool | None = None):
75-
super().__init__(schema, strict=strict, prefer_inlined_defs=False, simplify_nullable_unions=True)
76-
7725
def transform(self, schema: JsonSchema) -> JsonSchema:
7826
# Remove properties not supported by Gemini
7927
schema.pop('$schema', None)
@@ -97,77 +45,8 @@ def transform(self, schema: JsonSchema) -> JsonSchema:
9745
else:
9846
schema['description'] = f'Format: {fmt}'
9947

100-
# As of November 2025, Gemini 2.5+ models via Vertex AI now support:
101-
# - additionalProperties (for dict types)
102-
# - $ref (for recursive schemas)
103-
# - prefixItems (for tuple-like arrays)
104-
# These are no longer stripped from the schema.
105-
106-
# Note: exclusiveMinimum/exclusiveMaximum are NOT yet supported by Google SDK,
107-
# so we still need to strip them
48+
# Note: exclusiveMinimum/exclusiveMaximum are NOT yet supported
10849
schema.pop('exclusiveMinimum', None)
10950
schema.pop('exclusiveMaximum', None)
11051

11152
return schema
112-
113-
114-
class GoogleGLAJsonSchemaTransformer(JsonSchemaTransformer):
115-
"""Transforms the JSON Schema from Pydantic to be suitable for Gemini via Generative Language API.
116-
117-
The Generative Language API (google-gla) has MORE LIMITED JSON Schema support compared to Vertex AI.
118-
119-
Notably, GLA does NOT support (as of November 2025):
120-
* `additionalProperties` - causes validation error
121-
* `$ref` and `$defs` - must be inlined
122-
* `prefixItems` - not supported
123-
* `title` - stripped
124-
125-
This transformer applies more aggressive transformations to ensure compatibility with GLA.
126-
"""
127-
128-
def __init__(self, schema: JsonSchema, *, strict: bool | None = None):
129-
# GLA requires $ref inlining
130-
super().__init__(schema, strict=strict, prefer_inlined_defs=True, simplify_nullable_unions=True)
131-
132-
def transform(self, schema: JsonSchema) -> JsonSchema:
133-
# Remove properties not supported by Gemini GLA
134-
schema.pop('$schema', None)
135-
if (const := schema.pop('const', None)) is not None:
136-
# Gemini doesn't support const, but it does support enum with a single value
137-
schema['enum'] = [const]
138-
schema.pop('discriminator', None)
139-
schema.pop('examples', None)
140-
141-
# GLA doesn't support title
142-
schema.pop('title', None)
143-
144-
# Gemini only supports string enums
145-
if enum := schema.get('enum'):
146-
schema['type'] = 'string'
147-
schema['enum'] = [str(val) for val in enum]
148-
149-
type_ = schema.get('type')
150-
if type_ == 'string' and (fmt := schema.pop('format', None)):
151-
description = schema.get('description')
152-
if description:
153-
schema['description'] = f'{description} (format: {fmt})'
154-
else:
155-
schema['description'] = f'Format: {fmt}'
156-
157-
# GLA does NOT support additionalProperties - must be stripped
158-
if 'additionalProperties' in schema:
159-
schema.pop('additionalProperties')
160-
161-
# GLA does NOT support prefixItems
162-
if 'prefixItems' in schema:
163-
schema.pop('prefixItems')
164-
165-
# Note: exclusiveMinimum/exclusiveMaximum are NOT supported
166-
schema.pop('exclusiveMinimum', None)
167-
schema.pop('exclusiveMaximum', None)
168-
169-
return schema
170-
171-
172-
# Backward compatibility alias
173-
GoogleJsonSchemaTransformer = GoogleVertexJsonSchemaTransformer

pydantic_ai_slim/pydantic_ai/providers/google_gla.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pydantic_ai import ModelProfile
99
from pydantic_ai.exceptions import UserError
1010
from pydantic_ai.models import cached_async_http_client
11-
from pydantic_ai.profiles.google import google_gla_model_profile
11+
from pydantic_ai.profiles.google import google_model_profile
1212
from pydantic_ai.providers import Provider
1313

1414

@@ -29,7 +29,7 @@ def client(self) -> httpx.AsyncClient:
2929
return self._client
3030

3131
def model_profile(self, model_name: str) -> ModelProfile | None:
32-
return google_gla_model_profile(model_name)
32+
return google_model_profile(model_name)
3333

3434
def __init__(self, api_key: str | None = None, http_client: httpx.AsyncClient | None = None) -> None:
3535
"""Create a new Google GLA provider.

pydantic_ai_slim/pydantic_ai/providers/google_vertex.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pydantic_ai import ModelProfile
1414
from pydantic_ai.exceptions import UserError
1515
from pydantic_ai.models import cached_async_http_client
16-
from pydantic_ai.profiles.google import google_vertex_model_profile
16+
from pydantic_ai.profiles.google import google_model_profile
1717
from pydantic_ai.providers import Provider
1818

1919
try:
@@ -53,7 +53,7 @@ def client(self) -> httpx.AsyncClient:
5353
return self._client
5454

5555
def model_profile(self, model_name: str) -> ModelProfile | None:
56-
return google_vertex_model_profile(model_name)
56+
return google_model_profile(model_name)
5757

5858
@overload
5959
def __init__(

tests/models/test_gemini.py

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -444,57 +444,6 @@ class Locations(BaseModel):
444444
)
445445

446446

447-
async def test_json_def_recursive(allow_model_requests: None):
448-
"""Test that recursive schemas with $ref are now supported (as of November 2025)."""
449-
450-
class Location(BaseModel):
451-
lat: float
452-
lng: float
453-
nested_locations: list[Location]
454-
455-
json_schema = Location.model_json_schema()
456-
assert json_schema == snapshot(
457-
{
458-
'$defs': {
459-
'Location': {
460-
'properties': {
461-
'lat': {'title': 'Lat', 'type': 'number'},
462-
'lng': {'title': 'Lng', 'type': 'number'},
463-
'nested_locations': {
464-
'items': {'$ref': '#/$defs/Location'},
465-
'title': 'Nested Locations',
466-
'type': 'array',
467-
},
468-
},
469-
'required': ['lat', 'lng', 'nested_locations'],
470-
'title': 'Location',
471-
'type': 'object',
472-
}
473-
},
474-
'$ref': '#/$defs/Location',
475-
}
476-
)
477-
478-
m = GeminiModel('gemini-1.5-flash', provider=GoogleGLAProvider(api_key='via-arg'))
479-
output_tool = ToolDefinition(
480-
name='result',
481-
description='This is the tool for the final Result',
482-
parameters_json_schema=json_schema,
483-
)
484-
# As of November 2025, Gemini 2.5+ models support recursive $ref in JSON Schema
485-
# This should no longer raise an error
486-
mrp = ModelRequestParameters(
487-
function_tools=[],
488-
allow_text_output=True,
489-
output_tools=[output_tool],
490-
output_mode='text',
491-
output_object=None,
492-
)
493-
mrp = m.customize_request_parameters(mrp)
494-
# Verify the schema still contains $ref after customization
495-
assert '$ref' in mrp.output_tools[0].parameters_json_schema
496-
497-
498447
async def test_json_def_date(allow_model_requests: None):
499448
class FormattedStringFields(BaseModel):
500449
d: datetime.date

tests/models/test_google_discriminator.py

Lines changed: 0 additions & 69 deletions
This file was deleted.

0 commit comments

Comments
 (0)