mirror of
https://github.com/bendtherules/opencode-plugin-dsml-fix.git
synced 2026-10-17 16:56:50 +00:00
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.
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 bendtherules
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,34 +1,84 @@
|
|||||||
# opencode-plugin-dsml-fix
|
# opencode-plugin-dsml-fix
|
||||||
|
|
||||||
OpenCode V2 plugin that recovers DeepSeek DSML tool calls when the provider's
|
OpenCode V2 plugin that recovers DeepSeek DSML tool calls when the provider's
|
||||||
parser fails and the raw markup leaks into assistant text.
|
parser fails and raw markup leaks into assistant text as prose.
|
||||||
|
|
||||||
## What it does
|
## The problem
|
||||||
|
|
||||||
1. **Streaming recovery (strategy B).** Wraps the provider language model via
|
On DeepSeek served through OpenCode (notably `opencode-go/deepseek-*`), tool
|
||||||
`ctx.aisdk.hook("language")`. Leaked DSML blocks in `text-delta` parts become
|
calls sometimes arrive as visible XML-ish markup in the assistant message
|
||||||
real AI-SDK `tool-input-*` + `tool-call` parts, and `finishReason` flips from
|
instead of executing — the agent stalls or pastes the markup back at you. It
|
||||||
`stop` to `tool-calls` — so the agent loop dispatches the intended call instead
|
happens most in long sessions, where the provider's parser degrades: the outer
|
||||||
of stalling. Reasoning and native tool parts pass through untouched.
|
block opener goes missing, invoke openers get mangled, and only the inner
|
||||||
2. **History sanitization.** Strips leaked DSML spans from replayed assistant text
|
parameter tags survive.
|
||||||
so the model stops imitating its own degraded output. Never synthesizes calls
|
|
||||||
from history (no double execution).
|
This plugin repairs those leaks client-side, in OpenCode, without waiting on
|
||||||
3. **Correction directive.** A system instruction stating the exact required envelope
|
the provider. Normal responses pass through byte-identical.
|
||||||
(complete outer block layer, one invoke per call, parameters inside invokes).
|
|
||||||
4. **Auto-resume safety net (strategy A).** On session idle with unrecovered DSML
|
|
||||||
markup, sends one short retry nudge, capped per message via plugin storage.
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Add to `opencode.jsonc`:
|
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
|
// opencode.jsonc
|
||||||
{
|
{
|
||||||
"plugins": ["/code/opencode-dsml-plugin"]
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"plugins": ["opencode-plugin-dsml-fix"],
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Or publish and reference the package name. Requires OpenCode V2 (`2.0.x`).
|
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:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"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
|
## Configuration
|
||||||
|
|
||||||
@@ -36,75 +86,54 @@ Or publish and reference the package name. Requires OpenCode V2 (`2.0.x`).
|
|||||||
{
|
{
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"package": "/code/opencode-dsml-plugin",
|
"package": "opencode-plugin-dsml-fix",
|
||||||
"options": {
|
"options": {
|
||||||
"providers": ["opencode-go", "opencode"],
|
"providers": ["opencode-go", "opencode"],
|
||||||
"mode": "relaxed",
|
"mode": "relaxed",
|
||||||
"resume": { "enabled": true, "maxAttempts": 3 }
|
"resume": { "enabled": true, "maxAttempts": 3 },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Default | Meaning |
|
| Option | Default | Meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `providers` | `["opencode-go","opencode"]` | Provider IDs in scope. Empty disables the plugin. |
|
| `providers` | `["opencode-go","opencode"]` | Provider IDs in scope. Empty disables the plugin. |
|
||||||
| `mode` | `"relaxed"` | `"relaxed"` = full catalogue; `"strict"` = complete blocks only. |
|
| `mode` | `"relaxed"` | `"relaxed"` = full variant catalogue; `"strict"` = complete blocks only. |
|
||||||
| `rescue.orphanInvoke` | mode | Recover invokes with no outer opener. |
|
| `rescue.orphanInvoke` | mode | Invokes with no outer opener. |
|
||||||
| `rescue.looseParameters` | mode | Tolerant re-scan past broken closers. |
|
| `rescue.looseParameters` | mode | Tolerant re-scan past broken closers. |
|
||||||
| `rescue.rawJsonBody` | mode | Bare-JSON invoke bodies. |
|
| `rescue.rawJsonBody` | mode | Bare-JSON invoke bodies. |
|
||||||
| `rescue.wrappedTool` | mode | Orphan `<parameter name="X">` with complete inner params. |
|
| `rescue.wrappedTool` | mode | Orphan `<parameter name="X">` with complete inner params. |
|
||||||
| `bufferLimit` | `65536` | Streaming hold cap in bytes. |
|
| `bufferLimit` | `65536` | Streaming hold cap in bytes. |
|
||||||
| `response.sse` | `true` | Native path: rewrite SSE bytes in `http.response`. |
|
| `response.sse` | `true` | Native path: rewrite SSE bytes (`http.response`). |
|
||||||
| `response.aisdkStream` | `true` | AI-SDK path: wrap language model, streaming parts. |
|
| `response.aisdkStream` | `true` | AI-SDK path: streaming parts. |
|
||||||
| `response.aisdkGenerate` | `true` | AI-SDK path: wrap language model, generate results. |
|
| `response.aisdkGenerate` | `true` | AI-SDK path: non-streaming results. |
|
||||||
| `request.sanitize` | `true` | Strip leaked spans from replayed history (+ one note). |
|
| `request.sanitize` | `true` | Strip leaked spans from replayed history. |
|
||||||
| `request.directive` | `true` | Preventive system directive (alias: `prompt.enabled`). |
|
| `request.directive` | `true` | Preventive system directive. |
|
||||||
| `request.systemNudge` | `true` | Conditional correction in system on unrecovered tail. |
|
| `request.systemNudge` | `true` | Conditional correction when a turn ends unrecovered. |
|
||||||
| `resume.enabled` | `true` | Idle watchdog wake (alias: `strategies.watchdog`). |
|
| `resume.enabled` | `true` | Idle watchdog wake. |
|
||||||
| `resume.maxAttempts` | `3` | Cap per assistant message. |
|
| `resume.maxAttempts` | `3` | Cap per assistant message. |
|
||||||
| `resume.channel` | `"system"` | `"system"` = minimal ping + system nudge; `"user"` = legacy full-text synthetic turn. |
|
| `resume.channel` | `"system"` | `"system"` = minimal ping + nudge; `"user"` = legacy full-text turn. |
|
||||||
| `debug` | `false` | Log recoveries to stderr + probe file. |
|
| `debug` | `false` | Per-turn impact logging (see above). |
|
||||||
|
|
||||||
`recovery.*` and `strategies.*` remain as deprecated aliases
|
Older names (`recovery.*`, `strategies.*`, `resume.*`, `prompt.*`) still work
|
||||||
(`sse`→`response.sse`, `stream`→`response.aisdkStream`,
|
as aliases. Precedence: `response`/`request` > `recovery` > `strategies` >
|
||||||
`generate`→`response.aisdkGenerate`, `sanitize`/`directive`/`systemNudge`→`request.*`,
|
legacy names > defaults.
|
||||||
`watchdog`→`resume.enabled`). Precedence: `response`/`request` > `recovery` >
|
|
||||||
`strategies` > legacy `resume`/`prompt` > default.
|
|
||||||
If any layer misfires, switch that one flag off (or `mode: "strict"`) — the plugin
|
|
||||||
degrades to the safer subset and never breaks normal inference.
|
|
||||||
|
|
||||||
## Per-turn impact logging (`debug: true`)
|
|
||||||
|
|
||||||
Every request turn logs one line; every response stream logs one line on close.
|
|
||||||
Turns are identified as `sessionID/user-messageID` — OpenCode's own message
|
|
||||||
identity — with `#n` counting physical requests (retries) within 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
|
|
||||||
[dsml] response ses_abc/msg_xyz#3 held-candidate-no-call
|
|
||||||
```
|
|
||||||
|
|
||||||
- `sanitized=Nmsgs/Mparts`: replayed history messages/parts stripped (+ note).
|
|
||||||
- `directive`/`nudge`: whether system text was added this turn.
|
|
||||||
- `recovered=N`: tool calls recovered from leaked markup this turn.
|
|
||||||
- `passthrough`: no DSML candidate ever seen — bytes untouched.
|
|
||||||
- `held-candidate-no-call`: something looked like markup but parsed to nothing
|
|
||||||
(emitted verbatim). If you see this often, send the log excerpt — it's a new
|
|
||||||
variant to catalogue.
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun install
|
bun install
|
||||||
bun test # 111 tests across grammar/parser/stream/wrapper/config/sse
|
bun test # 115 tests: grammar, parser, stream, wrapper, config, sse, golden
|
||||||
bun run check # typecheck + tests (tsc --noEmit)
|
bun run check # typecheck + tests
|
||||||
```
|
```
|
||||||
|
|
||||||
- `docs/PATTERNS.md` — variant catalogue (source of truth for every regex)
|
- `docs/PATTERNS.md` — failure-shape catalogue (source of truth for recovery)
|
||||||
- `docs/TEST-MATRIX.md` — behaviour ↔ test map
|
- `docs/TEST-MATRIX.md` — behaviour ↔ test map
|
||||||
- `docs/STATUS.md` — handoff / project status
|
- `test/golden/` — synthetic byte-faithful fixtures + generator
|
||||||
- `test/fixtures.ts` — fixture builders (the repo never stores a live DSML token)
|
(`build-fixtures.ts`); the repo never stores live markup tokens
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE](./LICENSE).
|
||||||
|
|||||||
+22
-7
@@ -6,22 +6,37 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "OpenCode V2 plugin that recovers DeepSeek DSML tool calls leaked as assistant text",
|
"description": "OpenCode V2 plugin that recovers DeepSeek DSML tool calls leaked as assistant text",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"check": "bun run typecheck && bun test"
|
"check": "bun run typecheck && bun test",
|
||||||
|
"build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm --external @opencode/plugin --external @ai-sdk/provider",
|
||||||
|
"prepublishOnly": "bun run check && bun run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "^3.0.8"
|
"@ai-sdk/provider": "^3.0.8",
|
||||||
|
"@opencode/plugin": "2.0.22"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opencode/plugin": "2.0.22",
|
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
"typescript": "^5.6.0"
|
"typescript": "^5.6.0"
|
||||||
}
|
},
|
||||||
|
"keywords": [
|
||||||
|
"opencode",
|
||||||
|
"opencode-plugin",
|
||||||
|
"deepseek",
|
||||||
|
"dsml",
|
||||||
|
"tool-calls"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/bendtherules/opencode-plugin-dsml-fix.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/bendtherules/opencode-plugin-dsml-fix",
|
||||||
|
"author": "bendtherules"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user