From f466ea91c9a035dd20f4ec88962cfbfa25d8a0f8 Mon Sep 17 00:00:00 2001 From: Benson K B Date: Sat, 15 Aug 2026 22:22:11 +0530 Subject: [PATCH] fix(kilocode): strip unsupported response_format for DeepSeek V4 Flash (400 regression) (#10458) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --------- Co-authored-by: benzntech Co-authored-by: diegosouzapw --- open-sse/executors/default.ts | 47 +++++++++++++----- tests/unit/executor-default-base.test.ts | 62 ++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 52dd139871..d46207aae9 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -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(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; 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> = Array.isArray(record.messages) ? (record.messages as Array>).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; } diff --git a/tests/unit/executor-default-base.test.ts b/tests/unit/executor-default-base.test.ts index dc02b94c12..d8a5d1a046 100644 --- a/tests/unit/executor-default-base.test.ts +++ b/tests/unit/executor-default-base.test.ts @@ -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 = {