mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-16 12:12:23 +03:00
fix(kilocode): strip unsupported response_format for DeepSeek V4 Flash (400 regression) (#10458)
* fix(kilocode): strip unsupported response_format for DeepSeek (400 regression) kilocode's DeepSeek V4 Flash rejects ANY response_format — both json_schema AND json_object 400 with 'Invalid input: response_format' (verified live 2026-08-15 via the Hindsight fact-extraction path on kilocode/deepseek/deepseek-v4-flash). The default executor's applyJsonSchemaFallback only covered openai-compatible-* providers and only downgraded json_schema -> json_object, so kilocode forwarded the unsupported format raw. Same bug class as the opencode fix #9992. For kilocode: strip response_format entirely and inject the schema (or a plain 'valid JSON only' instruction for json_object) into the system prompt. openai-compatible-* keeps the existing json_schema downgrade and json_object passthrough (they accept both). Regression tests: kilocode json_schema is stripped + schema-injected; kilocode json_object is stripped + JSON-only instruction; both verified to fail without the fix (sabotage: 2 fail). All 49 executor-default-base tests pass. * fix(kilocode): drop as-any casts in new tests to clear the frozen ESLint baseline The file's frozen no-explicit-any baseline is count 42; the new kilocode strip tests added 3 net-new 'as any' casts, tripping the --max-warnings 0 lint-guard. Replace them with typed assertions that carry the same checks. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> --------- Co-authored-by: benzntech <benzntech@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
This commit is contained in:
@@ -605,24 +605,41 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
|
||||
/**
|
||||
* Downgrade `response_format: { type: "json_schema" }` to `json_object` for
|
||||
* `openai-compatible-*` providers, injecting the JSON schema into the system
|
||||
* prompt instead. DeepSeek / Ollama / local OpenAI-compatible models often
|
||||
* lack native Structured Output and return empty or malformed content when a
|
||||
* `json_schema` response_format is forwarded as-is. Gated on the
|
||||
* `openai-compatible-` provider family so providers with native Structured
|
||||
* Output support keep the native `json_schema` path.
|
||||
* `openai-compatible-*` providers AND `kilocode`, injecting the JSON schema
|
||||
* into the system prompt instead. DeepSeek / Ollama / local OpenAI-compatible
|
||||
* models often lack native Structured Output and return empty or malformed
|
||||
* content when a `json_schema` response_format is forwarded as-is (kilocode's
|
||||
* DeepSeek V4 Flash rejects it with HTTP 400 `Invalid input: response_format`,
|
||||
* verified live 2026-08-15 — same class as #9992's opencode fix). Gated so
|
||||
* providers with native Structured Output support keep the native
|
||||
* `json_schema` path.
|
||||
*/
|
||||
applyJsonSchemaFallback<T>(body: T): T {
|
||||
if (!this.provider?.startsWith?.("openai-compatible-")) return body;
|
||||
const provider = this.provider ?? "";
|
||||
const isOpenAiCompatible = provider.startsWith("openai-compatible-");
|
||||
const isKiloCode = provider === "kilocode";
|
||||
if (!isOpenAiCompatible && !isKiloCode) return body;
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
||||
|
||||
const record = body as Record<string, unknown>;
|
||||
const rf = record.response_format as
|
||||
{ type?: string; json_schema?: { schema?: unknown } } | undefined;
|
||||
if (rf?.type !== "json_schema" || !rf.json_schema?.schema) return body;
|
||||
| { type?: string; json_schema?: { schema?: unknown } }
|
||||
| undefined;
|
||||
if (!rf) return body;
|
||||
|
||||
const schemaJson = JSON.stringify(rf.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.`;
|
||||
// openai-compatible-* providers accept json_object natively — only the
|
||||
// json_schema form needs downgrading there. kilocode rejects BOTH forms,
|
||||
// so it enters the strip path below regardless.
|
||||
if (isOpenAiCompatible && rf.type === "json_object") return body;
|
||||
|
||||
const schema = rf.type === "json_schema" ? rf.json_schema?.schema : undefined;
|
||||
if (rf.type === "json_schema" && !schema) return body;
|
||||
|
||||
const schemaJson = schema ? JSON.stringify(schema, null, 2) : null;
|
||||
const prompt =
|
||||
schemaJson !== null
|
||||
? `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.`
|
||||
: "You must respond with valid JSON only (a single JSON object), no other text.";
|
||||
|
||||
const messages: Array<Record<string, unknown>> = Array.isArray(record.messages)
|
||||
? (record.messages as Array<Record<string, unknown>>).map((m) => ({ ...m }))
|
||||
@@ -638,6 +655,14 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
messages.unshift({ role: "system", content: prompt });
|
||||
}
|
||||
|
||||
// kilocode's DeepSeek rejects ANY response_format (verified live 2026-08-15:
|
||||
// both json_schema AND json_object 400 with `param: response_format`) — strip
|
||||
// it entirely and rely on the schema prompt. openai-compatible-* providers
|
||||
// accept json_object, so keep the downgrade there.
|
||||
if (isKiloCode) {
|
||||
const { response_format: _dropped, ...rest } = record;
|
||||
return { ...rest, messages } as T;
|
||||
}
|
||||
return { ...record, messages, response_format: { type: "json_object" } } as T;
|
||||
}
|
||||
|
||||
|
||||
@@ -1063,6 +1063,68 @@ test("DefaultExecutor.transformRequest appends the json_schema prompt to an exis
|
||||
assert.equal(body.messages[0].content, "You are concise.");
|
||||
});
|
||||
|
||||
// kilocode's DeepSeek V4 Flash rejects ANY `response_format` with HTTP 400
|
||||
// (verified live 2026-08-15: both json_schema AND json_object 400 with
|
||||
// `param: response_format`) — same class as the opencode #9992 fix, but the
|
||||
// default executor's gate only covered `openai-compatible-*`, so kilocode
|
||||
// forwarded the unsupported format raw. For kilocode the format must be
|
||||
// STRIPPED entirely (schema injected into the system prompt), because even
|
||||
// the json_object downgrade is rejected.
|
||||
test("DefaultExecutor.transformRequest strips response_format for kilocode (DeepSeek 400 regression)", () => {
|
||||
const executor = new DefaultExecutor("kilocode");
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: { answer: { type: "string" } },
|
||||
required: ["answer"],
|
||||
};
|
||||
const body = {
|
||||
model: "deepseek/deepseek-v4-flash",
|
||||
messages: [{ role: "user", content: "give me JSON" }],
|
||||
response_format: {
|
||||
type: "json_schema",
|
||||
json_schema: { name: "answer_schema", schema },
|
||||
},
|
||||
};
|
||||
|
||||
const result = executor.transformRequest("deepseek/deepseek-v4-flash", body, true, {
|
||||
providerSpecificData: { baseUrl: "https://api.kilo.ai/v1" },
|
||||
}) as unknown as {
|
||||
response_format?: { type?: string };
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
};
|
||||
|
||||
// response_format is REMOVED entirely (kilocode rejects json_object too).
|
||||
assert.equal(result.response_format, undefined);
|
||||
assert.equal(result.messages[0].role, "system");
|
||||
assert.match(result.messages[0].content, /strictly follows this JSON schema/);
|
||||
assert.ok(result.messages[0].content.includes('"answer"'));
|
||||
assert.equal(result.messages[1].role, "user");
|
||||
assert.equal(result.messages[1].content, "give me JSON");
|
||||
// Original body is not mutated.
|
||||
assert.equal(body.response_format.type, "json_schema");
|
||||
assert.equal(body.messages.length, 1);
|
||||
});
|
||||
|
||||
test("DefaultExecutor.transformRequest strips response_format for kilocode json_object requests too", () => {
|
||||
const executor = new DefaultExecutor("kilocode");
|
||||
const body = {
|
||||
model: "deepseek/deepseek-v4-flash",
|
||||
messages: [{ role: "user", content: "give me JSON" }],
|
||||
response_format: { type: "json_object" },
|
||||
};
|
||||
|
||||
const result = executor.transformRequest("deepseek/deepseek-v4-flash", body, true, {
|
||||
providerSpecificData: { baseUrl: "https://api.kilo.ai/v1" },
|
||||
}) as unknown as {
|
||||
response_format?: { type?: string };
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
};
|
||||
|
||||
assert.equal(result.response_format, undefined);
|
||||
assert.equal(result.messages[0].role, "system");
|
||||
assert.match(result.messages[0].content, /valid JSON only/);
|
||||
});
|
||||
|
||||
test("DefaultExecutor.transformRequest leaves json_schema response_format untouched for native providers", () => {
|
||||
const executor = new DefaultExecutor("openai");
|
||||
const responseFormat = {
|
||||
|
||||
Reference in New Issue
Block a user