Bug: every tool call fails with unknown tool "" when gateway sends explicit null id/name on tool-call deltas
#3069
Replies: 1 comment
|
Confirmed the same root cause on the same gateway shape ( I applied a fix that treats empty as absent and guarantees a non-empty fallback id at every site where a tool-call block is assembled:
Tests: updated the two cases that previously asserted the empty-id output, added a regression for the explicit empty-string delta, full Cherry-pickable single commit on master: |
Uh oh!
There was an error while loading. Please reload this page.
Summary
Every tool call fails with
ToolNotFoundError: unknown tool ""(empty tool name) when the DeepSeek API is reached through a gateway/proxy that sends explicitnulls on tool-call continuation frames. Reproduced withbaseURL: https://opencode.ai/zen/go/v1+deepseek-v4-flashon0.1.0-rc.6(bug also present in rc.7 — I diffed the translate code).The official
https://api.deepseek.comendpoint is unaffected (it omits the fields entirely), so this only bites users behind third-party gateways.Symptom / log signature
Session log (
session.jsonl) — the first delta carries the call identity, every continuation frame clobbers it:{"type":"tool-call-delta","index":2,"id":"call_b72f0fa7...","name":"bash","argumentsDelta":""} {"type":"tool-call-delta","index":2,"id":"","name":null,"argumentsDelta":"{"} {"type":"tool-call-delta","index":2,"id":"","name":null,"argumentsDelta":"\"command\": \"pwd && echo"}Final assembled block:
{"type":"tool-call","id":"","name":"","arguments":"..."}→ tool executor looks up tool""→ToolNotFoundError: unknown tool ""→ the model retries the same failing call every step and eventually gives up (its reasoning even starts hallucinating<invoke name="">XML — a red herring, the protocol is structuredtool_calls).Root cause
translate()inpackages/llm/llm-deepseek/src/translate.tsguards the accumulated call identity with!== undefined:The standard OpenAI-compatible stream contract omits
id/nameon continuation frames, but some gateways send them as explicitnull. Sincenull !== undefinedistrue, each continuation frame overwrites the accumulatedcallId/namewithnull→ every subsequent delta carriesid: ""/name: null→ final block has emptyname.Suggested fix (2 lines + regression test)
Use
!= nullso bothundefinedandnullare rejected:I verified: with the exact wire shape from the failing session, the old code reproduces the empty
id/namebyte-for-byte; the new code preserves the identity across the stream, and the tool call executes normally.I have a prepared branch with the fix + a regression test in
tests/translate.spec.ts(31/31 tests pass): https://github.com/realchendahuang/deepseek-harness/tree/fix/null-tool-call-deltasSince external PRs are not accepted yet, happy for maintainers to cherry-pick from that branch, or I can apply any changes you'd prefer.
Workaround for affected users (until fixed)
Patch the two lines in the installed package and restart dsh:
(Linux: drop the quotes after
-i. Note: re-apply after upgrades.)All reactions