Skip to content

[Bug] DeepSeek-V4 tool-call parser leaks raw DSML into content when the model omits the <|DSML|tool_calls> START token (long context) #48931

Closed

Description

@whguswka

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

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_regex match, 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

  1. Ranoobaba commented on Jul 21, 2026

    @Ranoobaba

    Hi, I would like to take this one. I have opened a draft PR with a fix: #49117. Happy to adjust based on feedback.

  2. added a commit that references this issue on Aug 9, 2026
    f1fbf8e
  3. jinbagi commented on Aug 12, 2026

    @jinbagi

    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.

  4. rasatpetabit commented on Aug 13, 2026

    @rasatpetabit

    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 START
    wrapper) 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:

    1. Assistant prose that merely mentions an <|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.
    2. A recovered call commits even when the tool's required arguments
      are missing, so near-miss prose becomes a malformed call instead of
      staying content.
    3. Requests that declare no tools still enter tool parsing, and a
      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 model
    misbehavior 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions