Skip to content

fix(gemini): stop splitting one tool call into two when signature arrives late - #28438

Closed
Ricardo-M-L wants to merge 1 commit into
NousResearch:mainfrom
Ricardo-M-L:contrib/fix-gemini-signature-dedup-key
Closed

fix(gemini): stop splitting one tool call into two when signature arrives late#28438
Ricardo-M-L wants to merge 1 commit into
NousResearch:mainfrom
Ricardo-M-L:contrib/fix-gemini-signature-dedup-key

Conversation

@Ricardo-M-L

Copy link
Copy Markdown
Contributor

What does this PR do?

translate_stream_event() in agent/gemini_native_adapter.py keys tool-call slots on (part_index, name, thought_signature). Because the thought_signature is part of the dedup key, a single tool call whose chunks carry the signature inconsistently — which Gemini 3 thinking models do (e.g., signature is empty on early chunks and present on a later one) — gets split into two separate slots:

  • slot 0: built from the early chunks → no signature, partial args
  • slot 1: built from the later chunk → has signature, fuller args

Both slots are emitted as deltas, so the agent records two tool calls for what was logically one. On the next turn the slot without a signature is replayed back to Gemini, which 400s with:

Function call is missing a thought_signature in functionCall parts.

Fix

Dedup on (part_index, name) only. The signature is still surfaced through the per-chunk extra_content field, and the downstream streaming accumulator in run_agent.py already does latest-non-None-wins on extra_content per slot — so whichever chunk carried the signature gets it merged into the single slot.

The change is one-line in the dedup key plus a comment explaining the constraint.

Test

Adds test_stream_event_translation_does_not_split_slot_when_signature_arrives_late in tests/agent/test_gemini_native_adapter.py. The test feeds two chunks of the same tool call where only the second carries a thoughtSignature and asserts the events flow into a single slot. Fails on main (two slots emitted, both reported as distinct tool calls) and passes here.

Before submitting

  • Did you read the contributor guideline?
  • Did you write any new necessary tests? — Yes, regression test added that fails on main.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/gemini Google Gemini (AI Studio, Cloud Code) duplicate This issue or pull request already exists labels May 19, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #15739 (same fix: dedup gemini tool-call slots on (part_index, name) instead of including thought_signature in the key). Both PRs fix the same root cause in gemini_native_adapter.py.

@Ricardo-M-L

Copy link
Copy Markdown
Contributor Author

@alt-glitch yes — #15739 is my earlier version of the exact same fix; it was 2k+ commits behind main and couldn't rebase cleanly. I closed it on May 19 when I opened this one. This PR is the rebased version against current main + adds the regression test test_stream_event_translation_does_not_split_slot_when_signature_arrives_late (fails on main, passes here). Same one-key dedup change; just re-applied on top of HEAD.

…ives late

translate_stream_event() in agent/gemini_native_adapter.py keys
tool_call slots on (part_index, name, thought_signature). Because the
thought_signature is part of the dedup key, a single tool call whose
chunks carry the signature inconsistently (e.g., empty on early chunks,
present on a later one — which Gemini 3 thinking models do) is split
into two separate slots:

- slot 0: built from the early chunks → no signature, partial args
- slot 1: built from the later chunk → has signature, fuller args

Both slots are emitted as deltas, so the agent records *two* tool calls
for what was logically one. On the next turn the slot without a
signature is replayed back to Gemini, which 400s with:

    Function call is missing a thought_signature in functionCall parts.

Fix: dedup on (part_index, name) only. The signature is still surfaced
through the per-chunk extra_content field, and the downstream
streaming accumulator (run_agent.py) already does latest-non-None-wins
on extra_content per slot — so whichever chunk carried the signature
gets it merged into the single slot.

Adds a regression test that fails on main and passes here:
test_stream_event_translation_does_not_split_slot_when_signature_arrives_late

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Ricardo-M-L
Ricardo-M-L force-pushed the contrib/fix-gemini-signature-dedup-key branch from 9112aee to 09ce3a2 Compare June 13, 2026 14:42
@teknium1

teknium1 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Thanks for the focused regression fix. I verified the premise against current main and this still reproduces.

Suggested changes

  • Optional cleanup only: add a blank line before test_stream_event_translation_does_not_split_slot_when_signature_arrives_late in tests/agent/test_gemini_native_adapter.py so it matches the spacing style around the neighboring top-level tests.

Evidence checked:

  • On current main, agent/gemini_native_adapter.py:670-676 still includes thought_signature in the streaming call_key, which splits one logical call when the signature arrives on a later chunk.
  • A direct two-event repro against current main produced two slots for the same part_index/name case: first chunk index 0 without extra_content, second chunk index 1 with {'google': {'thought_signature': 'sig-late'}}.
  • The downstream streaming accumulator does latest-non-None for extra_content at agent/chat_completion_helpers.py:1973-1979, so the PR’s (part_index, name) dedup shape is consistent with the existing accumulator.
  • I did not find a sibling native Gemini streaming path with the same keying bug; the Cloud Code adapter intentionally allocates fresh indices with tool_call_counter at agent/gemini_cloudcode_adapter.py:509-548 and does not use thought signatures for this slot key.

Automated hermes-sweeper review.

@Ricardo-M-L

Copy link
Copy Markdown
Contributor Author

Hi maintainers! This PR fixes a Gemini tool call splitting bug where a single tool call gets incorrectly split into two when the signature arrives late, causing downstream parsing failures.

Please let me know if you'd like me to add tests, adjust the approach, or rebase onto the latest main. Thanks!

@alt-glitch alt-glitch removed the duplicate This issue or pull request already exists label Jun 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Correcting a stale duplicate label on re-triage: #15739 is the author's own earlier version of this exact fix, which was closed (2k+ commits behind main, could not rebase) when this rebased resubmission was opened. A resubmission of a closed predecessor is not a duplicate — relinking as related_to #15739 and dropping the duplicate label. Maintainer verified the premise still applies on current main.

@Ricardo-M-L

Copy link
Copy Markdown
Contributor Author

Hi! Just a friendly ping on this PR. It's been open for a while — would appreciate a review when you get a chance. Thanks for your time!

@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Not a duplicate: #15739 is CLOSED (never merged) and is the author's earlier un-rebased branch — this is the active rebased resubmission. Verified on main: translate_stream_event() still keys tool-call slots on thought_signature, so the late-signature split is real and unfixed. Related (same function, different bug): #54355 / #24676 fix the parallel-stream key collision, not the late-signature split.

@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related (not a duplicate): this is @Ricardo-M-L's rebased resubmission of the closed #15739 (same author, earlier PR closed 2026-05-19 because it couldn't rebase cleanly). Verified the fix is not yet on main (translate_stream_event still includes thought_signature in the dedup key), so this is a valid re-submission, not a dup. Also related to open #54355, which touches the SAME function for a DIFFERENT bug (parallel-streaming call_key collision vs late-signature split) — the two are complementary, not competing.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused late-signature regression. The reported split remains real on current main: agent/gemini_native_adapter.py:687-703 includes thought_signature in the cross-event slot key, and the streaming generator reuses that key map at agent/gemini_native_adapter.py:977-979.

Problems

  • Blocking: removing the signature from the sole key makes every separate parts[0] call with the same function name share one slot. The downstream accumulator appends arguments for that slot at agent/chat_completion_helpers.py:2436-2437, so parallel calls can become concatenated invalid JSON. This is the active collision scenario documented in #24676; this patch additionally collides calls whose signatures differ.

Suggested changes

  • Rework the slot matcher to support both late-signature continuation and distinct parallel same-name calls.
  • Retain this regression and add separate-event, same-name parallel calls with distinct arguments/signatures; assert separate ids/indexes and isolated valid argument payloads.

Automated hermes-sweeper review.

@@ -657,12 +657,19 @@ def translate_stream_event(event: Dict[str, Any], model: str, tool_call_indices:
args_str = json.dumps(fc.get("args") or {}, ensure_ascii=False, sort_keys=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping the signature from the only cross-event key merges distinct same-name parts[0] calls into one slot. _stream_completion() reuses tool_call_indices for the entire response, and the downstream accumulator appends arguments for a shared slot (agent/chat_completion_helpers.py:2436-2437). Please preserve a reliable distinct-call discriminator and add a parallel-call regression before removing this dimension.

@Ricardo-M-L

Copy link
Copy Markdown
Contributor Author

Friendly ping - this PR fixes Gemini splitting one tool call into two. Would appreciate a review. Thanks!

@Ricardo-M-L

Copy link
Copy Markdown
Contributor Author

Closing duplicate: superseded by #88149 (gemini signature dedup)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/gemini Google Gemini (AI Studio, Cloud Code) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants