fix: round-trip Gemini thoughtSignature through OpenAI-compat tool calls#176
Merged
Merged
Conversation
1 task
Contributor
Author
|
Thanks again @warren618 for the Gemini thoughtSignature round-trip fix. |
warren618
pushed a commit
that referenced
this pull request
Jun 8, 2026
…t path (#184) #176 added the Gemini thought_signature round-trip but only covered the in-memory AIMessage path its unit tests exercise. The AgentLoop replays conversation history as OpenAI-format dicts, stamping the signature into tool_calls[i].extra_content.google.thought_signature (loop.py _attach_tool_call_thought_signatures). LangChain's _convert_dict_to_message discards extra_content during the early _convert_input step of invoke()/stream() -- before _get_request_payload re-injects -- so _signature_maps reads an empty additional_kwargs and the outbound request goes out unsigned. Gemini then rejects the next tool-using turn with HTTP 400 "Function call is missing a thought_signature in functionCall parts". This broke every tool-using loop turn on gemini-3.x (single or parallel), e.g. the parallel load_skill case. Override ChatOpenAIWithReasoning._convert_input -- the single chokepoint both invoke and stream call while input is still raw dicts -- to lift signatures back onto the converted AIMessage in the same additional_kwargs["tool_call_thought_signatures"] shape the in-memory path uses, so the existing _signature_maps / _inject_tool_call_thought_signatures machinery handles both paths identically. Idempotent via an isinstance(raw, dict) guard. No parallel-specific handling needed: Gemini signs only the first of N parallel calls and that signature covers the block. Add regression tests in TestChatOpenAIWithReasoningOutboundPayload that drive the real dict path (the gap #176's tests missed): red without the fix (KeyError: 'extra_content'), green with it.
Contributor
Author
|
Thanks @warren618 for landing this. Round-tripping the Gemini thoughtSignature keeps tool calls intact through the OpenAI-compat layer. |
longmfe
pushed a commit
to longmfe/Vibe-Trading-Mars
that referenced
this pull request
Jul 14, 2026
…lls (HKUDS#176) Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
longmfe
pushed a commit
to longmfe/Vibe-Trading-Mars
that referenced
this pull request
Jul 14, 2026
…t path (HKUDS#184) HKUDS#176 added the Gemini thought_signature round-trip but only covered the in-memory AIMessage path its unit tests exercise. The AgentLoop replays conversation history as OpenAI-format dicts, stamping the signature into tool_calls[i].extra_content.google.thought_signature (loop.py _attach_tool_call_thought_signatures). LangChain's _convert_dict_to_message discards extra_content during the early _convert_input step of invoke()/stream() -- before _get_request_payload re-injects -- so _signature_maps reads an empty additional_kwargs and the outbound request goes out unsigned. Gemini then rejects the next tool-using turn with HTTP 400 "Function call is missing a thought_signature in functionCall parts". This broke every tool-using loop turn on gemini-3.x (single or parallel), e.g. the parallel load_skill case. Override ChatOpenAIWithReasoning._convert_input -- the single chokepoint both invoke and stream call while input is still raw dicts -- to lift signatures back onto the converted AIMessage in the same additional_kwargs["tool_call_thought_signatures"] shape the in-memory path uses, so the existing _signature_maps / _inject_tool_call_thought_signatures machinery handles both paths identically. Idempotent via an isinstance(raw, dict) guard. No parallel-specific handling needed: Gemini signs only the first of N parallel calls and that signature covers the block. Add regression tests in TestChatOpenAIWithReasoningOutboundPayload that drive the real dict path (the gap HKUDS#176's tests missed): red without the fix (KeyError: 'extra_content'), green with it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Gemini's per-function-call thought signature now round-trips through the OpenAI-compatible tool-call path, so multi-turn function calling against Gemini 2.5/3.x thinking models no longer fails with
INVALID_ARGUMENT("Function call is missing a thought_signature").Why
Closes #170.
ChatOpenAIWithReasoninginagent/src/providers/llm.pyalready preservedreasoning_contentat message granularity, but LangChain's fixed{id, name, args, type}tool-call schema dropped the per-tool-call signature at message reconstruction._extract_tool_call_thought_signaturenow reads the signature from each inbound tool call (including theextra_content.googleextension) and re-injects it onto the matching outboundtool_calls[j].ToolCallRequestinagent/src/providers/chat.pygains an optionalthought_signature: str | None, threaded through the assistant-message rebuild inagent/src/agent/loop.py. The field is default-absent, so OpenAI / DeepSeek / OpenRouter providers never populate it and stay byte-identical.Changes
agent/src/providers/llm.pycaptures and re-emits the thought signature per tool call during assistant-message reconstruction.agent/src/providers/chat.pyadds an optionalthought_signaturetoToolCallRequest, andagent/src/agent/loop.pythreads that signature through the rebuild path.Test Plan
pytest --ignore=agent/tests/e2e_backtest --tb=short -q)agent/tests/test_kimi_reasoning_content.pyis extended with the round-trip cases: a multi-turn exchange re-emits each signature on the matching tool call, mixed calls preserve per-call mapping, and a no-signature response serializes identically to today (field staysNone).Checklist
src/agent/,src/session/,src/providers/) without prior discussion — issue thoughtSignature for Gemini 2.5 and 3.0 - AgentLoop error #170 is the maintainer'shelp wantedscoping of this exact change ("Happy to review a PR along these lines"), following the preferred Route A.