fix(streaming): merge tool-call fragments when providers reuse index with fresh per-fragment ids - #45164
Conversation
…with fresh per-fragment ids Kimi (kimi-for-coding/K2.6) and LM-Studio stream a single tool call with the index held at 0 but a FRESH id on every argument fragment; the function name only appears on the first chunk. The existing "different id at the same index -> new slot" rule treated each fragment as a separate tool call, shattering one call into many invalid-JSON pieces. Downstream that trips has_truncated_tool_args, which upgrades finish_reason to "length", so the user gets "Response truncated due to output length limit" even though nothing was actually truncated (only happens on tool-calling turns, which makes it look intermittent). Only open a new slot when the delta also carries a function.name: per the OpenAI streaming convention only the first chunk of a tool call carries the name; argument-continuation chunks never do. This keeps the existing Ollama parallel-call detection (a new call always begins with a name) and adds a regression test for the Kimi/LM-Studio fragment pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Verified: this is a clean, well-scoped fix with a clear root-cause analysis and solid test coverage. What's correct:
No issues found. The fix is surgical — one boolean guard + one test, no behavioral side effects on existing providers. |
khamis1992
left a comment
There was a problem hiding this comment.
Hermes Agent Review — APPROVE ✅
Verdict: Ready to merge.
What it does
Fixes streaming tool-call accumulation for providers (Kimi, LM-Studio) that keep index=0 fixed but emit a fresh ID on every argument fragment. Previously each fragment opened a new slot, shattering one call into many invalid-JSON pieces. The fix adds a guard: only treat a changed ID as a new tool call when the delta also carries a function.name (per OpenAI convention, only the first chunk carries the name).
Review
- Correctness: ✅ The fix correctly follows the OpenAI streaming convention. The name guard is the right discriminator.
- Security: ✅ No concerns.
- Testing: ✅ New test
test_kimi_fresh_id_per_fragment_merges_into_one_tool_callwith 7 fragments across 6 different IDs — verifies they merge into one call with valid JSON. Also asserts finish_reason staystool_calls(not upgraded to length truncation). - Blast radius: LOW — affects only the streaming tool-call accumulator, and only when a provider emits fresh IDs per fragment.
- Code quality: ✅ Clear comment explaining the rationale. Minimal change.
Suggestions (non-blocking)
- None.
Reviewed by Hermes Agent (builder profile)
|
Thanks for the focused regression coverage. Current The proposed name guard is consistent with current behavior: genuine same-index second calls are named in The target blocks remain materially unchanged despite later line movement, so this is a high-salvageability mechanical transplant onto current main. This is an automated hermes-sweeper review. |
Summary
Some providers stream a single tool call with
indexheld at0but a freshidon every argument fragment (the function name only on the first chunk). The streaming accumulator's "different id at the same index → new slot" rule treats each fragment as a separate tool call, shattering one call into many invalid-JSON pieces. Downstream this tripshas_truncated_tool_args, which upgradesfinish_reasonto"length", so the user gets "Response truncated due to output length limit" even though nothing was truncated.This affects Kimi
kimi-for-coding/K2.6 (https://api.kimi.com/coding/v1) and LM-Studio (cf. #5331). It only happens on tool-calling turns, which makes it look intermittent — plain-text replies are fine.Root cause (captured deltas)
A single
skill_viewcall streamed from Kimi:Result: 11 tool-call slots, each holding an invalid-JSON fragment;
completion_tokenswas ~230 (no real length truncation).Fix
Open a new slot only when the delta carries a
function.name. Per the OpenAI streaming convention only the first chunk of a tool call carries the name; argument-continuation chunks never do. This preserves the existing Ollama parallel-call detection (a genuine new call always begins with a name) and is more robust than an arguments-length heuristic (it doesn't depend on fragment size).Test
Adds
test_kimi_fresh_id_per_fragment_merges_into_one_tool_call. Verified it fails without the guard (shatters into 11 calls) and passes with it; existingtest_ollama_reused_index_*and the MiniMax/accumulator tests stay green (11 passed).Related
Same root cause as #5331 (LM-Studio) — this proposes a smaller, provider-agnostic condition (gate on name vs. an
len(args) < 100heuristic) and adds Kimi coverage. Also adjacent to #25046 (Gemini streaming JSON). Refs #18742.🤖 Generated with Claude Code