Skip to content

fix: round-trip Gemini thoughtSignature through OpenAI-compat tool calls#176

Merged
warren618 merged 1 commit into
HKUDS:mainfrom
mvanhorn:fix/170-gemini-thought-signature
Jun 5, 2026
Merged

fix: round-trip Gemini thoughtSignature through OpenAI-compat tool calls#176
warren618 merged 1 commit into
HKUDS:mainfrom
mvanhorn:fix/170-gemini-thought-signature

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

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. ChatOpenAIWithReasoning in agent/src/providers/llm.py already preserved reasoning_content at 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_signature now reads the signature from each inbound tool call (including the extra_content.google extension) and re-injects it onto the matching outbound tool_calls[j]. ToolCallRequest in agent/src/providers/chat.py gains an optional thought_signature: str | None, threaded through the assistant-message rebuild in agent/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.py captures and re-emits the thought signature per tool call during assistant-message reconstruction. agent/src/providers/chat.py adds an optional thought_signature to ToolCallRequest, and agent/src/agent/loop.py threads that signature through the rebuild path.

Test Plan

  • Existing tests pass (pytest --ignore=agent/tests/e2e_backtest --tb=short -q)
  • New tests added (if applicable)
  • Tested manually (describe below) (N/A: covered by the automated round-trip tests below; no live Gemini call was made)

agent/tests/test_kimi_reasoning_content.py is 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 stays None).

Checklist

  • No changes to protected areas (src/agent/, src/session/, src/providers/) without prior discussion — issue thoughtSignature for Gemini 2.5 and 3.0 - AgentLoop error #170 is the maintainer's help wanted scoping of this exact change ("Happy to review a PR along these lines"), following the preferred Route A.
  • No hardcoded values (API keys, file paths, magic numbers)
  • Code follows CONTRIBUTING.md guidelines
  • Documentation updated (if user-facing change)

@warren618 warren618 merged commit 379bc37 into HKUDS:main Jun 5, 2026
@mvanhorn

mvanhorn commented Jun 5, 2026

Copy link
Copy Markdown
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.
@mvanhorn

Copy link
Copy Markdown
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

thoughtSignature for Gemini 2.5 and 3.0 - AgentLoop error

2 participants