diff --git a/docs/models/google.md b/docs/models/google.md index f7fe3bba73..1f3aedb433 100644 --- a/docs/models/google.md +++ b/docs/models/google.md @@ -214,7 +214,7 @@ from pydantic_ai.models.google import GoogleModel, GoogleModelSettings settings = GoogleModelSettings( temperature=0.2, max_tokens=1024, - google_thinking_config={'thinking_budget': 2048}, + google_thinking_config={'thinking_level': 'low'}, google_safety_settings=[ { 'category': HarmCategory.HARM_CATEGORY_HATE_SPEECH, @@ -222,14 +222,14 @@ settings = GoogleModelSettings( } ] ) -model = GoogleModel('gemini-2.5-flash') +model = GoogleModel('gemini-2.5-pro') agent = Agent(model, model_settings=settings) ... ``` ### Disable thinking -You can disable thinking by setting the `thinking_budget` to `0` on the `google_thinking_config`: +On models older than Gemini 2.5 Pro, you can disable thinking by setting the `thinking_budget` to `0` on the `google_thinking_config`: ```python from pydantic_ai import Agent diff --git a/pydantic_ai_slim/pydantic_ai/models/__init__.py b/pydantic_ai_slim/pydantic_ai/models/__init__.py index 98214910bd..b43681b0a4 100644 --- a/pydantic_ai_slim/pydantic_ai/models/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/models/__init__.py @@ -145,24 +145,28 @@ 'cohere:command-r7b-12-2024', 'deepseek:deepseek-chat', 'deepseek:deepseek-reasoner', + 'google-gla:gemini-flash-latest', + 'google-gla:gemini-flash-lite-latest', 'google-gla:gemini-2.0-flash', 'google-gla:gemini-2.0-flash-lite', 'google-gla:gemini-2.5-flash', + 'google-gla:gemini-2.5-flash-preview-09-2025', + 'google-gla:gemini-2.5-flash-image', 'google-gla:gemini-2.5-flash-lite', 'google-gla:gemini-2.5-flash-lite-preview-09-2025', - 'google-gla:gemini-2.5-flash-preview-09-2025', 'google-gla:gemini-2.5-pro', - 'google-gla:gemini-flash-latest', - 'google-gla:gemini-flash-lite-latest', + 'google-gla:gemini-3-pro-preview', + 'google-vertex:gemini-flash-latest', + 'google-vertex:gemini-flash-lite-latest', 'google-vertex:gemini-2.0-flash', 'google-vertex:gemini-2.0-flash-lite', 'google-vertex:gemini-2.5-flash', + 'google-vertex:gemini-2.5-flash-preview-09-2025', + 'google-vertex:gemini-2.5-flash-image', 'google-vertex:gemini-2.5-flash-lite', 'google-vertex:gemini-2.5-flash-lite-preview-09-2025', - 'google-vertex:gemini-2.5-flash-preview-09-2025', 'google-vertex:gemini-2.5-pro', - 'google-vertex:gemini-flash-latest', - 'google-vertex:gemini-flash-lite-latest', + 'google-vertex:gemini-3-pro-preview', 'grok:grok-2-image-1212', 'grok:grok-2-vision-1212', 'grok:grok-3', diff --git a/pydantic_ai_slim/pydantic_ai/models/google.py b/pydantic_ai_slim/pydantic_ai/models/google.py index b3ee8731fa..f54eac84b2 100644 --- a/pydantic_ai_slim/pydantic_ai/models/google.py +++ b/pydantic_ai_slim/pydantic_ai/models/google.py @@ -38,6 +38,7 @@ VideoUrl, ) from ..profiles import ModelProfileSpec +from ..profiles.google import GoogleModelProfile from ..providers import Provider, infer_provider from ..settings import ModelSettings from ..tools import ToolDefinition @@ -93,15 +94,17 @@ ) from _import_error LatestGoogleModelNames = Literal[ + 'gemini-flash-latest', + 'gemini-flash-lite-latest', 'gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-2.5-flash', 'gemini-2.5-flash-preview-09-2025', - 'gemini-flash-latest', + 'gemini-2.5-flash-image', 'gemini-2.5-flash-lite', 'gemini-2.5-flash-lite-preview-09-2025', - 'gemini-flash-lite-latest', 'gemini-2.5-pro', + 'gemini-3-pro-preview', ] """Latest Gemini models.""" @@ -228,12 +231,17 @@ def system(self) -> str: def prepare_request( self, model_settings: ModelSettings | None, model_request_parameters: ModelRequestParameters ) -> tuple[ModelSettings | None, ModelRequestParameters]: + supports_native_output_with_builtin_tools = GoogleModelProfile.from_profile( + self.profile + ).google_supports_native_output_with_builtin_tools if model_request_parameters.builtin_tools and model_request_parameters.output_tools: if model_request_parameters.output_mode == 'auto': - model_request_parameters = replace(model_request_parameters, output_mode='prompted') + output_mode = 'native' if supports_native_output_with_builtin_tools else 'prompted' + model_request_parameters = replace(model_request_parameters, output_mode=output_mode) else: + output_mode = 'NativeOutput' if supports_native_output_with_builtin_tools else 'PromptedOutput' raise UserError( - 'Google does not support output tools and built-in tools at the same time. Use `output_type=PromptedOutput(...)` instead.' + f'Google does not support output tools and built-in tools at the same time. Use `output_type={output_mode}(...)` instead.' ) return super().prepare_request(model_settings, model_request_parameters) @@ -418,9 +426,9 @@ async def _build_content_and_config( response_mime_type = None response_schema = None if model_request_parameters.output_mode == 'native': - if tools: + if model_request_parameters.function_tools: raise UserError( - 'Google does not support `NativeOutput` and tools at the same time. Use `output_type=ToolOutput(...)` instead.' + 'Google does not support `NativeOutput` and function tools at the same time. Use `output_type=ToolOutput(...)` instead.' ) response_mime_type = 'application/json' output_object = model_request_parameters.output_object @@ -685,9 +693,15 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: for part in parts: if part.thought_signature: + # Per https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#thought-signatures: + # - Always send the thought_signature back to the model inside its original Part. + # - Don't merge a Part containing a signature with one that does not. This breaks the positional context of the thought. + # - Don't combine two Parts that both contain signatures, as the signature strings cannot be merged. + signature = base64.b64encode(part.thought_signature).decode('utf-8') + # Attach signature to most recent thinking part, if there was one yield self._parts_manager.handle_thinking_delta( - vendor_part_id='thinking', + vendor_part_id=None, signature=signature, provider_name=self.provider_name, ) @@ -695,13 +709,9 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: if part.text is not None: if len(part.text) > 0: if part.thought: - yield self._parts_manager.handle_thinking_delta( - vendor_part_id='thinking', content=part.text - ) + yield self._parts_manager.handle_thinking_delta(vendor_part_id=None, content=part.text) else: - maybe_event = self._parts_manager.handle_text_delta( - vendor_part_id='content', content=part.text - ) + maybe_event = self._parts_manager.handle_text_delta(vendor_part_id=None, content=part.text) if maybe_event is not None: # pragma: no branch yield maybe_event elif part.function_call: @@ -760,6 +770,7 @@ def timestamp(self) -> datetime: def _content_model_response(m: ModelResponse, provider_name: str) -> ContentDict: # noqa: C901 parts: list[PartDict] = [] thought_signature: bytes | None = None + function_call_requires_signature: bool = True for item in m.parts: part: PartDict = {} if thought_signature: @@ -769,6 +780,15 @@ def _content_model_response(m: ModelResponse, provider_name: str) -> ContentDict if isinstance(item, ToolCallPart): function_call = FunctionCallDict(name=item.tool_name, args=item.args_as_dict(), id=item.tool_call_id) part['function_call'] = function_call + if function_call_requires_signature and not part.get('thought_signature'): + # Per https://ai.google.dev/gemini-api/docs/gemini-3?thinking=high#migrating_from_other_models: + # > If you are transferring a conversation trace from another model (e.g., Gemini 2.5) or injecting + # > a custom function call that was not generated by Gemini 3, you will not have a valid signature. + # > To bypass strict validation in these specific scenarios, populate the field with this specific + # > dummy string: "thoughtSignature": "context_engineering_is_the_way_to_go" + part['thought_signature'] = b'context_engineering_is_the_way_to_go' + # Only the first function call requires a signature + function_call_requires_signature = False elif isinstance(item, TextPart): part['text'] = item.content elif isinstance(item, ThinkingPart): @@ -881,7 +901,7 @@ def _function_declaration_from_tool(tool: ToolDefinition) -> FunctionDeclaration f = FunctionDeclarationDict( name=tool.name, description=tool.description or '', - parameters=json_schema, # type: ignore + parameters_json_schema=json_schema, ) return f diff --git a/pydantic_ai_slim/pydantic_ai/profiles/google.py b/pydantic_ai_slim/pydantic_ai/profiles/google.py index 2e691bb42c..44caa7a83d 100644 --- a/pydantic_ai_slim/pydantic_ai/profiles/google.py +++ b/pydantic_ai_slim/pydantic_ai/profiles/google.py @@ -1,18 +1,34 @@ from __future__ import annotations as _annotations +from dataclasses import dataclass + from .._json_schema import JsonSchema, JsonSchemaTransformer from . import ModelProfile +@dataclass(kw_only=True) +class GoogleModelProfile(ModelProfile): + """Profile for models used with `GoogleModel`. + + ALL FIELDS MUST BE `google_` PREFIXED SO YOU CAN MERGE THEM WITH OTHER MODELS. + """ + + google_supports_native_output_with_builtin_tools: bool = False + """Whether the model supports native output with builtin tools. + See https://ai.google.dev/gemini-api/docs/structured-output?example=recipe#structured_outputs_with_tools""" + + def google_model_profile(model_name: str) -> ModelProfile | None: """Get the model profile for a Google model.""" is_image_model = 'image' in model_name - return ModelProfile( + is_3_or_newer = 'gemini-3' in model_name + return GoogleModelProfile( json_schema_transformer=GoogleJsonSchemaTransformer, supports_image_output=is_image_model, supports_json_schema_output=not is_image_model, supports_json_object_output=not is_image_model, supports_tools=not is_image_model, + google_supports_native_output_with_builtin_tools=is_3_or_newer, ) diff --git a/pydantic_ai_slim/pyproject.toml b/pydantic_ai_slim/pyproject.toml index d408bf88cc..6e815a4f52 100644 --- a/pydantic_ai_slim/pyproject.toml +++ b/pydantic_ai_slim/pyproject.toml @@ -70,7 +70,7 @@ logfire = ["logfire[httpx]>=3.14.1"] openai = ["openai>=1.107.2"] cohere = ["cohere>=5.18.0; platform_system != 'Emscripten'"] vertexai = ["google-auth>=2.36.0", "requests>=2.32.2"] -google = ["google-genai>=1.50.1"] +google = ["google-genai>=1.51.0"] anthropic = ["anthropic>=0.70.0"] groq = ["groq>=0.25.0"] mistral = ["mistralai>=1.9.10"] diff --git a/tests/models/cassettes/test_google/test_google_builtin_tools_with_other_tools.yaml b/tests/models/cassettes/test_google/test_google_builtin_tools_with_other_tools.yaml index 9dfd73584d..5a9d810ea3 100644 --- a/tests/models/cassettes/test_google/test_google_builtin_tools_with_other_tools.yaml +++ b/tests/models/cassettes/test_google/test_google_builtin_tools_with_other_tools.yaml @@ -8,7 +8,7 @@ interactions: connection: - keep-alive content-length: - - '526' + - '560' content-type: - application/json host: @@ -19,10 +19,13 @@ interactions: - parts: - text: What is the largest city in Mexico? role: user - generationConfig: {} + generationConfig: + responseModalities: + - TEXT systemInstruction: parts: - - text: |- + - text: |2 + Always respond with a JSON object that's compatible with this schema: {"properties": {"city": {"type": "string"}, "country": {"type": "string"}}, "required": ["city", "country"], "title": "CityLocation", "type": "object"} @@ -41,7 +44,7 @@ interactions: content-type: - application/json; charset=UTF-8 server-timing: - - gfet4t7; dur=780 + - gfet4t7; dur=873 transfer-encoding: - chunked vary: @@ -58,15 +61,15 @@ interactions: groundingMetadata: {} index: 0 modelVersion: gemini-2.5-flash - responseId: 6Xq3aPnXNtqKqtsP8ZuDyAc + responseId: 0f4caf7iM-mgz7IPtLeryAc usageMetadata: candidatesTokenCount: 13 - promptTokenCount: 83 + promptTokenCount: 85 promptTokensDetails: - modality: TEXT - tokenCount: 83 - thoughtsTokenCount: 33 - totalTokenCount: 129 + tokenCount: 85 + thoughtsTokenCount: 32 + totalTokenCount: 130 status: code: 200 message: OK diff --git a/tests/models/cassettes/test_google/test_google_native_output_with_builtin_tools_gemini_3.yaml b/tests/models/cassettes/test_google/test_google_native_output_with_builtin_tools_gemini_3.yaml new file mode 100644 index 0000000000..42e123b905 --- /dev/null +++ b/tests/models/cassettes/test_google/test_google_native_output_with_builtin_tools_gemini_3.yaml @@ -0,0 +1,154 @@ +interactions: +- request: + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '392' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + parsed_body: + contents: + - parts: + - text: What is the largest city in Mexico? + role: user + generationConfig: + responseJsonSchema: + properties: + city: + type: string + country: + type: string + required: + - city + - country + title: CityLocation + type: object + responseMimeType: application/json + responseModalities: + - TEXT + tools: + - urlContext: {} + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-preview:generateContent + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '1644' + content-type: + - application/json; charset=UTF-8 + server-timing: + - gfet4t7; dur=3110 + transfer-encoding: + - chunked + vary: + - Origin + - X-Origin + - Referer + parsed_body: + candidates: + - content: + parts: + - text: "{\n \"city\": \"Mexico City\",\n \"country\": \"Mexico\"\n} " + thoughtSignature: EukFCuYFAdHtim8YAgcmaXkJk0welBnul+NQG8lripeLswuCLyJgUQ9sZempxMF8X9qvMMXAO5yITYZteum84MSW7H7jeRhQY4iy5SOnGAar2wgjakknIs2RFGz+FZ0dqBA2nMMAUvGHQeYWhZepzathEi1RVIXV3ZRnzJT0s+9PGSnmWcmyvRT7c+vJSXwcFWBHTIEsahJJCttCuNWWpoXw+wHtPiTSoD/6/ySCmwxYFV6DjwS3NO/4FHB5PEJ61Q1Q6n65UK4CcAFxxHtTi8c/dsoE9maa8KMuWA+l4183JFzPLR+cHl0VlpaY1YRj04oX2/jcTVGnC8o3EIPuA0Du7/7iI6as2qZkBlBK4e7fFfKi/jDPnV6ym/lQNZJy7G9yo3Fjy+Ldd8TKgsmQsYewBp2qwLU7m7FLVAoVjc3kmkJFdVLo2jGCpQNjPqtNmI0KoxNk4kfbueWAlU2laLthohDNH9RbQ1v+fuOpo8qjz33MyICOYnjtsV0gA5kEecwzjWP/+MjISKgkydmveHybSmGes2+2Pkt8yRBnjyyM77uInZMbUJTF/Fq9gEDIPE78e44QFml2sgNnGbtKN2pbUm/tRhBS2/Gs2BBG/kUp5k6iPM9metgdbbUYcP8+TLvgNBIIVSZ3sZU0dvttz5fPqvJPUbbdaICjANnUliUOOmj/ls+J4coknZ38dnGN/yT2q3NTnHQZv1+MnQoGkKdD43D8rsRWXTC9iaY4sAUNp6FTuINZtYwOJh2UJzZwqyGg0FEpvuKFwY2kj/zXbG3oT5kuq91SEoKaDh8trlzNopA0kwk/gQuuIu6t/OITZChumVGkoJrQowBbYssMSbAgetul83uDNxBMmnYtz/GQuH/I22wg31Ib+S5kWLUsYEyCypZOnH/UPjfPUuLxs+xaCYH7vP/Xn4G4kS+l9czBl8LKSX2AzomZvS9LajZ7O8cTOJhT7xzhcVuX3AObc99+4Asm8L0aDDk/mg== + role: model + finishReason: STOP + index: 0 + modelVersion: gemini-3-pro-preview + responseId: y_4cae6RH57hz7IP4uqFmQU + usageMetadata: + candidatesTokenCount: 21 + promptTokenCount: 9 + promptTokensDetails: + - modality: TEXT + tokenCount: 9 + thoughtsTokenCount: 182 + totalTokenCount: 212 + status: + code: 200 + message: OK +- request: + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '392' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + parsed_body: + contents: + - parts: + - text: What is the largest city in Mexico? + role: user + generationConfig: + responseJsonSchema: + properties: + city: + type: string + country: + type: string + required: + - city + - country + title: CityLocation + type: object + responseMimeType: application/json + responseModalities: + - TEXT + tools: + - urlContext: {} + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-preview:generateContent + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '2572' + content-type: + - application/json; charset=UTF-8 + server-timing: + - gfet4t7; dur=4541 + transfer-encoding: + - chunked + vary: + - Origin + - X-Origin + - Referer + parsed_body: + candidates: + - content: + parts: + - text: "{\n \"city\": \"Mexico City\",\n \"country\": \"Mexico\"\n} " + thoughtSignature: EqMLCqALAdHtim+jI7fQjPStrf7/y3pfC+CS4lMq2aVW/oMJlMqJqf7n/sMV4tCD/k92nc3A6togSEQGJAK21UDL6NJbaYlAttskSz7YwV3XCezelUlUvL8vHfVDdoctiL/iVdDnO+ZTS4wTWJgVfJIwh4s7OG0+fXXY5LerPfTnOxL3F3HXafsg4lfb744T+LatOBSBn2SVTQvjDcvN8sf+h7dj7RnHYilunbLvC/8PwyCrmY5isqmUEv7S5wYY/YWY30VKf3ooT1dC1X4MhbEwsuQZs3/F7lOgQmIe4N13JwZYYGwlqdSkwIWJpFOK7Yuh3XqfcDVxp6RsiQZyrc2HZEtcUmfYwJ7/MHUkZu3NoQCzyrSTno0TUOP8jMW9Y3be17pJE/DyW+N/O6dz8dplsm4XvQRaswfe/dE/mKo6GnftAcgegNrfBsiR6jIk3E8D7z3NGKeWMs2/etWEmywOZ3ui3AXAIS80VYDKduE2HFXq9SJzuwYT1o7Ey5GUyU/j38Fhr/SIcEsavklfFLUKI+N9YH3+WNrnEkvPhgyqssD0W5wQJsoMf6F1EqgSwSnnB3CoVifgN4GC6Ss4/um0XA0QwtFSkKQHOdUQI5W97qAWpwU3V1o6v7FV8BR3Tzf1ozqQQyafK7DR4mjYTOCnLkDvg35jXJJqE8jgA6UYcahXo8U0cHyMaIUYajWX7hmLsaATQH1kzOihqZfYTMs7V86Jr7gIfXh0pcq7gVabwweYxA7egtNjdJAGs7UUM6PYK30M3iT8Pj6bYfsoKZyMkliQ9/bhigAIwXABp9T4avdx6xt/wNFTN59J+8W9+xyOfpnVioPaeCE/65DxqvEF05TcdfwddqLQ/4ILEH78p44XzUcUSs4AUDiKi36I5xH6FDms9YrwrhH9+iescS1XdIJG15VDqTLtmUiOFolLgzndO7V52KhAFhbj4LezV3kshytaVHdYfuzBhrSianaaH9iKhR7GMjoLC2H5jcEGYpqP1LR7fXstpuIu0YdZH+hhv0KkA64VG6Jag5qHhFKAhKruUbtLKuyrY3+PLmL5PCRZoZjHHbWaaGxQxbft2r7JF27HL1WU8zblWnuqtxPWhJ9nKGAkTGenTwI57r2Uv+o9WMCyBIj0yQR0iF3evYMlMQf8D+AV+xeoWuzljNYX2w5BQGjszeztwO2vQBr/si2EmPwG97evKQ6KRX+FfVCEqmoB8b6XSXW1MZDkAxJp3CHih6i/9ZSFVXlO3C5WO7egeVlngYJ/gd4vBIgMWxM1ojj2ny+hdJdTzSk/52IA7YiCd2ZMhTeb1Zg0TCI6fUY1IkRxCwuMst/RrssPZ9UxcaSFUQPH3/B1YfYnF09H8ou/cmfXoO18fh1aiQ8wC4be0O4UijDvdfRmjUdAUNw77IuGdl0xgUB756aiSSXD83uwM++MMU4zMwp3wrBNKrE2OJJ2l1ARLzQXYhNJ3SM6E9b2EU5iENRahRvyYeA0pNbJO4VPr4odzIuTGDRQxiknwAw6vkfv7dwQyxGLmBT8kAQ1B2bDzZCz2SEmWPgPH2M8Rmir7AzlZNMj+Y2CuGLgTTOQfjAhr/MdLhC9wpaJCqiwwXYZMaPtr0+nZWmGuBKU6k8K8hfQj1WoKIrVjzrr02AJovugnSa5SGeU8Kc5JgrmENqeYjevs+E9seQRJwW+FBkJSFYAjWUUzSyjjGcCXWg+FeKTdvFKoxyNsuX84K3sPoW1J6LrgH0+bk6jhLqztQbWqLgwyRbfyW/gT1bzvfFX6rxfhyi3b/ckl7bYYUWOS9/jTGR2sqMcmE6BjCommVDMtUW1/Wxc2Wx9QAttL5X/kNS3brGT6ohWjbMqPWA/sIA2h01SgKZOPjxA5vSx6Axr5IVShVNaZDoYGZMBiNQILO9lgti4UbCetEJ5a8UK + role: model + finishReason: STOP + index: 0 + modelVersion: gemini-3-pro-preview + responseId: 0P4caYX6HI-Ez7IPopWIyQc + usageMetadata: + candidatesTokenCount: 21 + promptTokenCount: 9 + promptTokensDetails: + - modality: TEXT + tokenCount: 9 + thoughtsTokenCount: 342 + totalTokenCount: 372 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/cassettes/test_google/test_thinking_with_tool_calls_from_other_model.yaml b/tests/models/cassettes/test_google/test_thinking_with_tool_calls_from_other_model.yaml new file mode 100644 index 0000000000..1cef739561 --- /dev/null +++ b/tests/models/cassettes/test_google/test_thinking_with_tool_calls_from_other_model.yaml @@ -0,0 +1,372 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '330' + content-type: + - application/json + host: + - api.openai.com + method: POST + parsed_body: + include: + - reasoning.encrypted_content + input: + - content: What is the capital of the country? + role: user + model: gpt-5 + stream: false + tool_choice: auto + tools: + - description: null + name: get_country + parameters: + additionalProperties: false + properties: {} + type: object + strict: false + type: function + uri: https://api.openai.com/v1/responses + response: + headers: + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '3534' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '2957' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + background: false + billing: + payer: developer + created_at: 1763507707 + error: null + id: resp_04b26c9215811c7900691cfdfbd87c81a3b0091d3922488718 + incomplete_details: null + instructions: null + max_output_tokens: null + max_tool_calls: null + metadata: {} + model: gpt-5-2025-08-07 + object: response + output: + - encrypted_content: gAAAAABpHP3-7jMMMgFGJNuuDSXmU7RorU0Yc4KcvOzmB5E7vahRvUJ110C0d9qXQwPpzKk17FyP7QlCbsxAuuDmx4FFO8DXpmeRyf_AHnOmluIn9E7RCPaJUeYd9u7QcTxuLryGhv0wCwhKnwrcAjnDbbSXkh1U9hNCIrhaq0W4z2mDBbIwa8jCYprDpb_3rRj-vNsslcVfhP6PSJuZiHTpeGgeuOgSaBGNbGE-r51wSIU_Re0XSu4Wy1UUNrKTMKyks2JoiKjfE3kWpAh7Ycm0JSbFrnoGXPiThLGS12dMzrL_4rSlFm8fgqHcQ1PP4KG9tT7l6r5Cq4VP2SbC9W68P-2dVSztNm1lbkgAmyl9KW88l9qZkTXY087qnC_tJ0_AXwPnn_WBVK__1liZsiaPEzTXQO7xsiBGSDubvz_nQR0Xi36lLAwim4-f5Vqsa-WQCUE1z33qSSIofCIwKtU7y4NqLrqKVjNzjUkavxHRd65gZmTWnlzGJnv5GXSGRPDyk9OhO0QoHTXlSIxbuskytYjZ2nMgblS6jGGR69Purq-90yLr2635D_Zjx95N7ZHkp7ckHVZYgPtTyXzMVzjI82n_YFvR0KQjBUhV8ubAW_AwiV7ml8S0PU5UU1VSTRC5JF1D9ZxsHFIlvgPbe7xAp8ulxBkaCG8VeeM_suNK8ehBTfCduNq0l1Spt7O9KUmzgUlpzx1oDTNO75heUNKNPVhCFsjCDl9pMG1XFJmsFROpMb82blwYyb8C3N4JJm3j0a23YN7jrokOpMIWd4zXyHnfsyVvXe-qaaXFZ9j25n-FTTZ2wLGHPog2M1SwJF5b8Vu2hpNTtsW05WEhddTMDMgg8YvZkuxxmIES9X6E4WnLEMUcyoG5k32ytkP-yF3nayKsg0xCXNRZZTtJUQUZQfimQEDc9OOjZOj6ggj4Y_K3vj7flwhQ9P-ZZ5YSGPSjN1LdGGYY7OJd-bSSFSdIOvROTnJ_2-Fghe6rykx4DVTw2pp8iapm9zfCKdEpa7AenjskBNn3YYRZ4sn0nxt_5wrZSbnyhkDz-NQ1IYwWjyBU97Sb1bmJyZ-2dKnvv7sKIyxyp_1pYQYTPJs12cfndLEcoiKLRJGWzK61-CtA6q18H6kSNW0BerJT3EFWRHliQWhoJOq5wQnlqtaexg6aRYA6FKcv38SLMYDUiOOHGS9ojmtoT3FMS81uO36SiHWNG_iCpkHvX3q2fsH1Q76VGWwGdvM7orV7AXlWYGonsKOa-vUkqBliAzITGu95G2ZvNEmecrqV9XuMgTCH5FULP9jnaU8J7bNiiUFEEMnJZiQL2cvdtPK9soAPZXGKSESVCvLnk52mkyLlVPrmVsiN19QRT84l8PpsifvgNMuQ1hKjjSamJvPC3UnWg0RQ4BFXmEu2tLAgedZ_5D5zhx63ZFE_QXYFZz3nM-M6cAig1lil2z6Yw4xE2gA7gK_Y2akNg3irvnXXBFIq2SkipIJpzrBwCUfqN4UFnJTWzgoUx6u3OCLP_GNiYGbMFeGYhVnPjHeJrGoepeAyXkq-F7FvCEO2ClONAixMz0fMJbVr3lGlzvwKeKOK7oVPTtZSrgVbhmcws8J3KExEg7izNhZgdt5udxPpnj0txsgLaYzWEb_LdubeAgyJocayhMoqzbmw0kK9uTZlrsLtZkDA4xfJ4Q8AbhSEwZucGM0ROEz3w94yYjq1--YxfQ7FeyTHj_lIthAa6-YhiQD-IuxjgUFu1cXbCCKaEw== + id: rs_04b26c9215811c7900691cfdfc923481a3aa441c74099dbb41 + summary: [] + type: reasoning + - arguments: '{}' + call_id: call_hmYRgPAT7F1QVPF4fYgzZiKd + id: fc_04b26c9215811c7900691cfdfe98e081a3b7846f465ec0b0ae + name: get_country + status: completed + type: function_call + parallel_tool_calls: true + previous_response_id: null + prompt_cache_key: null + prompt_cache_retention: null + reasoning: + effort: medium + summary: null + safety_identifier: null + service_tier: default + status: completed + store: true + temperature: 1.0 + text: + format: + type: text + verbosity: medium + tool_choice: auto + tools: + - description: null + name: get_country + parameters: + additionalProperties: false + properties: {} + type: object + strict: false + type: function + top_logprobs: 0 + top_p: 1.0 + truncation: disabled + usage: + input_tokens: 37 + input_tokens_details: + cached_tokens: 0 + output_tokens: 144 + output_tokens_details: + reasoning_tokens: 128 + total_tokens: 181 + user: null + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '2470' + content-type: + - application/json + cookie: + - __cf_bm=r14Om.0DpcmrBz8tj783Gvs33gasNZ7UaG7IFdlsArQ-1763507710-1.0.1.1-7fYQrFuRizqyG6XcvzHeCVzY0kHno5odRJ6wg2X4Jj1pV71CuuweXXiZvlKPD_QM7udr729mUbkdhIH2yP_nTjA8Jn4kbWZn5lDf91YBEmo; + _cfuvid=fZKvzCKZ4uj3gJktc4vJo.lRIUxZCVLJ78epmTIYXj0-1763507710828-0.0.1.1-604800000 + host: + - api.openai.com + method: POST + parsed_body: + include: + - reasoning.encrypted_content + input: + - content: What is the capital of the country? + role: user + - encrypted_content: gAAAAABpHP3-7jMMMgFGJNuuDSXmU7RorU0Yc4KcvOzmB5E7vahRvUJ110C0d9qXQwPpzKk17FyP7QlCbsxAuuDmx4FFO8DXpmeRyf_AHnOmluIn9E7RCPaJUeYd9u7QcTxuLryGhv0wCwhKnwrcAjnDbbSXkh1U9hNCIrhaq0W4z2mDBbIwa8jCYprDpb_3rRj-vNsslcVfhP6PSJuZiHTpeGgeuOgSaBGNbGE-r51wSIU_Re0XSu4Wy1UUNrKTMKyks2JoiKjfE3kWpAh7Ycm0JSbFrnoGXPiThLGS12dMzrL_4rSlFm8fgqHcQ1PP4KG9tT7l6r5Cq4VP2SbC9W68P-2dVSztNm1lbkgAmyl9KW88l9qZkTXY087qnC_tJ0_AXwPnn_WBVK__1liZsiaPEzTXQO7xsiBGSDubvz_nQR0Xi36lLAwim4-f5Vqsa-WQCUE1z33qSSIofCIwKtU7y4NqLrqKVjNzjUkavxHRd65gZmTWnlzGJnv5GXSGRPDyk9OhO0QoHTXlSIxbuskytYjZ2nMgblS6jGGR69Purq-90yLr2635D_Zjx95N7ZHkp7ckHVZYgPtTyXzMVzjI82n_YFvR0KQjBUhV8ubAW_AwiV7ml8S0PU5UU1VSTRC5JF1D9ZxsHFIlvgPbe7xAp8ulxBkaCG8VeeM_suNK8ehBTfCduNq0l1Spt7O9KUmzgUlpzx1oDTNO75heUNKNPVhCFsjCDl9pMG1XFJmsFROpMb82blwYyb8C3N4JJm3j0a23YN7jrokOpMIWd4zXyHnfsyVvXe-qaaXFZ9j25n-FTTZ2wLGHPog2M1SwJF5b8Vu2hpNTtsW05WEhddTMDMgg8YvZkuxxmIES9X6E4WnLEMUcyoG5k32ytkP-yF3nayKsg0xCXNRZZTtJUQUZQfimQEDc9OOjZOj6ggj4Y_K3vj7flwhQ9P-ZZ5YSGPSjN1LdGGYY7OJd-bSSFSdIOvROTnJ_2-Fghe6rykx4DVTw2pp8iapm9zfCKdEpa7AenjskBNn3YYRZ4sn0nxt_5wrZSbnyhkDz-NQ1IYwWjyBU97Sb1bmJyZ-2dKnvv7sKIyxyp_1pYQYTPJs12cfndLEcoiKLRJGWzK61-CtA6q18H6kSNW0BerJT3EFWRHliQWhoJOq5wQnlqtaexg6aRYA6FKcv38SLMYDUiOOHGS9ojmtoT3FMS81uO36SiHWNG_iCpkHvX3q2fsH1Q76VGWwGdvM7orV7AXlWYGonsKOa-vUkqBliAzITGu95G2ZvNEmecrqV9XuMgTCH5FULP9jnaU8J7bNiiUFEEMnJZiQL2cvdtPK9soAPZXGKSESVCvLnk52mkyLlVPrmVsiN19QRT84l8PpsifvgNMuQ1hKjjSamJvPC3UnWg0RQ4BFXmEu2tLAgedZ_5D5zhx63ZFE_QXYFZz3nM-M6cAig1lil2z6Yw4xE2gA7gK_Y2akNg3irvnXXBFIq2SkipIJpzrBwCUfqN4UFnJTWzgoUx6u3OCLP_GNiYGbMFeGYhVnPjHeJrGoepeAyXkq-F7FvCEO2ClONAixMz0fMJbVr3lGlzvwKeKOK7oVPTtZSrgVbhmcws8J3KExEg7izNhZgdt5udxPpnj0txsgLaYzWEb_LdubeAgyJocayhMoqzbmw0kK9uTZlrsLtZkDA4xfJ4Q8AbhSEwZucGM0ROEz3w94yYjq1--YxfQ7FeyTHj_lIthAa6-YhiQD-IuxjgUFu1cXbCCKaEw== + id: rs_04b26c9215811c7900691cfdfc923481a3aa441c74099dbb41 + summary: [] + type: reasoning + - arguments: '{}' + call_id: call_hmYRgPAT7F1QVPF4fYgzZiKd + id: fc_04b26c9215811c7900691cfdfe98e081a3b7846f465ec0b0ae + name: get_country + type: function_call + - call_id: call_hmYRgPAT7F1QVPF4fYgzZiKd + output: Mexico + type: function_call_output + model: gpt-5 + stream: false + tool_choice: auto + tools: + - description: null + name: get_country + parameters: + additionalProperties: false + properties: {} + type: object + strict: false + type: function + uri: https://api.openai.com/v1/responses + response: + headers: + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '3750' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '3371' + openai-project: + - proj_dKobscVY9YJxeEaDJen54e3d + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + background: false + billing: + payer: developer + created_at: 1763507711 + error: null + id: resp_04b26c9215811c7900691cfdfefa5481a3a94e859e7afcb43c + incomplete_details: null + instructions: null + max_output_tokens: null + max_tool_calls: null + metadata: {} + model: gpt-5-2025-08-07 + object: response + output: + - encrypted_content: gAAAAABpHP4CsHT1-L3kzyepL4Oz3mHNvIJJoUUjWV7PncjSfecCWhKX-0AC50bNWjIXIeZp6NlTJUqJ5hF8xEP1IP_XvUGdAlsu9tm-CxS0ZTr4gFn1T1hG0PqE3sfICs_JOq2XODZ0MYCu78kxMpFElPa1MFuVt4TsQDNGIo-Xg_XYsfcjqMrS5oPfa52OolINUVkyIAv_pDu3VszrUXr_TS22rSQ2JdwegXMM-0Foq0pqZbxbp-NEmnN4duBh6SewhYBgE7iuy6_Tq7R3oPb8YGZEaWBhnNEtnO6cM1gdMHpceb7ngQNpkMkwDjpwDkLBFuJNLiGLyt2AL5-2e7C7oXiRYGWqh470-UeQHSFrbWKklzVVdEZYBvmqCmSen56D8pZcfjqA7KqceR3el85QN1Zg2HIDlkT1aAff_QfclfykfM3MN0ZUHVM2_1kRoJDsZYA-bbA9zoF5j_oabO2dWa5ftLjhCjZ7IKUNJRNGsFFj_U1URDbLfnDvcIgzWiO4LtjehsXkQR82BA0NPKJwPCsCQRzvLJ6qBNelVg027ccX5B7qVoUkfORDNZ8_6O-XBV4GNAP5gpjOpa8jkV_81MWFy5pFpuHYF5s5cx83DkAEZXdcamP0YtFQjamm63UErbhtyHqsEkElDSHutAGEU2c4KZiAXBq5JoDK6IO6b2u8im2Pp9-fXHJu0GL4pfpcW5ipRwG67HpOjAdMm5OZBEtqKuFlHMdBc2byrVkxDetXy0Dw1V696hgKx1nyHPZxMhvUkebTZhet58r2qubkoPtLn9lsxmaL81OZ1wpkYbyZg2vcyMbV2YxQYzbHkYjk4Fm7r1RApLzNuIxuzVohuzX8AoX7BqOgdp1lmuumWROFaTxdz1kIMPCKoLrNcMc4W0gSXneHCIWPkxN1xObnt_KmNRROEg6SK0YsM3nvJjfcf-7EZ03S_f7E7b1k15NYTQKx0GpcFQwKqw3rTO2hRsiM0jiAk3JqxTZAoBQkbobUOea78mY-bOQDIk7Lxo4C2O4or9uOQ7HtODk07M4ZN-KtuqyFiF-Fg5XgFyVzqP0_VfmyXYP9WJSD2SJWpAGBe-AJ9-f0Cts_zL13NPTcVLPGyO7vOCuUOqa44nXdmS-xJi9NELknARbI5C3q2pN9bI97tHqviqEjGDxIEZ7yvsCcpuSdxhC73Vb_YYwLJLbzB-qJ4pc6oix69bIZsOQ2k9oxHPOWy2cFgXYU9zpV8xRpSov3s6QmOzh0FM_ZnAFZRN32mZ6qkFxCMGPsZfriyaATCzrrvfkKPOKnAQAevo6tTsOLBARrkDv6LeWDzaFRGYXXdAyyflbwTxHahzkO1mGTfHWTpW7hK7HQFkKAvvG2nxh6RHsuv9Job4QUIqvXR6aRooZdEdr9FlSq6bAH39NgrL4u9Zf9bDeXendQaY6KAVE2j4-p2wCOGx2-e613FR-w7XnbU3XsK45fDgyI-E0H559RJxs4HURWQ94PBxpnM_c8fbK6KroVxLmlIaMyOmVBeH5wgdvoREgK4zxpXr6zfBem7qPoJbw0oGTtocbxTWKsjqJJRnKapyfK3zvdKNSa6leKwL1PmEIEVAbX52wYL0jhXm9OaSqv8jjmprZlZ56IevbWKllcXLoWUh_Yqa0-gJrotKe4censDhgS8qwRB0WQVX1M55Ad-nC89s9K-mbZBIWwqRaEQ4wZWM7ojlsFpZTFAdMEvHTbS4gSlrXQOYYbRZgD_iK0uI2iqEBnZoahWsS_dp9WGYuPDlee3VS_nLAkjSV7VzBurSkwHnZZBmslJEnmA778QZ5tX7iRALPepcXD6i1eAgTkXkcO2PDzs80ZU4DFAPnIsvSnf3ynwtRu + id: rs_04b26c9215811c7900691cfdff96fc81a380dd7550e3e883a2 + summary: [] + type: reasoning + - content: + - annotations: [] + logprobs: [] + text: Mexico City (Ciudad de México). + type: output_text + id: msg_04b26c9215811c7900691cfe021a5c81a39010d5c07746a330 + role: assistant + status: completed + type: message + parallel_tool_calls: true + previous_response_id: null + prompt_cache_key: null + prompt_cache_retention: null + reasoning: + effort: medium + summary: null + safety_identifier: null + service_tier: default + status: completed + store: true + temperature: 1.0 + text: + format: + type: text + verbosity: medium + tool_choice: auto + tools: + - description: null + name: get_country + parameters: + additionalProperties: false + properties: {} + type: object + strict: false + type: function + top_logprobs: 0 + top_p: 1.0 + truncation: disabled + usage: + input_tokens: 206 + input_tokens_details: + cached_tokens: 0 + output_tokens: 141 + output_tokens_details: + reasoning_tokens: 128 + total_tokens: 347 + user: null + status: + code: 200 + message: OK +- request: + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '1061' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + parsed_body: + contents: + - parts: + - text: What is the capital of the country? + role: user + - parts: + - functionCall: + args: {} + id: call_hmYRgPAT7F1QVPF4fYgzZiKd + name: get_country + thoughtSignature: Y29udGV4dF9lbmdpbmVlcmluZ19pc190aGVfd2F5X3RvX2dv + role: model + - parts: + - functionResponse: + id: call_hmYRgPAT7F1QVPF4fYgzZiKd + name: get_country + response: + return_value: Mexico + role: user + generationConfig: + responseModalities: + - TEXT + toolConfig: + functionCallingConfig: + allowedFunctionNames: + - get_country + - final_result + mode: ANY + tools: + - functionDeclarations: + - description: '' + name: get_country + parameters_json_schema: + additionalProperties: false + properties: {} + type: object + - description: The final response which ends this conversation + name: final_result + parameters_json_schema: + properties: + city: + type: string + country: + type: string + required: + - city + - country + title: CityLocation + type: object + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-preview:generateContent + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '1259' + content-type: + - application/json; charset=UTF-8 + server-timing: + - gfet4t7; dur=1935 + transfer-encoding: + - chunked + vary: + - Origin + - X-Origin + - Referer + parsed_body: + candidates: + - content: + parts: + - functionCall: + args: + city: Mexico City + country: Mexico + name: final_result + thoughtSignature: EsYCCsMCAdHtim/P+x13Eq3lUYgRDz8XYnqaHKD/Bt4GaUssLGOXQTU60cCcqOb1SnqKi8PD5bcVc0yBYidk4of+SP4M3clu75M3zQ5GS24SImMIimCKguTc+O6MiWvZezzkBDi1x74eBlDz0uOMz8EnMpSSyGSuB447xD/TAXkuUJr7B4PTgs+nDvfd36/tqufLL5Rk/GwBNKcsFNv4EBQP+AyjjC/Q4L6UDpRfLA4gyIjQdJf5cktxujhr+tzaQ5KNUIx3hCyzmw8jYio+kOUXqcEEzhGeHyWS9XzciyBWB6KSHc+UhgrjG7O6SGTFOZ7gW6590Z9acd1+DfM+CCZnWmzKz/fCOaRBTaVMlpagiOoAHzhGYclO4HY2czKOvFo8YUUpY6HgDGzBLvuNC4LSdOsJnq/kQYWGR8mxp4tEvQKEsOPa01E= + role: model + finishMessage: Model generated function call(s). + finishReason: STOP + index: 0 + modelVersion: gemini-3-pro-preview + responseId: BP4caYjhKs2Fz7IP_IXfgAk + usageMetadata: + candidatesTokenCount: 23 + promptTokenCount: 114 + promptTokensDetails: + - modality: TEXT + tokenCount: 114 + thoughtsTokenCount: 68 + totalTokenCount: 205 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/test_google.py b/tests/models/test_google.py index 7b5f3a9aac..b2d1af331d 100644 --- a/tests/models/test_google.py +++ b/tests/models/test_google.py @@ -2390,7 +2390,7 @@ async def get_user_country() -> str: with pytest.raises( UserError, match=re.escape( - 'Google does not support `NativeOutput` and tools at the same time. Use `output_type=ToolOutput(...)` instead.' + 'Google does not support `NativeOutput` and function tools at the same time. Use `output_type=ToolOutput(...)` instead.' ), ): await agent.run('What is the largest city in the user country?') @@ -2797,6 +2797,37 @@ class CityLocation(BaseModel): assert result.output == snapshot(CityLocation(city='Mexico City', country='Mexico')) +async def test_google_native_output_with_builtin_tools_gemini_3( + allow_model_requests: None, google_provider: GoogleProvider +): + m = GoogleModel('gemini-3-pro-preview', provider=google_provider) + + class CityLocation(BaseModel): + city: str + country: str + + agent = Agent(m, output_type=ToolOutput(CityLocation), builtin_tools=[UrlContextTool()]) + + with pytest.raises( + UserError, + match=re.escape( + 'Google does not support output tools and built-in tools at the same time. Use `output_type=NativeOutput(...)` instead.' + ), + ): + await agent.run('What is the largest city in Mexico?') + + agent = Agent(m, output_type=NativeOutput(CityLocation), builtin_tools=[UrlContextTool()]) + + result = await agent.run('What is the largest city in Mexico?') + assert result.output == snapshot(CityLocation(city='Mexico City', country='Mexico')) + + # Will default to native output + agent = Agent(m, output_type=CityLocation, builtin_tools=[UrlContextTool()]) + + result = await agent.run('What is the largest city in Mexico?') + assert result.output == snapshot(CityLocation(city='Mexico City', country='Mexico')) + + async def test_google_image_generation(allow_model_requests: None, google_provider: GoogleProvider): m = GoogleModel('gemini-2.5-flash-image', provider=google_provider) agent = Agent(m, output_type=BinaryImage) @@ -3518,6 +3549,140 @@ async def test_cache_point_filtering(): assert content[1] == {'text': 'text after'} +async def test_thinking_with_tool_calls_from_other_model( + allow_model_requests: None, google_provider: GoogleProvider, openai_api_key: str +): + openai_model = OpenAIResponsesModel('gpt-5', provider=OpenAIProvider(api_key=openai_api_key)) + + class CityLocation(BaseModel): + city: str + country: str + + agent = Agent() + + @agent.tool_plain + def get_country() -> str: + return 'Mexico' + + result = await agent.run('What is the capital of the country?', model=openai_model) + assert result.output == snapshot('Mexico City (Ciudad de México).') + messages = result.all_messages() + assert messages == snapshot( + [ + ModelRequest( + parts=[ + UserPromptPart( + content='What is the capital of the country?', + timestamp=IsDatetime(), + ) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ThinkingPart( + content='', + id=IsStr(), + signature=IsStr(), + provider_name='openai', + ), + ToolCallPart( + tool_name='get_country', + args='{}', + tool_call_id=IsStr(), + id=IsStr(), + ), + ], + usage=RequestUsage(input_tokens=37, output_tokens=144, details={'reasoning_tokens': 128}), + model_name='gpt-5-2025-08-07', + timestamp=IsDatetime(), + provider_name='openai', + provider_details={'finish_reason': 'completed'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ModelRequest( + parts=[ + ToolReturnPart( + tool_name='get_country', + content='Mexico', + tool_call_id=IsStr(), + timestamp=IsDatetime(), + ) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ThinkingPart( + content='', + id=IsStr(), + signature=IsStr(), + provider_name='openai', + ), + TextPart( + content='Mexico City (Ciudad de México).', + id=IsStr(), + ), + ], + usage=RequestUsage(input_tokens=206, output_tokens=141, details={'reasoning_tokens': 128}), + model_name='gpt-5-2025-08-07', + timestamp=IsDatetime(), + provider_name='openai', + provider_details={'finish_reason': 'completed'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + model = GoogleModel('gemini-3-pro-preview', provider=google_provider) + + result = await agent.run(model=model, message_history=messages[:-1], output_type=CityLocation) + assert result.output == snapshot(CityLocation(city='Mexico City', country='Mexico')) + assert result.new_messages() == snapshot( + [ + ModelResponse( + parts=[ + ThinkingPart( + content='', + signature=IsStr(), + provider_name='google-gla', + ), + ToolCallPart( + tool_name='final_result', + args={'city': 'Mexico City', 'country': 'Mexico'}, + tool_call_id=IsStr(), + ), + ], + usage=RequestUsage( + input_tokens=114, output_tokens=91, details={'thoughts_tokens': 68, 'text_prompt_tokens': 114} + ), + model_name='gemini-3-pro-preview', + timestamp=IsDatetime(), + provider_name='google-gla', + provider_details={'finish_reason': 'STOP'}, + provider_response_id=IsStr(), + finish_reason='stop', + run_id=IsStr(), + ), + ModelRequest( + parts=[ + ToolReturnPart( + tool_name='final_result', + content='Final result processed.', + tool_call_id=IsStr(), + timestamp=IsDatetime(), + ) + ], + run_id=IsStr(), + ), + ] + ) + + @pytest.mark.parametrize( 'error_class,error_response,expected_status', [ diff --git a/tests/models/test_model_names.py b/tests/models/test_model_names.py index b27aa2d8c2..058ffe28c7 100644 --- a/tests/models/test_model_names.py +++ b/tests/models/test_model_names.py @@ -15,7 +15,7 @@ from pydantic_ai.models.anthropic import AnthropicModelName from pydantic_ai.models.bedrock import BedrockModelName from pydantic_ai.models.cohere import CohereModelName - from pydantic_ai.models.gemini import GeminiModelName + from pydantic_ai.models.google import GoogleModelName from pydantic_ai.models.groq import GroqModelName from pydantic_ai.models.huggingface import HuggingFaceModelName from pydantic_ai.models.mistral import MistralModelName @@ -60,8 +60,8 @@ def get_model_names(model_name_type: Any) -> Iterator[str]: anthropic_names = [f'anthropic:{n}' for n in get_model_names(AnthropicModelName)] cohere_names = [f'cohere:{n}' for n in get_model_names(CohereModelName)] - google_names = [f'google-gla:{n}' for n in get_model_names(GeminiModelName)] + [ - f'google-vertex:{n}' for n in get_model_names(GeminiModelName) + google_names = [f'google-gla:{n}' for n in get_model_names(GoogleModelName)] + [ + f'google-vertex:{n}' for n in get_model_names(GoogleModelName) ] grok_names = [f'grok:{n}' for n in get_model_names(GrokModelName)] groq_names = [f'groq:{n}' for n in get_model_names(GroqModelName)] diff --git a/uv.lock b/uv.lock index deec3732f8..b9bb2f6a6d 100644 --- a/uv.lock +++ b/uv.lock @@ -1996,7 +1996,7 @@ wheels = [ [[package]] name = "google-genai" -version = "1.50.1" +version = "1.51.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2008,9 +2008,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/74/1382f655a8c24adc2811f113018ff2b3884f333284ba9bff5c57f8dbcbba/google_genai-1.50.1.tar.gz", hash = "sha256:8f0d95b1b165df71e6a7e1c0d0cadb5fad30f913f42c6b131b9ebb504eec0e5f", size = 254693, upload-time = "2025-11-13T23:17:22.526Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/1c/29245699c7c274ed5709b33b6a5192af2d57da5da3d2f189f222d1895336/google_genai-1.51.0.tar.gz", hash = "sha256:596c1ec964b70fec17a6ccfe6ee4edede31022584e8b1d33371d93037c4001b1", size = 258060, upload-time = "2025-11-18T05:32:47.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/6b/78a7588d9a4f6c8c8ed326a32385d0566a3262c91c3f7a005e4231207894/google_genai-1.50.1-py3-none-any.whl", hash = "sha256:15ae694b080269c53d325dcce94622f33e94cf81bd2123f029ab77e6b8f09eab", size = 257324, upload-time = "2025-11-13T23:17:21.259Z" }, + { url = "https://files.pythonhosted.org/packages/c6/28/0185dcda66f1994171067cfdb0e44a166450239d5b11b3a8a281dd2da459/google_genai-1.51.0-py3-none-any.whl", hash = "sha256:bfb7d0c6ba48ba9bda539f0d5e69dad827d8735a8b1e4703bafa0a2945d293e1", size = 260483, upload-time = "2025-11-18T05:32:45.938Z" }, ] [[package]] @@ -5670,7 +5670,7 @@ requires-dist = [ { name = "fastmcp", marker = "extra == 'fastmcp'", specifier = ">=2.12.0" }, { name = "genai-prices", specifier = ">=0.0.40" }, { name = "google-auth", marker = "extra == 'vertexai'", specifier = ">=2.36.0" }, - { name = "google-genai", marker = "extra == 'google'", specifier = ">=1.50.1" }, + { name = "google-genai", marker = "extra == 'google'", specifier = ">=1.51.0" }, { name = "griffe", specifier = ">=1.3.2" }, { name = "groq", marker = "extra == 'groq'", specifier = ">=0.25.0" }, { name = "httpx", specifier = ">=0.27" },