Skip to content

Malformed XML/DSML tool-call output from Opencode Go models #50206

Open

Description

@lirc572

Description

Many opencode-hosted models often emit XML/DSML-like chat content instead of valid tool-call payloads, causing tool execution to fail. E.g.:

<|DSML| parameter name="edit">
 <|DSML| parameter name="edits" string="false">[...]</|DSML| parameter>
 <|DSML| parameter name="path" string="true">/home/ubuntu/project/src/main.rs</|DSML| parameter>
</|DSML| invoke>

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.

Plugins

NA

OpenCode version

NA

Steps to reproduce

Having long session with DeepSeek v4.1.

Screenshot and/or share link

NA

Operating System

Windows 11

Terminal

Windows Terminal

Activity

  1. fwang commented on Sep 21, 2026

    @fwang
    Contributor

    can you share the session id?

  2. lirc572 commented on Sep 21, 2026

    @lirc572
    Author

    can you share the session id?

    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.

  3. heimoshuiyu commented on Sep 21, 2026

    @heimoshuiyu
    Contributor

    @lirc572 what agent client are you using? Did you set the temperature?

  4. lirc572 commented on Sep 21, 2026

    @lirc572
    Author

    Pi coding agent. Temperature not set. Same configuration worked flawlessly days before.

  5. KevinWen7415 commented on Sep 21, 2026

    @KevinWen7415

    Same symptom here: DSML tool calls leaked as plain text, DeepSeek V4.1 Flash via OpenCode Go (Codex client)

    Environment:

    • Client: Codex desktop 26.915.31945 on Windows 11, wire_api = "responses" → local cc-switch proxy 3.20.3 (127.0.0.1:15724) → https://opencode.ai/zen/go/v1/chat/completions
    • Model: deepseek-v4.1-flash (only model in use)
    • Usage: long-running agent sessions (single thread, hundreds of tool calls, Codex context ~600K tokens near the end)

    Timeline (local time UTC+8), by scanning the Codex rollout JSONL:

    • Sessions on 09-20 and earlier: 0 DSML occurrences.
    • 09-21 sessions suddenly full of it, leaked into assistant output_text:
      • session 01a0c2a9-7d49-70f1-92b6-6abb4e7dc2f8 (started 14:31) — 88 occurrences
      • session 01a0c2bf-15c8-7e61-9e09-4074a36f6150 (14:54) — 22
      • 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.

  6. 627-hub commented on Oct 4, 2026

    @627-hub

    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:

    1. synthesize an invalid-style tool_result carrying that text, so the model gets a corrective signal and can retry through the proper channel, or
    2. 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.

  7. santiagofn commented on Oct 4, 2026

    @santiagofn

    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.
  8. pauldupuyjr commented on Oct 15, 2026

    @pauldupuyjr

    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:

    function=read><parameter=filePath>C:\app\src\control.ts</parameter><parameter=limit>160</parameter><parameter=offset>120</parameter></function>
    

    (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:

    rep><parameter=include>*.{json,mjs,ts,js}</parameter><parameter=path>C:\app</parameter><parameter=pattern>shard-weights|shardWeights</parameter></function>
    

    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.

  9. frank321 commented on Oct 17, 2026

    @frank321

    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.

    What I found while digging (same class as the vLLM reports, e.g.
    vllm-project/vllm#48931 and #54686):

    • 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.

  10. heimoshuiyu commented on Oct 18, 2026

    @heimoshuiyu
    Contributor

    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.

  11. hieuck commented on Oct 18, 2026

    @hieuck

    hôm nay tôi mới gặp lỗi như này
    </|DSML| invoke>
    </|DSML| calls>

  12. frank321 commented on Oct 18, 2026

    @frank321

    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.

  13. lordsilver commented on Oct 20, 2026

    @lordsilver

    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:

    1. DSML markup leaks into assistant text (as reported above).
    2. 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.

  14. MLGSW4G commented on Oct 27, 2026

    @MLGSW4G

    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:

    </DSML| parameter>
    <|DSML| parameter name="timeout" string="true">600000</DSML| parameter>
    </DSML| invoke>
    </DSML| calls>
    

    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.

  15. opencodejr commented on Oct 28, 2026

    @opencodejr

    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).

  16. audioscavenger commented on Oct 29, 2026

    @audioscavenger

    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:

    Read File src/class-server.js
    
    <dots_function_call>
      <invoke name="read">
        <parameter name="path"> src/plugin-registry.js </parameter>
      </invoke>
    </dots_function_call>
    <dots_function_call>
      <invoke name="read">
        <parameter name="path"> src/proxy-router.js </parameter>
      </invoke>
    </dots_function_call>
    <dots_function_call>
      <invoke name="read">
        <parameter name="path"> src/surf-server.js </parameter>
      </invoke>
    </dots_function_call>
    

    (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.

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