fix(provider): align GPT-5 reasoning variants (#26268)

This commit is contained in:
Kit Langton
2026-05-07 23:32:47 -04:00
committed by GitHub
parent db6a038829
commit 1cf8123bc6
2 changed files with 216 additions and 47 deletions

View File

@@ -500,6 +500,13 @@ export function topK(model: Provider.Model) {
const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"]
const OPENAI_EFFORTS = ["none", "minimal", ...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
const OPENAI_GPT5_1_EFFORTS = ["none", ...WIDELY_SUPPORTED_EFFORTS]
const OPENAI_GPT5_2_PLUS_EFFORTS = [...OPENAI_GPT5_1_EFFORTS, "xhigh"]
const OPENAI_GPT5_PRO_EFFORTS = ["high"]
const OPENAI_GPT5_PRO_2_PLUS_EFFORTS = ["medium", "high", "xhigh"]
const OPENAI_GPT5_CHAT_EFFORTS = ["medium"]
const OPENAI_GPT5_CODEX_XHIGH_EFFORTS = [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
const OPENAI_GPT5_CODEX_3_PLUS_EFFORTS = ["none", ...OPENAI_GPT5_CODEX_XHIGH_EFFORTS]
// OpenAI rolled out the `none` reasoning_effort tier on this date (Responses API).
// Models released before it 400 on `reasoning_effort: "none"`, so we only expose
@@ -513,17 +520,49 @@ const OPENAI_XHIGH_EFFORT_RELEASE_DATE = "2025-12-04"
// "gpt-5", "gpt-5-nano", "gpt-5.4", "openai/gpt-5.4-codex".
// Anchored to start-of-string or "/" so it doesn't false-match "gpt-50" or "gpt-5o".
const GPT5_FAMILY_RE = /(?:^|\/)gpt-5(?:[.-]|$)/
const GPT5_VERSION_RE = /(?:^|\/)gpt-5[.-](\d+)(?:[.-]|$)/
const GPT5_PRO_RE = /(?:^|\/)gpt-5[.-]?pro(?:[.-]|$)/
const GPT5_VERSIONED_PRO_RE = /(?:^|\/)gpt-5[.-]\d+[.-]pro(?:[.-]|$)/
function gpt5Version(apiId: string) {
return Number(GPT5_VERSION_RE.exec(apiId)?.[1]) || undefined
}
function versionedGpt5ReasoningEfforts(apiId: string) {
if (GPT5_VERSIONED_PRO_RE.test(apiId)) return OPENAI_GPT5_PRO_2_PLUS_EFFORTS
const version = gpt5Version(apiId)
if (version === undefined) return undefined
if (version === 1) return OPENAI_GPT5_1_EFFORTS
return OPENAI_GPT5_2_PLUS_EFFORTS
}
function gpt5CodexReasoningEfforts(apiId: string) {
if (!GPT5_FAMILY_RE.test(apiId) || !apiId.includes("codex")) return undefined
const version = gpt5Version(apiId)
if (version !== undefined && version >= 3) return OPENAI_GPT5_CODEX_3_PLUS_EFFORTS
if (apiId.includes("codex-max") || (version !== undefined && version >= 2)) return OPENAI_GPT5_CODEX_XHIGH_EFFORTS
return WIDELY_SUPPORTED_EFFORTS
}
function gpt5ChatReasoningEfforts(apiId: string) {
if (!GPT5_FAMILY_RE.test(apiId) || !apiId.includes("-chat")) return undefined
return gpt5Version(apiId) === undefined ? [] : OPENAI_GPT5_CHAT_EFFORTS
}
// Computes the reasoning_effort tiers an OpenAI (or OpenAI-compatible upstream
// routed through it, e.g. cf-ai-gateway) model exposes. Returns null for models
// with no tunable effort knob (gpt-5-pro). Effort order: weakest to strongest.
function openaiReasoningEfforts(apiId: string, releaseDate: string): string[] | null {
// routed through it, e.g. cf-ai-gateway) model exposes. Effort order: weakest
// to strongest.
function openaiReasoningEfforts(apiId: string, releaseDate: string) {
const id = apiId.toLowerCase()
if (id === "gpt-5-pro" || id === "openai/gpt-5-pro") return null
if (id.includes("codex")) {
if (id.includes("5.2") || id.includes("5.3")) return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
return [...WIDELY_SUPPORTED_EFFORTS]
}
const chatEfforts = gpt5ChatReasoningEfforts(id)
if (chatEfforts) return chatEfforts
if (GPT5_PRO_RE.test(id)) return OPENAI_GPT5_PRO_EFFORTS
const codexEfforts = gpt5CodexReasoningEfforts(id)
if (codexEfforts) return codexEfforts
const versionedEfforts = versionedGpt5ReasoningEfforts(id)
// GPT-5.1 replaced GPT-5's `minimal` effort with `none`; GPT-5.2+
// additionally accepts `xhigh`. Model pages list the supported subset.
if (versionedEfforts) return versionedEfforts
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
if (GPT5_FAMILY_RE.test(id)) efforts.unshift("minimal")
if (releaseDate >= OPENAI_NONE_EFFORT_RELEASE_DATE) efforts.unshift("none")
@@ -531,6 +570,14 @@ function openaiReasoningEfforts(apiId: string, releaseDate: string): string[] |
return efforts
}
function openaiCompatibleReasoningEfforts(id: string) {
const apiId = id.toLowerCase()
const chatEfforts = gpt5ChatReasoningEfforts(apiId)
if (chatEfforts) return chatEfforts
if (GPT5_PRO_RE.test(apiId)) return OPENAI_GPT5_PRO_EFFORTS
return gpt5CodexReasoningEfforts(apiId) ?? versionedGpt5ReasoningEfforts(apiId) ?? OPENAI_EFFORTS
}
function anthropicAdaptiveEfforts(apiId: string): string[] | null {
if (["opus-4-7", "opus-4.7"].some((v) => apiId.includes(v))) {
return ["low", "medium", "high", "xhigh", "max"]
@@ -577,8 +624,13 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
switch (model.api.npm) {
case "@openrouter/ai-sdk-provider":
if (!model.id.includes("gpt") && !model.id.includes("gemini-3") && !model.id.includes("claude")) return {}
return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning: { effort } }]))
if (!id.includes("gpt") && !id.includes("gemini-3") && !id.includes("claude")) return {}
return Object.fromEntries(
(id.includes("gpt") ? openaiCompatibleReasoningEfforts(id) : OPENAI_EFFORTS).map((effort) => [
effort,
{ reasoning: { effort } },
]),
)
case "ai-gateway-provider": {
// Cloudflare AI Gateway routes every upstream through its OpenAI-compatible
@@ -589,7 +641,6 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
// models that support it.
if (model.api.id.startsWith("openai/")) {
const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
if (!efforts) return {}
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
}
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
@@ -652,7 +703,9 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
]),
)
}
return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
return Object.fromEntries(
openaiCompatibleReasoningEfforts(model.api.id).map((effort) => [effort, { reasoningEffort: effort }]),
)
case "@ai-sdk/github-copilot":
if (model.id.includes("gemini")) {
@@ -700,12 +753,11 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
case "@ai-sdk/azure":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
if (id === "o1-mini") return {}
const azureEfforts = ["low", "medium", "high"]
if (id.includes("gpt-5-") || id === "gpt-5") {
azureEfforts.unshift("minimal")
}
return Object.fromEntries(
azureEfforts.map((effort) => [
(GPT5_FAMILY_RE.test(id) && gpt5Version(id) === undefined
? ["minimal", ...WIDELY_SUPPORTED_EFFORTS]
: WIDELY_SUPPORTED_EFFORTS
).map((effort) => [
effort,
{
reasoningEffort: effort,
@@ -717,7 +769,6 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
case "@ai-sdk/openai": {
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
if (!efforts) return {}
return Object.fromEntries(
efforts.map((effort) => [
effort,
@@ -1102,6 +1153,11 @@ export function smallOptions(model: Provider.Model) {
model.api.npm === "@ai-sdk/github-copilot"
) {
if (model.api.id.includes("gpt-5")) {
if (model.api.id.includes("-chat")) {
if (gpt5Version(model.api.id) === undefined) return { store: false }
return { store: false, reasoningEffort: "medium" }
}
if (model.api.id.includes("search-api")) return { store: false }
if (model.api.id.includes("5.") || model.api.id.includes("5-mini")) {
return { store: false, reasoningEffort: "low" }
}

View File

@@ -2464,6 +2464,32 @@ describe("ProviderTransform.variants", () => {
expect(result.high).toEqual({ reasoning: { effort: "high" } })
})
for (const testCase of [
{ id: "openai/gpt-5.4", efforts: ["none", "low", "medium", "high", "xhigh"] },
{ id: "openai/gpt-5-pro", efforts: ["high"] },
{ id: "openai/gpt-5.5-pro", efforts: ["medium", "high", "xhigh"] },
{ id: "openai/gpt-5.2-codex", efforts: ["low", "medium", "high", "xhigh"] },
{ id: "openai/gpt-5.3-codex", efforts: ["none", "low", "medium", "high", "xhigh"] },
{ id: "openai/gpt-5.3-codex-max", efforts: ["none", "low", "medium", "high", "xhigh"] },
{ id: "openai/gpt-5-chat-latest", efforts: [] },
{ id: "openai/gpt-5.2-chat-latest", efforts: ["medium"] },
]) {
test(`${testCase.id} returns supported OpenAI reasoning efforts`, () => {
const result = ProviderTransform.variants(
createMockModel({
id: testCase.id,
providerID: "openrouter",
api: {
id: testCase.id,
url: "https://openrouter.ai",
npm: "@openrouter/ai-sdk-provider",
},
}),
)
expect(Object.keys(result)).toEqual(testCase.efforts)
})
}
test("gemini-3 returns OPENAI_EFFORTS with reasoning", () => {
const model = createMockModel({
id: "openrouter/gemini-3-5-pro",
@@ -2651,6 +2677,32 @@ describe("ProviderTransform.variants", () => {
expect(result.low).toEqual({ reasoningEffort: "low" })
expect(result.high).toEqual({ reasoningEffort: "high" })
})
for (const testCase of [
{ id: "openai/gpt-5-5", efforts: ["none", "low", "medium", "high", "xhigh"] },
{ id: "openai/gpt-5-pro", efforts: ["high"] },
{ id: "openai/gpt-5-5-pro", efforts: ["medium", "high", "xhigh"] },
{ id: "openai/gpt-5-2-codex", efforts: ["low", "medium", "high", "xhigh"] },
{ id: "openai/gpt-5-3-codex", efforts: ["none", "low", "medium", "high", "xhigh"] },
{ id: "openai/gpt-5-3-codex-max", efforts: ["none", "low", "medium", "high", "xhigh"] },
{ id: "openai/gpt-5-chat-latest", efforts: [] },
{ id: "openai/gpt-5-2-chat-latest", efforts: ["medium"] },
]) {
test(`${testCase.id} returns supported OpenAI reasoning efforts`, () => {
const result = ProviderTransform.variants(
createMockModel({
id: testCase.id,
providerID: "gateway",
api: {
id: testCase.id,
url: "https://gateway.ai",
npm: "@ai-sdk/gateway",
},
}),
)
expect(Object.keys(result)).toEqual(testCase.efforts)
})
}
})
describe("@ai-sdk/github-copilot", () => {
@@ -2929,10 +2981,27 @@ describe("ProviderTransform.variants", () => {
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["minimal", "low", "medium", "high"])
})
for (const id of ["gpt-5-4", "gpt-5-5"]) {
test(`${id} does not add minimal effort`, () => {
const result = ProviderTransform.variants(
createMockModel({
id,
providerID: "azure",
api: {
id,
url: "https://azure.com",
npm: "@ai-sdk/azure",
},
}),
)
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
})
}
})
describe("@ai-sdk/openai", () => {
test("gpt-5-pro returns empty object", () => {
test("gpt-5-pro returns only high effort", () => {
const model = createMockModel({
id: "gpt-5-pro",
providerID: "openai",
@@ -2943,7 +3012,7 @@ describe("ProviderTransform.variants", () => {
},
})
const result = ProviderTransform.variants(model)
expect(result).toEqual({})
expect(Object.keys(result)).toEqual(["high"])
})
test("standard openai models return custom efforts with reasoningSummary", () => {
@@ -2983,10 +3052,10 @@ describe("ProviderTransform.variants", () => {
test("models after 2025-12-04 include 'xhigh' effort", () => {
const model = createMockModel({
id: "openai/gpt-5-chat",
id: "openai/gpt-5-reasoning",
providerID: "openai",
api: {
id: "gpt-5-chat",
id: "gpt-5-reasoning",
url: "https://api.openai.com",
npm: "@ai-sdk/openai",
},
@@ -2996,20 +3065,38 @@ describe("ProviderTransform.variants", () => {
expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
})
test("dotted gpt-5.x ids include 'minimal' (regression: matcher used to miss gpt-5.4)", () => {
const model = createMockModel({
id: "gpt-5.4",
providerID: "openai",
api: {
id: "gpt-5.4",
url: "https://api.openai.com",
npm: "@ai-sdk/openai",
},
release_date: "2026-03-05",
for (const testCase of [
{ id: "gpt-5.1", releaseDate: "2025-11-13", efforts: ["none", "low", "medium", "high"] },
{ id: "gpt-5.4", releaseDate: "2026-03-05", efforts: ["none", "low", "medium", "high", "xhigh"] },
{ id: "gpt-5.5", modelID: "gpt-5-5", releaseDate: "2026-04-23", efforts: ["none", "low", "medium", "high", "xhigh"] },
{ id: "gpt-5.4-pro", releaseDate: "2026-03-05", efforts: ["medium", "high", "xhigh"] },
{ id: "gpt-5.5-pro", releaseDate: "2026-04-23", efforts: ["medium", "high", "xhigh"] },
{ id: "gpt-5-codex", releaseDate: "2025-09-23", efforts: ["low", "medium", "high"] },
{ id: "gpt-5.1-codex", releaseDate: "2025-11-13", efforts: ["low", "medium", "high"] },
{ id: "gpt-5.1-codex-max", releaseDate: "2025-11-13", efforts: ["low", "medium", "high", "xhigh"] },
{ id: "gpt-5.2-codex", releaseDate: "2025-12-11", efforts: ["low", "medium", "high", "xhigh"] },
{ id: "gpt-5.3-codex", releaseDate: "2026-01-22", efforts: ["none", "low", "medium", "high", "xhigh"] },
{ id: "gpt-5.3-codex-max", releaseDate: "2026-01-22", efforts: ["none", "low", "medium", "high", "xhigh"] },
{ id: "gpt-5-chat-latest", releaseDate: "2025-08-07", efforts: [] },
{ id: "gpt-5.1-chat-latest", releaseDate: "2025-11-13", efforts: ["medium"] },
{ id: "gpt-5.2-chat-latest", releaseDate: "2025-12-11", efforts: ["medium"] },
]) {
test(`${testCase.id} returns supported reasoning efforts`, () => {
const result = ProviderTransform.variants(
createMockModel({
id: testCase.modelID ?? testCase.id,
providerID: "openai",
api: {
id: testCase.id,
url: "https://api.openai.com",
npm: "@ai-sdk/openai",
},
release_date: testCase.releaseDate,
}),
)
expect(Object.keys(result)).toEqual(testCase.efforts)
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
})
}
test("gpt-50 (lookalike) does not get gpt-5 family treatment", () => {
const model = createMockModel({
@@ -3486,18 +3573,20 @@ describe("ProviderTransform.variants", () => {
release_date: releaseDate,
})
test("openai gpt-5.4 includes xhigh effort (regression: variant=xhigh used to be silently ignored)", () => {
const result = ProviderTransform.variants(cfModel("openai/gpt-5.4", "2026-03-05"))
expect(result.xhigh).toEqual({ reasoningEffort: "xhigh" })
expect(result.high).toEqual({ reasoningEffort: "high" })
expect(Object.keys(result)).toContain("minimal")
})
test("openai gpt-5.2-codex includes xhigh", () => {
const result = ProviderTransform.variants(cfModel("openai/gpt-5.2-codex", "2025-12-11"))
expect(result.xhigh).toEqual({ reasoningEffort: "xhigh" })
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
})
for (const testCase of [
{ id: "openai/gpt-5.4", efforts: ["none", "low", "medium", "high", "xhigh"] },
{ id: "openai/gpt-5.2-codex", efforts: ["low", "medium", "high", "xhigh"] },
{ id: "openai/gpt-5.3-codex", efforts: ["none", "low", "medium", "high", "xhigh"] },
{ id: "openai/gpt-5-pro", efforts: ["high"] },
{ id: "openai/gpt-5.2-pro", efforts: ["medium", "high", "xhigh"] },
{ id: "openai/gpt-5-chat-latest", efforts: [] },
{ id: "openai/gpt-5.2-chat-latest", efforts: ["medium"] },
]) {
test(`${testCase.id} returns supported reasoning efforts`, () => {
const result = ProviderTransform.variants(cfModel(testCase.id, "2026-03-05"))
expect(Object.keys(result)).toEqual(testCase.efforts)
})
}
test("openai gpt-4o (no reasoning) returns empty", () => {
const model = cfModel("openai/gpt-4o")
@@ -3517,6 +3606,30 @@ describe("ProviderTransform.variants", () => {
})
})
describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => {
const createModel = (apiId: string) =>
({
id: `openai/${apiId}`,
providerID: "openai",
api: {
id: apiId,
url: "https://api.openai.com",
npm: "@ai-sdk/openai",
},
}) as any
for (const testCase of [
{ id: "gpt-5-chat-latest", options: { store: false } },
{ id: "gpt-5.1-chat-latest", options: { store: false, reasoningEffort: "medium" } },
{ id: "gpt-5.2-chat-latest", options: { store: false, reasoningEffort: "medium" } },
{ id: "gpt-5-search-api", options: { store: false } },
]) {
test(`${testCase.id} returns only supported small options`, () => {
expect(ProviderTransform.smallOptions(createModel(testCase.id))).toEqual(testCase.options)
})
}
})
describe("ProviderTransform.providerOptions - ai-gateway-provider", () => {
const createModel = (overrides: Partial<any> = {}) =>
({