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:<tool_name>, 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) <noreply@anthropic.com>
This commit is contained in:
Idris Almalki 2026-04-25 17:19:22 +03:00 committed by Teknium
parent f65d105cbb
commit 8d119832b4
2 changed files with 43 additions and 2 deletions

View file

@ -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

View file

@ -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