From 8d119832b4f1f02ade9f72484ff48639d436af50 Mon Sep 17 00:00:00 2001 From: Idris Almalki Date: Sat, 25 Apr 2026 17:19:22 +0300 Subject: [PATCH] fix(gemini): emit thoughtSignature sentinel for cross-provider tool_calls in native adapter When Hermes fails over from a non-Gemini provider (xAI, Anthropic, etc.) to Gemini mid-conversation, the existing assistant tool_calls in history carry no Gemini ``extra_content.google.thought_signature`` (the originating provider never emits one). The native adapter's ``_translate_tool_call_to_gemini`` omitted ``thoughtSignature`` entirely in that case, so Gemini 3 thinking models rejected every replayed turn with:: HTTP 400 INVALID_ARGUMENT Function call is missing a thought_signature in functionCall parts. Additional data, function call default_api:, position N. The Cloud Code Assist sibling adapter already handles this exact case by emitting a sentinel ``"skip_thought_signature_validator"`` (see ``agent/gemini_cloudcode_adapter.py:106``, originally added in #11270 and documented as matching ``opencode-gemini-auth``'s approach). This change mirrors that fallback in the native adapter so the two paths behave identically when replaying cross-provider history. Verified live against ``generativelanguage.googleapis.com/v1beta`` with ``gemini-3-pro-preview``: synthetic 2-turn conversation with no real ``thoughtSignature`` returns 400 without the sentinel and 200 with it. Test added: ``test_build_native_request_emits_sentinel_for_cross_provider_tool_call``. Co-Authored-By: Claude Opus 4.7 (1M context) --- agent/gemini_native_adapter.py | 8 +++-- tests/agent/test_gemini_native_adapter.py | 37 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/agent/gemini_native_adapter.py b/agent/gemini_native_adapter.py index 1c25f1e6cf0..b4f6e6386e7 100644 --- a/agent/gemini_native_adapter.py +++ b/agent/gemini_native_adapter.py @@ -270,8 +270,12 @@ def _translate_tool_call_to_gemini(tool_call: Dict[str, Any]) -> Dict[str, Any]: } } thought_signature = _tool_call_extra_signature(tool_call) - if thought_signature: - part["thoughtSignature"] = thought_signature + # Fallback sentinel for cross-provider tool_calls (e.g. fallback from + # xAI/Anthropic to Gemini, where the original tool_call carries no + # Gemini thoughtSignature). Mirrors gemini_cloudcode_adapter.py:106. + # Without this, Gemini 3 thinking models reject replayed history with + # 400 INVALID_ARGUMENT on the missing thoughtSignature. + part["thoughtSignature"] = thought_signature or "skip_thought_signature_validator" return part diff --git a/tests/agent/test_gemini_native_adapter.py b/tests/agent/test_gemini_native_adapter.py index e797d6b2ae7..e7493fe4918 100644 --- a/tests/agent/test_gemini_native_adapter.py +++ b/tests/agent/test_gemini_native_adapter.py @@ -52,6 +52,43 @@ def test_build_native_request_preserves_thought_signature_on_tool_replay(): assert parts[0]["thoughtSignature"] == "sig-123" +def test_build_native_request_emits_sentinel_for_cross_provider_tool_call(): + """Cross-provider tool_calls (xAI/Anthropic -> Gemini fallback) carry no + Gemini thoughtSignature. Without a sentinel, Gemini 3 thinking models + reject the request with 400 INVALID_ARGUMENT. The native adapter must + emit the same ``skip_thought_signature_validator`` sentinel that the + Cloud Code Assist adapter already uses for the same scenario. + """ + from agent.gemini_native_adapter import build_gemini_request + + request = build_gemini_request( + messages=[ + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": { + "name": "get_weather", + "arguments": '{"city": "Paris"}', + }, + # No extra_content — this tool_call originated from a + # non-Gemini provider during fallback. + } + ], + }, + ], + tools=[], + tool_choice=None, + ) + + parts = request["contents"][0]["parts"] + assert parts[0]["functionCall"]["name"] == "get_weather" + assert parts[0]["thoughtSignature"] == "skip_thought_signature_validator" + + def test_build_native_request_uses_original_function_name_for_tool_result(): from agent.gemini_native_adapter import build_gemini_request