Bug: Tool calls fail with unknown tool "" (empty name) when a large tool call is split into multiple stream chunks
#2855
Replies: 3 comments
|
Verified on rc.7 ( 1. Your two defects confirm at rc.7 source:
2. Your backfill fix is the correct third layer. The earlier fixes (translate.ts truthy guards — zoahdev's 3. Cross-thread convergence for the fix. The family now has a complete two-layer fix blueprint:
4. Session-validation note: the reason this becomes a permanent dead session (not just a dead step) is Worth posting your repro + fix on #2540 (the empty-string thread with the fullest root-cause writeup) so the maintainers get the complete two-layer picture in one place — or I can cross-link. Either way, the family is now fully diagnosed end-to-end. |
|
There is a source-level alternate route that avoids both the affected DeepSeek translator and its empty block-end identity. A transport-owning Pi provider such as dsh plugin --profile web add pi2dsh
dsh plugin --profile web add pi-provider-litellm
export LITELLM_BASE_URL="<your gateway root URL>"
export LITELLM_API_KEY="<your key>"
# restart dsh and select the LiteLLM routeI audited the complete source path before suggesting this. I do not have your gateway deployment or subscription, so this is source-audited rather than an account-level E2E claim. If you try it and still reproduce the failure, please post a redacted stream/error at https://github.com/weijiafu14/pi2dsh/issues and I will follow the real response shape through the bridge. |
|
Confirming this still reproduces on the current published build, and the root cause is unchanged at Environment
Root cause confirmed at current
Why only There's already a patch with unit tests in a fork — see #3384 ( |
Uh oh!
There was an error while loading. Please reload this page.
Bug: Tool calls fail with
unknown tool ""(empty name) when a large tool call is split into multiple stream chunksEnvironment
0.1.0-rc.6(npm @deepseek-ai/dsh)deepseek-officialroute → opencode-go gateway (https://opencode.ai/zen/go/v1), modeldeepseek-v4-flashSymptom
After a tool call whose
argumentsare split across many SSE chunks, every subsequent tool call fails withError: unknown tool ""(ToolNotFoundError, codeUNKNOWN_TOOL). This is systemic, not per-tool:bash,grep,read,echo,web_searchall fail. Once it starts it never recovers in the same step; the session is effectively dead for tool use.Root cause (confirmed with raw stream bytes)
In
packages/llm/llm/src/assembler.ts(BlockAssembler), two defects combine:tool-call-deltaid capture is unconditional — it clobbers the first-captured id:When the gateway emits an empty
idon subsequent deltas,partial.toolCallIdbecomes''.block-end+assemble()short-circuit discards the accumulated name/id even when they were correctly captured:So even when
partial.toolCallNamewas set correctly,assemble()returns the rawblock-endblock whosename/idmay be empty →ToolNotFoundError: unknown tool "".Evidence (raw chunks from a failed session)
A failing
bashcall, same block index, name present on the first delta only, then all-emptyid/name; theblock-endcarries an empty block:{"type":"assistant/chunk","data":{"chunk":{"type":"tool-call-delta","index":2,"id":"call_2a60dd...","name":"bash","argumentsDelta":""}}} {"type":"assistant/chunk","data":{"chunk":{"type":"tool-call-delta","index":2,"id":"","name":null,"argumentsDelta":"{"}}} {"type":"assistant/chunk","data":{"chunk":{"type":"tool-call-delta","index":2,"id":"","name":null,"argumentsDelta":"\"command\": ..."}}} ... many more deltas, all id="" name=null, argumentsDelta only ... {"type":"assistant/chunk","data":{"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","name":"","id":"","arguments":"{...full args...}"}}}}Contrast a healthy call where every delta carries
name:{"type":"assistant/chunk","data":{"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_...","name":"job_list","argumentsDelta":"{}"}}}Trigger condition
A single tool call whose
argumentsare split into many stream chunks (large/full-content arguments — e.g. a big file read or long command). The gateway emitsname/idonly on the first delta; when the (non-deterministic) chunk-split boundary separates the name-bearing delta from the argument deltas, the assembler drops the name. This is why it appears "random" and reproduces across fresh sessions and restarts.Minimal reproduction
Suggested fix
Make
assemble()treat the accumulated deltas as authoritative for any field the rawblock-endblock is missing:and guard the id capture to match name (
if (chunk.id) partial.toolCallId = chunk.id;).The assembler already documents "log the raw chunks for replay fidelity" — the accumulated
partial.*values are provably correct here, so backfilling is safe and cannot regress the healthy path (verified: a fully-populatedblock-endis returned unchanged).Notes
rc.7(0.1.0-rc.7) does not fix this: commit84257eachanges only max-tokens replay-metadata alignment;assemble()still short-circuits onpartial.block.All reactions