Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/core/src/common/openai-thinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -20,6 +20,6 @@ export function buildThinkingRequestOptions(

return {
thinking,
...(thinkingEnabled ? { extra_body: { reasoning_effort: reasoningEffort } } : {}),
...(thinkingEnabled ? { reasoning_effort: reasoningEffort } : {}),
};
}
42 changes: 38 additions & 4 deletions packages/core/src/tests/openai-thinking.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -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<string, unknown> = {};
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" } : {}),
});
}
});
16 changes: 14 additions & 2 deletions packages/core/src/tests/tool-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | undefined;
let prompt = "";
const editResult = await handleEditTool(
{
Expand All @@ -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: [
Expand All @@ -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."
Expand Down Expand Up @@ -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<string, unknown> | undefined;
const editResult = await handleEditTool(
{
snippet_id: snippet.id,
Expand All @@ -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: [
Expand All @@ -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"),
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/tools/edit-handler.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -692,7 +694,7 @@ async function inferOldStringNotFoundReasonWithLLM(
},
],
...buildThinkingRequestOptions(thinkingEnabled, baseURL, reasoningEffort),
},
} as ChatCompletionCreateParamsNonStreaming,
{ signal: context.signal }
);
context.signal?.throwIfAborted();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -777,7 +780,7 @@ async function correctEscapedStringsWithLLM(
},
],
...buildThinkingRequestOptions(thinkingEnabled, baseURL, reasoningEffort),
},
} as ChatCompletionCreateParamsNonStreaming,
{ signal: context.signal }
);
context.signal?.throwIfAborted();
Expand Down
Loading