Repository navigation
DeepSeek DSML control tokens leak into text-dialect tool calls; the call is dropped without a diagnostic #204
Description
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:
- Recognise DSML control tokens as terminators in the text dialect, or strip them before parsing. They are a known DeepSeek vocabulary, not arbitrary text.
- Fall back on a well-formed JSON object carrying
name/argumentsinside an unterminated<tool_call>block, rather than discarding the block whole. - At minimum, log it. A
warn!naming the tool and the unmatched opener, and ideally aToolErrorback 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
tinysweeper commentedon Sep 23, 2026
DeepSeek DSML control tokens leak into text-dialect tool calls, causing parsing failure and silent drop without any diagnostic or recovery.
Labelled
priority: p1.senamakel commentedon Sep 30, 2026
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 #204in #235 did not auto-close, because closing keywords are not linking issues in this repo.