Skip to content

DeepSeek DSML control tokens leak into text-dialect tool calls; the call is dropped without a diagnostic #204

Closed
tinyhumansai/tinytools
#37

Description

@senamakel

DeepSeek <|DSML| …> control tokens leak into text-dialect tool calls, the call fails to parse, and it is dropped without a diagnostic.

Observed against deepseek/deepseek-v4.1-flash over OpenRouter, pinned at 157186c (v2.1.2), driving a real agent turn. The model opened a tool call with the text dialect's <tool_call> and closed it with DeepSeek's own DSML control tokens. Nothing parsed it, nothing logged it, and the turn continued as though the model had said nothing.

The agent had finished its research and was writing its deliverable. The file was never created, and the turn ended with the model reporting the work as still to do.

What the model emitted

Verbatim from the assistant message (\n shown as real newlines, tokens as sent):

Heredocs aren't working in this shell. Writing the script to a file instead.

<tool_call>
{"arguments":{"path":"baggage-policy/work/extract.py","content":"import re, html, sys\n\nt = open('work/overview.html', ...).read()\nt = re.sub(r'<script.*?</script>', ' ', t, flags=re.S)\n..."}}</|DSML| parameter>
<|DSML| parameter name="name":"file_write"}</|DSML| parameter>
</|DSML| invoke>
<|DSML| invoke>
{"arguments":{"category":"read","command":"cd baggage-policy && python3 --version && ls work"},"name":"shell"}</|DSML| parameter>
</|DSML| invoke>
</|DSML| calls>

Two tool calls are unambiguously present and well-formed as JSON — a file_write and a shell. Neither ran.

The opener is <tool_call>, which the text dialect advertises. Every closer is a DSML control token: </|DSML| parameter>, </|DSML| invoke>, </|DSML| calls>. </tool_call> never arrives, so a parser scanning for the matching close finds no terminator and yields nothing.

Note the payload itself contains </script> and <[^>]+> inside a JSON string. Whatever the recovery path ends up being, it has to tolerate angle-bracket sequences inside argument values rather than scanning naively for the next <.

Why this is not a model-only problem

The same turn made 72 tool calls that parsed fine (63 shell, 7 web_fetch, 2 web_search_tool). This is not a model that cannot emit tool calls — it is a model that degrades intermittently, and the harness has no floor under it.

The harness contribution is the silence. Grepping that run's log for malformed|unparsed|parse error|failed to parse|invalid tool call|unterminated|DSML returns 0 hits. There is no warning, no ToolError back to the model, and no counter. The model is never told its call did not happen, so it cannot retry; the operator sees only a turn that quietly did less than it claimed.

Three things would each have saved this turn, in rough order of preference:

  1. Recognise DSML control tokens as terminators in the text dialect, or strip them before parsing. They are a known DeepSeek vocabulary, not arbitrary text.
  2. Fall back on a well-formed JSON object carrying name/arguments inside an unterminated <tool_call> block, rather than discarding the block whole.
  3. At minimum, log it. A warn! naming the tool and the unmatched opener, and ideally a ToolError back to the model saying the call did not parse, turns a silent no-op into something both the model and the operator can act on.

Relationship to #143

Related, not the same. #143 is a call written into message.content with no message.tool_calls field — a native dialect recovery gap. This one is a text dialect call whose framing is corrupted mid-emission by control tokens from the model's own chat template.

They share a consequence, and it is the expensive part: a tool call the model clearly intended is discarded in silence. If the fix for #143 establishes a general "we found something that looks like a tool call but could not use it" path, this case should route into it.

Reproduction

Not deterministic — it is an intermittent degradation. It surfaced on a long turn (40 model calls, ~2.1M prompt tokens) where the model had been emitting large content payloads containing code and HTML. Two later runs of the same scenario on the same model failed at different points without reproducing this exact emission, which is consistent with intermittency rather than a specific trigger.

Found while running OpenHuman's scripts/life-scenarios benchmark (baggage-policy).

Activity

  1. added
    priority: p1Next. Wrong behaviour a user will hit, or a security weakness behind a condition.
    on Sep 23, 2026
  2. tinysweeper commented on Sep 23, 2026

    @tinysweeper

    DeepSeek DSML control tokens leak into text-dialect tool calls, causing parsing failure and silent drop without any diagnostic or recovery.

    Labelled priority: p1.

  3. added theissue type on Sep 23, 2026
  4. self-assigned this
    on Sep 30, 2026
  5. senamakel commented on Sep 30, 2026

    @senamakel
    MemberAuthor

    Fixed by #235 ("Recover mixed DeepSeek DSML tool calls"), together with the parser fix in tinyhumansai/tinytools#22 pulled in by #205. Closing manually: Closes #204 in #235 did not auto-close, because closing keywords are not linking issues in this repo.

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpriority: p1Next. Wrong behaviour a user will hit, or a security weakness behind a condition.

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions