fix: default OpenAI reasoning effort to high

This commit is contained in:
Peter Steinberger
2026-04-08 16:28:34 +01:00
parent dfa22f5826
commit f27d382873
5 changed files with 197 additions and 16 deletions

View File

@@ -534,6 +534,59 @@ describe("openai transport stream", () => {
expect(params.input?.[0]).toMatchObject({ role: "developer" });
});
it("defaults OpenAI Responses reasoning effort to high when unset", () => {
const params = buildOpenAIResponsesParams(
{
id: "gpt-5.4",
name: "GPT-5.4",
api: "openai-responses",
provider: "openai",
baseUrl: "https://api.openai.com/v1",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"openai-responses">,
{
systemPrompt: "system",
messages: [],
tools: [],
} as never,
undefined,
) as { reasoning?: unknown; include?: string[] };
expect(params.reasoning).toEqual({ effort: "high", summary: "auto" });
expect(params.include).toEqual(["reasoning.encrypted_content"]);
});
it("uses shared stream reasoning as OpenAI Responses effort", () => {
const params = buildOpenAIResponsesParams(
{
id: "gpt-5.4",
name: "GPT-5.4",
api: "openai-responses",
provider: "openai",
baseUrl: "https://api.openai.com/v1",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"openai-responses">,
{
systemPrompt: "system",
messages: [],
tools: [],
} as never,
{
reasoning: "high",
} as never,
) as { reasoning?: unknown };
expect(params.reasoning).toEqual({ effort: "high", summary: "auto" });
});
it.each([
{
label: "openai",
@@ -980,6 +1033,58 @@ describe("openai transport stream", () => {
expect(params.messages?.[0]?.content).toBe("Stable prefix\nDynamic suffix");
});
it("uses shared stream reasoning as OpenAI completions effort", () => {
const params = buildOpenAICompletionsParams(
{
id: "gpt-5.4",
name: "GPT-5.4",
api: "openai-completions",
provider: "openai",
baseUrl: "https://api.openai.com/v1",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"openai-completions">,
{
systemPrompt: "system",
messages: [],
tools: [],
} as never,
{
reasoning: "medium",
} as never,
) as { reasoning_effort?: unknown };
expect(params.reasoning_effort).toBe("medium");
});
it("defaults OpenAI completions reasoning effort to high when unset", () => {
const params = buildOpenAICompletionsParams(
{
id: "gpt-5.4",
name: "GPT-5.4",
api: "openai-completions",
provider: "openai",
baseUrl: "https://api.openai.com/v1",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"openai-completions">,
{
systemPrompt: "system",
messages: [],
tools: [],
} as never,
undefined,
) as { reasoning_effort?: unknown };
expect(params.reasoning_effort).toBe("high");
});
it("uses system role and streaming usage compat for native Qwen completions providers", () => {
const params = buildOpenAICompletionsParams(
{

View File

@@ -40,6 +40,8 @@ import { mergeTransportMetadata, sanitizeTransportPayloadText } from "./transpor
const DEFAULT_AZURE_OPENAI_API_VERSION = "2024-12-01-preview";
type OpenAIReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
type BaseStreamOptions = {
temperature?: number;
maxTokens?: number;
@@ -52,7 +54,8 @@ type BaseStreamOptions = {
};
type OpenAIResponsesOptions = BaseStreamOptions & {
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
reasoning?: OpenAIReasoningEffort;
reasoningEffort?: OpenAIReasoningEffort;
reasoningSummary?: "auto" | "detailed" | "concise" | null;
serviceTier?: ResponseCreateParamsStreaming["service_tier"];
};
@@ -68,7 +71,8 @@ type OpenAICompletionsOptions = BaseStreamOptions & {
name: string;
};
};
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
reasoning?: OpenAIReasoningEffort;
reasoningEffort?: OpenAIReasoningEffort;
};
type OpenAIModeModel = Model<Api> & {
@@ -726,6 +730,10 @@ function getPromptCacheRetention(
return baseUrl?.includes("api.openai.com") ? "24h" : undefined;
}
function resolveOpenAIReasoningEffort(options: OpenAIResponsesOptions | undefined) {
return options?.reasoningEffort ?? options?.reasoning ?? "high";
}
export function buildOpenAIResponsesParams(
model: Model<Api>,
context: Context,
@@ -770,14 +778,15 @@ export function buildOpenAIResponsesParams(
});
}
if (model.reasoning) {
if (options?.reasoningEffort || options?.reasoningSummary) {
if (options?.reasoningEffort || options?.reasoning || options?.reasoningSummary) {
params.reasoning = {
effort: options?.reasoningEffort || "medium",
effort: resolveOpenAIReasoningEffort(options),
summary: options?.reasoningSummary || "auto",
};
params.include = ["reasoning.encrypted_content"];
} else if (model.provider !== "github-copilot") {
params.reasoning = { effort: "none" };
params.reasoning = { effort: "high", summary: "auto" };
params.include = ["reasoning.encrypted_content"];
}
}
applyOpenAIResponsesPayloadPolicy(params as Record<string, unknown>, payloadPolicy);
@@ -1229,6 +1238,10 @@ function mapReasoningEffort(effort: string, reasoningEffortMap: Record<string, s
return reasoningEffortMap[effort] ?? effort;
}
function resolveOpenAICompletionsReasoningEffort(options: OpenAICompletionsOptions | undefined) {
return options?.reasoningEffort ?? options?.reasoning ?? "high";
}
function convertTools(
tools: NonNullable<Context["tools"]>,
compat: ReturnType<typeof getCompat>,
@@ -1296,13 +1309,14 @@ export function buildOpenAICompletionsParams(
if (options?.toolChoice) {
params.tool_choice = options.toolChoice;
}
if (compat.thinkingFormat === "openrouter" && model.reasoning && options?.reasoningEffort) {
const completionsReasoningEffort = resolveOpenAICompletionsReasoningEffort(options);
if (compat.thinkingFormat === "openrouter" && model.reasoning && completionsReasoningEffort) {
params.reasoning = {
effort: mapReasoningEffort(options.reasoningEffort, compat.reasoningEffortMap),
effort: mapReasoningEffort(completionsReasoningEffort, compat.reasoningEffortMap),
};
} else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
} else if (completionsReasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
params.reasoning_effort = mapReasoningEffort(
options.reasoningEffort,
completionsReasoningEffort,
compat.reasoningEffortMap,
);
}

View File

@@ -19,6 +19,7 @@ type WsOptions = Parameters<StreamFn>[2] & {
toolChoice?: unknown;
textVerbosity?: string;
text_verbosity?: string;
reasoning?: string;
reasoningEffort?: string;
reasoningSummary?: string;
};
@@ -69,15 +70,16 @@ export function buildOpenAIWebSocketResponseCreatePayload(params: {
extraParams.tool_choice = streamOpts.toolChoice;
}
if (
streamOpts?.reasoningEffort !== "none" &&
(streamOpts?.reasoningEffort || streamOpts?.reasoningSummary)
) {
const reasoningEffort =
streamOpts?.reasoningEffort ??
streamOpts?.reasoning ??
(params.model.reasoning ? "high" : undefined);
if (reasoningEffort !== "none" && (reasoningEffort || streamOpts?.reasoningSummary)) {
const reasoning: { effort?: string; summary?: string } = {};
if (streamOpts.reasoningEffort !== undefined) {
reasoning.effort = streamOpts.reasoningEffort;
if (reasoningEffort !== undefined) {
reasoning.effort = reasoningEffort;
}
if (streamOpts.reasoningSummary !== undefined) {
if (streamOpts?.reasoningSummary !== undefined) {
reasoning.summary = streamOpts.reasoningSummary;
}
extraParams.reasoning = reasoning;

View File

@@ -3044,6 +3044,65 @@ describe("createOpenAIWebSocketStreamFn", () => {
expect(sent.reasoning).toEqual({ effort: "high", summary: "auto" });
});
it("defaults response.create reasoning effort to high for reasoning models", async () => {
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-reason-default");
const stream = streamFn(
{ ...modelStub, reasoning: true } as Parameters<typeof streamFn>[0],
contextStub as Parameters<typeof streamFn>[1],
undefined,
);
await new Promise<void>((resolve, reject) => {
queueMicrotask(async () => {
try {
await new Promise((r) => setImmediate(r));
MockManager.lastInstance!.simulateEvent({
type: "response.completed",
response: makeResponseObject("resp-reason-default", "Default thought"),
});
for await (const _ of await resolveStream(stream)) {
/* consume */
}
resolve();
} catch (e) {
reject(e);
}
});
});
const sent = MockManager.lastInstance!.sentEvents[0] as Record<string, unknown>;
expect(sent.type).toBe("response.create");
expect(sent.reasoning).toEqual({ effort: "high" });
});
it("forwards shared reasoning to response.create reasoning effort", async () => {
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-reason-shared");
const opts = { reasoning: "medium" };
const stream = streamFn(
modelStub as Parameters<typeof streamFn>[0],
contextStub as Parameters<typeof streamFn>[1],
opts as unknown as Parameters<typeof streamFn>[2],
);
await new Promise<void>((resolve, reject) => {
queueMicrotask(async () => {
try {
await new Promise((r) => setImmediate(r));
MockManager.lastInstance!.simulateEvent({
type: "response.completed",
response: makeResponseObject("resp-reason-shared", "Shared thought"),
});
for await (const _ of await resolveStream(stream)) {
/* consume */
}
resolve();
} catch (e) {
reject(e);
}
});
});
const sent = MockManager.lastInstance!.sentEvents[0] as Record<string, unknown>;
expect(sent.type).toBe("response.create");
expect(sent.reasoning).toEqual({ effort: "medium" });
});
it("omits response.create reasoning when reasoningEffort is none", async () => {
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-reason-none");
const opts = { reasoningEffort: "none" };