fix(executors): strip invalid OpenCode stream options (#13819)

Co-authored-by: William Echo <175406538+qinghuanandejiangshi@users.noreply.github.com>
This commit is contained in:
William Echo
2026-09-18 22:33:31 +08:00
committed by GitHub
parent 164043d301
commit 2782258846
2 changed files with 88 additions and 0 deletions

View File

@@ -1153,6 +1153,11 @@ export class OpencodeExecutor extends BaseExecutor {
}
if (modifiedBody && typeof modifiedBody === "object" && !Array.isArray(modifiedBody)) {
const mb = modifiedBody as Record<string, unknown>;
// 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 =

View File

@@ -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<string, unknown>[] = [];
t.mock.method(globalThis, "fetch", async (_url: unknown, init: RequestInit) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
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);
});
});