Bug: streamed tool calls assembled with empty name/callId when continuation deltas carry explicit id:""/name:null (dsh-llm-deepseek, rc.6 & rc.7) #3281
Replies: 3 comments
|
补充一个面向操作者的判别方法。这里最容易误诊成“工具未注册”,但决定性证据其实在 identity 最后一次成功出现的位置:
最小证据包建议只保留:同一 call index 的有序 wire deltas、对应 Session raw chunks 与 completed 回归测试需要至少两包:首包非空 identity,续包 完整的英文 capture、route isolation、source path 和 acceptance checklist: 说明:这是按当前 rc.7 源码核对的独立社区 runbook,不代表修复已经进入官方发行版。 |
|
Proposed fix (fork PR; upstream rejects external PRs per CONTRIBUTING): hungnnvidia#1 — only assign tool-call id/name from non-empty strings so empty continuation deltas cannot clobber the first-delta identity. |
|
Proposed fix (fork PR; upstream rejects external PRs per CONTRIBUTING): hungnnvidia#1 — only assign tool-call id/name from non-empty strings so empty continuation deltas do not clobber the first-delta identity. |
Uh oh!
There was an error while loading. Please reload this page.
Versions:
@deepseek-ai/dsh0.1.0-rc.6 and 0.1.0-rc.7 (both reproduce) · headless profile ·DSH_TOOLS_MODE=native· modeldeepseek-v4-flashserved through an OpenAI-compatible gateway (DEEPSEEK_BASE_URLoverride).Symptom
Every streamed tool call is assembled with an empty
nameandcallId, so it fails tool-policy checks (tool "" is disabled …). The model retries a few steps, gives up, and the turn ends cleanly (finish=stop) with no output and nothing on stderr — from the harness's perspective nothing failed, which makes this very hard to notice in headless/CI use.The same setup with
deepseek-v4-proworks fine, which hid the bug for a while.Root cause
dsh-llm-deepseek'stranslate()(lib/index.js ~L321 in the published package):The guards only exclude
undefined. Per the OpenAI streaming convention,tool_calls[].idandfunction.nameare sent once in the first delta and continuation deltas omit them — but some backends/gateways serialize continuations with explicitid: ""andfunction: {name: null}instead of omitting the fields. Those explicit empty values pass the!== undefinedguards and clobber the values captured from the first delta.closeBlock()then emits the block withname: block.name ?? ""→"", andBlockAssembler's "first close wins" rule locks the empty block in.Observed on the wire (session JSONL): first
tool-call-deltahasid:"call_…", name:"read"; every continuation hasid:"", name:null; the assembledtool/callevent hasname:"",callId:"".Fix (verified)
Tighten both guards from "defined" to "non-empty":
With this two-line change, tool calls assemble correctly (
name:"read", real callId) and the repro passes on both rc.6 and rc.7. An empty-string id/name carries no information, so ignoring falsy values can't lose anything a conformant stream would send.Minimal repro
against any backend that emits explicit
id:""/name:nullin tool-call continuation deltas (100% reproducible for us withdeepseek-v4-flashbehind vLLM-style gateways;deepseek-v4-proomits the fields and is unaffected).A related hardening suggestion: when a tool call does end up with an empty name, surfacing an explicit stream/protocol error would make this class of failure loud instead of a silent empty turn.
Happy to provide full session JSONL excerpts if useful. (Filed as a discussion since Issues are disabled on this repo.)
All reactions