diff --git a/open-sse/executors/commandCode.ts b/open-sse/executors/commandCode.ts index 70453c8419..8906c01e03 100644 --- a/open-sse/executors/commandCode.ts +++ b/open-sse/executors/commandCode.ts @@ -145,12 +145,46 @@ function clampMaxTokens(value: unknown): number { return Math.max(1, Math.min(Math.floor(numeric), MAX_COMMAND_CODE_TOKENS)); } +// Reasoning/thinking fields that payload rules or clients may inject and that +// CommandCode's upstream accepts inside `params`. Without this pass-through, +// payload-rule overrides on these fields are silently dropped (#2986 follow-up). +const COMMAND_CODE_PASSTHROUGH_FIELDS = [ + "reasoning_effort", + "reasoning", + "thinking", + "effort", + "output_config", + "extra_body", +] as const; + function buildCommandCodeBody(model: string, body: unknown, stream = false): JsonRecord { const input = isRecord(body) ? body : {}; const converted = convertMessages(input.messages); const explicitSystem = typeof input.system === "string" ? input.system : ""; const system = [converted.system, explicitSystem].filter(Boolean).join("\n\n"); + // Payload rules may rewrite `body.model` (e.g. deepseek-v4-pro-max → + // deepseek/deepseek-v4-pro for the command-code provider). Prefer the + // rewritten value if present; fall back to the resolved combo model arg. + const resolvedModel = + typeof input.model === "string" && input.model.trim().length > 0 ? input.model : model; + + const params: JsonRecord = { + model: resolvedModel, + messages: converted.messages, + tools: convertTools(input.tools), + system, + max_tokens: clampMaxTokens(input.max_tokens ?? input.max_completion_tokens), + stream: true, + }; + + for (const field of COMMAND_CODE_PASSTHROUGH_FIELDS) { + const value = input[field]; + if (value !== undefined && value !== null) { + params[field] = value; + } + } + return { config: { workingDir: "/workspace", @@ -167,14 +201,7 @@ function buildCommandCodeBody(model: string, body: unknown, stream = false): Jso taste: "", skills: "", permissionMode: "standard", - params: { - model, - messages: converted.messages, - tools: convertTools(input.tools), - system, - max_tokens: clampMaxTokens(input.max_tokens ?? input.max_completion_tokens), - stream: true, - }, + params, }; } diff --git a/tests/unit/command-code-executor.test.ts b/tests/unit/command-code-executor.test.ts index 32eaefee40..71d00f74cd 100644 --- a/tests/unit/command-code-executor.test.ts +++ b/tests/unit/command-code-executor.test.ts @@ -46,7 +46,7 @@ function commandCodeStream(lines: unknown[], { sse = false } = {}) { return new Response(text, { status: 200, headers: { "Content-Type": "application/x-ndjson" } }); } -function toPlainHeaders(headers: any) { +function toPlainHeaders(headers: Headers | Record) { if (headers instanceof Headers) return Object.fromEntries(headers.entries()); return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, String(value)])); } @@ -90,8 +90,10 @@ test("getExecutor returns the specialized Command Code executor", () => { assert.ok(getExecutor("cmd") instanceof CommandCodeExecutor); }); +type FetchCall = { url: string; init: Record; body?: unknown }; + test("Command Code executor posts wrapped body and required headers to /alpha/generate", async () => { - const calls: any[] = []; + const calls: FetchCall[] = []; globalThis.fetch = async (url, init = {}) => { calls.push({ url: String(url), init }); return commandCodeStream([{ type: "text-delta", text: "hello" }, { type: "finish" }]); @@ -141,8 +143,66 @@ test("Command Code executor posts wrapped body and required headers to /alpha/ge assert.equal(json.choices[0].message.content, "hello"); }); +test("Command Code executor passes reasoning/thinking fields through to params (#2986 follow-up)", async () => { + const calls: FetchCall[] = []; + globalThis.fetch = async (url, init = {}) => { + calls.push({ url: String(url), init }); + return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); + }; + + await getExecutor("command-code").execute({ + model: "deepseek/deepseek-v4-pro", + stream: false, + credentials: { apiKey: "cc_test_key" }, + body: { + stream: false, + messages: [{ role: "user", content: "Hi" }], + reasoning_effort: "high", + thinking: { type: "enabled" }, + effort: "high", + output_config: { effort: "high" }, + extra_body: { enable_thinking: true }, + }, + }); + + const posted = JSON.parse(String(calls[0].init.body)); + assert.equal(posted.params.reasoning_effort, "high"); + assert.deepEqual(posted.params.thinking, { type: "enabled" }); + assert.equal(posted.params.effort, "high"); + assert.deepEqual(posted.params.output_config, { effort: "high" }); + assert.deepEqual(posted.params.extra_body, { enable_thinking: true }); +}); + +test("Command Code executor honors body.model rewrite from payload rules", async () => { + const calls: FetchCall[] = []; + globalThis.fetch = async (url, init = {}) => { + calls.push({ url: String(url), init }); + return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); + }; + + // Simulate a payload-rule rewrite: combo resolves to "deepseek-v4-pro-max" + // (passed as the execute() model arg), but the payload rule overwrites + // body.model to "deepseek/deepseek-v4-pro" (the vendor-prefixed form + // Command Code's API expects). + await getExecutor("command-code").execute({ + model: "deepseek-v4-pro-max", + stream: false, + credentials: { apiKey: "cc_test_key" }, + body: { + stream: false, + model: "deepseek/deepseek-v4-pro", + messages: [{ role: "user", content: "Hi" }], + reasoning_effort: "max", + }, + }); + + const posted = JSON.parse(String(calls[0].init.body)); + assert.equal(posted.params.model, "deepseek/deepseek-v4-pro"); + assert.equal(posted.params.reasoning_effort, "max"); +}); + test("Command Code raw NDJSON stream becomes OpenAI chat SSE chunks", async () => { - const calls: any[] = []; + const calls: FetchCall[] = []; globalThis.fetch = async (url, init = {}) => { calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) }); return commandCodeStream([