fix(ai): match reasoning efforts case-insensitively without lowering provider values (#102993)

* fix(ai): match reasoning effort case-insensitively

* fix(ai): preserve mapped reasoning effort casing

* fix(ai): preserve unmapped reasoning effort casing

* fix(ai): match reasoning effort map keys by case

* fix(ai): harden reasoning effort normalization

Co-authored-by: Ted Li <tl2493@columbia.edu>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Ted Li
2026-07-09 20:07:57 -07:00
committed by GitHub
parent 5bdd580320
commit 4c4609d42b
3 changed files with 167 additions and 8 deletions

View File

@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import {
resolveOpenAIReasoningEffortForModel,
resolveOpenAISupportedReasoningEfforts,
supportsOpenAIReasoningEffort,
} from "./openai-reasoning-effort.js";
describe("OpenAI reasoning effort support", () => {
@@ -36,6 +37,14 @@ describe("OpenAI reasoning effort support", () => {
expect(resolveOpenAIReasoningEffortForModel({ model, effort: "medium" })).toBe("medium");
});
it("matches canonical reasoning efforts case-insensitively", () => {
const model = { provider: "openai", id: "gpt-5.6-sol" };
expect(resolveOpenAIReasoningEffortForModel({ model, effort: "HIGH" })).toBe("high");
expect(resolveOpenAIReasoningEffortForModel({ model, effort: " XHIGH " })).toBe("xhigh");
expect(resolveOpenAIReasoningEffortForModel({ model, effort: "MAX" })).toBe("max");
});
it("does not downgrade xhigh when model compat metadata declares it explicitly", () => {
const model = {
provider: "openai",
@@ -81,13 +90,107 @@ describe("OpenAI reasoning effort support", () => {
).toBe("none");
});
it("omits unsupported disabled reasoning instead of falling back to enabled effort", () => {
it("preserves provider-native compat values mapped from canonical efforts", () => {
const model = {
provider: "example",
id: "custom-reasoning",
compat: {
supportedReasoningEfforts: ["ProviderDefault"],
reasoningEffortMap: {
high: "ProviderDefault",
},
},
};
expect(
resolveOpenAIReasoningEffortForModel({
model: { provider: "groq", id: "openai/gpt-oss-120b" },
effort: "off",
model,
effort: "HIGH",
fallbackMap: model.compat.reasoningEffortMap,
}),
).toBeUndefined();
).toBe("ProviderDefault");
});
it("matches canonical fallback map keys case-insensitively", () => {
const model = {
provider: "example",
id: "custom-reasoning",
compat: {
supportedReasoningEfforts: ["ProviderLow", "ProviderHigh"],
reasoningEffortMap: {
HIGH: "ProviderHigh",
},
},
};
expect(
resolveOpenAIReasoningEffortForModel({
model,
effort: "HIGH",
fallbackMap: model.compat.reasoningEffortMap,
}),
).toBe("ProviderHigh");
});
it("preserves canonical-looking provider-native compat values mapped from canonical efforts", () => {
const model = {
provider: "example",
id: "custom-reasoning",
compat: {
supportedReasoningEfforts: ["LOW", "MEDIUM", "HIGH"],
reasoningEffortMap: {
high: "HIGH",
},
},
};
expect(resolveOpenAISupportedReasoningEfforts(model)).toEqual(["LOW", "MEDIUM", "HIGH"]);
expect(
resolveOpenAIReasoningEffortForModel({
model,
effort: "HIGH",
fallbackMap: model.compat.reasoningEffortMap,
}),
).toBe("HIGH");
});
it("requires an explicit map for canonical-looking provider casing", () => {
const model = {
provider: "example",
id: "custom-reasoning",
compat: {
supportedReasoningEfforts: ["NONE", "HIGH"],
},
};
expect(resolveOpenAIReasoningEffortForModel({ model, effort: "none" })).toBeUndefined();
expect(
resolveOpenAIReasoningEffortForModel({
model,
effort: "none",
fallbackMap: { none: "NONE" },
}),
).toBe("NONE");
});
it("does not fold provider-native compat values", () => {
const model = {
provider: "example",
id: "custom-reasoning",
compat: {
supportedReasoningEfforts: ["ProviderDefault"],
},
};
expect(supportsOpenAIReasoningEffort(model, "ProviderDefault")).toBe(true);
expect(supportsOpenAIReasoningEffort(model, "providerdefault")).toBe(false);
});
it("omits unsupported disabled reasoning instead of falling back to enabled effort", () => {
const model = { provider: "groq", id: "openai/gpt-oss-120b" };
expect(resolveOpenAIReasoningEffortForModel({ model, effort: "off" })).toBeUndefined();
expect(resolveOpenAIReasoningEffortForModel({ model, effort: "OFF" })).toBeUndefined();
});
it("honors compat metadata that disables reasoning effort payloads", () => {

View File

@@ -39,6 +39,16 @@ const GPT_5_PRO_REASONING_EFFORTS = ["high"] as const;
const GPT_51_CODEX_MAX_REASONING_EFFORTS = ["none", "medium", "high", "xhigh"] as const;
const GPT_51_CODEX_MINI_REASONING_EFFORTS = ["medium"] as const;
const GENERIC_REASONING_EFFORTS = ["low", "medium", "high"] as const;
const CANONICAL_REASONING_EFFORTS = new Set([
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
"off",
]);
function normalizeModelId(id: string | null | undefined): string {
return normalizeLowercaseStringOrEmpty(id ?? "").replace(/-\d{4}-\d{2}-\d{2}$/u, "");
@@ -59,7 +69,10 @@ export function isOpenAIGpt55Model(model: OpenAIReasoningModel): boolean {
/** Normalize user-facing reasoning effort names to API effort names. */
export function normalizeOpenAIReasoningEffort(effort: string): string {
return effort === "minimal" ? "minimal" : effort;
const trimmed = effort.trim();
const folded = trimmed.toLowerCase();
// Only fold canonical names; provider-native values can be case-sensitive.
return CANONICAL_REASONING_EFFORTS.has(folded) ? folded : trimmed;
}
function readCompatReasoningEfforts(compat: unknown): OpenAIApiReasoningEffort[] | undefined {
@@ -140,8 +153,16 @@ export function resolveOpenAIReasoningEffortForModel(params: {
fallbackMap?: Record<string, string>;
}): OpenAIApiReasoningEffort | undefined {
const requested = normalizeOpenAIReasoningEffort(params.effort);
const mapped = params.fallbackMap?.[requested] ?? requested;
const normalized = normalizeOpenAIReasoningEffort(mapped);
// Config preserves map-key casing, so only canonical keys get a folded lookup.
const mapped =
params.fallbackMap?.[requested] ??
(params.fallbackMap && CANONICAL_REASONING_EFFORTS.has(requested)
? Object.entries(params.fallbackMap).find(
([effort]) => normalizeOpenAIReasoningEffort(effort) === requested,
)?.[1]
: undefined);
// Fallback maps emit provider-native payload labels; keep their case for exact compat lists.
const normalized = mapped === undefined ? requested : mapped.trim();
const supported = resolveOpenAISupportedReasoningEfforts(params.model);
if (supported.includes(normalized as OpenAIApiReasoningEffort)) {
return normalized as OpenAIApiReasoningEffort;
@@ -161,5 +182,7 @@ export function resolveOpenAIReasoningEffortForModel(params: {
if (requested === "max" && supported.includes("xhigh")) {
return "xhigh";
}
return supported.find((effort) => effort !== "none");
return supported.find(
(effort) => !isDisabledReasoningEffort(normalizeOpenAIReasoningEffort(effort)),
);
}

View File

@@ -5791,6 +5791,39 @@ describe("openai transport stream", () => {
expect(params.reasoning).toEqual({ effort: "high", summary: "auto" });
});
it("normalizes canonical reasoning casing in Responses and Chat Completions payloads", () => {
const context = {
systemPrompt: "system",
messages: [],
tools: [],
} as never;
const baseModel = {
id: "gpt-5.5",
name: "GPT-5.5",
provider: "openai",
baseUrl: "https://api.openai.com/v1",
reasoning: true,
input: ["text"] as Model["input"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_000_000,
maxTokens: 128_000,
};
const responses = buildOpenAIResponsesParams(
{ ...baseModel, api: "openai-responses" } satisfies Model<"openai-responses">,
context,
{ reasoningEffort: " XHIGH " } as never,
) as { reasoning?: unknown };
const completions = buildOpenAICompletionsParams(
{ ...baseModel, api: "openai-completions" } satisfies Model<"openai-completions">,
context,
{ reasoningEffort: " XHIGH " } as never,
) as { reasoning_effort?: unknown };
expect(responses.reasoning).toEqual({ effort: "xhigh", summary: "auto" });
expect(completions.reasoning_effort).toBe("xhigh");
});
it("uses disabled OpenAI Responses reasoning when the model supports none", () => {
const params = buildOpenAIResponsesParams(
{