Streamed tool calls lose name/id against OpenAI-compatible servers that send explicit nulls (SGLang) — every call fails as UNKNOWN_TOOL #1713
Replies: 1 comment
|
Verified every claim against current master (47f9438) — the report is exact, and the fix is right. Adding the details a maintainer would want before merging: 1. Confirmation with line-level detail
2. The regression-test gap is concrete
{ choices: [{ delta: { tool_calls: [{ index: 0, id: null, type: 'function', function: { name: null, arguments: '{"city"' } }] } }] }— fails on current code (name/callId become empty) and passes after the 3. Scope is nicely localized This is the only OpenAI-compatible delta translator in-tree ( 4. Classification: this is the identity-loss sibling of the #1519 family #1519 (malformed 5. Suggested fix block (for when PRs reopen): if (call.id != null) block.callId = call.id
if (call.function?.name != null) block.name = call.function.nameplus |
Uh oh!
There was an error while loading. Please reload this page.
Summary
When the runtime streams from an OpenAI-compatible server that sends explicit
nulls in tool-call continuation chunks (SGLang does), the delta accumulator inpackages/llm/llm-deepseek/src/translate.tswipes the already-captured tool name and call id on every argument fragment. Every tool call then reaches the tool layer with an empty name and fails asToolNotFoundError: unknown tool ""— with fully intact arguments. This effectively breaks agentic tool use for anyone self-hosting DeepSeek-V4-Flash behindDEEPSEEK_BASE_URL.Environment
deepseek-harness-sdk==0.1.0rc6/deepseek-harness-runtime-bin==0.1.0rc6(Linux x64)examples/jsonrpc-agent/minimal.cordis.ymlcomposition, per the Python SDK guideDEEPSEEK_BASE_URL→ SGLang OpenAI-compatible endpoint (0.0.0.dev1+g4ef1660cd) servingDeepSeek-V4-FlashObserved
Session JSONL, every tool call:
{"turn": 1, "step": 1, "callId": "", "name": "", "arguments": "{\"command\": \"pwd && ls -la && cat greeting.txt ...\"}"} {"error": {"name": "ToolNotFoundError", "code": "UNKNOWN_TOOL"}}The model retries until the step budget runs out; the task never completes.
Root cause
Captured wire chunks from SGLang for one bash call — first delta carries
id+function.name, continuation deltas repeat the fields as explicitnull(where the official DeepSeek API omits them):{"delta":{"tool_calls":[{"id":"call_585b9e17c76f4bd591e549c5","index":0,"type":"function","function":{"name":"bash","arguments":""}}]}} {"delta":{"tool_calls":[{"id":null,"index":0,"type":"function","function":{"name":null,"arguments":"{"}}]}} {"delta":{"tool_calls":[{"id":null,"index":0,"type":"function","function":{"name":null,"arguments":"\"command\": \"pwd\"}"}}]}}translate.ts(master @47f9438, lines 159–160) merges deltas with strict-undefined checks, so the explicit nulls overwrite the captured values:After the last fragment,
callIdandnamearenull, and the tool dispatch sees"".Suggested fix
Treat null and undefined alike when merging continuation fields, per the OpenAI streaming convention that an absent-or-null field means "no change":
(
block.name !== undefinedat line 167 has the same hazard oncenamehas been assignednull.)Workaround
A proxy in front of the model server that drops null
id/type/function.name/function.argumentskeys from streamedtool_callsdeltas restores correct behavior with no other changes — confirmed end-to-end with the same composition and model.All reactions