fix(opencode): fallback unsupported DeepSeek json schema output (#9992)

This commit is contained in:
Aman
2026-08-11 05:52:00 -06:00
committed by GitHub
parent 2b5253da79
commit 1423259fec
2 changed files with 285 additions and 2 deletions

View File

@@ -241,8 +241,7 @@ export class OpencodeExecutor extends BaseExecutor {
if (isKeyless && isPremiumOpencodeModel(input.model, this.provider)) {
const bodyJson = JSON.stringify({
error: {
message:
"This model requires an opencode API key — add one in Settings → Providers.",
message: "This model requires an opencode API key — add one in Settings → Providers.",
type: "invalid_request_error",
code: "premium_model_requires_key",
},
@@ -399,6 +398,77 @@ export class OpencodeExecutor extends BaseExecutor {
return headers;
}
/**
* OpenCode's free DeepSeek V4 Flash endpoint accepts json_object but
* rejects json_schema response_format with HTTP 400. Preserve the schema
* as an instruction and downgrade only this proven-incompatible route to
* json_object so callers still receive structured JSON.
*/
private applyDeepSeekJsonSchemaFallback<T>(model: string, body: T): T {
if (
model !== "deepseek-v4-flash-free" ||
(this.provider !== "opencode" && this.provider !== "opencode-zen")
) {
return body;
}
if (!body || typeof body !== "object" || Array.isArray(body)) {
return body;
}
const record = body as Record<string, unknown>;
const responseFormat = record.response_format as
| {
type?: string;
json_schema?: {
schema?: unknown;
};
}
| undefined;
if (responseFormat?.type !== "json_schema" || !responseFormat.json_schema?.schema) {
return body;
}
const schemaJson = JSON.stringify(responseFormat.json_schema.schema, null, 2);
const prompt =
"You must respond with valid JSON that strictly follows " +
"this JSON schema:\\n```json\\n" +
schemaJson +
"\\n```\\nRespond ONLY with the JSON object, no other text.";
const messages: Array<Record<string, unknown>> = Array.isArray(record.messages)
? (record.messages as Array<Record<string, unknown>>).map((message) => ({ ...message }))
: [];
const systemMessage = messages.find((message) => message.role === "system");
if (systemMessage) {
if (typeof systemMessage.content === "string") {
systemMessage.content = `${systemMessage.content}\\n\\n${prompt}`;
} else if (Array.isArray(systemMessage.content)) {
systemMessage.content.push({
type: "text",
text: `\\n\\n${prompt}`,
});
}
} else {
messages.unshift({
role: "system",
content: prompt,
});
}
return {
...record,
messages,
response_format: {
type: "json_object",
},
} as T;
}
transformRequest(
model: string,
body: any,
@@ -406,6 +476,7 @@ export class OpencodeExecutor extends BaseExecutor {
credentials: ProviderCredentials
): any {
let modifiedBody = super.transformRequest(model, body, stream, credentials);
modifiedBody = this.applyDeepSeekJsonSchemaFallback(model, modifiedBody);
// 9router#1442: OpenCode upstreams (e.g. kimi-k2.6 via opencode-go) return
// 400 "Extra inputs are not permitted, field: 'client_metadata'" — an
// OpenAI-Codex/Claude-CLI passthrough field with no equivalent here. The

View File

@@ -0,0 +1,212 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
const { OpencodeExecutor } = await import("../../open-sse/executors/opencode.ts");
type TransformedBody = Record<string, unknown> & {
messages: Array<Record<string, unknown>>;
response_format?: unknown;
};
function transform(model: string, body: Record<string, unknown>) {
const executor = new OpencodeExecutor("opencode");
return executor.transformRequest(model, body, false, {}) as TransformedBody;
}
describe("OpenCode DeepSeek json_schema fallback", () => {
it("downgrades DeepSeek V4 Flash Free json_schema to json_object and preserves schema in instructions", () => {
const schema = {
type: "object",
additionalProperties: false,
properties: {
ok: { type: "boolean" },
},
required: ["ok"],
};
const result = transform("deepseek-v4-flash-free", {
model: "deepseek-v4-flash-free",
messages: [
{
role: "user",
content: "Return the structured result.",
},
],
response_format: {
type: "json_schema",
json_schema: {
name: "desktop_probe",
strict: true,
schema,
},
},
});
assert.deepEqual(result.response_format, { type: "json_object" });
const system = result.messages.find(
(message: Record<string, unknown>) => message.role === "system"
);
assert.ok(system);
assert.equal(typeof system.content, "string");
assert.match(system.content, /strictly follows this JSON schema/i);
assert.match(system.content, /"ok"/);
assert.equal(
result.messages.some(
(message: Record<string, unknown>) =>
message.role === "user" && message.content === "Return the structured result."
),
true
);
});
it("leaves DeepSeek V4 Flash Free json_object unchanged", () => {
const body = {
model: "deepseek-v4-flash-free",
messages: [
{
role: "user",
content: "Return JSON.",
},
],
response_format: {
type: "json_object",
},
};
const result = transform("deepseek-v4-flash-free", body);
assert.deepEqual(result.response_format, { type: "json_object" });
assert.equal(
result.messages.some((message: Record<string, unknown>) => message.role === "system"),
false
);
});
it("does not change json_schema for unrelated OpenCode models", () => {
const schema = {
type: "object",
properties: {
value: { type: "string" },
},
required: ["value"],
};
const originalFormat = {
type: "json_schema",
json_schema: {
name: "other_model_probe",
strict: true,
schema,
},
};
const result = transform("big-pickle", {
model: "big-pickle",
messages: [
{
role: "user",
content: "Return the result.",
},
],
response_format: originalFormat,
});
assert.deepEqual(result.response_format, originalFormat);
});
it("applies the fallback for the opencode-zen provider", () => {
const executor = new OpencodeExecutor("opencode-zen");
const result = executor.transformRequest(
"deepseek-v4-flash-free",
{
model: "deepseek-v4-flash-free",
messages: [
{
role: "user",
content: "Return the structured result.",
},
],
response_format: {
type: "json_schema",
json_schema: {
name: "zen_probe",
schema: {
type: "object",
properties: {
ok: { type: "boolean" },
},
required: ["ok"],
},
},
},
},
false,
{}
) as TransformedBody;
assert.deepEqual(result.response_format, { type: "json_object" });
});
it("does not apply the fallback to opencode-go", () => {
const executor = new OpencodeExecutor("opencode-go");
const originalFormat = {
type: "json_schema",
json_schema: {
name: "go_scope_probe",
schema: {
type: "object",
properties: {
ok: { type: "boolean" },
},
required: ["ok"],
},
},
};
const result = executor.transformRequest(
"deepseek-v4-flash-free",
{
model: "deepseek-v4-flash-free",
messages: [
{
role: "user",
content: "Return the structured result.",
},
],
response_format: originalFormat,
},
false,
{}
) as TransformedBody;
assert.deepEqual(result.response_format, originalFormat);
});
it("does not alter an ordinary DeepSeek request", () => {
const result = transform("deepseek-v4-flash-free", {
model: "deepseek-v4-flash-free",
messages: [
{
role: "user",
content: "Hello.",
},
],
});
assert.equal(result.response_format, undefined);
assert.equal(
result.messages.some((message: Record<string, unknown>) => message.role === "system"),
false
);
});
});