-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Fix structured output with nested definitions with Gemini via OpenRouter #3618
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+498
−6
Merged
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
d1d5802
fix broken note and add another note about not falling back on valida…
dsfaccini c0f6c79
Merge branch 'pydantic:main' into docs-stuff
dsfaccini 2c4166e
interim json schema fix for gemini models via openrouter
dsfaccini 53ccd39
review adjsutments
dsfaccini ae2ee1b
add test for coverage
dsfaccini ce16c0b
more coverage
dsfaccini 735386a
remove lines and add tool test
dsfaccini File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
tests/models/cassettes/test_openrouter/test_openrouter_google_nested_schema.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| interactions: | ||
| - request: | ||
| headers: | ||
| accept: | ||
| - application/json | ||
| accept-encoding: | ||
| - gzip, deflate | ||
| connection: | ||
| - keep-alive | ||
| content-length: | ||
| - '914' | ||
| content-type: | ||
| - application/json | ||
| host: | ||
| - openrouter.ai | ||
| method: POST | ||
| parsed_body: | ||
| messages: | ||
| - content: It's a house with a ground floor that has an entryway, a living room and a garage. | ||
| role: user | ||
| model: google/gemini-2.5-flash | ||
| stream: false | ||
| tool_choice: required | ||
| tools: | ||
| - function: | ||
| description: Insert a level with its spaces. | ||
| name: insert_level_with_spaces | ||
| parameters: | ||
| description: Insert a level with its spaces. | ||
| properties: | ||
| level: | ||
| default: null | ||
| nullable: true | ||
| properties: | ||
| level_name: | ||
| type: string | ||
| level_type: | ||
| enum: | ||
| - ground | ||
| - basement | ||
| - floor | ||
| - attic | ||
| type: string | ||
| required: | ||
| - level_name | ||
| - level_type | ||
| type: object | ||
| spaces: | ||
| items: | ||
| properties: | ||
| space_name: | ||
| type: string | ||
| space_type: | ||
| enum: | ||
| - entryway | ||
| - living-room | ||
| - kitchen | ||
| - bedroom | ||
| - bathroom | ||
| - garage | ||
| type: string | ||
| required: | ||
| - space_name | ||
| - space_type | ||
| type: object | ||
| type: array | ||
| required: | ||
| - spaces | ||
| type: object | ||
| strict: true | ||
| type: function | ||
| uri: https://openrouter.ai/api/v1/chat/completions | ||
| response: | ||
| headers: | ||
| access-control-allow-origin: | ||
| - '*' | ||
| connection: | ||
| - keep-alive | ||
| content-length: | ||
| - '1163' | ||
| content-type: | ||
| - application/json | ||
| permissions-policy: | ||
| - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" "https://js.stripe.com" "https://*.js.stripe.com" | ||
| "https://hooks.stripe.com") | ||
| referrer-policy: | ||
| - no-referrer, strict-origin-when-cross-origin | ||
| transfer-encoding: | ||
| - chunked | ||
| vary: | ||
| - Accept-Encoding | ||
| parsed_body: | ||
| choices: | ||
| - finish_reason: tool_calls | ||
| index: 0 | ||
| logprobs: null | ||
| message: | ||
| content: '' | ||
| reasoning: null | ||
| refusal: null | ||
| role: assistant | ||
| tool_calls: | ||
| - function: | ||
| arguments: '{"level":{"level_name":"ground floor","level_type":"ground"},"spaces":[{"space_name":"entryway","space_type":"entryway"},{"space_type":"living-room","space_name":"living | ||
| room"},{"space_type":"garage","space_name":"garage"}]}' | ||
| name: insert_level_with_spaces | ||
| id: tool_insert_level_with_spaces_Dp2OGkGjAwsmqUUwHwmj | ||
| index: 0 | ||
| type: function | ||
| native_finish_reason: STOP | ||
| created: 1764781178 | ||
| id: gen-1764781178-SLI9BlpnJoXSOwuii48G | ||
| model: google/gemini-2.5-flash | ||
| object: chat.completion | ||
| provider: Google AI Studio | ||
| usage: | ||
| completion_tokens: 89 | ||
| completion_tokens_details: | ||
| image_tokens: 0 | ||
| reasoning_tokens: 0 | ||
| cost: 0 | ||
| cost_details: | ||
| upstream_inference_completions_cost: 0.0002225 | ||
| upstream_inference_cost: 0.000296 | ||
| upstream_inference_prompt_cost: 7.35e-05 | ||
| is_byok: true | ||
| prompt_tokens: 245 | ||
| prompt_tokens_details: | ||
| audio_tokens: 0 | ||
| cached_tokens: 0 | ||
| video_tokens: 0 | ||
| total_tokens: 334 | ||
| status: | ||
| code: 200 | ||
| message: OK | ||
| version: 1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ | |
| from pydantic_ai.profiles.anthropic import AnthropicJsonSchemaTransformer, 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 GoogleJsonSchemaTransformer, google_model_profile | ||
| from pydantic_ai.profiles.google import google_model_profile | ||
| 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 | ||
|
|
@@ -26,7 +26,10 @@ | |
| import openai | ||
|
|
||
| from pydantic_ai.models.openrouter import OpenRouterModel | ||
| from pydantic_ai.providers.openrouter import OpenRouterProvider | ||
| from pydantic_ai.providers.openrouter import ( | ||
| OpenRouterProvider, | ||
| _OpenRouterGoogleJsonSchemaTransformer, # pyright: ignore[reportPrivateUsage] | ||
| ) | ||
|
|
||
|
|
||
| pytestmark = [ | ||
|
|
@@ -109,12 +112,12 @@ def test_openrouter_provider_model_profile(mocker: MockerFixture): | |
| google_profile = provider.model_profile('google/gemini-2.5-pro-preview') | ||
| google_model_profile_mock.assert_called_with('gemini-2.5-pro-preview') | ||
| assert google_profile is not None | ||
| assert google_profile.json_schema_transformer == GoogleJsonSchemaTransformer | ||
| assert google_profile.json_schema_transformer == _OpenRouterGoogleJsonSchemaTransformer | ||
|
|
||
| google_profile = provider.model_profile('google/gemma-3n-e4b-it:free') | ||
| google_model_profile_mock.assert_called_with('gemma-3n-e4b-it') | ||
| assert google_profile is not None | ||
| assert google_profile.json_schema_transformer == GoogleJsonSchemaTransformer | ||
| assert google_profile.json_schema_transformer == _OpenRouterGoogleJsonSchemaTransformer | ||
|
|
||
| openai_profile = provider.model_profile('openai/o1-mini') | ||
| openai_model_profile_mock.assert_called_with('o1-mini') | ||
|
|
@@ -170,3 +173,40 @@ def test_openrouter_provider_model_profile(mocker: MockerFixture): | |
| unknown_profile = provider.model_profile('unknown/model') | ||
| assert unknown_profile is not None | ||
| assert unknown_profile.json_schema_transformer == OpenAIJsonSchemaTransformer | ||
|
|
||
|
|
||
| def test_openrouter_google_json_schema_transformer(): | ||
| """Test _OpenRouterGoogleJsonSchemaTransformer covers all transformation cases.""" | ||
| schema = { | ||
| '$schema': 'http://json-schema.org/draft-07/schema#', | ||
| 'title': 'TestSchema', | ||
| 'type': 'object', | ||
| 'properties': { | ||
| 'status': {'const': 'active'}, | ||
| 'category': {'oneOf': [{'type': 'string'}, {'type': 'integer'}]}, | ||
| 'email': {'type': 'string', 'format': 'email', 'description': 'User email'}, | ||
| 'date': {'type': 'string', 'format': 'date'}, | ||
| }, | ||
| } | ||
|
|
||
| transformer = _OpenRouterGoogleJsonSchemaTransformer(schema) | ||
| result = transformer.walk() | ||
|
|
||
| # const -> enum conversion (line 59) | ||
| assert result['properties']['status'] == {'enum': ['active'], 'type': 'string'} | ||
|
|
||
| # oneOf -> anyOf conversion (line 68) | ||
| assert 'anyOf' in result['properties']['category'] | ||
| assert 'oneOf' not in result['properties']['category'] | ||
|
|
||
| # format -> description with existing description (lines 73-75) | ||
| assert result['properties']['email']['description'] == 'User email (format: email)' | ||
| assert 'format' not in result['properties']['email'] | ||
|
|
||
| # format -> description without existing description (lines 76-77) | ||
|
||
| assert result['properties']['date']['description'] == 'Format: date' | ||
| assert 'format' not in result['properties']['date'] | ||
|
|
||
| # Removed fields | ||
| assert '$schema' not in result | ||
| assert 'title' not in result | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use an
Agentwith a@agent.toolso we are sure that it goes through the exact same processing that users would see