mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-14 16:42:38 +00:00
fix(provider): align GPT-5 reasoning variants (#26268)
This commit is contained in:
@@ -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" }
|
||||
}
|
||||
|
||||
@@ -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> = {}) =>
|
||||
({
|
||||
|
||||
Reference in New Issue
Block a user