fix: ensure thought_signature is included for Gemini 3 function calls (#8590)
This fixes a 400 INVALID_ARGUMENT error from Google's Gemini API where function calls were missing required thought_signature in functionCall parts. Changes: - Allow signatures when self.model is None (backwards compatibility for older messages that may not have had their model field set) - Only add thought_signature to the FIRST function call for parallel tool calls, per Google's docs - Take the first non-None signature found (don't keep overwriting) Reference: https://ai.google.dev/gemini-api/docs/thought-signatures Closes #8589 🤖 Generated with [Letta Code](https://letta.com) Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com> Co-authored-by: datadog-official[bot] <datadog-official[bot]@users.noreply.github.com> Co-authored-by: Kian Jones <11655409+kianjones9@users.noreply.github.com>
This commit is contained in:
committed by
Sarah Wooders
parent
ea36633cd5
commit
e914075b04
@@ -1904,15 +1904,25 @@ class Message(BaseMessage):
|
||||
|
||||
if self.tool_calls is not None:
|
||||
# Check if there's a signature in the content that should be included with function calls
|
||||
# Google Vertex requires thought_signature to be echoed back in function calls
|
||||
# Google Vertex/Gemini 3 requires thought_signature to be echoed back in function calls
|
||||
# Per Google docs: https://ai.google.dev/gemini-api/docs/thought-signatures
|
||||
# - For parallel function calls, only the FIRST functionCall should have the signature
|
||||
# - For sequential function calls (multi-step), each function call has its own signature
|
||||
thought_signature = None
|
||||
if self.content and current_model == self.model:
|
||||
# Allow signatures when models match OR when self.model is None (backwards compatibility
|
||||
# for older messages that may not have had their model field set)
|
||||
models_compatible = self.model is None or current_model == self.model
|
||||
if self.content and models_compatible:
|
||||
for content in self.content:
|
||||
# Check for signature in ReasoningContent, TextContent, or ToolCallContent
|
||||
# Take the first non-None signature found (don't keep overwriting)
|
||||
if isinstance(content, (ReasoningContent, TextContent, ToolCallContent)):
|
||||
thought_signature = getattr(content, "signature", None)
|
||||
sig = getattr(content, "signature", None)
|
||||
if sig is not None and thought_signature is None:
|
||||
thought_signature = sig
|
||||
|
||||
# NOTE: implied support for multiple calls
|
||||
is_first_function_call = True
|
||||
for tool_call in self.tool_calls:
|
||||
function_name = tool_call.function.name
|
||||
function_args = tool_call.function.arguments
|
||||
@@ -1939,9 +1949,11 @@ class Message(BaseMessage):
|
||||
}
|
||||
}
|
||||
|
||||
# Include thought_signature if we found one
|
||||
if thought_signature is not None:
|
||||
# Include thought_signature only on the FIRST function call
|
||||
# Per Google docs, for parallel function calls, only the first gets the signature
|
||||
if thought_signature is not None and is_first_function_call:
|
||||
function_call_part["thought_signature"] = thought_signature
|
||||
is_first_function_call = False
|
||||
|
||||
parts.append(function_call_part)
|
||||
else:
|
||||
@@ -1952,15 +1964,20 @@ class Message(BaseMessage):
|
||||
parts.append({"text": text_content})
|
||||
|
||||
if self.content and len(self.content) > 1:
|
||||
# Use the same models_compatible check defined above for consistency
|
||||
# Allow signatures when models match OR when self.model is None (backwards compatibility)
|
||||
models_compatible = self.model is None or current_model == self.model
|
||||
native_google_content_parts = []
|
||||
# Track if we've seen the first function call (for parallel tool calls)
|
||||
seen_first_function_call = False
|
||||
for content in self.content:
|
||||
if isinstance(content, TextContent):
|
||||
native_part = {"text": content.text}
|
||||
if content.signature and current_model == self.model:
|
||||
if content.signature and models_compatible:
|
||||
native_part["thought_signature"] = content.signature
|
||||
native_google_content_parts.append(native_part)
|
||||
elif isinstance(content, ReasoningContent):
|
||||
if current_model == self.model:
|
||||
if models_compatible:
|
||||
native_google_content_parts.append({"text": content.reasoning, "thought": True})
|
||||
elif isinstance(content, ToolCallContent):
|
||||
native_part = {
|
||||
@@ -1969,8 +1986,11 @@ class Message(BaseMessage):
|
||||
"args": content.input,
|
||||
},
|
||||
}
|
||||
if content.signature and current_model == self.model:
|
||||
# Only include signature on the FIRST function call (for parallel tool calls)
|
||||
# Per Google docs: https://ai.google.dev/gemini-api/docs/thought-signatures
|
||||
if content.signature and models_compatible and not seen_first_function_call:
|
||||
native_part["thought_signature"] = content.signature
|
||||
seen_first_function_call = True
|
||||
native_google_content_parts.append(native_part)
|
||||
else:
|
||||
# silently drop other content types
|
||||
|
||||
Reference in New Issue
Block a user