Repository navigation
Description
Summary
On DeepSeek-V4-Flash, at long input context (~95k+ tokens), the model intermittently omits the <|DSML|tool_calls> START wrapper token while still emitting a well-formed <|DSML|invoke ...>…</|DSML|invoke> block. deepseekv32_tool_parser.py (inherited by V4) anchors tool-call detection solely on the exact START-token string, so when it is missing the entire invoke block is returned as plain assistant content (raw DSML leak) and no tool_calls are produced. Occurs in both non-streaming and streaming paths. Clients then hang because the tool call is delivered as text.
Environment
- vLLM
0.1.dev17209(2026-07-01 build) - Model: DeepSeek-V4-Flash, tensor-parallel TP=2
- Flags:
--tool-call-parser deepseek_v4 --reasoning-parser deepseek_v4 --tokenizer-mode deepseek_v4 --default-chat-template-kwargs {"thinking":false}(reasoning parser falls back to Identity)
Root cause
- Non-streaming
extract_tool_calls(deepseekv32_tool_parser.py, ~L186-189):if self.tool_call_start_token not in model_output: return ExtractedToolCallInformation(tools_called=False, ..., content=model_output)— a complete invoke/parameter/END sequence is ignored when the START token is absent. - Streaming
_process_streaming_buffer(~L403-421): in thenot _in_tool_callsstate it emits the buffer as content until the START token appears; if it never comes, the whole block leaks. - The START omission is a model behavior at long context, documented in
vllm/reasoning/deepseek_v4_reasoning_parser.pydocstring (the model may skip</think>and open<|DSML|tool_calls>); the same skipping affects the tool_calls START token. - Out of scope of [Bug]: DeepSeek V4 DSML tool parser mishandles wrapped and reserved arguments #41240 / [Bugfix] DeepSeekV32/v4: respect string='true|false' attribute andunwrap arguments/input wrapper #41801 / [Bug]: DeepSeek V4 intermittently leaks DSML fragments in auto + streaming mode, causing unstable tool-call parsing #40800-40801: those addressed
string="true"attribute handling, wrapper repair, and streaming flush of split markers. This is a complete START-token omission, not a split/edge-case marker.
Reproduction (parser-level, no GPU)
Feed a complete invoke block without the <|DSML|tool_calls> START wrapper (the END token may or may not be present):
<|DSML|invoke name="terminal"><|DSML|parameter name="command" string="true">echo hi</|DSML|parameter></|DSML|invoke></|DSML|tool_calls>
extract_tool_calls(model_output, request) → tools_called=False, content = the raw DSML above. Prepending <|DSML|tool_calls> makes the identical payload parse into a valid terminal tool call. Verified byte-for-byte against a real user leak.
Suggested fix (orphan-invoke fallback)
When the START token is absent but a complete <|DSML|invoke …>…</|DSML|invoke> block exists:
- non-streaming: synthesize the missing START/END wrapper around the invoke span, then run the normal wrapper-based parse (return
None/plain content if no complete invoke block exists — prose merely mentioning the marker is never misparsed); - streaming: enter tool mode only on a full
invoke_start_regexmatch, with bounded hold-back plus an EOS safety valve that retains only in-progress marker bytes so no content is silently swallowed on a mis-detected stream end (note<|DSML|tool_calls>is 6 ordinary BPE tokens, not a single special token, so a multi-token marker can span an empty mid-stream delta).
I currently run this as a local patch (68 regression checks incl. mid-stream empty-delta and multi-invoke; normal START-present traffic is byte-identical). Happy to open a PR if maintainers agree on the approach.
Activity
Ranoobaba commentedon Jul 21, 2026
Hi, I would like to take this one. I have opened a draft PR with a fix: #49117. Happy to adjust based on feedback.
A6000: DSML orphan-invoke recovery (upstream #49117) + island-aware a…
[Bugfix] Recover DSML tool calls when the start wrapper token is missing
jinbagi commentedon Aug 12, 2026
I opened a fresh issue for this newer variant and current environment: #51914.
The new report focuses on DeepSeek-V4-Flash-0731 + vLLM v0.27.1 + DSpark, where the opening DSML wrapper is not fully omitted but malformed (for example,
tool_calls->toolcalls). I moved the synthetic example and details there so it can be triaged independently from this older long-context wrapper-omission report.A6000: DSML orphan-invoke recovery (upstream vllm-project#49117) + is…
rasatpetabit commentedon Aug 13, 2026
Following up with field results on this issue's failure mode (START
wrapper omitted at long context) and on the recovery path from #49117,
which we have been running hardened in production.
We reproduced this issue's payload byte-for-byte (orphan
<|DSML|invoke …>with the trailing</|DSML|tool_calls>but no STARTwrapper) serving DeepSeek-V4-Flash under agentic load. The #49117-style
recovery does rescue it — the call commits with intact arguments,
streaming and non-streaming. But as shipped, the recovery over-triggers:
<|DSML|invoke …>marker(e.g. the model explaining tool syntax) is recovered into a phantom
tool call. A recovered call must be required to close before it
commits.
are missing, so near-miss prose becomes a malformed call instead of
staying content.
completion quoting DSML markup is consumed — the client gets empty
content with
finish_reason: stop.We fixed these in a six-commit series (close-before-commit, a
required-argument schema gate with defensive handling of malformed
schemas, and a no-tools guard), each with CPU-only regression tests.
The same series also recovers #51914's corrupted-wrapper variant
(
<|DSML|toolcalls>), which appears to be the same underlying modelmisbehavior caught by a different corruption of the envelope.
Since deploying, the wrapper-omission leak has not recurred for us.
Patch series + regression tests:
https://gist.github.com/rasatpetabit/c66923a17305f04b2544f952ab8f50cb
Our baseline is a DSv4 serving fork, so paths may need adaptation to
mainline.
patches: add 0009-0015 - DSML orphan-invoke tool-call recovery + hard…
feat(provider): recover DeepSeek tool calls that arrive as text