feat: DSML tool-call recovery plugin for OpenCode V2

Recovers DeepSeek DSML tool calls that leak as assistant text: streaming
middleware (AI-SDK path), SSE rewrite (native path), history sanitization,
preventive directive, conditional system nudge, and a capped idle watchdog.
116 tests, synthetic golden fixtures.
This commit is contained in:
bendtherules
2026-10-05 11:47:57 +05:30
commit 52b7776d5a
26 changed files with 4600 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
/** The dependency-free language-model wrapper: stream + generate recovery. */
import type { LanguageModelV3, LanguageModelV3CallOptions } from "@ai-sdk/provider"
import { describe, expect, test } from "bun:test"
import { recoverGenerateResult, wrapDsmlLanguageModel } from "../src/middleware.ts"
import { block, invoke, param } from "./fixtures.ts"
function fakeModel(deltas: string[], finish: "stop" | "tool-calls" = "stop"): LanguageModelV3 {
return {
specificationVersion: "v3",
provider: "opencode-go",
modelId: "deepseek-v4.1-flash",
supportedUrls: {},
async doGenerate() {
return {
content: [{ type: "text", text: deltas.join("") }],
finishReason: { unified: finish, raw: finish },
usage: { inputTokens: { total: 1 }, outputTokens: { total: 1 } },
warnings: [],
}
},
async doStream() {
const parts = [
{ type: "stream-start", warnings: [] },
{ type: "text-start", id: "t1" },
...deltas.map((delta) => ({ type: "text-delta", id: "t1", delta })),
{ type: "text-end", id: "t1" },
{ type: "finish", finishReason: { unified: finish, raw: finish }, usage: {} },
]
return {
stream: new ReadableStream({
start(c) {
for (const p of parts) c.enqueue(p)
c.close()
},
}),
}
},
} as unknown as LanguageModelV3
}
const opts = {} as unknown as LanguageModelV3CallOptions
async function collect(stream: ReadableStream) {
const out: Array<{ type: string; [k: string]: unknown }> = []
const reader = stream.getReader()
for (;;) {
const { done, value } = await reader.read()
if (done) break
out.push(value)
}
return out
}
describe("wrapDsmlLanguageModel", () => {
test("stream recovers a call and flips finish", async () => {
const wrapped = wrapDsmlLanguageModel(fakeModel([block([invoke("bash", [param("command", "pwd")])])]))
const result = await wrapped.doStream(opts)
const events = await collect(result.stream as ReadableStream)
expect(events.some((e) => e.type === "tool-call")).toBe(true)
const finish = events.find((e) => e.type === "finish") as unknown as { finishReason: { unified: string } }
expect(finish.finishReason.unified).toBe("tool-calls")
})
test("plain text passes through untouched", async () => {
const wrapped = wrapDsmlLanguageModel(fakeModel(["hello world"]))
const result = await wrapped.doStream(opts)
const events = await collect(result.stream as ReadableStream)
expect(events.some((e) => e.type === "tool-call")).toBe(false)
expect(events.filter((e) => e.type === "text-delta").map((e) => e.delta).join("")).toBe("hello world")
})
test("generate recovers a call", async () => {
const wrapped = wrapDsmlLanguageModel(fakeModel(["lead ", block([invoke("q", [param("a", "1")])]), " tail"]))
const result = await wrapped.doGenerate(opts)
expect(result.content.some((p) => p.type === "tool-call")).toBe(true)
expect(result.finishReason.unified).toBe("tool-calls")
})
test("delegates identity fields", () => {
const wrapped = wrapDsmlLanguageModel(fakeModel([]))
expect(wrapped.provider).toBe("opencode-go")
expect(wrapped.modelId).toBe("deepseek-v4.1-flash")
expect(wrapped.specificationVersion).toBe("v3")
})
})
describe("recoverGenerateResult", () => {
test("unparseable block returns input unchanged", () => {
const content = [{ type: "text", text: "plain" }]
const finishReason = { unified: "stop", raw: "stop" } as const
const r = recoverGenerateResult(content as never, finishReason)
expect(r.recovered).toBe(0)
expect(r.finishReason.unified).toBe("stop")
})
})