From 1423259feccd969339853b22ba4a503dccb69f54 Mon Sep 17 00:00:00 2001 From: Aman <1402357+Zartharas@users.noreply.github.com> Date: Tue, 11 Aug 2026 05:52:00 -0600 Subject: [PATCH] fix(opencode): fallback unsupported DeepSeek json schema output (#9992) --- open-sse/executors/opencode.ts | 75 ++++++- ...code-deepseek-json-schema-fallback.test.ts | 212 ++++++++++++++++++ 2 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 tests/unit/opencode-deepseek-json-schema-fallback.test.ts diff --git a/open-sse/executors/opencode.ts b/open-sse/executors/opencode.ts index 12dccb1b02..75f7e372f5 100644 --- a/open-sse/executors/opencode.ts +++ b/open-sse/executors/opencode.ts @@ -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(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; + 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> = Array.isArray(record.messages) + ? (record.messages as Array>).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 diff --git a/tests/unit/opencode-deepseek-json-schema-fallback.test.ts b/tests/unit/opencode-deepseek-json-schema-fallback.test.ts new file mode 100644 index 0000000000..08ad44d7f6 --- /dev/null +++ b/tests/unit/opencode-deepseek-json-schema-fallback.test.ts @@ -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 & { + messages: Array>; + response_format?: unknown; +}; + +function transform(model: string, body: Record) { + 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) => 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) => + 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) => 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) => message.role === "system"), + false + ); + }); +});