mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-14 19:22:32 +03:00
fix(opencode): fallback unsupported DeepSeek json schema output (#9992)
This commit is contained in:
@@ -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
|
||||
|
||||
212
tests/unit/opencode-deepseek-json-schema-fallback.test.ts
Normal file
212
tests/unit/opencode-deepseek-json-schema-fallback.test.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user