diff --git a/packages/core/src/common/openai-thinking.ts b/packages/core/src/common/openai-thinking.ts index 1585cbdf..fe2fe94c 100644 --- a/packages/core/src/common/openai-thinking.ts +++ b/packages/core/src/common/openai-thinking.ts @@ -6,9 +6,9 @@ type ThinkingConfig = { type ThinkingRequestOptions = { thinking?: ThinkingConfig; - extra_body?: { - reasoning_effort?: ReasoningEffort; - }; + // Top level, not `extra_body`: `extra_body` is a Python-SDK convention. The Node OpenAI SDK sends it + // as a literal JSON key, which the DeepSeek API ignores. + reasoning_effort?: ReasoningEffort; }; export function buildThinkingRequestOptions( @@ -20,6 +20,6 @@ export function buildThinkingRequestOptions( return { thinking, - ...(thinkingEnabled ? { extra_body: { reasoning_effort: reasoningEffort } } : {}), + ...(thinkingEnabled ? { reasoning_effort: reasoningEffort } : {}), }; } diff --git a/packages/core/src/tests/openai-thinking.test.ts b/packages/core/src/tests/openai-thinking.test.ts index 07102323..040778cb 100644 --- a/packages/core/src/tests/openai-thinking.test.ts +++ b/packages/core/src/tests/openai-thinking.test.ts @@ -1,5 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; +import OpenAI from "openai"; import { buildThinkingRequestOptions } from "../common/openai-thinking"; test("buildThinkingRequestOptions explicitly disables thinking", () => { @@ -17,27 +18,60 @@ test("buildThinkingRequestOptions uses the same disabled payload for volces endp test("buildThinkingRequestOptions enables thinking with default reasoning effort", () => { assert.deepEqual(buildThinkingRequestOptions(true, "https://api.deepseek.com"), { thinking: { type: "enabled" }, - extra_body: { reasoning_effort: "max" }, + reasoning_effort: "max", }); }); test("buildThinkingRequestOptions uses the same enabled payload for volces endpoints", () => { assert.deepEqual(buildThinkingRequestOptions(true, "https://ark.cn-beijing.volces.com/api/v3"), { thinking: { type: "enabled" }, - extra_body: { reasoning_effort: "max" }, + reasoning_effort: "max", }); }); test("buildThinkingRequestOptions accepts high reasoning effort", () => { assert.deepEqual(buildThinkingRequestOptions(true, "https://api.deepseek.com", "high"), { thinking: { type: "enabled" }, - extra_body: { reasoning_effort: "high" }, + reasoning_effort: "high", }); }); test("buildThinkingRequestOptions accepts low reasoning effort", () => { assert.deepEqual(buildThinkingRequestOptions(true, "https://api.deepseek.com", "low"), { thinking: { type: "enabled" }, - extra_body: { reasoning_effort: "low" }, + reasoning_effort: "low", }); }); + +test("OpenAI SDK sends thinking options at the top level", async () => { + let body: Record = {}; + const client = new OpenAI({ + apiKey: "test-key", + maxRetries: 0, + fetch: async (_url, init) => { + body = JSON.parse(String(init?.body)); + return new Response('{"choices":[]}', { headers: { "content-type": "application/json" } }); + }, + }); + + for (const [enabled, effort] of [ + [true, "low"], + [true, "high"], + [true, "max"], + [true, undefined], + [false, "max"], + ] as const) { + await client.chat.completions.create({ + model: "deepseek-flash", + messages: [{ role: "user", content: "hello" }], + ...buildThinkingRequestOptions(enabled, undefined, effort), + } as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming); + + assert.deepEqual(body, { + model: "deepseek-flash", + messages: [{ role: "user", content: "hello" }], + thinking: { type: enabled ? "enabled" : "disabled" }, + ...(enabled ? { reasoning_effort: effort ?? "max" } : {}), + }); + } +}); diff --git a/packages/core/src/tests/tool-handlers.test.ts b/packages/core/src/tests/tool-handlers.test.ts index de3b7a2d..754daaa0 100644 --- a/packages/core/src/tests/tool-handlers.test.ts +++ b/packages/core/src/tests/tool-handlers.test.ts @@ -631,6 +631,7 @@ test("Edit appends an LLM diagnosis when old_string is not found", async () => { assert.ok(snippet); let llmCalls = 0; + let llmRequest: Record | undefined; let prompt = ""; const editResult = await handleEditTool( { @@ -645,6 +646,7 @@ test("Edit appends an LLM diagnosis when old_string is not found", async () => { completions: { create: async (request: { messages?: Array<{ content?: string }> }) => { llmCalls += 1; + llmRequest = request; prompt = String(request.messages?.[1]?.content ?? ""); return { choices: [ @@ -661,13 +663,17 @@ test("Edit appends an LLM diagnosis when old_string is not found", async () => { }, } as any, model: "test-model", - thinkingEnabled: false, + thinkingEnabled: true, + reasoningEffort: "max", }), }) ); assert.equal(editResult.ok, false); assert.equal(llmCalls, 1); + assert.deepEqual(llmRequest?.thinking, { type: "enabled" }); + assert.equal(llmRequest?.reasoning_effort, "max"); + assert.equal("extra_body" in llmRequest!, false); assert.equal( editResult.error, "old_string not found in file. The requested function name is computeTotal, but the snippet contains computeSubtotal." @@ -972,6 +978,7 @@ test("Edit uses LLM correction when straight and curly quotation marks differ", const sessionId = "quotation-mark-correction"; const snippet = await readSnippet(filePath, sessionId, workspace); let llmCalls = 0; + let llmRequest: Record | undefined; const editResult = await handleEditTool( { snippet_id: snippet.id, @@ -989,6 +996,7 @@ test("Edit uses LLM correction when straight and curly quotation marks differ", completions: { create: async (request: { messages?: Array<{ content?: string }> }) => { llmCalls += 1; + llmRequest = request; assert.match(String(request.messages?.[0]?.content ?? ""), /the only problem is quotation mark/); return { choices: [ @@ -1013,13 +1021,17 @@ test("Edit uses LLM correction when straight and curly quotation marks differ", }, } as any, model: "test-model", - thinkingEnabled: false, + thinkingEnabled: true, + reasoningEffort: "max", }), }) ); assert.equal(editResult.ok, true); assert.equal(llmCalls, 1); + assert.deepEqual(llmRequest?.thinking, { type: "enabled" }); + assert.equal(llmRequest?.reasoning_effort, "max"); + assert.equal("extra_body" in llmRequest!, false); assert.equal(editResult.metadata?.matched_via, "llm_escape_correction"); assert.equal( fs.readFileSync(filePath, "utf8"), diff --git a/packages/core/src/tools/edit-handler.ts b/packages/core/src/tools/edit-handler.ts index fd2e1248..4f4fec03 100644 --- a/packages/core/src/tools/edit-handler.ts +++ b/packages/core/src/tools/edit-handler.ts @@ -1,5 +1,6 @@ import * as fs from "fs"; import { z } from "zod"; +import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions"; import { buildThinkingRequestOptions } from "../common/openai-thinking"; import type { ToolExecutionContext, ToolExecutionResult } from "./executor"; import { @@ -662,6 +663,7 @@ async function inferOldStringNotFoundReasonWithLLM( try { context.signal?.throwIfAborted(); + // DeepSeek supports "max", which is absent from the OpenAI SDK types. const response = await client.chat.completions.create( { model, @@ -692,7 +694,7 @@ async function inferOldStringNotFoundReasonWithLLM( }, ], ...buildThinkingRequestOptions(thinkingEnabled, baseURL, reasoningEffort), - }, + } as ChatCompletionCreateParamsNonStreaming, { signal: context.signal } ); context.signal?.throwIfAborted(); @@ -748,6 +750,7 @@ async function correctEscapedStringsWithLLM( try { const problemDescription = describeCorrectionProblems(oldString, matchedText); context.signal?.throwIfAborted(); + // DeepSeek supports "max", which is absent from the OpenAI SDK types. const response = await client.chat.completions.create( { model, @@ -777,7 +780,7 @@ async function correctEscapedStringsWithLLM( }, ], ...buildThinkingRequestOptions(thinkingEnabled, baseURL, reasoningEffort), - }, + } as ChatCompletionCreateParamsNonStreaming, { signal: context.signal } ); context.signal?.throwIfAborted();