From 27822588463a76ee4fbd71cb4d63c7477df20643 Mon Sep 17 00:00:00 2001 From: William Echo <2054936695@qq.com> Date: Fri, 18 Sep 2026 22:33:31 +0800 Subject: [PATCH] fix(executors): strip invalid OpenCode stream options (#13819) Co-authored-by: William Echo <175406538+qinghuanandejiangshi@users.noreply.github.com> --- open-sse/executors/opencode.ts | 5 ++ .../opencode-stream-options-13699.test.ts | 83 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 tests/unit/opencode-stream-options-13699.test.ts diff --git a/open-sse/executors/opencode.ts b/open-sse/executors/opencode.ts index 9c18e7945a..ac4996d5a7 100644 --- a/open-sse/executors/opencode.ts +++ b/open-sse/executors/opencode.ts @@ -1153,6 +1153,11 @@ export class OpencodeExecutor extends BaseExecutor { } if (modifiedBody && typeof modifiedBody === "object" && !Array.isArray(modifiedBody)) { const mb = modifiedBody as Record; + // OpenCode accepts stream_options only on streaming Chat Completions (#13699). + const format = this._requestFormat ?? resolveOpencodeTargetFormat(this.provider, model); + if (format !== "openai" || mb.stream !== true) { + delete mb.stream_options; + } const parsed = parseEffortLevel(model); if (parsed) { const deepseekFamily = diff --git a/tests/unit/opencode-stream-options-13699.test.ts b/tests/unit/opencode-stream-options-13699.test.ts new file mode 100644 index 0000000000..2154157f15 --- /dev/null +++ b/tests/unit/opencode-stream-options-13699.test.ts @@ -0,0 +1,83 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { OpencodeExecutor } from "../../open-sse/executors/opencode.ts"; + +describe("OpenCode stream_options (#13699)", () => { + for (const provider of ["opencode-go", "opencode-zen"]) { + for (const stream of [false, undefined, true]) { + it(`${provider}: only streaming Chat Completions retains stream_options (${stream})`, () => { + const executor = new OpencodeExecutor(provider); + executor._requestFormat = "openai"; + const body = { + model: "deepseek-v4.1-flash", + messages: [{ role: "user", content: "hello" }], + ...(stream === undefined ? {} : { stream }), + stream_options: { include_usage: true }, + }; + const original = structuredClone(body); + const outgoing = executor.transformRequest(body.model, body, stream === true, {}); + assert.equal(Object.hasOwn(outgoing, "stream_options"), stream === true); + if (stream) assert.deepEqual(outgoing.stream_options, { include_usage: true }); + assert.deepEqual(body, original, "must not mutate the caller's request"); + assert.deepEqual(outgoing.messages, body.messages); + }); + } + } + + for (const format of ["claude", "openai-responses"]) { + it(`does not forward Chat Completions stream_options to ${format}`, () => { + const executor = new OpencodeExecutor("opencode-zen"); + executor._requestFormat = format; + const body = { model: "test-model", stream: true, stream_options: { include_usage: true } }; + const outgoing = executor.transformRequest(body.model, body, true, {}); + assert.equal(Object.hasOwn(outgoing, "stream_options"), false); + assert.equal(outgoing.stream, true); + assert.deepEqual(body.stream_options, { include_usage: true }); + }); + } + + it("preserves streaming options when transformRequest resolves the format without execute", () => { + const executor = new OpencodeExecutor("opencode-go"); + const body = { + model: "deepseek-v4.1-flash", + stream: true, + stream_options: { include_usage: true }, + }; + assert.deepEqual( + executor.transformRequest(body.model, body, true, {}).stream_options, + body.stream_options + ); + }); + + it("removes inherited options from the actual execute request body", async (t) => { + const sent: Record[] = []; + t.mock.method(globalThis, "fetch", async (_url: unknown, init: RequestInit) => { + const body = JSON.parse(String(init.body)) as Record; + sent.push(body); + const invalid = body.stream !== true && Object.hasOwn(body, "stream_options"); + return new Response( + JSON.stringify(invalid ? { error: "stream_options requires streaming" } : { choices: [] }), + { + status: invalid ? 400 : 200, + headers: { "content-type": "application/json" }, + } + ); + }); + const executor = new OpencodeExecutor("opencode-go"); + const result = await executor.execute({ + model: "deepseek-v4.1-flash", + stream: false, + credentials: { apiKey: "test-key" }, + body: { + model: "deepseek-v4.1-flash", + stream: false, + stream_options: { include_usage: true }, + messages: [{ role: "user", content: "hello" }], + }, + }); + assert.ok(result.url.endsWith("/chat/completions")); + assert.equal(result.response.status, 200); + assert.equal(sent.length, 1); + assert.equal(Object.hasOwn(sent[0], "stream_options"), false); + }); +});