You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Malformed XML/DSML tool-call output from Opencode Go models #50206
Expected: A valid structured tool call, or a clean rejection/fallback path.
Actual: The output is treated as plain text / malformed structured content, so the tool never runs correctly.
Impact:
Tool calls fail or are ignored
Automation breaks
Models produce “fake” tool attempts that don’t execute
Observed with: DeepSeek v4.1 flash and similar models (happened before but I can't remember the exact models). For DeepSeek v4.1 flash, I started observing this behavior today, and model speed is also a lot slower than a few days ago. Probably means I am now routed to models deployed by opencode instead of official DeepSeek API used when first launched.
session 01a0c319-bf08-7f31-9933-d8a647907b0b (16:33, follow-up of the first) — 9
Verbatim example from an assistant message (normal prose, then this):
...</|DSML| parameter>
<tool_call>
<tool_name>exec_command
git status --short
<parameter name="justif...
In most cases the turn still ends with task_complete, and last_agent_message is just prose + the broken </|DSML| parameter> tail — no tool executes, so from the user side the task simply "stops". The same sessions show many manual turn_aborted (reason: interrupted) on my side after waiting (one turn ran 9m38s before I stopped it). Since my client goes through a Responses→Chat conversion proxy, the DSML arrives as message content, never as structured tool_calls.
To distinguish from other failures: earlier in the week I did see genuine upstream errors through the same chain (a wave of HTTP 503/500 on 09-16, and HTTP 550: Upstream response was not valid JSON on 09-20 16:52), but on 09-21 there were no HTTP/proxy errors at all — requests succeed and the DSML text is what comes back. This matches the routing change you mentioned.
Happy to provide trimmed rollout files or repro details if useful.
Same symptom, and a narrowing of where the silent failure actually comes from
Adding a data point from the opencode CLI client itself (not a third-party agent), plus what I think is a useful distinction for whoever picks this up.
Environment
Client: opencode CLI on macOS (darwin)
Model: deepseek-v4.1-flash via opencode-go
Repro: long single-threaded session; reproduced twice in one session after heavy tool-call volume
What I see
The model emits DSML-ish pseudo-XML with a mix of parameter / invoke wrappers, some with a malformed trailing close tag. It lands in the assistant message content, so the TUI just renders it as ordinary text. No tool runs, no error is surfaced, and — importantly — nothing is fed back to the model, so it keeps producing the same shape.
The distinction I think matters
There is already a repair path for malformed tool calls, and it does return an error to the model:
experimental_repairToolCall in packages/opencode/src/session/llm.ts (around line 296) rewrites the failed call — either lowercasing the tool name when that matches a known tool, or otherwise replacing the call with the invalid fallback tool, carrying failed.error.message as its input.
packages/opencode/src/tool/invalid.ts defines that invalid tool, whose execute returns The arguments provided to the tool are invalid: ... as a tool result — so the model does get told.
The gap is when that path runs. It only fires when the SDK surfaces a failed tool call (unparseable JSON args, schema validation failure, truncation). When DSML arrives as message content, it never becomes a tool call at all — so there is no failed tool call, the repair hook is never invoked, and the model receives zero feedback. The result is the "I changed the file but nothing happened, and I'm not sure why" loop.
So I'd frame the harness-side ask as: text-content leakage bypasses the existing repair chain entirely, rather than "malformed tool args are silently ignored" — the latter is mostly already handled. That also explains why #21688 (truncated tool calls) doesn't cover this, and I'd guess #50814 is adjacent but compaction-specific.
Concrete ask, if feasible
When an assistant message's text content looks like an unparsed tool call (DSML/pseudo-XML), could the client either:
synthesize an invalid-style tool_result carrying that text, so the model gets a corrective signal and can retry through the proper channel, or
at minimum surface a visible warning in the TUI that a tool call was emitted as text?
Either would turn an invisible failure into a self-correcting one. Happy to provide trimmed session JSON if that helps reproduce.
Not filing a separate issue since this looks like the same root cause — adding here instead.
Another reproduction: DeepSeek V4.1 Flash on the Opencode Go provider (V2)
Confirming this on the exact model named in the report, on the V2 line.
Environment
opencode 2.0.22
provider/model: opencode-go / deepseek-v4.1-flash
long, context-heavy session: many large file reads and many tool calls before it started
What happens
Intermittently, when the model decides to call a tool, the invocation is emitted as a normal
assistant text part containing an in-band XML/DSML block, instead of as a structured tool
call. Nothing executes, and the turn reads to the user as though the tool ran. The markup has
the same shape as the example in the original report (an invoke block wrapping parameter
elements). It recurred several times in a row in the same session, each time on a different
tool.
Evidence from the session store
The failing turns contain an assistant text part whose content is the DSML block. No
structured tool part exists for those turns: in this session there were 6 text parts
carrying the markup and no corresponding tool parts.
I also caught it describing itself: an assistant text part whose content was the literal
string "you printed this:" followed by the DSML block.
Notes
It is not the oldString matching family: the edit tool never runs at all, so the failure is
upstream of argument handling. Re-reading the file after a failed attempt confirmed nothing
was applied.
It correlates strongly with session length / context size, matching the report's "long
session" observation and the v3.2 report's sequential-tool-call observation.
Workaround on my side: it stopped reproducing after shortening the session (e.g. compacting);
it returns as the context grows again.
Same failure family on a different serving lane, plus one artifact not yet reported here: a truncated structured tool call arriving alongside the text leak. Adding our evidence to this thread rather than filing a separate issue.
Environment
opencode 1.18.25, Windows 11, running as an ACP server (embedded in a host application rather than the TUI), about 30 tools exposed to the model
Model: xiaomi/mimo-v2.6-flash through OpenRouter with a BYO API key (custom openai-compatible provider), reasoning effort low - so not the opencode-go tier, and not opencode-hosted serving
One dense 25-minute ACP session with over a hundred tool calls, otherwise clean
(Local paths inside the quoted fragments below are redacted; the markup shape is verbatim. The evidence was collected on v1 - the host pins the 1.18.x CLI - but the artifacts are transport-level: a truncated structured tool call and a dropped emission are what the provider lane puts on the wire regardless of client version, and the text-leak mode is already confirmed on 2.0.22 upthread. The wire probe below drives the provider API directly and transfers unchanged to any client version.)
Mode 1: whole emission as content (the mode this thread is about)
2026-10-15 17:34 Central (22:34 UTC), a read call arrived entirely inside delta.content, no structured call was emitted, and the tool never ran:
(The leading < never arrived; quoted as captured. The V2.6 in-band format here is <function=name><parameter=key>value</parameter></function>, which differs from the tml:function_calls wrapper reported for MiMo V2.5 in #43385.)
The agent re-issued the read 30 seconds later and got the real file, so the turn recovered on its own.
Mode 2: truncated structured call plus the tail as content
2026-10-15 17:43 Central (22:43 UTC), one grep emission produced three artifacts in the same burst: a structured tool call named g (the fragment left when grep is cut after its first character), which the ACP client's tool router rejected as an unavailable tool; the tail of the same emission inside delta.content:
and no executing call for that emission - the grep that returned results a second later was a different call with different arguments (its matches included .md files, outside this call's include filter).
The interesting part mechanically: a truncated structured call can only come from the serving lane's own text-to-tool_calls conversion. opencode v1 has no in-band markup parser (git grep finds no handler for this format in the 1.18 tree or current dev), so the OpenRouter lane for this model does convert in-band markup server-side, and mode 1 is that same conversion dropping an emission entirely. That also fits the session-length correlation reported upthread: our failures happened deep into a dense session, never early.
Wire-level checks
Fourteen direct requests to OpenRouter's chat completions endpoint (12 streaming, 2 non-streaming, single small get_weather tool) came back clean every time: id on the first chunk, name in one chunk, arguments split across continuations, no content leaks. So the malformation is rare and load-dependent, not something a minimal repro reaches quickly.
Relevance to the asks upthread
The truncated-call variant at least surfaced as a router rejection in our setup, but mode 1 was fully silent, which matches the earlier analysis here: text-content leakage bypasses the repairToolCall / invalid chain entirely. Both proposed mitigations (synthesize an invalid-style tool result carrying the text, or at minimum warn) would have turned both of our failures into self-correcting ones. Raw probe runs and the harness that produced them are available on request.
Adding a data point, because I hit this exact failure mode hard and ended up
building a fix.
The shape reported here is the one I traced.< parameter name="edit">
with the outer < tool_calls> opener missing, and only the inner < parameter> elements carrying the DSML marker. In my sessions this only
happened late in very long contexts, never early — which matches the
"having a long session with DeepSeek v4.1" repro above. The parser degrades
quietly, passes the markup through as ordinary text, and the tool never runs.
The provider's text→tool-call extraction is the layer that silently fails at
long context; it is not the model declining to call the tool.
Once one malformed call lands in the transcript, the model imitates its own
degraded output and every subsequent turn fails the same way until you clear
or repair the history. So history sanitization matters more than the live
recovery.
The shapes vary: missing outer opener, mangled invoke opener, mis-closed
parameters, bare-JSON bodies, and (rarest) a markerless <parameter> whose
argument key is stripped.
Since OpenCode has no built-in DSML handling, I wrote an OpenCode V2 plugin
that sits in the response path and repairs these in-session: it recovers the
leaked call into a real tool call, sanitizes the poisoned history, and nudges a
stalled turn to re-issue. I run it with DeepSeek V4.1 on DeepInfra.
It is MIT, a fork of the earlier opencode-plugin-dsml-fix (credit to
bendtherules for the original buffering/parse architecture). It is a personal
fix — tested on one provider/model combination, no support matrix — but if
you're hitting this on V4.1 it may save you the manual repair loop.
If it helps, I'm happy to share the specific leak bytes I captured (the
markerless <parameter name="shell"> shape was the hardest one) for anyone
looking to fix this lower down in the provider path.
From the request logs, almost all requests that hit malformed DSML output have tools count = 0. Can you check if you're using an API format conversion tool? It might be causing the issue.
Thanks — good observation. Answering directly: no conversion layer here.
It's OpenCode → DeepInfra direct, no API-format shim.
I checked the tools = 0 correlation on our side by logging the outbound request
at two points: the tool count at assembly, and the tools array on the wire.
Across 4 malformed-DSML turns (all held-candidate, outer < tool_calls>
opener missing — the shape from my first report):
context depth
tools at assembly
tools on wire
86 msgs
12
12
387 msgs
12
12
108 msgs
12
12
122 msgs
12
12
Every leak shipped 12 tools to dispatch, and across the whole log 424/424 requests had tools=12 — tools=0 never appears. So the correlation
doesn't reproduce on this path: the tools array isn't dropped here before the
request leaves OpenCode. That points at the text→tool-call extraction after the
request lands, not at the tools array.
To verify your own setup: the plugin now logs request … tools=N and wire … tools=N names=[...] when you enable "debug": true. (Caveat: this only
covers our path, so it doesn't contradict what you're seeing at your tier.)
Disclosure: drafted with an AI assistant; the measurements are my own.
Adding a data point from a long OpenCode CLI session on 2026-10-20.
Environment
opencode 2.0.32, Linux/WSL2 (x86_64), VS Code integrated terminal
provider/model: opencode / deepseek-v4.1-flash-free, variant max
no plugins configured
long single session, ~2,000 stored events and heavy tool use
What happened
While editing files, the model emitted a malformed tool call using the full-width DSML delimiters (||DSML|| parameter>, ||DSML|| invoke>, ||DSML|| calls>). The client did not execute it, did not render it as assistant text, and did not surface an error. The assistant turn simply ended empty. The next stored message is the user asking why the agent stopped.
Unlike the text-leak cases above, nothing from the malformed turn was persisted: there is no assistant message between the last successful tool call and the user's next message. Retrying the same tool call manually worked.
So the failure has at least two presentations:
DSML markup leaks into assistant text (as reported above).
The malformed call is silently discarded, leaving an empty turn with no error and no automatic retry.
Expected
Either parse/repair the malformed call, or surface a parse error and let the turn continue/retry. Silently ending the turn makes the agent look like it stopped for no reason.
I can provide a private session ID or share link to maintainers if that helps.
Still reproduces on 2.2.0 — #58027 and #58932 were on 2.1.2 / 2.1.3 and were closed as duplicates, so the tracker currently reads as fixed when it is not.
Environment
opencode 2.2.0
OS: Windows 10.0.26200 (win32 x64)
Provider/model: opencode-go / deepseek-v4.1-flash
Agent: Code Mode disabled
Trigger: long session (5m38s turn), intermittent; it had not recurred early in the session
Because it reproduces with Code Mode disabled, #57344 is a subset of this rather than a separate bug.
Where the markup lands
The assistant text part — not a tool part, not reasoning. The web UI identifies it directly as data-component="text-part" with data-timeline-part-id="…:text:0", and the TUI and web UI both render it as ordinary message text.
Verbatim, immediately after a sentence ending in a colon:
Note it begins with a closing tag: this is a malformed fragment, not a complete call whose parse failed. So there is nothing for the client to execute — the step ends idle, no tool part is recorded, and nothing is written at ERROR or WARN.
Minimal repro for the text claim
Open the affected message in the web UI and press copy response. The DSML text comes with it, because it is part of the text part.
Request
Since the payload is malformed at the source, a parser fix may not be possible here. What would help most is defensive: detect tool-call markup arriving in text content and surface it — a warning, or a visible marker on the message — instead of silently ending the turn.
The silence is the part that cost the most in diagnosis: with no error, no failed tool, and idle being the same state as a completed turn, a malformed response is indistinguishable from the model simply finishing its answer. That ambiguity is what led me to misdiagnose this three times before arriving at the text-part evidence above.
Thanks for the report and for the session details everyone has added here.
We couldn't reproduce this in our automated sandbox. The problem is in what the served deepseek-v4.1-flash model on OpenCode Go returns: tool calls come back as DSML markup in the message text instead of structured tool calls. Seeing that needs a real Go account and long, tool-heavy sessions, which the sandbox doesn't have. A maintainer with access to the Go request logs will need to look into it, and the team is already following the leads raised in this thread.
On the client side, we checked the latest dev: OpenCode has no handling for tool-call markup that arrives as text. Its existing repair path only runs for tool calls that are already structured but malformed, which is why these turns end with no error. Until this is resolved, users here report that compacting or shortening the session helps, and the community plugin mentioned above (opencode-plugin-dsml-recover) can recover some of these calls.
Related: #58027 and #58932 (closed as duplicates of this one), #57344 (Code Mode variant), #43385 (similar markup leak with MiMo models).
hi, may I provide Additional datapoint / same model family?
Environment
OpenCode v2.2.0 (still reproducing; first seen at v2.1.4)
Via OpenChamber 2.5.3 desktop (which bundles the OpenCode build) and nodejs as a module (v24.22.1); Windows 10, Git Bash
Provider: OpenCode inference, free tier -> opencode/deepseek-v4.1-flash-free
(also seen with opencode/beam-free)
What I see
On an ordinary coding task, while the model is thinking it emits a batch of file-read
tool calls as raw XML/text in the message stream instead of structured tool-call
payloads, then the turn settles and the agent stops as if it finished. Verbatim from a
session:
(dots_function_call is the client's name for the function-call block; the inner <invoke name=...> markup is the tool-call syntax leaking into the text stream.)
Frequent with the free/flash models, not deterministic. Matches the "model emits
tool-call XML as text" path this issue describes. Related: #55378 (turn settles
silently) and #59567 (repeats to the output cap).
Happy to attach a full session export or log if it helps reproduce.
Activity
fwang commentedon Sep 21, 2026
can you share the session id?
lirc572 commentedon Sep 21, 2026
Session id is lirc572.
Happened around the time I created this issue, many times. You can filter for responses without tool calls that end with xml blocks.
heimoshuiyu commentedon Sep 21, 2026
@lirc572 what agent client are you using? Did you set the temperature?
lirc572 commentedon Sep 21, 2026
Pi coding agent. Temperature not set. Same configuration worked flawlessly days before.
KevinWen7415 commentedon Sep 21, 2026
Same symptom here: DSML tool calls leaked as plain text, DeepSeek V4.1 Flash via OpenCode Go (Codex client)
Environment:
wire_api = "responses"→ local cc-switch proxy 3.20.3 (127.0.0.1:15724) →https://opencode.ai/zen/go/v1/chat/completionsdeepseek-v4.1-flash(only model in use)Timeline (local time UTC+8), by scanning the Codex rollout JSONL:
DSMLoccurrences.output_text:01a0c2a9-7d49-70f1-92b6-6abb4e7dc2f8(started 14:31) — 88 occurrences01a0c2bf-15c8-7e61-9e09-4074a36f6150(14:54) — 2201a0c319-bf08-7f31-9933-d8a647907b0b(16:33, follow-up of the first) — 9Verbatim example from an assistant message (normal prose, then this):
In most cases the turn still ends with
task_complete, andlast_agent_messageis just prose + the broken</|DSML| parameter>tail — no tool executes, so from the user side the task simply "stops". The same sessions show many manualturn_aborted (reason: interrupted)on my side after waiting (one turn ran 9m38s before I stopped it). Since my client goes through a Responses→Chat conversion proxy, the DSML arrives as message content, never as structuredtool_calls.To distinguish from other failures: earlier in the week I did see genuine upstream errors through the same chain (a wave of HTTP 503/500 on 09-16, and
HTTP 550: Upstream response was not valid JSONon 09-20 16:52), but on 09-21 there were no HTTP/proxy errors at all — requests succeed and the DSML text is what comes back. This matches the routing change you mentioned.Happy to provide trimmed rollout files or repro details if useful.
627-hub commentedon Oct 4, 2026
Same symptom, and a narrowing of where the silent failure actually comes from
Adding a data point from the opencode CLI client itself (not a third-party agent), plus what I think is a useful distinction for whoever picks this up.
Environment
deepseek-v4.1-flashvia opencode-goWhat I see
The model emits DSML-ish pseudo-XML with a mix of
parameter/invokewrappers, some with a malformed trailing close tag. It lands in the assistant message content, so the TUI just renders it as ordinary text. No tool runs, no error is surfaced, and — importantly — nothing is fed back to the model, so it keeps producing the same shape.The distinction I think matters
There is already a repair path for malformed tool calls, and it does return an error to the model:
experimental_repairToolCallinpackages/opencode/src/session/llm.ts(around line 296) rewrites the failed call — either lowercasing the tool name when that matches a known tool, or otherwise replacing the call with theinvalidfallback tool, carryingfailed.error.messageas its input.packages/opencode/src/tool/invalid.tsdefines thatinvalidtool, whoseexecutereturnsThe arguments provided to the tool are invalid: ...as a tool result — so the model does get told.The gap is when that path runs. It only fires when the SDK surfaces a failed tool call (unparseable JSON args, schema validation failure, truncation). When DSML arrives as message content, it never becomes a tool call at all — so there is no failed tool call, the repair hook is never invoked, and the model receives zero feedback. The result is the "I changed the file but nothing happened, and I'm not sure why" loop.
So I'd frame the harness-side ask as: text-content leakage bypasses the existing repair chain entirely, rather than "malformed tool args are silently ignored" — the latter is mostly already handled. That also explains why #21688 (truncated tool calls) doesn't cover this, and I'd guess #50814 is adjacent but compaction-specific.
Concrete ask, if feasible
When an assistant message's text content looks like an unparsed tool call (DSML/pseudo-XML), could the client either:
invalid-style tool_result carrying that text, so the model gets a corrective signal and can retry through the proper channel, orEither would turn an invisible failure into a self-correcting one. Happy to provide trimmed session JSON if that helps reproduce.
Not filing a separate issue since this looks like the same root cause — adding here instead.
santiagofn commentedon Oct 4, 2026
Another reproduction: DeepSeek V4.1 Flash on the Opencode Go provider (V2)
Confirming this on the exact model named in the report, on the V2 line.
Environment
2.0.22opencode-go/deepseek-v4.1-flashWhat happens
Intermittently, when the model decides to call a tool, the invocation is emitted as a normal
assistant text part containing an in-band XML/DSML block, instead of as a structured tool
call. Nothing executes, and the turn reads to the user as though the tool ran. The markup has
the same shape as the example in the original report (an invoke block wrapping parameter
elements). It recurred several times in a row in the same session, each time on a different
tool.
Evidence from the session store
structured tool part exists for those turns: in this session there were 6 text parts
carrying the markup and no corresponding tool parts.
string "you printed this:" followed by the DSML block.
Notes
oldStringmatching family: the edit tool never runs at all, so the failure isupstream of argument handling. Re-reading the file after a failed attempt confirmed nothing
was applied.
session" observation and the v3.2 report's sequential-tool-call observation.
it returns as the context grows again.
pauldupuyjr commentedon Oct 15, 2026
Same failure family on a different serving lane, plus one artifact not yet reported here: a truncated structured tool call arriving alongside the text leak. Adding our evidence to this thread rather than filing a separate issue.
Environment
(Local paths inside the quoted fragments below are redacted; the markup shape is verbatim. The evidence was collected on v1 - the host pins the 1.18.x CLI - but the artifacts are transport-level: a truncated structured tool call and a dropped emission are what the provider lane puts on the wire regardless of client version, and the text-leak mode is already confirmed on 2.0.22 upthread. The wire probe below drives the provider API directly and transfers unchanged to any client version.)
Mode 1: whole emission as content (the mode this thread is about)
2026-10-15 17:34 Central (22:34 UTC), a read call arrived entirely inside delta.content, no structured call was emitted, and the tool never ran:
(The leading
<never arrived; quoted as captured. The V2.6 in-band format here is<function=name><parameter=key>value</parameter></function>, which differs from thetml:function_callswrapper reported for MiMo V2.5 in #43385.)The agent re-issued the read 30 seconds later and got the real file, so the turn recovered on its own.
Mode 2: truncated structured call plus the tail as content
2026-10-15 17:43 Central (22:43 UTC), one grep emission produced three artifacts in the same burst: a structured tool call named
g(the fragment left whengrepis cut after its first character), which the ACP client's tool router rejected as an unavailable tool; the tail of the same emission inside delta.content:and no executing call for that emission - the grep that returned results a second later was a different call with different arguments (its matches included .md files, outside this call's include filter).
The interesting part mechanically: a truncated structured call can only come from the serving lane's own text-to-tool_calls conversion. opencode v1 has no in-band markup parser (git grep finds no handler for this format in the 1.18 tree or current dev), so the OpenRouter lane for this model does convert in-band markup server-side, and mode 1 is that same conversion dropping an emission entirely. That also fits the session-length correlation reported upthread: our failures happened deep into a dense session, never early.
Wire-level checks
Fourteen direct requests to OpenRouter's chat completions endpoint (12 streaming, 2 non-streaming, single small get_weather tool) came back clean every time: id on the first chunk, name in one chunk, arguments split across continuations, no content leaks. So the malformation is rare and load-dependent, not something a minimal repro reaches quickly.
Relevance to the asks upthread
The truncated-call variant at least surfaced as a router rejection in our setup, but mode 1 was fully silent, which matches the earlier analysis here: text-content leakage bypasses the repairToolCall / invalid chain entirely. Both proposed mitigations (synthesize an invalid-style tool result carrying the text, or at minimum warn) would have turned both of our failures into self-correcting ones. Raw probe runs and the harness that produced them are available on request.
frank321 commentedon Oct 17, 2026
Adding a data point, because I hit this exact failure mode hard and ended up
building a fix.
The shape reported here is the one I traced.
< parameter name="edit">with the outer
< tool_calls>opener missing, and only the inner< parameter>elements carrying the DSML marker. In my sessions this onlyhappened late in very long contexts, never early — which matches the
"having a long session with DeepSeek v4.1" repro above. The parser degrades
quietly, passes the markup through as ordinary text, and the tool never runs.
What I found while digging (same class as the vLLM reports, e.g.
vllm-project/vllm#48931 and #54686):
long context; it is not the model declining to call the tool.
degraded output and every subsequent turn fails the same way until you clear
or repair the history. So history sanitization matters more than the live
recovery.
parameters, bare-JSON bodies, and (rarest) a markerless
<parameter>whoseargument key is stripped.
Since OpenCode has no built-in DSML handling, I wrote an OpenCode V2 plugin
that sits in the response path and repairs these in-session: it recovers the
leaked call into a real tool call, sanitizes the poisoned history, and nudges a
stalled turn to re-issue. I run it with DeepSeek V4.1 on DeepInfra.
It is MIT, a fork of the earlier
opencode-plugin-dsml-fix(credit tobendtherules for the original buffering/parse architecture). It is a personal
fix — tested on one provider/model combination, no support matrix — but if
you're hitting this on V4.1 it may save you the manual repair loop.
If it helps, I'm happy to share the specific leak bytes I captured (the
markerless
<parameter name="shell">shape was the hardest one) for anyonelooking to fix this lower down in the provider path.
heimoshuiyu commentedon Oct 18, 2026
From the request logs, almost all requests that hit malformed DSML output have tools count = 0. Can you check if you're using an API format conversion tool? It might be causing the issue.
hieuck commentedon Oct 18, 2026
hôm nay tôi mới gặp lỗi như này
</|DSML| invoke>
</|DSML| calls>
feat(diagnostic): log outbound tool count and wire tools array
frank321 commentedon Oct 18, 2026
Thanks — good observation. Answering directly: no conversion layer here.
It's OpenCode → DeepInfra direct, no API-format shim.
I checked the
tools = 0correlation on our side by logging the outbound requestat two points: the tool count at assembly, and the
toolsarray on the wire.Across 4 malformed-DSML turns (all
held-candidate, outer< tool_calls>opener missing — the shape from my first report):
Every leak shipped 12 tools to dispatch, and across the whole log
424/424 requests had
tools=12—tools=0never appears. So the correlationdoesn't reproduce on this path: the tools array isn't dropped here before the
request leaves OpenCode. That points at the text→tool-call extraction after the
request lands, not at the tools array.
To verify your own setup: the plugin now logs
request … tools=Nandwire … tools=N names=[...]when you enable"debug": true. (Caveat: this onlycovers our path, so it doesn't contradict what you're seeing at your tier.)
Disclosure: drafted with an AI assistant; the measurements are my own.
lordsilver commentedon Oct 20, 2026
Adding a data point from a long OpenCode CLI session on 2026-10-20.
Environment
opencode/deepseek-v4.1-flash-free, variantmaxWhat happened
While editing files, the model emitted a malformed tool call using the full-width DSML delimiters (
||DSML|| parameter>,||DSML|| invoke>,||DSML|| calls>). The client did not execute it, did not render it as assistant text, and did not surface an error. The assistant turn simply ended empty. The next stored message is the user asking why the agent stopped.Unlike the text-leak cases above, nothing from the malformed turn was persisted: there is no assistant message between the last successful tool call and the user's next message. Retrying the same tool call manually worked.
So the failure has at least two presentations:
Expected
Either parse/repair the malformed call, or surface a parse error and let the turn continue/retry. Silently ending the turn makes the agent look like it stopped for no reason.
I can provide a private session ID or share link to maintainers if that helps.
MLGSW4G commentedon Oct 27, 2026
Still reproduces on 2.2.0 — #58027 and #58932 were on 2.1.2 / 2.1.3 and were closed as duplicates, so the tracker currently reads as fixed when it is not.
Environment
opencode-go/deepseek-v4.1-flashBecause it reproduces with Code Mode disabled, #57344 is a subset of this rather than a separate bug.
Where the markup lands
The assistant text part — not a tool part, not reasoning. The web UI identifies it directly as
data-component="text-part"withdata-timeline-part-id="…:text:0", and the TUI and web UI both render it as ordinary message text.Verbatim, immediately after a sentence ending in a colon:
Note it begins with a closing tag: this is a malformed fragment, not a complete call whose parse failed. So there is nothing for the client to execute — the step ends
idle, no tool part is recorded, and nothing is written atERRORorWARN.Minimal repro for the text claim
Open the affected message in the web UI and press copy response. The DSML text comes with it, because it is part of the text part.
Request
Since the payload is malformed at the source, a parser fix may not be possible here. What would help most is defensive: detect tool-call markup arriving in text content and surface it — a warning, or a visible marker on the message — instead of silently ending the turn.
The silence is the part that cost the most in diagnosis: with no error, no failed tool, and
idlebeing the same state as a completed turn, a malformed response is indistinguishable from the model simply finishing its answer. That ambiguity is what led me to misdiagnose this three times before arriving at the text-part evidence above.opencodejr commentedon Oct 28, 2026
Thanks for the report and for the session details everyone has added here.
We couldn't reproduce this in our automated sandbox. The problem is in what the served
deepseek-v4.1-flashmodel on OpenCode Go returns: tool calls come back as DSML markup in the message text instead of structured tool calls. Seeing that needs a real Go account and long, tool-heavy sessions, which the sandbox doesn't have. A maintainer with access to the Go request logs will need to look into it, and the team is already following the leads raised in this thread.On the client side, we checked the latest
dev: OpenCode has no handling for tool-call markup that arrives as text. Its existing repair path only runs for tool calls that are already structured but malformed, which is why these turns end with no error. Until this is resolved, users here report that compacting or shortening the session helps, and the community plugin mentioned above (opencode-plugin-dsml-recover) can recover some of these calls.Related: #58027 and #58932 (closed as duplicates of this one), #57344 (Code Mode variant), #43385 (similar markup leak with MiMo models).
audioscavenger commentedon Oct 29, 2026
hi, may I provide Additional datapoint / same model family?
Environment
opencode/deepseek-v4.1-flash-free(also seen with
opencode/beam-free)What I see
On an ordinary coding task, while the model is thinking it emits a batch of file-read
tool calls as raw XML/text in the message stream instead of structured tool-call
payloads, then the turn settles and the agent stops as if it finished. Verbatim from a
session:
(
dots_function_callis the client's name for the function-call block; the inner<invoke name=...>markup is the tool-call syntax leaking into the text stream.)Frequent with the free/flash models, not deterministic. Matches the "model emits
tool-call XML as text" path this issue describes. Related: #55378 (turn settles
silently) and #59567 (repeats to the output cap).
Happy to attach a full session export or log if it helps reproduce.