1414from .._output import OutputObjectDefinition
1515from .._run_context import RunContext
1616from ..builtin_tools import CodeExecutionTool , ImageGenerationTool , UrlContextTool , WebSearchTool
17- from ..exceptions import UserError
17+ from ..exceptions import ModelHTTPError , UserError
1818from ..messages import (
1919 BinaryContent ,
2020 BuiltinToolCallPart ,
3838 VideoUrl ,
3939)
4040from ..profiles import ModelProfileSpec
41+ from ..profiles .google import GoogleModelProfile
4142from ..providers import Provider , infer_provider
4243from ..settings import ModelSettings
4344from ..tools import ToolDefinition
5152)
5253
5354try :
54- from google .genai import Client
55+ from google .genai import Client , errors
5556 from google .genai .types import (
5657 BlobDict ,
5758 CodeExecutionResult ,
9394 ) from _import_error
9495
9596LatestGoogleModelNames = Literal [
97+ 'gemini-flash-latest' ,
98+ 'gemini-flash-lite-latest' ,
9699 'gemini-2.0-flash' ,
97100 'gemini-2.0-flash-lite' ,
98101 'gemini-2.5-flash' ,
99102 'gemini-2.5-flash-preview-09-2025' ,
100- 'gemini-flash-latest ' ,
103+ 'gemini-2.5- flash-image ' ,
101104 'gemini-2.5-flash-lite' ,
102105 'gemini-2.5-flash-lite-preview-09-2025' ,
103- 'gemini-flash-lite-latest' ,
104106 'gemini-2.5-pro' ,
107+ 'gemini-3-pro-preview' ,
105108]
106109"""Latest Gemini models."""
107110
@@ -228,12 +231,17 @@ def system(self) -> str:
228231 def prepare_request (
229232 self , model_settings : ModelSettings | None , model_request_parameters : ModelRequestParameters
230233 ) -> tuple [ModelSettings | None , ModelRequestParameters ]:
234+ supports_native_output_with_builtin_tools = GoogleModelProfile .from_profile (
235+ self .profile
236+ ).google_supports_native_output_with_builtin_tools
231237 if model_request_parameters .builtin_tools and model_request_parameters .output_tools :
232238 if model_request_parameters .output_mode == 'auto' :
233- model_request_parameters = replace (model_request_parameters , output_mode = 'prompted' )
239+ output_mode = 'native' if supports_native_output_with_builtin_tools else 'prompted'
240+ model_request_parameters = replace (model_request_parameters , output_mode = output_mode )
234241 else :
242+ output_mode = 'NativeOutput' if supports_native_output_with_builtin_tools else 'PromptedOutput'
235243 raise UserError (
236- 'Google does not support output tools and built-in tools at the same time. Use `output_type=PromptedOutput (...)` instead.'
244+ f 'Google does not support output tools and built-in tools at the same time. Use `output_type={ output_mode } (...)` instead.'
237245 )
238246 return super ().prepare_request (model_settings , model_request_parameters )
239247
@@ -394,7 +402,16 @@ async def _generate_content(
394402 ) -> GenerateContentResponse | Awaitable [AsyncIterator [GenerateContentResponse ]]:
395403 contents , config = await self ._build_content_and_config (messages , model_settings , model_request_parameters )
396404 func = self .client .aio .models .generate_content_stream if stream else self .client .aio .models .generate_content
397- return await func (model = self ._model_name , contents = contents , config = config ) # type: ignore
405+ try :
406+ return await func (model = self ._model_name , contents = contents , config = config ) # type: ignore
407+ except errors .APIError as e :
408+ if (status_code := e .code ) >= 400 :
409+ raise ModelHTTPError (
410+ status_code = status_code ,
411+ model_name = self ._model_name ,
412+ body = cast (Any , e .details ), # pyright: ignore[reportUnknownMemberType]
413+ ) from e
414+ raise # pragma: lax no cover
398415
399416 async def _build_content_and_config (
400417 self ,
@@ -409,9 +426,9 @@ async def _build_content_and_config(
409426 response_mime_type = None
410427 response_schema = None
411428 if model_request_parameters .output_mode == 'native' :
412- if tools :
429+ if model_request_parameters . function_tools :
413430 raise UserError (
414- 'Google does not support `NativeOutput` and tools at the same time. Use `output_type=ToolOutput(...)` instead.'
431+ 'Google does not support `NativeOutput` and function tools at the same time. Use `output_type=ToolOutput(...)` instead.'
415432 )
416433 response_mime_type = 'application/json'
417434 output_object = model_request_parameters .output_object
@@ -676,23 +693,25 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
676693
677694 for part in parts :
678695 if part .thought_signature :
696+ # Per https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#thought-signatures:
697+ # - Always send the thought_signature back to the model inside its original Part.
698+ # - Don't merge a Part containing a signature with one that does not. This breaks the positional context of the thought.
699+ # - Don't combine two Parts that both contain signatures, as the signature strings cannot be merged.
700+
679701 signature = base64 .b64encode (part .thought_signature ).decode ('utf-8' )
702+ # Attach signature to most recent thinking part, if there was one
680703 yield self ._parts_manager .handle_thinking_delta (
681- vendor_part_id = 'thinking' ,
704+ vendor_part_id = None ,
682705 signature = signature ,
683706 provider_name = self .provider_name ,
684707 )
685708
686709 if part .text is not None :
687710 if len (part .text ) > 0 :
688711 if part .thought :
689- yield self ._parts_manager .handle_thinking_delta (
690- vendor_part_id = 'thinking' , content = part .text
691- )
712+ yield self ._parts_manager .handle_thinking_delta (vendor_part_id = None , content = part .text )
692713 else :
693- maybe_event = self ._parts_manager .handle_text_delta (
694- vendor_part_id = 'content' , content = part .text
695- )
714+ maybe_event = self ._parts_manager .handle_text_delta (vendor_part_id = None , content = part .text )
696715 if maybe_event is not None : # pragma: no branch
697716 yield maybe_event
698717 elif part .function_call :
@@ -751,6 +770,7 @@ def timestamp(self) -> datetime:
751770def _content_model_response (m : ModelResponse , provider_name : str ) -> ContentDict : # noqa: C901
752771 parts : list [PartDict ] = []
753772 thought_signature : bytes | None = None
773+ function_call_requires_signature : bool = True
754774 for item in m .parts :
755775 part : PartDict = {}
756776 if thought_signature :
@@ -760,6 +780,15 @@ def _content_model_response(m: ModelResponse, provider_name: str) -> ContentDict
760780 if isinstance (item , ToolCallPart ):
761781 function_call = FunctionCallDict (name = item .tool_name , args = item .args_as_dict (), id = item .tool_call_id )
762782 part ['function_call' ] = function_call
783+ if function_call_requires_signature and not part .get ('thought_signature' ):
784+ # Per https://ai.google.dev/gemini-api/docs/gemini-3?thinking=high#migrating_from_other_models:
785+ # > If you are transferring a conversation trace from another model (e.g., Gemini 2.5) or injecting
786+ # > a custom function call that was not generated by Gemini 3, you will not have a valid signature.
787+ # > To bypass strict validation in these specific scenarios, populate the field with this specific
788+ # > dummy string: "thoughtSignature": "context_engineering_is_the_way_to_go"
789+ part ['thought_signature' ] = b'context_engineering_is_the_way_to_go'
790+ # Only the first function call requires a signature
791+ function_call_requires_signature = False
763792 elif isinstance (item , TextPart ):
764793 part ['text' ] = item .content
765794 elif isinstance (item , ThinkingPart ):
@@ -872,7 +901,7 @@ def _function_declaration_from_tool(tool: ToolDefinition) -> FunctionDeclaration
872901 f = FunctionDeclarationDict (
873902 name = tool .name ,
874903 description = tool .description or '' ,
875- parameters = json_schema , # type: ignore
904+ parameters_json_schema = json_schema ,
876905 )
877906 return f
878907
0 commit comments