Skip to content

Commit 5544e9f

Browse files
authored
Fix deprecated GeminiModel structured output and tool call thought signatures (#3517)
1 parent ab6de27 commit 5544e9f

10 files changed

+458
-244
lines changed

pydantic_ai_slim/pydantic_ai/models/gemini.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ async def _make_request(
240240

241241
output_object = model_request_parameters.output_object
242242
assert output_object is not None
243-
generation_config['response_schema'] = self._map_response_schema(output_object)
243+
generation_config['response_json_schema'] = self._map_response_schema(output_object)
244244
elif model_request_parameters.output_mode == 'prompted' and not tools:
245245
generation_config['response_mime_type'] = 'application/json'
246246

@@ -620,7 +620,7 @@ class _GeminiGenerationConfig(TypedDict, total=False):
620620
stop_sequences: list[str]
621621
thinking_config: ThinkingConfig
622622
response_mime_type: str
623-
response_schema: dict[str, Any]
623+
response_json_schema: dict[str, Any]
624624

625625

626626
class _GeminiContent(TypedDict):
@@ -630,9 +630,20 @@ class _GeminiContent(TypedDict):
630630

631631
def _content_model_response(m: ModelResponse) -> _GeminiContent:
632632
parts: list[_GeminiPartUnion] = []
633+
function_call_requires_signature = True
633634
for item in m.parts:
634635
if isinstance(item, ToolCallPart):
635-
parts.append(_function_call_part_from_call(item))
636+
part = _function_call_part_from_call(item)
637+
if function_call_requires_signature and not part.get('thought_signature'):
638+
# Per https://ai.google.dev/gemini-api/docs/gemini-3?thinking=high#migrating_from_other_models:
639+
# > If you are transferring a conversation trace from another model (e.g., Gemini 2.5) or injecting
640+
# > a custom function call that was not generated by Gemini 3, you will not have a valid signature.
641+
# > To bypass strict validation in these specific scenarios, populate the field with this specific
642+
# > dummy string: "thoughtSignature": "context_engineering_is_the_way_to_go"
643+
part['thought_signature'] = b'context_engineering_is_the_way_to_go'
644+
# Only the first function call requires a signature
645+
function_call_requires_signature = False
646+
parts.append(part)
636647
elif isinstance(item, ThinkingPart):
637648
# NOTE: We don't send ThinkingPart to the providers yet. If you are unsatisfied with this,
638649
# please open an issue. The below code is the code to send thinking to the provider.
@@ -691,6 +702,8 @@ class _GeminiThoughtPart(TypedDict):
691702
class _GeminiFunctionCallPart(_BasePart):
692703
function_call: Annotated[_GeminiFunctionCall, pydantic.Field(alias='functionCall')]
693704

705+
thought_signature: NotRequired[Annotated[bytes, pydantic.Field(alias='thoughtSignature')]]
706+
694707

695708
def _function_call_part_from_call(tool: ToolCallPart) -> _GeminiFunctionCallPart:
696709
return _GeminiFunctionCallPart(function_call=_GeminiFunctionCall(name=tool.tool_name, args=tool.args_as_dict()))
@@ -787,18 +800,12 @@ class _GeminiTools(TypedDict):
787800
class _GeminiFunction(TypedDict):
788801
name: str
789802
description: str
790-
parameters: NotRequired[dict[str, Any]]
791-
"""
792-
ObjectJsonSchema isn't really true since Gemini only accepts a subset of JSON Schema
793-
<https://ai.google.dev/gemini-api/docs/function-calling#function_declarations>
794-
and
795-
<https://ai.google.dev/api/caching#FunctionDeclaration>
796-
"""
803+
parameters_json_schema: NotRequired[dict[str, Any]]
797804

798805

799806
def _function_from_abstract_tool(tool: ToolDefinition) -> _GeminiFunction:
800807
json_schema = tool.parameters_json_schema
801-
f = _GeminiFunction(name=tool.name, description=tool.description or '', parameters=json_schema)
808+
f = _GeminiFunction(name=tool.name, description=tool.description or '', parameters_json_schema=json_schema)
802809
return f
803810

804811

tests/models/cassettes/test_gemini/test_gemini_additional_properties_is_false.yaml

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interactions:
88
connection:
99
- keep-alive
1010
content-length:
11-
- '296'
11+
- '360'
1212
content-type:
1313
- application/json
1414
host:
@@ -20,10 +20,11 @@ interactions:
2020
- text: What is the temperature in Tokyo?
2121
role: user
2222
tools:
23-
function_declarations:
24-
- description: null
23+
functionDeclarations:
24+
- description: ''
2525
name: get_temperature
26-
parameters:
26+
parameters_json_schema:
27+
additionalProperties: false
2728
properties:
2829
city:
2930
type: string
@@ -32,18 +33,19 @@ interactions:
3233
required:
3334
- city
3435
- country
36+
title: CurrentLocation
3537
type: object
36-
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent
38+
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent
3739
response:
3840
headers:
3941
alt-svc:
4042
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
4143
content-length:
42-
- '748'
44+
- '752'
4345
content-type:
4446
- application/json; charset=UTF-8
4547
server-timing:
46-
- gfet4t7; dur=523
48+
- gfet4t7; dur=723
4749
transfer-encoding:
4850
- chunked
4951
vary:
@@ -52,24 +54,25 @@ interactions:
5254
- Referer
5355
parsed_body:
5456
candidates:
55-
- avgLogprobs: -0.12538558465463143
57+
- avgLogprobs: -0.13673184228980023
5658
content:
5759
parts:
5860
- text: |
59-
The available tools lack the ability to access real-time information, including current temperature. Therefore, I cannot answer your question.
61+
I need the country to find the temperature in Tokyo. Could you please tell me which country Tokyo is in?
6062
role: model
6163
finishReason: STOP
62-
modelVersion: gemini-1.5-flash
64+
modelVersion: gemini-2.0-flash
65+
responseId: V_cgaZvoEIGhz7IP0LnHiA4
6366
usageMetadata:
64-
candidatesTokenCount: 27
67+
candidatesTokenCount: 23
6568
candidatesTokensDetails:
6669
- modality: TEXT
67-
tokenCount: 27
68-
promptTokenCount: 14
70+
tokenCount: 23
71+
promptTokenCount: 10
6972
promptTokensDetails:
7073
- modality: TEXT
71-
tokenCount: 14
72-
totalTokenCount: 41
74+
tokenCount: 10
75+
totalTokenCount: 33
7376
status:
7477
code: 200
7578
message: OK

tests/models/cassettes/test_gemini/test_gemini_additional_properties_is_true.yaml

Lines changed: 154 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interactions:
88
connection:
99
- keep-alive
1010
content-length:
11-
- '264'
11+
- '561'
1212
content-type:
1313
- application/json
1414
host:
@@ -20,27 +20,43 @@ interactions:
2020
- text: What is the temperature in Tokyo?
2121
role: user
2222
tools:
23-
function_declarations:
23+
functionDeclarations:
2424
- description: ''
2525
name: get_temperature
26-
parameters:
26+
parameters_json_schema:
27+
$defs:
28+
CurrentLocation:
29+
additionalProperties: false
30+
properties:
31+
city:
32+
type: string
33+
country:
34+
type: string
35+
required:
36+
- city
37+
- country
38+
title: CurrentLocation
39+
type: object
40+
additionalProperties: false
2741
properties:
2842
location:
43+
additionalProperties:
44+
$ref: '#/$defs/CurrentLocation'
2945
type: object
3046
required:
3147
- location
3248
type: object
33-
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent
49+
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
3450
response:
3551
headers:
3652
alt-svc:
3753
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
3854
content-length:
39-
- '741'
55+
- '1227'
4056
content-type:
4157
- application/json; charset=UTF-8
4258
server-timing:
43-
- gfet4t7; dur=534
59+
- gfet4t7; dur=965
4460
transfer-encoding:
4561
- chunked
4662
vary:
@@ -49,24 +65,145 @@ interactions:
4965
- Referer
5066
parsed_body:
5167
candidates:
52-
- avgLogprobs: -0.15060695580073766
53-
content:
68+
- content:
5469
parts:
55-
- text: |
56-
I need a location dictionary to use the `get_temperature` function. I cannot provide the temperature in Tokyo without more information.
70+
- functionCall:
71+
args:
72+
location:
73+
city: Tokyo
74+
name: get_temperature
75+
thoughtSignature: CqsCAdHtim8g6Mb+p/TOM/hcAQMuHORfvoYEEpINyxRabZ/ZiiBArydE5DaexwckI0MaKPd18/tx5SlEuLBURrOtQr3Fy3i00cItVVR+H6+K+WyxVUe8AmsYWTGntnwflq81uC6P79FHQhc2cOMsKbRNu3mtNF7mncsBJKQ3NZ4ioi1YffVc4V6PNGbw9rjZtVx1iViqNZ6il7LZ/WvwrZR1HCmFd03X0koCTpBt/jMx3fSIJt79YGjEGvb3huk1wGcuWH54Uj7WhU+E7MTrUAz/Oa5lmWwf8NNrbLPepuLVFTPVKukErjwlZlX4oCaXiz3M5OewJwQR14L5Jub7GIdsx/++p9XJgROw9v1vifauXo35zVDUqqosfWvz/iD0Iv4hhlOBipdTtLzYK/k=
5776
role: model
77+
finishMessage: Model generated function call(s).
5878
finishReason: STOP
59-
modelVersion: gemini-1.5-flash
79+
index: 0
80+
modelVersion: gemini-2.5-flash
81+
responseId: 7PggabeLH--tz7IPmu30uQo
6082
usageMetadata:
61-
candidatesTokenCount: 28
62-
candidatesTokensDetails:
83+
candidatesTokenCount: 19
84+
promptTokenCount: 40
85+
promptTokensDetails:
6386
- modality: TEXT
64-
tokenCount: 28
65-
promptTokenCount: 12
87+
tokenCount: 40
88+
thoughtsTokenCount: 65
89+
totalTokenCount: 124
90+
status:
91+
code: 200
92+
message: OK
93+
- request:
94+
headers:
95+
accept:
96+
- '*/*'
97+
accept-encoding:
98+
- gzip, deflate
99+
connection:
100+
- keep-alive
101+
content-length:
102+
- '1115'
103+
content-type:
104+
- application/json
105+
host:
106+
- generativelanguage.googleapis.com
107+
method: POST
108+
parsed_body:
109+
contents:
110+
- parts:
111+
- text: What is the temperature in Tokyo?
112+
role: user
113+
- parts:
114+
- functionCall:
115+
args:
116+
location:
117+
city: Tokyo
118+
name: get_temperature
119+
thoughtSignature: context_engineering_is_the_way_to_go
120+
role: model
121+
- parts:
122+
- functionResponse:
123+
name: get_temperature
124+
response:
125+
call_error: |-
126+
1 validation error:
127+
```json
128+
[
129+
{
130+
"type": "model_type",
131+
"loc": [
132+
"location",
133+
"city"
134+
],
135+
"msg": "Input should be a valid dictionary or instance of CurrentLocation",
136+
"input": "Tokyo"
137+
}
138+
]
139+
```
140+
141+
Fix the errors and try again.
142+
role: user
143+
tools:
144+
functionDeclarations:
145+
- description: ''
146+
name: get_temperature
147+
parameters_json_schema:
148+
$defs:
149+
CurrentLocation:
150+
additionalProperties: false
151+
properties:
152+
city:
153+
type: string
154+
country:
155+
type: string
156+
required:
157+
- city
158+
- country
159+
title: CurrentLocation
160+
type: object
161+
additionalProperties: false
162+
properties:
163+
location:
164+
additionalProperties:
165+
$ref: '#/$defs/CurrentLocation'
166+
type: object
167+
required:
168+
- location
169+
type: object
170+
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
171+
response:
172+
headers:
173+
alt-svc:
174+
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
175+
content-length:
176+
- '4298'
177+
content-type:
178+
- application/json; charset=UTF-8
179+
server-timing:
180+
- gfet4t7; dur=3078
181+
transfer-encoding:
182+
- chunked
183+
vary:
184+
- Origin
185+
- X-Origin
186+
- Referer
187+
parsed_body:
188+
candidates:
189+
- content:
190+
parts:
191+
- text: I was not able to get the temperature in Tokyo. There seems to be an issue with the way the location is
192+
being processed by the tool.
193+
thoughtSignature: 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
194+
role: model
195+
finishReason: STOP
196+
index: 0
197+
modelVersion: gemini-2.5-flash
198+
responseId: 7_ggadDZJfaEz7IPvafbuAg
199+
usageMetadata:
200+
candidatesTokenCount: 29
201+
promptTokenCount: 164
66202
promptTokensDetails:
67203
- modality: TEXT
68-
tokenCount: 12
69-
totalTokenCount: 40
204+
tokenCount: 164
205+
thoughtsTokenCount: 563
206+
totalTokenCount: 756
70207
status:
71208
code: 200
72209
message: OK

0 commit comments

Comments
 (0)