Repository navigation
[BUG] Streaming tool-call argument deltas are silently dropped when arguments arrive before the tool-call id #695
Description
Description
When a provider streams a tool call whose first delta(s) carry argument bytes
before the tool-call id is known, those leading argument bytes are silently
discarded by NativeToolCallParser.processRawChunk. The accumulator then begins
mid-JSON, so the finalized arguments are incomplete or unparseable. Downstream
this surfaces as a misleading "missing required parameter" error even though the
model did supply the parameter.
This is the streaming-path root cause behind intermittent tool-call failures. Here the data is lost in transit during
streaming reassembly, so by the time finalization runs the parameter genuinely
is absent from the accumulated JSON.
Related issues
- [BUG] ask_followup_question fails with "missing required parameter follow_up" #690 --
ask_followup_question"missing required parameter follow_up" - [Fix] OAuth callback page shows garbled text after MCP sign-in #218 -- "tool call silently dropped when id and name arrive in separate SSE
chunks" (P1-high). Same class of cross-chunk-identity defect; closest prior art. - [BUG] DeepSeek models (V3/R1/V4 Flash/Pro) exhibit 5 structural tool-calling defects: Missing Required Params (Strict Mode), Draft 2020-12 violations, reasoning_content loss, XML fallback missing, parallel ordering mismatch. #547 -- DeepSeek 5-defect compatibility report. Its Defect 1 ("Missing Required
Parameters" / infinite retry) and Defect 4 ("XML Tag Fallback Missing": model
emits<read_file>...</read_file>as plain text under load) overlap on
symptoms (missing params, XML leaking into content) but attribute them to
schema strict-mode and XML-fallback detection rather than streaming data loss.
Defect 4 corroborates that the observed "XML in response content" is model-side
output, not environment serialization of native calls into XML.
Root Cause
In src/core/assistant-message/NativeToolCallParser.ts, processRawChunk
(approx. lines 99-164) keys its rawChunkTracker by the stream index but only
initializes a tracker entry when a chunk carries an id:
let tracked = this.rawChunkTracker.get(index)
// Initialize new tool call tracking when we receive an id
if (id && !tracked) {
tracked = { id, name: name || "", hasStarted: false, deltaBuffer: [] }
this.rawChunkTracker.set(index, tracked)
}
if (!tracked) {
return events // <-- any `arguments` on this chunk are dropped permanently
}If the first chunk for a given index contains arguments but no id, the
early return discards those bytes. The existing deltaBuffer only buffers
deltas that arrive after a tracker exists (between tracker creation and the
tool_call_start emission); it cannot help pre-id deltas because no tracker
exists yet to hold them.
Failure chain
- Leading
argumentsfragment arrives withoutid-> dropped at the early return. idarrives in a later chunk -> tracker created, accumulation starts mid-JSON.finalizeStreamingToolCallcannot recover the missing leading keys -> returns
nullor an object missing the first parameter(s).Task.ts(approx. line 2817) marks the tool complete with missing params;
the tool guard reports "missing required parameter" -- a misleading symptom.
Provider Scope
The fix is centralized: every provider funnels tool deltas through the single
processRawChunk function, so all of them benefit. The most exposed providers
are the raw-passthrough ones that forward id/name/arguments exactly as
received from upstream:
lite-llm.ts(proxies arbitrary upstream models -- highest exposure)- the OpenAI-compatible family (
openai.ts,openrouter.ts,requesty.ts,
unbound.ts,deepseek.ts,vercel-ai-gateway.ts,zoo-gateway.ts,
base-openai-compatible-provider.ts, etc.)
openai-native.ts and openai-codex.ts already self-guard by withholding
tool_call_partial until a name is known; they are not currently susceptible,
and the parser fix makes that defensive logic redundant (cleanup is optional and
out of scope here).
This is a known, documented limitation
The weakness is already called out in code comments at the two providers that
work around it -- confirming the parser, not the providers, is the correct place
to fix it:
-
src/api/providers/openai-native.ts(~line 1235):// Avoid emitting incomplete tool_call_partial chunks; the downstream // NativeToolCallParser needs a name to start a call.
-
src/api/providers/openai-codex.ts(~line 954):// Codex/Responses may stream tool-call arguments, but these delta events are not guaranteed // to include a stable id/name. Avoid emitting incomplete tool_call_partial chunks because // NativeToolCallParser requires a name to start a call.
These guards prevent the drop for those two providers but were never added to the
raw-passthrough providers (LiteLLM, OpenAI-compatible family), which is precisely
why those remain exposed. Fixing processRawChunk resolves the limitation
centrally and renders both guards redundant.
Affected Files
src/core/assistant-message/NativeToolCallParser.ts(processRawChunk, ~99-164)
Proposed Fix
Initialize the tracker on first sight of an index, independent of id:
- Create the
rawChunkTrackerentry when anindexis first seen, withid
left optional until it arrives. - Buffer all
argumentsintodeltaBufferwhilehasStartedis false (covers
both pre-idand pre-namecases). - Record
idandnamewhen they arrive; emittool_call_startand flush the
buffered deltas only once bothidandnameare known. - Keep the downstream event sequence identical so no provider or
Task.ts
changes are required.
Test Procedure
- Add a parser-layer unit test in
src/core/assistant-message/__tests__/NativeToolCallParser.spec.tsthat feeds
processRawChunka leading chunk withargumentsand noid, followed by a
chunk carryingidandname, and asserts the full argument string is
preserved throughfinalizeStreamingToolCall. - Verify existing provider specs that drive
processRawChunkdirectly still
pass (lmstudio-native-tools.spec.ts,qwen-code-native-tools.spec.ts,
openrouter.spec.ts). - Manual: reproduce a tool call via LiteLLM in F5 development mode and confirm
no spurious "missing required parameter" error.
Activity
leesilverman commentedon Sep 8, 2026
Caveat up front: I am a power-user, not a developer. I am trying to help by providing with real-world data from my own fleet (several dev containers plus my workstation running Zoo Code). Happy to run experiments or tests on my own setup and report back. I am writing this comment using zoocode + Spark 1.3. (This model can be quite terse, so please ask if you need more detail.)
Corroboration for the pre-id delta-drop path on raw-passthrough routing:
Fleet shape: same 24 of 24 live faults with "input": {} byte-identical across write_to_file, execute_command, and attempt_completion, non-deterministic, at any token level. Below-limit by construction — faults strike ordinary mid-chat turns (loop-chat cumulative tokensOut 4K-13K vs 32K per-request ceiling), so the mechanism must be ordering-dependent, never usage-dependent. Pre-id argument-delta loss predicts exactly that.
Routing exposure: every fleet call routes exclusively through LiteLLM v1.99.1 with no provider-side incomplete-chunk guard. Your body already names this gap: openai-native.ts and openai-codex.ts withhold incomplete tool_call_partial chunks until a name is known, but those guards were never added to the raw-passthrough providers (LiteLLM plus the OpenAI-compatible family) — precisely the path every fleet call takes. We are the exposed class with no guard in front of the parser.
Proxy-excluded control: LiteLLM proxy ruled out as an arg-stripper on all three surfaces — tool-policy cannot strip (allow-through, zero blocks), compression/cache 0-hit under load and never touches request args, passthrough preserves 200 OK with spend rows intact. That isolates the loss to client-side streaming reassembly, which is the layer this issue attributes it to.
Version carry, no released fix: identical fault reproduces across every running version from 3.81.100441 through 3.83.100458 (workstation active 3.83.100457, one container host already on 3.83.100458).
Offer to test: happy to test any pre-id buffering build (tracker init on first index sighting, buffer pre-id/pre-name deltas, flush once id plus name known) and to report fault rate plus any double-present or dropped-call regression on my setup. I considered but decided against patching the minified bundle locally. Waiting for a merged, released build. But I will run whatever test build or spec you point me at.
Not filing a new issue — adding fleet signal to this thread only. No fix claimed.
Environment signature: written using zoocode 3.81.100441 + Spark 1.3.
fix: streaming tool-call argument loss in NativeToolCallParser (Zoo-C…
refactor(parser): cut PR Zoo-Code-Org#700 to parser-only streaming scope
fix: streaming tool-call argument loss in NativeToolCallParser (Zoo-C…
refactor(parser): cut PR Zoo-Code-Org#700 to parser-only streaming scope
fix: streaming tool-call argument loss in NativeToolCallParser (Zoo-C…
Merge branch 'main' into fix/shared-sdk-error-handle-gap