bendtherules 6c52cbbe28 chore: npm release readiness (compiled dist, manifest, LICENSE, README)
- Build to dist/ via bun build (externals stay external); exports point
  at dist/index.js so installed consumers never resolve .ts sources.
- Move @opencode/plugin to dependencies (imported at runtime).
- Add LICENSE (MIT), keywords, repository, homepage, author.
- prepublishOnly runs typecheck + tests + build.
- Rewrite README usage-first: problem, install, verify, config, dev.
2026-10-05 18:26:23 +05:30

opencode-plugin-dsml-fix

OpenCode V2 plugin that recovers DeepSeek DSML tool calls when the provider's parser fails and raw markup leaks into assistant text as prose.

The problem

On DeepSeek served through OpenCode (notably opencode-go/deepseek-*), tool calls sometimes arrive as visible XML-ish markup in the assistant message instead of executing — the agent stalls or pastes the markup back at you. It happens most in long sessions, where the provider's parser degrades: the outer block opener goes missing, invoke openers get mangled, and only the inner parameter tags survive.

This plugin repairs those leaks client-side, in OpenCode, without waiting on the provider. Normal responses pass through byte-identical.

Install

// opencode.jsonc
{
  "$schema": "https://opencode.ai/config.json",
  "plugins": ["opencode-plugin-dsml-fix"],
}

Requires OpenCode V2 (2.0.x). No other setup — defaults cover the opencode-go and opencode (Zen) providers.

To confirm it's loaded, run any prompt and look for the probe log (debug: true, see below).

What it does

  • Streaming recovery. Leaked DSML blocks in response text become real tool calls, so the agent loop dispatches the intended call instead of stalling. Covers both the native SSE path (what opencode-go uses) and the AI-SDK path (other providers).
  • History sanitization. Strips leaked markup from replayed assistant messages so the model stops imitating its own degraded output. History is never turned into calls — no double execution.
  • Prevention. A system directive states the exact required envelope, plus a conditional nudge when a turn ends with unrecovered markup.
  • Safety net. An idle watchdog sends one short retry when a session stalls on unrecovered markup, capped per message.

Every layer is independently toggleable (see Configuration). If any layer misfires, switch that one flag off — the plugin degrades to a safer subset and never breaks normal inference.

Verify it's working

Enable debug logging:

{
  "plugins": [
    {
      "package": "opencode-plugin-dsml-fix",
      "options": { "debug": true },
    },
  ],
}

Each turn logs one request line and one line per response attempt, identified as sessionID/user-messageID (#n counts retries in the turn):

[dsml] request ses_abc/msg_xyz sanitized=2msgs/3parts directive=yes nudge=no
[dsml] response ses_abc/msg_xyz#1 recovered=1
[dsml] response ses_abc/msg_xyz#2 passthrough
  • sanitized=Nmsgs/Mparts — replayed history stripped (plus a short note).
  • recovered=N — tool calls rescued from leaked markup. The fix fired.
  • passthrough — no markup seen, bytes untouched.
  • held-candidate-no-call — something looked like markup but parsed to nothing (emitted verbatim). Frequent sightings mean a new variant worth reporting — file an issue with the excerpt.

Configuration

{
  "plugins": [
    {
      "package": "opencode-plugin-dsml-fix",
      "options": {
        "providers": ["opencode-go", "opencode"],
        "mode": "relaxed",
        "resume": { "enabled": true, "maxAttempts": 3 },
      },
    },
  ],
}
Option Default Meaning
providers ["opencode-go","opencode"] Provider IDs in scope. Empty disables the plugin.
mode "relaxed" "relaxed" = full variant catalogue; "strict" = complete blocks only.
rescue.orphanInvoke mode Invokes with no outer opener.
rescue.looseParameters mode Tolerant re-scan past broken closers.
rescue.rawJsonBody mode Bare-JSON invoke bodies.
rescue.wrappedTool mode Orphan <parameter name="X"> with complete inner params.
bufferLimit 65536 Streaming hold cap in bytes.
response.sse true Native path: rewrite SSE bytes (http.response).
response.aisdkStream true AI-SDK path: streaming parts.
response.aisdkGenerate true AI-SDK path: non-streaming results.
request.sanitize true Strip leaked spans from replayed history.
request.directive true Preventive system directive.
request.systemNudge true Conditional correction when a turn ends unrecovered.
resume.enabled true Idle watchdog wake.
resume.maxAttempts 3 Cap per assistant message.
resume.channel "system" "system" = minimal ping + nudge; "user" = legacy full-text turn.
debug false Per-turn impact logging (see above).

Older names (recovery.*, strategies.*, resume.*, prompt.*) still work as aliases. Precedence: response/request > recovery > strategies > legacy names > defaults.

Development

bun install
bun test        # 115 tests: grammar, parser, stream, wrapper, config, sse, golden
bun run check   # typecheck + tests
  • docs/PATTERNS.md — failure-shape catalogue (source of truth for recovery)
  • docs/TEST-MATRIX.md — behaviour ↔ test map
  • test/golden/ — synthetic byte-faithful fixtures + generator (build-fixtures.ts); the repo never stores live markup tokens

License

MIT — see LICENSE.

S
Description
OpenCode V2 plugin that recovers DeepSeek DSML tool calls leaked as assistant text
Readme MIT
135 KiB
Languages
TypeScript 100%