Summary
GoogleProvider synthesises tool-call ids as gem_<index> where the index resets every turn, so ids collide across turns of a multi-turn conversation. This corrupts functionResponse name resolution and breaks tool-call/tool-result pairing on the 2nd+ tool-using turn.
Details
Tool-call ids are minted per-response, scoped to a single turn:
Every Gemini turn that emits tool calls therefore produces ids starting again from gem_0, gem_1, …
When building the next request, GoogleRequest::from_request constructs an id_to_name map keyed by id over the whole message history with last-writer-wins:
So turn 1's gem_0 (e.g. fs.read) gets overwritten by turn 3's gem_0 (e.g. code.grep). When turn 1's Message::Tool { tool_call_id: "gem_0" } is converted to a functionResponse, the name resolves to the wrong tool:
Because Gemini matches functionResponse ↔ functionCall by name (no ids on the wire), this sends a mismatched/duplicated function name.
Impact
Cryptic Gemini 400s or wrong-tool attribution on any conversation that uses tools across more than one turn. Single-turn tool use is unaffected (ids are unique within a turn). Severity: high.
Suggested fix
Make the synthesised id unique across the whole conversation — e.g. include a turn/message counter (gem_<turn>_<index>) when building the assistant message, or carry the original wire-name alongside the id so result conversion never has to round-trip through a colliding map.
Summary
GoogleProvidersynthesises tool-call ids asgem_<index>where the index resets every turn, so ids collide across turns of a multi-turn conversation. This corruptsfunctionResponsename resolution and breaks tool-call/tool-result pairing on the 2nd+ tool-using turn.Details
Tool-call ids are minted per-response, scoped to a single turn:
crates/harness-llm/src/google.rs:535:let id = format!("gem_{}", tool_calls.len());crates/harness-llm/src/google.rs:618: streaming path,format!("gem_{index}")Every Gemini turn that emits tool calls therefore produces ids starting again from
gem_0,gem_1, …When building the next request,
GoogleRequest::from_requestconstructs anid_to_namemap keyed by id over the whole message history with last-writer-wins:crates/harness-llm/src/google.rs:302-305:id_to_name.insert(tc.id.as_str(), tc.name.as_str());So turn 1's
gem_0(e.g.fs.read) gets overwritten by turn 3'sgem_0(e.g.code.grep). When turn 1'sMessage::Tool { tool_call_id: "gem_0" }is converted to afunctionResponse, the name resolves to the wrong tool:crates/harness-llm/src/google.rs:358-362:id_to_name.get(tool_call_id).unwrap_or(tool_call_id)Because Gemini matches
functionResponse↔functionCallby name (no ids on the wire), this sends a mismatched/duplicated function name.Impact
Cryptic Gemini
400s or wrong-tool attribution on any conversation that uses tools across more than one turn. Single-turn tool use is unaffected (ids are unique within a turn). Severity: high.Suggested fix
Make the synthesised id unique across the whole conversation — e.g. include a turn/message counter (
gem_<turn>_<index>) when building the assistant message, or carry the original wire-name alongside the id so result conversion never has to round-trip through a colliding map.