Skip to content

Commit 7ad6ccd

Browse files
committed
Gemini 3 Pro
1 parent 2981b17 commit 7ad6ccd

File tree

8 files changed

+255
-32
lines changed

8 files changed

+255
-32
lines changed

docs/models/google.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,22 +214,22 @@ from pydantic_ai.models.google import GoogleModel, GoogleModelSettings
214214
settings = GoogleModelSettings(
215215
temperature=0.2,
216216
max_tokens=1024,
217-
google_thinking_config={'thinking_budget': 2048},
217+
google_thinking_config={'thinking_level': 'low'},
218218
google_safety_settings=[
219219
{
220220
'category': HarmCategory.HARM_CATEGORY_HATE_SPEECH,
221221
'threshold': HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
222222
}
223223
]
224224
)
225-
model = GoogleModel('gemini-2.5-flash')
225+
model = GoogleModel('gemini-2.5-pro')
226226
agent = Agent(model, model_settings=settings)
227227
...
228228
```
229229

230230
### Disable thinking
231231

232-
You can disable thinking by setting the `thinking_budget` to `0` on the `google_thinking_config`:
232+
On models older than Gemini 2.5 Pro, you can disable thinking by setting the `thinking_budget` to `0` on the `google_thinking_config`:
233233

234234
```python
235235
from pydantic_ai import Agent

pydantic_ai_slim/pydantic_ai/models/google.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
VideoUrl,
3939
)
4040
from ..profiles import ModelProfileSpec
41+
from ..profiles.google import GoogleModelProfile
4142
from ..providers import Provider, infer_provider
4243
from ..settings import ModelSettings
4344
from ..tools import ToolDefinition
@@ -228,12 +229,17 @@ def system(self) -> str:
228229
def prepare_request(
229230
self, model_settings: ModelSettings | None, model_request_parameters: ModelRequestParameters
230231
) -> tuple[ModelSettings | None, ModelRequestParameters]:
232+
supports_native_output_with_builtin_tools = GoogleModelProfile.from_profile(
233+
self.profile
234+
).google_supports_native_output_with_builtin_tools
231235
if model_request_parameters.builtin_tools and model_request_parameters.output_tools:
232236
if model_request_parameters.output_mode == 'auto':
233-
model_request_parameters = replace(model_request_parameters, output_mode='prompted')
237+
output_mode = 'native' if supports_native_output_with_builtin_tools else 'prompted'
238+
model_request_parameters = replace(model_request_parameters, output_mode=output_mode)
234239
else:
240+
output_mode = 'NativeOutput' if supports_native_output_with_builtin_tools else 'PromptedOutput'
235241
raise UserError(
236-
'Google does not support output tools and built-in tools at the same time. Use `output_type=PromptedOutput(...)` instead.'
242+
f'Google does not support output tools and built-in tools at the same time. Use `output_type={output_mode}(...)` instead.'
237243
)
238244
return super().prepare_request(model_settings, model_request_parameters)
239245

@@ -409,9 +415,9 @@ async def _build_content_and_config(
409415
response_mime_type = None
410416
response_schema = None
411417
if model_request_parameters.output_mode == 'native':
412-
if tools:
418+
if model_request_parameters.function_tools:
413419
raise UserError(
414-
'Google does not support `NativeOutput` and tools at the same time. Use `output_type=ToolOutput(...)` instead.'
420+
'Google does not support `NativeOutput` and function tools at the same time. Use `output_type=ToolOutput(...)` instead.'
415421
)
416422
response_mime_type = 'application/json'
417423
output_object = model_request_parameters.output_object
@@ -675,22 +681,19 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
675681
for part in parts:
676682
if part.thought_signature:
677683
signature = base64.b64encode(part.thought_signature).decode('utf-8')
684+
# Attach signature to most recent thinking part, if there was one
678685
yield self._parts_manager.handle_thinking_delta(
679-
vendor_part_id='thinking',
686+
vendor_part_id=None,
680687
signature=signature,
681688
provider_name=self.provider_name,
682689
)
683690

684691
if part.text is not None:
685692
if len(part.text) > 0:
686693
if part.thought:
687-
yield self._parts_manager.handle_thinking_delta(
688-
vendor_part_id='thinking', content=part.text
689-
)
694+
yield self._parts_manager.handle_thinking_delta(vendor_part_id=None, content=part.text)
690695
else:
691-
maybe_event = self._parts_manager.handle_text_delta(
692-
vendor_part_id='content', content=part.text
693-
)
696+
maybe_event = self._parts_manager.handle_text_delta(vendor_part_id=None, content=part.text)
694697
if maybe_event is not None: # pragma: no branch
695698
yield maybe_event
696699
elif part.function_call:
@@ -749,6 +752,7 @@ def timestamp(self) -> datetime:
749752
def _content_model_response(m: ModelResponse, provider_name: str) -> ContentDict: # noqa: C901
750753
parts: list[PartDict] = []
751754
thought_signature: bytes | None = None
755+
function_call_requires_signature: bool = True
752756
for item in m.parts:
753757
part: PartDict = {}
754758
if thought_signature:
@@ -758,6 +762,15 @@ def _content_model_response(m: ModelResponse, provider_name: str) -> ContentDict
758762
if isinstance(item, ToolCallPart):
759763
function_call = FunctionCallDict(name=item.tool_name, args=item.args_as_dict(), id=item.tool_call_id)
760764
part['function_call'] = function_call
765+
if function_call_requires_signature and not part.get('thought_signature'):
766+
# Per https://ai.google.dev/gemini-api/docs/gemini-3?thinking=high#rest_2:
767+
# > If you are transferring a conversation trace from another model (e.g., Gemini 2.5) or injecting
768+
# > a custom function call that was not generated by Gemini 3, you will not have a valid signature.
769+
# > To bypass strict validation in these specific scenarios, populate the field with this specific
770+
# > dummy string: "thoughtSignature": "context_engineering_is_the_way_to_go"
771+
part['thought_signature'] = b'context_engineering_is_the_way_to_go'
772+
# Only the first function call requires a signature
773+
function_call_requires_signature = False
761774
elif isinstance(item, TextPart):
762775
part['text'] = item.content
763776
elif isinstance(item, ThinkingPart):

pydantic_ai_slim/pydantic_ai/profiles/google.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,37 @@
11
from __future__ import annotations as _annotations
22

33
import warnings
4+
from dataclasses import dataclass
45

56
from pydantic_ai.exceptions import UserError
67

78
from .._json_schema import JsonSchema, JsonSchemaTransformer
89
from . import ModelProfile
910

1011

12+
@dataclass(kw_only=True)
13+
class GoogleModelProfile(ModelProfile):
14+
"""Profile for models used with `GoogleModel`.
15+
16+
ALL FIELDS MUST BE `google_` PREFIXED SO YOU CAN MERGE THEM WITH OTHER MODELS.
17+
"""
18+
19+
google_supports_native_output_with_builtin_tools: bool = False
20+
"""Whether the model supports native output with builtin tools.
21+
See https://ai.google.dev/gemini-api/docs/structured-output?example=recipe#structured_outputs_with_tools"""
22+
23+
1124
def google_model_profile(model_name: str) -> ModelProfile | None:
1225
"""Get the model profile for a Google model."""
1326
is_image_model = 'image' in model_name
14-
return ModelProfile(
27+
is_3_or_newer = 'gemini-3' in model_name
28+
return GoogleModelProfile(
1529
json_schema_transformer=GoogleJsonSchemaTransformer,
1630
supports_image_output=is_image_model,
1731
supports_json_schema_output=not is_image_model,
1832
supports_json_object_output=not is_image_model,
1933
supports_tools=not is_image_model,
34+
google_supports_native_output_with_builtin_tools=is_3_or_newer,
2035
)
2136

2237

pydantic_ai_slim/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ logfire = ["logfire[httpx]>=3.14.1"]
7070
openai = ["openai>=1.107.2"]
7171
cohere = ["cohere>=5.18.0; platform_system != 'Emscripten'"]
7272
vertexai = ["google-auth>=2.36.0", "requests>=2.32.2"]
73-
google = ["google-genai>=1.50.1"]
73+
google = ["google-genai>=1.51.0"]
7474
anthropic = ["anthropic>=0.70.0"]
7575
groq = ["groq>=0.25.0"]
7676
mistral = ["mistralai>=1.9.10"]

tests/models/cassettes/test_google/test_google_builtin_tools_with_other_tools.yaml

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interactions:
88
connection:
99
- keep-alive
1010
content-length:
11-
- '526'
11+
- '560'
1212
content-type:
1313
- application/json
1414
host:
@@ -19,13 +19,16 @@ interactions:
1919
- parts:
2020
- text: What is the largest city in Mexico?
2121
role: user
22-
generationConfig: {}
22+
generationConfig:
23+
responseModalities:
24+
- TEXT
2325
systemInstruction:
2426
parts:
25-
- text: |-
27+
- text: |2
28+
2629
Always respond with a JSON object that's compatible with this schema:
2730
28-
{"properties": {"city": {"type": "string"}, "country": {"type": "string"}}, "required": ["city", "country"], "title": "CityLocation", "type": "object"}
31+
{"properties": {"city": {"type": "string"}, "country": {"type": "string"}}, "required": ["city", "country"], "type": "object", "title": "CityLocation"}
2932
3033
Don't include any text or Markdown fencing before or after.
3134
role: user
@@ -37,11 +40,11 @@ interactions:
3740
alt-svc:
3841
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
3942
content-length:
40-
- '626'
43+
- '595'
4144
content-type:
4245
- application/json; charset=UTF-8
4346
server-timing:
44-
- gfet4t7; dur=780
47+
- gfet4t7; dur=804
4548
transfer-encoding:
4649
- chunked
4750
vary:
@@ -58,15 +61,14 @@ interactions:
5861
groundingMetadata: {}
5962
index: 0
6063
modelVersion: gemini-2.5-flash
61-
responseId: 6Xq3aPnXNtqKqtsP8ZuDyAc
64+
responseId: BegcaeXaA7qgz7IP_qOzwAc
6265
usageMetadata:
6366
candidatesTokenCount: 13
64-
promptTokenCount: 83
67+
promptTokenCount: 85
6568
promptTokensDetails:
6669
- modality: TEXT
67-
tokenCount: 83
68-
thoughtsTokenCount: 33
69-
totalTokenCount: 129
70+
tokenCount: 85
71+
totalTokenCount: 98
7072
status:
7173
code: 200
7274
message: OK
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
interactions:
2+
- request:
3+
headers:
4+
accept:
5+
- '*/*'
6+
accept-encoding:
7+
- gzip, deflate
8+
connection:
9+
- keep-alive
10+
content-length:
11+
- '430'
12+
content-type:
13+
- application/json
14+
host:
15+
- generativelanguage.googleapis.com
16+
method: POST
17+
parsed_body:
18+
contents:
19+
- parts:
20+
- text: What is the largest city in Mexico?
21+
role: user
22+
generationConfig:
23+
responseMimeType: application/json
24+
responseModalities:
25+
- TEXT
26+
responseSchema:
27+
properties:
28+
city:
29+
type: STRING
30+
country:
31+
type: STRING
32+
property_ordering:
33+
- city
34+
- country
35+
required:
36+
- city
37+
- country
38+
title: CityLocation
39+
type: OBJECT
40+
tools:
41+
- urlContext: {}
42+
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-preview:generateContent
43+
response:
44+
headers:
45+
alt-svc:
46+
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
47+
content-length:
48+
- '1448'
49+
content-type:
50+
- application/json; charset=UTF-8
51+
server-timing:
52+
- gfet4t7; dur=2843
53+
transfer-encoding:
54+
- chunked
55+
vary:
56+
- Origin
57+
- X-Origin
58+
- Referer
59+
parsed_body:
60+
candidates:
61+
- content:
62+
parts:
63+
- text: "{\n \"city\": \"Mexico City\",\n \"country\": \"Mexico\"\n} "
64+
thoughtSignature: EtcECtQEAdHtim/CS2N3y71XJ9wjmj1QDm3Y7R7cun7pMFmxQC3zalFnbopV9EjBFsSRntkmA/QdmURQ7kc2e0K+uXmuzDnHk2h7mXyo9warZzg0cv8lOAyvcslmTZjg58ArKIf4hJ8o3f4qI0y36Pv4eDDO9EOya7C4resSA6qdQadCJRrZt7Jfms/KTZgVWpwm96hBmof1hTdkSjAEwKGxRsA3eWcTISBsECaVqAQw+bYFMT47O6M/29KnIhmxmojARpx43G8yV4pjKmgIjGxVmnS9TyHtEmU9iYr8LeREU7IXMUhoKp8alcNWFwqlxnbuOCwu0ar4IgxnPIk0Kfw9RoR00H+GW6WJaJfXPByyjPoR8ArmkDkG6fvKmRb+yG7S6Eq5ewHOHQIzWSZ+A4+Ngs4om04CpeSpDf0M7UlumQvzTyJE9ljkWbMcfEIL4Dv56Uj5dmbmNg71vnesDak1xSIu25EccJmhfptH18+vomIKd1EgEip+f1enoKiPN2rtk9biVdLgfAHjf5bpL5hAo40Q763cUs8nWRv/s/vYqGO/HL5+mZWheQMdg2hQmw6an0sIAWI+srpQMXz9PsLxSOc6H3yOPCOYkmG0yDtfuxe4X8HndoSmCF/C4Zu/1VmnWoZBhTFaPNyvlL1yL502Zp5qG/jYJ2gNIu78r89N33Yk3RVSrFWoNcB2z2DYY4EXCz8+1e1qyCPgQsNXVMFO2KO2CcmsssODEIDB0+d9ysiGuNW9Bc5dhW7Iy25s6mvHtQRb2S4a86kIJVP/yvUcwapiKk3slNY=
65+
role: model
66+
finishReason: STOP
67+
index: 0
68+
modelVersion: gemini-3-pro-preview
69+
responseId: I-scaZ7wFOiyqtsP0rvOoQc
70+
usageMetadata:
71+
candidatesTokenCount: 21
72+
promptTokenCount: 9
73+
promptTokensDetails:
74+
- modality: TEXT
75+
tokenCount: 9
76+
thoughtsTokenCount: 135
77+
totalTokenCount: 165
78+
status:
79+
code: 200
80+
message: OK
81+
- request:
82+
headers:
83+
accept:
84+
- '*/*'
85+
accept-encoding:
86+
- gzip, deflate
87+
connection:
88+
- keep-alive
89+
content-length:
90+
- '430'
91+
content-type:
92+
- application/json
93+
host:
94+
- generativelanguage.googleapis.com
95+
method: POST
96+
parsed_body:
97+
contents:
98+
- parts:
99+
- text: What is the largest city in Mexico?
100+
role: user
101+
generationConfig:
102+
responseMimeType: application/json
103+
responseModalities:
104+
- TEXT
105+
responseSchema:
106+
properties:
107+
city:
108+
type: STRING
109+
country:
110+
type: STRING
111+
property_ordering:
112+
- city
113+
- country
114+
required:
115+
- city
116+
- country
117+
title: CityLocation
118+
type: OBJECT
119+
tools:
120+
- urlContext: {}
121+
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-preview:generateContent
122+
response:
123+
headers:
124+
alt-svc:
125+
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
126+
content-length:
127+
- '1300'
128+
content-type:
129+
- application/json; charset=UTF-8
130+
server-timing:
131+
- gfet4t7; dur=2548
132+
transfer-encoding:
133+
- chunked
134+
vary:
135+
- Origin
136+
- X-Origin
137+
- Referer
138+
parsed_body:
139+
candidates:
140+
- content:
141+
parts:
142+
- text: "{\n \"city\": \"Mexico City\",\n \"country\": \"Mexico\"\n} "
143+
thoughtSignature: EugDCuUDAdHtim8d0XRoXBnV7jLdb4TtGjESPsOynIUtVoSXym1duR5bwzlaoUOkgu83FEscE1UlxByIXybHerIyzzvuCnBK+q3Z24cQ21mqWi0ITUcrix6I4oMTJuihgseowtOc45Z/P9gve+mTh1JlZPFQZRVuBXxxvFtGTeKYzl4R7yTndHRF2qJRcYLnl2EtaZmGDbvWyybdLgmcNrMrtKG8NEWF9yriL0GduayEPwOlPk8d2QpbMgV79PXGtvBQ7kmE1VpHL1Y7zdRsl2edVtlx+nwXnIZlim6QC+ff2lNxRBtqeyDxrESDbZuW4PTzBM1McHyg3HkR27zcxScs6JtMP1gNHxuVZCFkz1aP5uP0IyvqjFUR5LPfx1I/1eWL23C9TTkxxkaiyAIFnpq04ebWS/mcwKFpUxHRrRtK6Zvtxyb4/TmRwknx+T9U2PfPGLASLAxa/1G7cJh7HPpX4UTipM+6hNOJX5XjQo5FLHBsPyHzmFrVbyYFOT8pSqwDdqR+3QozY/y87GdKLgLTZjwO0UqPsAkO4lvnB6++NaGxeZWW7qsnH2gz+T9QAVT9BEq7pf67VFicOaP8MdDs3mII7D8vs7P4J+GqjoP2gTC4sIjwn8TiSR4fzjTNoHqkvfLOP9PZMAU=
144+
role: model
145+
finishReason: STOP
146+
index: 0
147+
modelVersion: gemini-3-pro-preview
148+
responseId: JuscaY35DYmymtkPr67luA0
149+
usageMetadata:
150+
candidatesTokenCount: 21
151+
promptTokenCount: 9
152+
promptTokensDetails:
153+
- modality: TEXT
154+
tokenCount: 9
155+
thoughtsTokenCount: 108
156+
totalTokenCount: 138
157+
status:
158+
code: 200
159+
message: OK
160+
version: 1

0 commit comments

Comments
 (0)