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:
Benson K B
2026-08-15 22:22:11 +05:30
committed by GitHub
parent 8ff7f7daf0
commit f466ea91c9
2 changed files with 98 additions and 11 deletions

View File

@@ -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;
}

View File

@@ -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 = {