Conversation
translate_stream_event() correlated streamed functionCall parts across SSE chunks using a call_key of (part_index, name, thought_signature). part_index is the position within the CURRENT chunk's parts[] array, which is 0 whenever a chunk carries a single part -- the common case when Gemini streams several tool calls, each in its own chunk. When a model turn contains multiple DIFFERENT calls to the SAME tool name (e.g. several ha_call_service calls in one turn, one per entity), every call collided onto the identical call_key and was treated as the same call growing incrementally. The startswith-based diffing then failed to reconcile unrelated JSON payloads and fell through to re-emitting the full args each time, which the downstream OpenAI-style delta accumulator appends (+=) onto the same slot -- concatenating multiple complete, individually-valid JSON objects into one unparseable "arguments" string. Symptom: "Unrepairable tool_call arguments ... replaced with empty object" followed by finish_reason='length' and, after 4 retries, "Response truncated due to output length limit". Gemini 3+ already stamps each distinct function call with a stable, unique id (see gemini_requires_tool_call_ids()) -- the adapter already read fc["id"], but only to seed the outgoing tool-call id, never to correlate calls across chunks. Fix: prefer fc["id"] in call_key when present, falling back to the previous (part_index, name, thought_signature) heuristic only when id is absent (Gemini 2.x, which doesn't advertise per-call ids and isn't used for parallel calls in practice). Adds a regression test replicating the exact collision (three distinct calls to the same tool name, each in its own chunk, with per-call ids) and confirms the existing same-call-resent-twice test (no id, Gemini 2.x path) still passes unchanged.
Duplicate of #75528 — same fix: key Gemini streaming tool-call slots on the provider |
|
The Gemini parallel tool-call slot collision is landing via #111686 (salvage of #75528 by @jmiguellucas, co-credited to the first submitter @cdbartholomew #24676; armed to auto-merge on green): Gemini 3 ids are the slot identity, no-id calls disambiguate by value with reachable |
Problem
translate_stream_event()inagent/gemini_native_adapter.pycorrelatesstreamed
functionCallparts across SSE chunks using acall_keyof(part_index, name, thought_signature).part_indexis the position within the current chunk'sparts[]array, which is
0whenever a chunk carries a single part — the commoncase when Gemini streams several tool calls, each arriving in its own
chunk. When a model turn contains multiple different calls to the
same tool name (e.g. several
ha_call_servicecalls in one turn, oneper Home-Assistant entity being updated), every call collides onto the
identical
call_keyand gets treated as the same call growingincrementally.
The
startswith-based diffing then fails to reconcile unrelated JSONpayloads and falls through to re-emitting the full
argseach time,which the downstream OpenAI-style delta accumulator appends (
+=) ontothe same slot — concatenating multiple complete, individually-valid JSON
objects into one unparseable
argumentsstring.Observed symptom in production (Discord platform,
gemini-3.1-flash-lite):...repeating across 4 retries, ending in "Response truncated due to
output length limit" — even though the model was behaving correctly and
sending well-formed, complete tool calls.
Fix
Gemini 3+ already stamps each distinct function call with a stable,
unique
id(seegemini_requires_tool_call_ids()) — the adapter alreadyread
fc["id"], but only to seed the outgoing tool-call id, never tocorrelate calls across streaming chunks.
call_keynow prefersfc["id"]when present, falling back to theprevious
(part_index, name, thought_signature)heuristic only whenidis absent (Gemini 2.x, which doesn't advertise per-call ids andisn't exercised for parallel same-name calls in practice — no behavior
change there).
Testing
test_stream_event_translation_disambiguates_parallel_same_name_calls_by_id,replicating the exact collision: three distinct calls to the same tool
name, each in its own chunk, each with a Gemini-3-style per-call
id.Confirms each call now lands in its own slot with independently valid
(non-concatenated) JSON arguments.
test_stream_event_translation_emits_tool_call_delta_with_stable_index(same call resent twice, no
id, Gemini 2.x path) still passesunchanged.
pytest tests/agent/test_gemini_native_adapter.py— 30 passed.Notes
Found and fixed while diagnosing a real-world failure where a Discord
user reported several aquarium water-parameter readings at once and the
agent tried to batch the corresponding
input_number.set_valuecalls(via
parallel_tool_call_guidance) into a single turn.