fix: preserve Gemini thought_signature in tool call messages

Gemini 3 thinking models attach extra_content with thought_signature
to function call responses. This must be echoed back on subsequent
API calls or the server rejects with a 400 error. The assistant
message builder was dropping this field, causing all Gemini 3 Flash/Pro
tool-calling flows to fail after the first function call.
This commit is contained in:
0xbyt4 2026-02-28 18:01:13 +03:00
parent 2390728cc3
commit dfd50ceccd
2 changed files with 32 additions and 4 deletions

View file

@ -1369,8 +1369,9 @@ class AIAgent:
]
if assistant_message.tool_calls:
msg["tool_calls"] = [
{
tc_list = []
for tool_call in assistant_message.tool_calls:
tc_dict = {
"id": tool_call.id,
"type": tool_call.type,
"function": {
@ -1378,8 +1379,17 @@ class AIAgent:
"arguments": tool_call.function.arguments
}
}
for tool_call in assistant_message.tool_calls
]
# Preserve extra_content (e.g. Gemini thought_signature) so it
# is sent back on subsequent API calls. Without this, Gemini 3
# thinking models reject the request with a 400 error.
extra = getattr(tool_call, "extra_content", None)
if extra is not None:
# Convert Pydantic models to plain dicts for JSON safety
if hasattr(extra, "model_dump"):
extra = extra.model_dump()
tc_dict["extra_content"] = extra
tc_list.append(tc_dict)
msg["tool_calls"] = tc_list
return msg

View file

@ -546,6 +546,24 @@ class TestBuildAssistantMessage:
result = agent._build_assistant_message(msg, "stop")
assert result["content"] == ""
def test_tool_call_extra_content_preserved(self, agent):
"""Gemini thinking models attach extra_content with thought_signature
to tool calls. This must be preserved so subsequent API calls include it."""
tc = _mock_tool_call(name="get_weather", arguments='{"city":"NYC"}', call_id="c2")
tc.extra_content = {"google": {"thought_signature": "abc123"}}
msg = _mock_assistant_msg(content="", tool_calls=[tc])
result = agent._build_assistant_message(msg, "tool_calls")
assert result["tool_calls"][0]["extra_content"] == {
"google": {"thought_signature": "abc123"}
}
def test_tool_call_without_extra_content(self, agent):
"""Standard tool calls (no thinking model) should not have extra_content."""
tc = _mock_tool_call(name="web_search", arguments='{}', call_id="c3")
msg = _mock_assistant_msg(content="", tool_calls=[tc])
result = agent._build_assistant_message(msg, "tool_calls")
assert "extra_content" not in result["tool_calls"][0]
class TestFormatToolsForSystemMessage:
def test_no_tools_returns_empty_array(self, agent):