From 996a0628ef5e7adfb181ed5e7aed2187828bc1c6 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 30 Jun 2026 15:25:14 -0500 Subject: [PATCH] feat(provider): use models.dev reasoning options --- packages/core/src/models-dev.ts | 22 + packages/opencode/src/provider/provider.ts | 10 + packages/opencode/src/provider/transform.ts | 724 +++++----- .../opencode/test/provider/provider.test.ts | 32 +- .../opencode/test/provider/transform.test.ts | 1167 +++++------------ 5 files changed, 656 insertions(+), 1299 deletions(-) diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index c2325cb57f..3539060724 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -44,6 +44,27 @@ const Cost = Schema.Struct({ ), }) +const ReasoningEffortValue = Schema.Union([ + Schema.Null, + Schema.Literals(["none", "minimal", "low", "medium", "high", "xhigh", "max", "default"]), +]) + +export const ReasoningOption = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("effort"), + values: Schema.Array(ReasoningEffortValue), + }), + Schema.Struct({ + type: Schema.Literal("toggle"), + }), + Schema.Struct({ + type: Schema.Literal("budget_tokens"), + min: Schema.optional(Schema.Finite), + max: Schema.optional(Schema.Finite), + }), +]) +export type ReasoningOption = typeof ReasoningOption.Type + export const Model = Schema.Struct({ id: Schema.String, name: Schema.String, @@ -51,6 +72,7 @@ export const Model = Schema.Struct({ release_date: Schema.String, attachment: Schema.Boolean, reasoning: Schema.Boolean, + reasoning_options: Schema.optional(Schema.Array(ReasoningOption)), temperature: Schema.Boolean, tool_call: Schema.Boolean, interleaved: Schema.optional( diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 0ece8e9ead..2084040eaa 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1022,6 +1022,7 @@ export const Model = Schema.Struct({ name: Schema.String, family: optional(Schema.String), capabilities: ProviderCapabilities, + reasoning_options: optional(Schema.Array(ModelsDev.ReasoningOption)), cost: ProviderCost, limit: ProviderLimit, status: ModelStatus, @@ -1185,6 +1186,13 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] { return result } +function reasoningOptions(model: ModelsDev.Model): Model["reasoning_options"] { + return model.reasoning_options?.map((option) => { + if (option.type === "effort") return { ...option, values: [...option.values] } + return { ...option } + }) +} + function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { const base: Model = { id: ModelV2.ID.make(model.id), @@ -1226,6 +1234,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model }, interleaved: model.interleaved ?? false, }, + reasoning_options: reasoningOptions(model), release_date: model.release_date ?? "", variants: {}, } @@ -1456,6 +1465,7 @@ const layer = Layer.effect( ? { field: "reasoning_content" } : false), }, + reasoning_options: existingModel?.reasoning_options, cost: { input: model?.cost?.input ?? existingModel?.cost?.input ?? 0, output: model?.cost?.output ?? existingModel?.cost?.output ?? 0, diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 4bb81b753b..56e1647135 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -3,7 +3,6 @@ import { mergeDeep, unique } from "remeda" import type { JSONSchema7 } from "@ai-sdk/provider" import type * as Provider from "./provider" import type * as ModelsDev from "@opencode-ai/core/models-dev" -import { iife } from "@/util/iife" type Modality = NonNullable["input"][number] @@ -515,134 +514,8 @@ export function topK(model: Provider.Model) { } const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"] +const ANTHROPIC_EFFORTS = [...WIDELY_SUPPORTED_EFFORTS, "xhigh", "max"] 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 -// it as a variant for models new enough to accept it. -const OPENAI_NONE_EFFORT_RELEASE_DATE = "2025-11-13" - -// OpenAI rolled out the `xhigh` reasoning_effort tier on this date. Same reasoning. -const OPENAI_XHIGH_EFFORT_RELEASE_DATE = "2025-12-04" - -// Matches members of the gpt-5 family across the id formats we encounter: -// "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. Effort order: weakest -// to strongest. -function openaiReasoningEfforts(apiId: string, releaseDate: string) { - const id = apiId.toLowerCase() - if (id.includes("deep-research")) return ["medium"] - 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") - if (releaseDate >= OPENAI_XHIGH_EFFORT_RELEASE_DATE) efforts.push("xhigh") - 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 anthropicOpus47OrLater(apiId: string) { - // Matches "opus-4.7" (Anthropic/Bedrock/Vertex) and "claude-4.7-opus" (SAP AI Core inverted). - // Greedy \d+ correctly extends to multi-digit majors (e.g. "claude-10.0-opus") for forward compatibility. - const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)|claude-(\d+)[.-](\d+)-opus(?:[.@-]|$)/i.exec(apiId) - if (!version) return false - const major = Number(version[1] ?? version[3]) - const minor = Number(version[2] ?? version[4]) - return major > 4 || (major === 4 && minor >= 7) -} - -function anthropicSonnet5OrLater(apiId: string) { - const version = /sonnet-(\d+)(?:[.@-]|$)|claude-(\d+)-sonnet(?:[.@-]|$)/i.exec(apiId) - if (!version) return false - return Number(version[1] ?? version[2]) >= 5 -} - -function anthropicAdaptiveEfforts(apiId: string): string[] | null { - if (anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")) { - return ["low", "medium", "high", "xhigh", "max"] - } - if ( - ["opus-4-6", "opus-4.6", "4-6-opus", "4.6-opus", "sonnet-4-6", "sonnet-4.6", "4-6-sonnet", "4.6-sonnet"].some((v) => - apiId.includes(v), - ) - ) { - return ["low", "medium", "high", "max"] - } - return null -} - -function anthropicOmitsThinking(apiId: string) { - return anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5") -} - -function googleThinkingLevelEfforts(apiId: string) { - const id = apiId.toLowerCase() - if (!id.includes("gemini-3")) return ["low", "high"] - if (id.includes("flash-image")) return ["minimal", "high"] - if (id.includes("pro-image")) return ["high"] - if (id.includes("flash")) return ["minimal", "low", "medium", "high"] - return ["low", "medium", "high"] -} - -function googleThinkingBudgetMax(apiId: string) { - const id = apiId.toLowerCase() - if (id.includes("2.5") && id.includes("pro") && !id.includes("flash")) return 32_768 - return 24_576 -} // SAP's Zod schema drops unknown top-level keys; reasoning controls survive // only via `modelParams` (catchall), forwarded verbatim by the SAP SDKs. @@ -650,31 +523,289 @@ function wrapInSapModelParams(variants: Record>): Re return Object.fromEntries(Object.entries(variants).map(([k, v]) => [k, { modelParams: v }])) } -function googleThinkingVariants(model: Provider.Model): Record> { - const id = model.api.id.toLowerCase() - if (id.includes("2.5")) { - return { - high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } }, - max: { - thinkingConfig: { includeThoughts: true, thinkingBudget: googleThinkingBudgetMax(id) }, - }, - } - } +function idIncludes(model: Provider.Model, value: string) { + return model.id.toLowerCase().includes(value) || model.api.id.toLowerCase().includes(value) +} + +function anthropicAdaptiveVariants() { return Object.fromEntries( - googleThinkingLevelEfforts(id).map((effort) => [ + ANTHROPIC_EFFORTS.map((effort) => [ + effort, + { + thinking: anthropicAdaptiveThinking(), + effort, + }, + ]), + ) +} + +function bedrockAnthropicAdaptiveVariants() { + return Object.fromEntries( + ANTHROPIC_EFFORTS.map((effort) => [ + effort, + { + reasoningConfig: { + type: "adaptive", + maxReasoningEffort: effort, + display: "summarized", + }, + }, + ]), + ) +} + +function sapAnthropicAdaptiveVariants() { + return wrapInSapModelParams( + Object.fromEntries( + ANTHROPIC_EFFORTS.map((effort) => [ + effort, + { + thinking: anthropicAdaptiveThinking(), + output_config: { effort }, + }, + ]), + ), + ) +} + +function googleThinkingLevelVariants() { + return Object.fromEntries( + WIDELY_SUPPORTED_EFFORTS.map((effort) => [ effort, { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }, ]), ) } +function openAIReasoningEffortVariants() { + return Object.fromEntries( + OPENAI_EFFORTS.map((effort) => [ + effort, + { + reasoningEffort: effort, + reasoningSummary: "auto", + include: INCLUDE_ENCRYPTED_REASONING, + }, + ]), + ) +} + +type ReasoningOption = NonNullable[number] +type ReasoningEffortOption = Extract +type ReasoningBudgetOption = Extract +type ReasoningEffortValue = ReasoningEffortOption["values"][number] + +function reasoningOption( + model: Provider.Model, + type: T, +): Extract | undefined { + return model.reasoning_options?.find((option): option is Extract => option.type === type) +} + +function reasoningBudgetVariants( + budget: ReasoningBudgetOption, + make: (budgetTokens: number) => Record, + output: number, +) { + const max = Math.max(1, Math.min(budget.max ?? 31_999, (output || OUTPUT_TOKEN_MAX) - 1)) + const min = Math.min(budget.min ?? 1, max) + return { + high: make(Math.max(min, Math.min(16_000, max))), + max: make(max), + } +} + +function anthropicAdaptiveFromOptions(effort: ReasoningEffortOption) { + return effort.values.includes("max") +} + +function anthropicAdaptiveThinking() { + return { + type: "adaptive", + display: "summarized", + } +} + +function effortVariant(model: Provider.Model, effort: ReasoningEffortValue) { + switch (model.api.npm) { + case "@openrouter/ai-sdk-provider": + return { reasoning: { effort } } + + case "ai-gateway-provider": + return { reasoningEffort: effort } + + case "@ai-sdk/gateway": + if (idIncludes(model, "anthropic")) { + const option = reasoningOption(model, "effort") + if (option && anthropicAdaptiveFromOptions(option)) { + return { + thinking: anthropicAdaptiveThinking(), + effort, + } + } + return { effort } + } + if (idIncludes(model, "google")) return { includeThoughts: true, thinkingLevel: effort } + return { reasoningEffort: effort } + + case "@ai-sdk/anthropic": + case "@ai-sdk/google-vertex/anthropic": { + const option = reasoningOption(model, "effort") + if (option && anthropicAdaptiveFromOptions(option)) { + return { + thinking: anthropicAdaptiveThinking(), + effort, + } + } + return { effort } + } + + case "@ai-sdk/amazon-bedrock": + if (model.api.id.includes("anthropic")) { + const option = reasoningOption(model, "effort") + const adaptive = option ? anthropicAdaptiveFromOptions(option) : false + return { + reasoningConfig: { + type: adaptive ? "adaptive" : "enabled", + maxReasoningEffort: effort, + ...(adaptive ? { display: "summarized" } : {}), + }, + } + } + return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } } + + case "@ai-sdk/google": + case "@ai-sdk/google-vertex": + return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } } + + case "@jerome-benoit/sap-ai-provider-v2": { + if (!model.api.id.includes("anthropic")) return { modelParams: { reasoning_effort: effort } } + const option = reasoningOption(model, "effort") + const modelParams = + option && anthropicAdaptiveFromOptions(option) + ? { + thinking: anthropicAdaptiveThinking(), + output_config: { effort }, + } + : { output_config: { effort } } + return { modelParams } + } + + case "@ai-sdk/azure": + return { + reasoningEffort: effort, + reasoningSummary: "auto", + include: INCLUDE_ENCRYPTED_REASONING, + } + + case "@ai-sdk/amazon-bedrock/mantle": + case "@ai-sdk/openai": + return { + reasoningEffort: effort, + reasoningSummary: "auto", + include: INCLUDE_ENCRYPTED_REASONING, + } + + case "@ai-sdk/github-copilot": + return model.id.includes("claude") + ? { reasoningEffort: effort } + : { + reasoningEffort: effort, + reasoningSummary: "auto", + include: INCLUDE_ENCRYPTED_REASONING, + } + + case "@ai-sdk/cerebras": + case "@ai-sdk/togetherai": + case "@ai-sdk/xai": + case "@ai-sdk/deepinfra": + case "venice-ai-sdk-provider": + case "@ai-sdk/openai-compatible": + case "@ai-sdk/groq": + return { reasoningEffort: effort } + } + return undefined +} + +function reasoningOptionVariants(model: Provider.Model): Record> | undefined { + if (!model.reasoning_options) return undefined + const effort = reasoningOption(model, "effort") + const budget = reasoningOption(model, "budget_tokens") + + if (effort) { + return Object.fromEntries( + effort.values.flatMap((value): [string, Record][] => { + // A null effort is a provider-specific escape hatch in models.dev. Skip + // exposing it as a selectable variant until variant IDs can represent it cleanly. + if (value === null) return [] + const variant = effortVariant(model, value) + return variant ? [[value, variant]] : [] + }), + ) + } + + // Toggle-only support needs a product decision about how to expose an on/off + // variant without clobbering users' current effort selection. Existing + // hand-authored toggle variants, like MiniMax M3 below, remain preserved. + if (!budget) return {} + + switch (model.api.npm) { + case "@ai-sdk/gateway": + if (idIncludes(model, "anthropic")) { + return reasoningBudgetVariants( + budget, + (budgetTokens) => ({ thinking: { type: "enabled", budgetTokens } }), + model.limit.output, + ) + } + if (idIncludes(model, "google")) { + return reasoningBudgetVariants( + budget, + (thinkingBudget) => ({ thinkingConfig: { includeThoughts: true, thinkingBudget } }), + model.limit.output, + ) + } + break + + case "@ai-sdk/anthropic": + case "@ai-sdk/google-vertex/anthropic": + return reasoningBudgetVariants(budget, (budgetTokens) => ({ thinking: { type: "enabled", budgetTokens } }), model.limit.output) + + case "@ai-sdk/amazon-bedrock": + if (model.api.id.includes("anthropic")) { + return reasoningBudgetVariants( + budget, + (budgetTokens) => ({ reasoningConfig: { type: "enabled", budgetTokens } }), + model.limit.output, + ) + } + break + + case "@ai-sdk/google": + case "@ai-sdk/google-vertex": + return reasoningBudgetVariants( + budget, + (thinkingBudget) => ({ thinkingConfig: { includeThoughts: true, thinkingBudget } }), + model.limit.output, + ) + + case "@jerome-benoit/sap-ai-provider-v2": + if (model.api.id.includes("anthropic")) { + return wrapInSapModelParams( + reasoningBudgetVariants(budget, (budget_tokens) => ({ thinking: { type: "enabled", budget_tokens } }), model.limit.output), + ) + } + break + } + + return {} +} + export function variants(model: Provider.Model): Record> { if (!model.capabilities.reasoning) return {} - const id = model.id.toLowerCase() - const glm52 = ["glm-5.2", "glm-5-2", "glm-5p2"].some( - (name) => id.includes(name) || model.api.id.toLowerCase().includes(name), - ) + // Historical exception: MiniMax M3's Anthropic-compatible surface exposes an + // explicit thinking toggle that predates models.dev reasoning_options. if ( model.api.id.toLowerCase().includes("minimax-m3") && ["@ai-sdk/anthropic", "@ai-sdk/openai-compatible"].includes(model.api.npm) @@ -684,161 +815,26 @@ export function variants(model: Provider.Model): Record [effort, { reasoning: { effort } }]), - ) + return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoning: { effort } }])) - case "ai-gateway-provider": { - // Cloudflare AI Gateway routes every upstream through its OpenAI-compatible - // /v1/compat endpoint, so the body is always OAI-shaped. The gateway - // translates `reasoning_effort` to the upstream provider's native control - // (e.g. Anthropic thinking budgets) when needed. Variants therefore stay - // OAI-style for all upstreams, with an extended effort set for OpenAI - // models that support it. - if (model.api.id.startsWith("openai/")) { - const efforts = openaiReasoningEfforts(model.api.id, model.release_date) - return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }])) - } + case "ai-gateway-provider": return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) - } case "@ai-sdk/gateway": - if (model.id.includes("anthropic")) { - if (adaptiveEfforts) { - return Object.fromEntries( - adaptiveEfforts.map((effort) => [ - effort, - { - thinking: { - type: "adaptive", - // Newer adaptive-only models default `display` to "omitted", which - // returns empty thinking blocks. Force "summarized" so summaries - // survive (4.6/Sonnet 4.6 already default to "summarized"). - ...(adaptiveThinkingOmitted ? { display: "summarized" } : {}), - }, - effort, - }, - ]), - ) - } - return { - high: { - thinking: { - type: "enabled", - budgetTokens: 16000, - }, - }, - max: { - thinking: { - type: "enabled", - budgetTokens: 31999, - }, - }, - } - } - if (model.id.includes("google")) { - if (id.includes("2.5")) { - return { - high: { - thinkingConfig: { - includeThoughts: true, - thinkingBudget: 16000, - }, - }, - max: { - thinkingConfig: { - includeThoughts: true, - thinkingBudget: googleThinkingBudgetMax(id), - }, - }, - } - } - return Object.fromEntries( - ["low", "high"].map((effort) => [ - effort, - { - includeThoughts: true, - thinkingLevel: effort, - }, - ]), - ) - } - return Object.fromEntries( - openaiCompatibleReasoningEfforts(model.api.id).map((effort) => [effort, { reasoningEffort: effort }]), - ) + if (idIncludes(model, "anthropic")) return anthropicAdaptiveVariants() + if (idIncludes(model, "google")) return googleThinkingLevelVariants() + if (idIncludes(model, "openai")) return openAIReasoningEffortVariants() + return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) case "@ai-sdk/github-copilot": - if (model.id.includes("gemini")) { - // currently github copilot only returns thinking - return {} - } - if (model.id.includes("claude")) { - return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) - } - const copilotEfforts = iife(() => { - if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3")) - return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"] - const arr = [...WIDELY_SUPPORTED_EFFORTS] - if (id.includes("gpt-5") && model.release_date >= "2025-12-04") arr.push("xhigh") - return arr - }) return Object.fromEntries( - copilotEfforts.map((effort) => [ + WIDELY_SUPPORTED_EFFORTS.map((effort) => [ effort, { reasoningEffort: effort, @@ -859,125 +855,26 @@ export function variants(model: Provider.Model): Record [effort, { reasoningEffort: effort }])) - } - const efforts = [...WIDELY_SUPPORTED_EFFORTS] - if (model.api.id.toLowerCase().includes("deepseek-v4")) { - efforts.push("max") - } - return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }])) + return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) case "@ai-sdk/azure": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure - if (id === "o1-mini") return {} - return Object.fromEntries( - openaiReasoningEfforts(id, model.release_date).map((effort) => [ - effort, - { - reasoningEffort: effort, - reasoningSummary: "auto", - include: INCLUDE_ENCRYPTED_REASONING, - }, - ]), - ) + return openAIReasoningEffortVariants() case "@ai-sdk/amazon-bedrock/mantle": case "@ai-sdk/openai": { // https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai - const efforts = openaiReasoningEfforts(model.api.id, model.release_date) - return Object.fromEntries( - efforts.map((effort) => [ - effort, - { - reasoningEffort: effort, - reasoningSummary: "auto", - include: INCLUDE_ENCRYPTED_REASONING, - }, - ]), - ) + return openAIReasoningEffortVariants() } case "@ai-sdk/anthropic": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic case "@ai-sdk/google-vertex/anthropic": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider - if (adaptiveEfforts) { - let efforts = [...adaptiveEfforts] - if (model.providerID === "github-copilot") { - if (model.api.id.includes("opus-4.7")) { - efforts = ["medium"] - } - // Efforts currently supported are: low, medium, high - efforts = efforts.filter((v) => v !== "max" && v !== "xhigh") - } - return Object.fromEntries( - efforts.map((effort) => [ - effort, - { - thinking: { - type: "adaptive", - ...(adaptiveThinkingOmitted ? { display: "summarized" } : {}), - }, - effort, - }, - ]), - ) - } - - if (["opus-4-5", "opus-4.5"].some((v) => model.api.id.includes(v))) { - return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { effort }])) - } - - return { - high: { - thinking: { - type: "enabled", - budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)), - }, - }, - max: { - thinking: { - type: "enabled", - budgetTokens: Math.min(31_999, model.limit.output - 1), - }, - }, - } + return anthropicAdaptiveVariants() case "@ai-sdk/amazon-bedrock": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock - if (adaptiveEfforts) { - return Object.fromEntries( - adaptiveEfforts.map((effort) => [ - effort, - { - reasoningConfig: { - type: "adaptive", - maxReasoningEffort: effort, - ...(adaptiveThinkingOmitted ? { display: "summarized" } : {}), - }, - }, - ]), - ) - } - // For Anthropic models on Bedrock, use reasoningConfig with budgetTokens - if (model.api.id.includes("anthropic")) { - return { - high: { - reasoningConfig: { - type: "enabled", - budgetTokens: 16000, - }, - }, - max: { - reasoningConfig: { - type: "enabled", - budgetTokens: 31999, - }, - }, - } - } - - // For Amazon Nova models, use reasoningConfig with maxReasoningEffort + if (idIncludes(model, "anthropic")) return bedrockAnthropicAdaptiveVariants() return Object.fromEntries( WIDELY_SUPPORTED_EFFORTS.map((effort) => [ effort, @@ -994,21 +891,11 @@ export function variants(model: Provider.Model): Record mistralId.includes(id))) return {} return { high: { reasoningEffort: "high" }, } @@ -1034,33 +921,12 @@ export function variants(model: Provider.Model): Record [ - effort, - { - thinking: { type: "adaptive", ...(adaptiveThinkingOmitted ? { display: "summarized" } : {}) }, - output_config: { effort }, - }, - ]), - ), - ) - } - return wrapInSapModelParams({ - high: { thinking: { type: "enabled", budget_tokens: 16000 } }, - max: { thinking: { type: "enabled", budget_tokens: 31999 } }, - }) - } - if (id.includes("gemini") && id.includes("2.5")) { - return wrapInSapModelParams(googleThinkingVariants(model)) - } - if (id.includes("gpt") || /\bo[1-9]/.test(id)) { - const efforts = openaiReasoningEfforts(id, model.release_date) - return wrapInSapModelParams(Object.fromEntries(efforts.map((effort) => [effort, { reasoning_effort: effort }]))) + if (idIncludes(model, "anthropic")) return sapAnthropicAdaptiveVariants() + if (idIncludes(model, "google")) return wrapInSapModelParams(googleThinkingLevelVariants()) + if (idIncludes(model, "openai")) { + return wrapInSapModelParams( + Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning_effort: effort }])), + ) } return wrapInSapModelParams( Object.fromEntries(["low", "medium", "high"].map((effort) => [effort, { reasoning_effort: effort }])), diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 18ec8f9fbe..d29101bace 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1426,6 +1426,30 @@ test("models.dev normalization fills required response fields", () => { expect(model.release_date).toBe("") }) +test("models.dev normalization preserves reasoning options for variant generation", () => { + const provider = { + id: "custom", + name: "Custom", + env: [], + npm: "@ai-sdk/openai-compatible", + models: { + reasoner: { + id: "reasoner", + name: "Reasoner", + reasoning: true, + reasoning_options: [{ type: "effort", values: ["high", "xhigh"] }], + cost: { input: 1, output: 2 }, + limit: { context: 128_000, output: 32_000 }, + }, + }, + } as unknown as ModelsDev.Provider + + const model = Provider.fromModelsDevProvider(provider).models.reasoner + expect(model.reasoning_options).toEqual([{ type: "effort", values: ["high", "xhigh"] }]) + expect(Object.keys(model.variants!)).toEqual(["high", "xhigh"]) + expect(model.variants!.xhigh).toEqual({ reasoningEffort: "xhigh" }) +}) + it.instance("model variants are generated for reasoning models", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") @@ -1524,7 +1548,13 @@ it.instance( anthropic: { models: { "claude-sonnet-4-20250514": { - variants: { high: { disabled: true }, max: { disabled: true } }, + variants: { + low: { disabled: true }, + medium: { disabled: true }, + high: { disabled: true }, + xhigh: { disabled: true }, + max: { disabled: true }, + }, }, }, }, diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index dcbaf81716..705aa72976 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -2827,6 +2827,132 @@ describe("ProviderTransform.variants", () => { expect(result).toEqual({}) }) + test("reasoning_options drive OpenAI-compatible effort variants", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "custom/new-reasoner", + providerID: "custom", + api: { + id: "new-reasoner", + url: "https://api.example.com", + npm: "@ai-sdk/openai-compatible", + }, + reasoning_options: [{ type: "effort", values: ["none", "high", "xhigh"] }], + }), + ) + expect(Object.keys(result)).toEqual(["none", "high", "xhigh"]) + expect(result.xhigh).toEqual({ reasoningEffort: "xhigh" }) + }) + + test("empty reasoning_options suppress heuristic variants", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "anthropic/claude-sonnet-5", + providerID: "anthropic", + api: { + id: "claude-sonnet-5", + url: "https://api.anthropic.com", + npm: "@ai-sdk/anthropic", + }, + reasoning_options: [], + }), + ) + expect(result).toEqual({}) + }) + + test("reasoning_options infer adaptive Anthropic variants without release heuristics", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "anthropic/claude-future", + providerID: "anthropic", + api: { + id: "claude-future", + url: "https://api.anthropic.com", + npm: "@ai-sdk/anthropic", + }, + reasoning_options: [{ type: "toggle" }, { type: "effort", values: ["low", "max"] }], + }), + ) + expect(Object.keys(result)).toEqual(["low", "max"]) + expect(result.max).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + effort: "max", + }) + }) + + test("reasoning_options keep legacy Anthropic effort shape when budget tokens are also supported", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "anthropic/claude-opus-4-5", + providerID: "anthropic", + api: { + id: "claude-opus-4-5", + url: "https://api.anthropic.com", + npm: "@ai-sdk/anthropic", + }, + reasoning_options: [ + { type: "effort", values: ["low", "medium", "high"] }, + { type: "budget_tokens", min: 1024 }, + ], + }), + ) + expect(Object.keys(result)).toEqual(["low", "medium", "high"]) + expect(result.high).toEqual({ effort: "high" }) + }) + + test("reasoning_options budget tokens generate budget variants when no effort is available", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "google/gemini-new", + providerID: "google", + api: { + id: "gemini-new", + url: "https://generativelanguage.googleapis.com", + npm: "@ai-sdk/google", + }, + reasoning_options: [{ type: "toggle" }, { type: "budget_tokens", min: 128, max: 32768 }], + }), + ) + expect(Object.keys(result)).toEqual(["high", "max"]) + expect(result.high).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } }) + expect(result.max).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 32768 } }) + }) + + test("reasoning_options ignore toggle-only models", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "custom/toggle-only", + providerID: "custom", + api: { + id: "toggle-only", + url: "https://api.example.com", + npm: "@ai-sdk/openai-compatible", + }, + reasoning_options: [{ type: "toggle" }], + }), + ) + expect(result).toEqual({}) + }) + + test("reasoning_options do not override existing MiniMax M3 toggle variants", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "minimax/minimax-m3", + providerID: "minimax", + api: { + id: "MiniMax-M3", + url: "https://api.minimax.com/anthropic/v1", + npm: "@ai-sdk/anthropic", + }, + reasoning_options: [], + }), + ) + expect(result).toEqual({ + none: { thinking: { type: "disabled" } }, + thinking: { thinking: { type: "adaptive" } }, + }) + }) + test("deepseek returns empty object", () => { const model = createMockModel({ id: "deepseek/deepseek-chat", @@ -2836,6 +2962,7 @@ describe("ProviderTransform.variants", () => { url: "https://api.deepseek.com", npm: "@ai-sdk/openai-compatible", }, + reasoning_options: [], }) const result = ProviderTransform.variants(model) expect(result).toEqual({}) @@ -2850,6 +2977,7 @@ describe("ProviderTransform.variants", () => { url: "https://api.minimax.com", npm: "@ai-sdk/openai-compatible", }, + reasoning_options: [], }) const result = ProviderTransform.variants(model) expect(result).toEqual({}) @@ -2888,21 +3016,7 @@ describe("ProviderTransform.variants", () => { }) }) - test("glm returns empty object", () => { - const model = createMockModel({ - id: "glm/glm-4", - providerID: "glm", - api: { - id: "glm-4", - url: "https://api.glm.com", - npm: "@ai-sdk/openai-compatible", - }, - }) - const result = ProviderTransform.variants(model) - expect(result).toEqual({}) - }) - - test("glm-5.2 returns native effort variants for openai-compatible providers", () => { + test("model-specific efforts are read from reasoning_options", () => { const model = createMockModel({ id: "zhipuai/glm-5.2", providerID: "zhipuai", @@ -2911,6 +3025,7 @@ describe("ProviderTransform.variants", () => { url: "https://open.bigmodel.cn/api/paas/v4", npm: "@ai-sdk/openai-compatible", }, + reasoning_options: [{ type: "effort", values: ["high", "max"] }], }) expect(ProviderTransform.variants(model)).toEqual({ high: { reasoningEffort: "high" }, @@ -2918,86 +3033,6 @@ describe("ProviderTransform.variants", () => { }) }) - test("recognizes GLM-5.2 provider model IDs", () => { - for (const id of ["accounts/fireworks/models/glm-5p2", "zai-org-glm-5-2", "umans-glm-5.2"]) { - const model = createMockModel({ - id: `test/${id}`, - api: { - id, - url: "https://api.test.com", - npm: "@ai-sdk/openai-compatible", - }, - }) - expect(ProviderTransform.variants(model)).toEqual({ - high: { reasoningEffort: "high" }, - max: { reasoningEffort: "max" }, - }) - } - }) - - test("recognizes GLM-5.2 from the API ID when the configured model ID is an alias", () => { - const model = createMockModel({ - id: "custom/my-glm", - api: { - id: "accounts/fireworks/models/glm-5p2", - url: "https://api.fireworks.ai/inference/v1", - npm: "@ai-sdk/openai-compatible", - }, - }) - expect(ProviderTransform.variants(model)).toEqual({ - high: { reasoningEffort: "high" }, - max: { reasoningEffort: "max" }, - }) - }) - - test("glm-5.2 returns openrouter effort variants for openrouter", () => { - const model = createMockModel({ - id: "openrouter/z-ai/glm-5.2", - providerID: "openrouter", - api: { - id: "z-ai/glm-5.2", - url: "https://openrouter.ai/api/v1", - npm: "@openrouter/ai-sdk-provider", - }, - }) - expect(ProviderTransform.variants(model)).toEqual({ - high: { reasoning: { effort: "high" } }, - xhigh: { reasoning: { effort: "xhigh" } }, - }) - }) - - test("glm-5.2 returns effort variants for anthropic-compatible providers", () => { - const model = createMockModel({ - id: "zai-coding-plan/glm-5.2", - providerID: "zai-coding-plan", - api: { - id: "glm-5.2", - url: "https://api.z.ai/api/anthropic", - npm: "@ai-sdk/anthropic", - }, - }) - expect(ProviderTransform.variants(model)).toEqual({ - high: { effort: "high" }, - max: { effort: "max" }, - }) - }) - - test("glm-5.2 falls back to provider defaults for other packages", () => { - const model = createMockModel({ - id: "test/glm-5.2", - api: { - id: "glm-5.2", - url: "https://api.test.com", - npm: "@ai-sdk/amazon-bedrock", - }, - }) - expect(ProviderTransform.variants(model)).toEqual({ - low: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } }, - medium: { reasoningConfig: { type: "enabled", maxReasoningEffort: "medium" } }, - high: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }, - }) - }) - test("mistral models with reasoning support return variants", () => { const model = createMockModel({ id: "mistral/mistral-small-latest", @@ -3047,21 +3082,6 @@ describe("ProviderTransform.variants", () => { expect(result).toEqual({}) }) - test("mistral large with reasoning returns empty object (only small supports reasoning)", () => { - const model = createMockModel({ - id: "mistral/mistral-large", - providerID: "mistral", - api: { - id: "mistral-large-latest", - url: "https://api.mistral.com", - npm: "@ai-sdk/mistral", - }, - capabilities: { reasoning: true }, - }) - const result = ProviderTransform.variants(model) - expect(result).toEqual({}) - }) - describe("@openrouter/ai-sdk-provider", () => { test("returns widely supported efforts for other reasoning models", () => { const model = createMockModel({ @@ -3078,286 +3098,26 @@ describe("ProviderTransform.variants", () => { expect(result.medium).toEqual({ reasoning: { effort: "medium" } }) }) - test("gpt models return OPENAI_EFFORTS with reasoning", () => { - const model = createMockModel({ - id: "openrouter/gpt-4", - providerID: "openrouter", - api: { - id: "gpt-4", - url: "https://openrouter.ai", - npm: "@openrouter/ai-sdk-provider", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) - expect(result.low).toEqual({ reasoning: { effort: "low" } }) - expect(result.high).toEqual({ reasoning: { effort: "high" } }) - }) - - for (const testCase of [ - { id: "openai/o3-mini", efforts: ["none", "minimal", "low", "medium", "high", "xhigh"] }, - { 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 widely supported efforts with reasoning", () => { - const model = createMockModel({ - id: "openrouter/gemini-3-5-pro", - providerID: "openrouter", - api: { - id: "gemini-3-5-pro", - url: "https://openrouter.ai", - npm: "@openrouter/ai-sdk-provider", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high"]) - }) - - test("grok-4 returns empty object", () => { - const model = createMockModel({ - id: "openrouter/grok-4", - providerID: "openrouter", - api: { - id: "grok-4", - url: "https://openrouter.ai", - npm: "@openrouter/ai-sdk-provider", - }, - }) - const result = ProviderTransform.variants(model) - expect(result).toEqual({}) - }) - - test("grok-3-mini returns low and high with reasoning", () => { - const model = createMockModel({ - id: "openrouter/grok-3-mini", - providerID: "openrouter", - api: { - id: "grok-3-mini", - url: "https://openrouter.ai", - npm: "@openrouter/ai-sdk-provider", - }, - }) - const result = ProviderTransform.variants(model) + test("model-specific OpenRouter efforts come from reasoning_options", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "openrouter/grok-3-mini", + providerID: "openrouter", + api: { + id: "grok-3-mini", + url: "https://openrouter.ai", + npm: "@openrouter/ai-sdk-provider", + }, + reasoning_options: [{ type: "effort", values: ["low", "high"] }], + }), + ) expect(Object.keys(result)).toEqual(["low", "high"]) expect(result.low).toEqual({ reasoning: { effort: "low" } }) - expect(result.high).toEqual({ reasoning: { effort: "high" } }) }) }) describe("@ai-sdk/gateway", () => { - test("anthropic sonnet 4.6 models return adaptive thinking options", () => { - const model = createMockModel({ - id: "anthropic/claude-sonnet-4-6", - providerID: "gateway", - api: { - id: "anthropic/claude-sonnet-4-6", - url: "https://gateway.ai", - npm: "@ai-sdk/gateway", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"]) - expect(result.medium).toEqual({ - thinking: { - type: "adaptive", - }, - effort: "medium", - }) - }) - - test("anthropic sonnet 4.6 dot-format models return adaptive thinking options", () => { - const model = createMockModel({ - id: "anthropic/claude-sonnet-4-6", - providerID: "gateway", - api: { - id: "anthropic/claude-sonnet-4.6", - url: "https://gateway.ai", - npm: "@ai-sdk/gateway", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"]) - expect(result.medium).toEqual({ - thinking: { - type: "adaptive", - }, - effort: "medium", - }) - }) - - test("anthropic opus 4.6 dot-format models return adaptive thinking options", () => { - const model = createMockModel({ - id: "anthropic/claude-opus-4-6", - providerID: "gateway", - api: { - id: "anthropic/claude-opus-4.6", - url: "https://gateway.ai", - npm: "@ai-sdk/gateway", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"]) - expect(result.high).toEqual({ - thinking: { - type: "adaptive", - }, - effort: "high", - }) - }) - - test("anthropic opus 4.7 models return adaptive thinking options with xhigh", () => { - const model = createMockModel({ - id: "anthropic/claude-opus-4-7", - providerID: "gateway", - api: { - id: "anthropic/claude-opus-4-7", - url: "https://gateway.ai", - npm: "@ai-sdk/gateway", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) - expect(result.xhigh).toEqual({ - thinking: { - type: "adaptive", - display: "summarized", - }, - effort: "xhigh", - }) - expect(result.max).toEqual({ - thinking: { - type: "adaptive", - display: "summarized", - }, - effort: "max", - }) - }) - - test("anthropic opus 4.7 dot-format models return adaptive thinking options with xhigh", () => { - const model = createMockModel({ - id: "anthropic/claude-opus-4-7", - providerID: "gateway", - api: { - id: "anthropic/claude-opus-4.7", - url: "https://gateway.ai", - npm: "@ai-sdk/gateway", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) - }) - - test("anthropic opus 4.8 forces display summarized for adaptive reasoning", () => { - const model = createMockModel({ - id: "anthropic/claude-opus-4-8", - providerID: "gateway", - api: { - id: "anthropic/claude-opus-4-8", - url: "https://gateway.ai", - npm: "@ai-sdk/gateway", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) - expect(result.high).toEqual({ - thinking: { - type: "adaptive", - display: "summarized", - }, - effort: "high", - }) - }) - - test("anthropic sonnet 5 returns adaptive thinking options with xhigh", () => { - const model = createMockModel({ - id: "anthropic/claude-sonnet-5", - providerID: "gateway", - api: { - id: "anthropic/claude-sonnet-5", - url: "https://gateway.ai", - npm: "@ai-sdk/gateway", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) - expect(result.high).toEqual({ - thinking: { - type: "adaptive", - display: "summarized", - }, - effort: "high", - }) - }) - - test("anthropic opus 4.6 omits display so it keeps the summarized default", () => { - const model = createMockModel({ - id: "anthropic/claude-opus-4-6", - providerID: "gateway", - api: { - id: "anthropic/claude-opus-4-6", - url: "https://gateway.ai", - npm: "@ai-sdk/gateway", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"]) - expect(result.high).toEqual({ - thinking: { - type: "adaptive", - }, - effort: "high", - }) - }) - - test("anthropic models return anthropic thinking options", () => { - const model = createMockModel({ - id: "anthropic/claude-sonnet-4", - providerID: "gateway", - api: { - id: "anthropic/claude-sonnet-4", - url: "https://gateway.ai", - npm: "@ai-sdk/gateway", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["high", "max"]) - expect(result.high).toEqual({ - thinking: { - type: "enabled", - budgetTokens: 16000, - }, - }) - expect(result.max).toEqual({ - thinking: { - type: "enabled", - budgetTokens: 31999, - }, - }) - }) - - test("returns OPENAI_EFFORTS with reasoningEffort", () => { + test("falls back to widely-supported reasoningEffort variants", () => { const model = createMockModel({ id: "gateway/gateway-model", providerID: "gateway", @@ -3368,36 +3128,66 @@ describe("ProviderTransform.variants", () => { }, }) const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) + expect(Object.keys(result)).toEqual(["low", "medium", "high"]) 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) + test("uses reasoning_options for Anthropic adaptive variants", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "anthropic/claude-sonnet-5", + providerID: "gateway", + api: { + id: "anthropic/claude-sonnet-5", + url: "https://gateway.ai", + npm: "@ai-sdk/gateway", + }, + reasoning_options: [{ type: "effort", values: ["low", "high", "xhigh", "max"] }], + }), + ) + expect(Object.keys(result)).toEqual(["low", "high", "xhigh", "max"]) + expect(result.high).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + effort: "high", }) - } + }) + + test("uses reasoning_options budget tokens for Anthropic variants", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "anthropic/claude-sonnet-4", + providerID: "gateway", + api: { + id: "anthropic/claude-sonnet-4", + url: "https://gateway.ai", + npm: "@ai-sdk/gateway", + }, + reasoning_options: [{ type: "budget_tokens", min: 1024, max: 32768 }], + }), + ) + expect(Object.keys(result)).toEqual(["high", "max"]) + expect(result.high).toEqual({ thinking: { type: "enabled", budgetTokens: 16_000 } }) + expect(result.max).toEqual({ thinking: { type: "enabled", budgetTokens: 32_768 } }) + }) + + test("uses reasoning_options budget tokens for Google variants", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "google/gemini-2.5-pro", + providerID: "gateway", + api: { + id: "google/gemini-2.5-pro", + url: "https://gateway.ai", + npm: "@ai-sdk/gateway", + }, + reasoning_options: [{ type: "budget_tokens", min: 128, max: 32768 }], + }), + ) + expect(Object.keys(result)).toEqual(["high", "max"]) + expect(result.high).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } }) + expect(result.max).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 32_768 } }) + }) }) describe("@ai-sdk/github-copilot", () => { @@ -3420,109 +3210,6 @@ describe("ProviderTransform.variants", () => { }) }) - test("gpt-5.1-codex-max includes xhigh", () => { - const model = createMockModel({ - id: "gpt-5.1-codex-max", - providerID: "github-copilot", - api: { - id: "gpt-5.1-codex-max", - url: "https://api.githubcopilot.com", - npm: "@ai-sdk/github-copilot", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"]) - }) - - test("gpt-5.1-codex-mini does not include xhigh", () => { - const model = createMockModel({ - id: "gpt-5.1-codex-mini", - providerID: "github-copilot", - api: { - id: "gpt-5.1-codex-mini", - url: "https://api.githubcopilot.com", - npm: "@ai-sdk/github-copilot", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high"]) - }) - - test("gpt-5.1-codex does not include xhigh", () => { - const model = createMockModel({ - id: "gpt-5.1-codex", - providerID: "github-copilot", - api: { - id: "gpt-5.1-codex", - url: "https://api.githubcopilot.com", - npm: "@ai-sdk/github-copilot", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high"]) - }) - - test("gpt-5.2 includes xhigh", () => { - const model = createMockModel({ - id: "gpt-5.2", - providerID: "github-copilot", - api: { - id: "gpt-5.2", - url: "https://api.githubcopilot.com", - npm: "@ai-sdk/github-copilot", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"]) - expect(result.xhigh).toEqual({ - reasoningEffort: "xhigh", - reasoningSummary: "auto", - include: ["reasoning.encrypted_content"], - }) - }) - - test("gpt-5.2-codex includes xhigh", () => { - const model = createMockModel({ - id: "gpt-5.2-codex", - providerID: "github-copilot", - api: { - id: "gpt-5.2-codex", - url: "https://api.githubcopilot.com", - npm: "@ai-sdk/github-copilot", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"]) - }) - - test("gpt-5.3-codex includes xhigh", () => { - const model = createMockModel({ - id: "gpt-5.3-codex", - providerID: "github-copilot", - api: { - id: "gpt-5.3-codex", - url: "https://api.githubcopilot.com", - npm: "@ai-sdk/github-copilot", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"]) - }) - - test("gpt-5.4 includes xhigh", () => { - const model = createMockModel({ - id: "gpt-5.4", - release_date: "2026-03-05", - providerID: "github-copilot", - api: { - id: "gpt-5.4", - url: "https://api.githubcopilot.com", - npm: "@ai-sdk/github-copilot", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"]) - }) }) describe("@ai-sdk/cerebras", () => { @@ -3562,7 +3249,7 @@ describe("ProviderTransform.variants", () => { }) describe("@ai-sdk/xai", () => { - test("grok-3 returns empty object", () => { + test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => { const model = createMockModel({ id: "xai/grok-3", providerID: "xai", @@ -3573,23 +3260,8 @@ describe("ProviderTransform.variants", () => { }, }) const result = ProviderTransform.variants(model) - expect(result).toEqual({}) - }) - - test("grok-3-mini returns low and high with reasoningEffort", () => { - const model = createMockModel({ - id: "xai/grok-3-mini", - providerID: "xai", - api: { - id: "grok-3-mini", - url: "https://api.x.ai", - npm: "@ai-sdk/xai", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "high"]) + expect(Object.keys(result)).toEqual(["low", "medium", "high"]) expect(result.low).toEqual({ reasoningEffort: "low" }) - expect(result.high).toEqual({ reasoningEffort: "high" }) }) }) @@ -3628,17 +3300,19 @@ describe("ProviderTransform.variants", () => { expect(result.high).toEqual({ reasoningEffort: "high" }) }) - test("north-mini-code-1-0 returns only none and high", () => { - const model = createMockModel({ - id: "cohere/north-mini-code-1-0", - providerID: "cohere", - api: { - id: "North-Mini-Code-1-0-latest", - url: "https://api.cohere.com/compatibility/v1", - npm: "@ai-sdk/openai-compatible", - }, - }) - const result = ProviderTransform.variants(model) + test("model-specific OpenAI-compatible efforts come from reasoning_options", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "cohere/north-mini-code-1-0", + providerID: "cohere", + api: { + id: "North-Mini-Code-1-0-latest", + url: "https://api.cohere.com/compatibility/v1", + npm: "@ai-sdk/openai-compatible", + }, + reasoning_options: [{ type: "effort", values: ["none", "high"] }], + }), + ) expect(result).toEqual({ none: { reasoningEffort: "none" }, high: { reasoningEffort: "high" }, @@ -3647,21 +3321,7 @@ describe("ProviderTransform.variants", () => { }) describe("@ai-sdk/azure", () => { - test("o1-mini returns empty object", () => { - const model = createMockModel({ - id: "o1-mini", - providerID: "azure", - api: { - id: "o1-mini", - url: "https://azure.com", - npm: "@ai-sdk/azure", - }, - }) - const result = ProviderTransform.variants(model) - expect(result).toEqual({}) - }) - - test("standard azure models return custom efforts with reasoningSummary", () => { + test("falls back to OpenAI-style efforts with reasoningSummary", () => { const model = createMockModel({ id: "o1", providerID: "azure", @@ -3672,67 +3332,17 @@ describe("ProviderTransform.variants", () => { }, }) const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high"]) + expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) expect(result.low).toEqual({ reasoningEffort: "low", reasoningSummary: "auto", include: ["reasoning.encrypted_content"], }) }) - - test("gpt-5 adds minimal effort", () => { - const model = createMockModel({ - id: "gpt-5", - providerID: "azure", - api: { - id: "gpt-5", - url: "https://azure.com", - npm: "@ai-sdk/azure", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["minimal", "low", "medium", "high"]) - }) - - for (const testCase of [ - { id: "gpt-5-1", efforts: ["none", "low", "medium", "high"] }, - { id: "gpt-5-4", efforts: ["none", "low", "medium", "high", "xhigh"] }, - { id: "gpt-5.4", efforts: ["none", "low", "medium", "high", "xhigh"] }, - { id: "gpt-5-5", efforts: ["none", "low", "medium", "high", "xhigh"] }, - ]) { - test(`${testCase.id} returns supported Azure reasoning efforts`, () => { - const result = ProviderTransform.variants( - createMockModel({ - id: testCase.id, - providerID: "azure", - api: { - id: testCase.id, - url: "https://azure.com", - npm: "@ai-sdk/azure", - }, - }), - ) - expect(Object.keys(result)).toEqual(testCase.efforts) - }) - } }) describe("@ai-sdk/openai", () => { - test("gpt-5-pro returns only high effort", () => { - const model = createMockModel({ - id: "gpt-5-pro", - providerID: "openai", - api: { - id: "gpt-5-pro", - url: "https://api.openai.com", - npm: "@ai-sdk/openai", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["high"]) - }) - - test("standard openai models return custom efforts with reasoningSummary", () => { + test("falls back to OpenAI-style efforts with reasoningSummary", () => { const model = createMockModel({ id: "gpt-5", providerID: "openai", @@ -3744,7 +3354,7 @@ describe("ProviderTransform.variants", () => { release_date: "2024-06-01", }) const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["minimal", "low", "medium", "high"]) + expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) expect(result.low).toEqual({ reasoningEffort: "low", reasoningSummary: "auto", @@ -3752,95 +3362,20 @@ describe("ProviderTransform.variants", () => { }) }) - test("models after 2025-11-13 include 'none' effort", () => { - const model = createMockModel({ - id: "gpt-5-nano", - providerID: "openai", - api: { - id: "gpt-5-nano", - url: "https://api.openai.com", - npm: "@ai-sdk/openai", - }, - release_date: "2025-11-14", - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high"]) - }) - - test("models after 2025-12-04 include 'xhigh' effort", () => { - const model = createMockModel({ - id: "openai/gpt-5-reasoning", - providerID: "openai", - api: { - id: "gpt-5-reasoning", - url: "https://api.openai.com", - npm: "@ai-sdk/openai", - }, - release_date: "2025-12-05", - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) - }) - - for (const testCase of [ - { id: "o1", releaseDate: "2024-12-17", efforts: ["low", "medium", "high"] }, - { id: "o1-pro", releaseDate: "2025-03-19", efforts: ["low", "medium", "high"] }, - { id: "o3", releaseDate: "2025-04-16", efforts: ["low", "medium", "high"] }, - { id: "o3-mini", releaseDate: "2025-01-31", efforts: ["low", "medium", "high"] }, - { id: "o3-pro", releaseDate: "2025-06-10", efforts: ["low", "medium", "high"] }, - { id: "o4-mini", releaseDate: "2025-04-16", efforts: ["low", "medium", "high"] }, - { id: "o3-deep-research", releaseDate: "2025-06-26", efforts: ["medium"] }, - { id: "o4-mini-deep-research", releaseDate: "2025-06-26", efforts: ["medium"] }, - { 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) - }) - } - - test("gpt-50 (lookalike) does not get gpt-5 family treatment", () => { - const model = createMockModel({ - id: "gpt-50", - providerID: "openai", - api: { - id: "gpt-50", - url: "https://api.openai.com", - npm: "@ai-sdk/openai", - }, - release_date: "2024-01-01", - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high"]) + test("model-specific OpenAI efforts come from reasoning_options", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "gpt-5-pro", + providerID: "openai", + api: { + id: "gpt-5-pro", + url: "https://api.openai.com", + npm: "@ai-sdk/openai", + }, + reasoning_options: [{ type: "effort", values: ["high"] }], + }), + ) + expect(Object.keys(result)).toEqual(["high"]) }) }) @@ -3857,7 +3392,7 @@ describe("ProviderTransform.variants", () => { release_date: "2026-04-23", }) const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["none", "low", "medium", "high", "xhigh"]) + expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) expect(result.medium).toEqual({ reasoningEffort: "medium", reasoningSummary: "auto", @@ -3878,13 +3413,13 @@ describe("ProviderTransform.variants", () => { name: "sonnet 4.6", apiIds: ["claude-sonnet-4-6", "claude-sonnet-4.6"], efforts: ["low", "medium", "high", "max"], - expectedHigh: { thinking: { type: "adaptive" }, effort: "high" }, + expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, }, { name: "opus 4.6", apiIds: ["claude-opus-4-6", "claude-opus-4.6"], efforts: ["low", "medium", "high", "max"], - expectedHigh: { thinking: { type: "adaptive" }, effort: "high" }, + expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, }, { name: "opus 4.7", @@ -3917,42 +3452,21 @@ describe("ProviderTransform.variants", () => { createMockModel({ id: `anthropic/${apiId}`, providerID: "anthropic", - api: { - id: apiId, - url: "https://api.anthropic.com", - npm: "@ai-sdk/anthropic", - }, - }), - ) + api: { + id: apiId, + url: "https://api.anthropic.com", + npm: "@ai-sdk/anthropic", + }, + reasoning_options: [{ type: "effort", values: testCase.efforts }], + }), + ) expect(Object.keys(result)).toEqual(testCase.efforts) expect(result.high).toEqual(testCase.expectedHigh) }) } } - test("github copilot opus 4.7 returns only medium reasoning effort", () => { - const model = createMockModel({ - id: "claude-opus-4.7", - providerID: "github-copilot", - api: { - id: "claude-opus-4.7", - url: "https://api.githubcopilot.com/v1", - npm: "@ai-sdk/anthropic", - }, - }) - const result = ProviderTransform.variants(model) - expect(result).toEqual({ - medium: { - thinking: { - type: "adaptive", - display: "summarized", - }, - effort: "medium", - }, - }) - }) - - test("returns high and max with thinking config", () => { + test("falls back to adaptive thinking efforts", () => { const model = createMockModel({ id: "anthropic/claude-4", providerID: "anthropic", @@ -3963,18 +3477,10 @@ describe("ProviderTransform.variants", () => { }, }) const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["high", "max"]) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) expect(result.high).toEqual({ - thinking: { - type: "enabled", - budgetTokens: 16000, - }, - }) - expect(result.max).toEqual({ - thinking: { - type: "enabled", - budgetTokens: 31999, - }, + thinking: { type: "adaptive", display: "summarized" }, + effort: "high", }) }) }) @@ -3990,6 +3496,7 @@ describe("ProviderTransform.variants", () => { url: "https://us-central1-aiplatform.googleapis.com", npm: "@ai-sdk/google-vertex/anthropic", }, + reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }], }), ) expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) @@ -4012,6 +3519,7 @@ describe("ProviderTransform.variants", () => { url: "https://us-central1-aiplatform.googleapis.com", npm: "@ai-sdk/google-vertex/anthropic", }, + reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }], }), ) expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) @@ -4035,6 +3543,7 @@ describe("ProviderTransform.variants", () => { url: "https://bedrock.amazonaws.com", npm: "@ai-sdk/amazon-bedrock", }, + reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "max"] }], }) const result = ProviderTransform.variants(model) expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"]) @@ -4042,6 +3551,7 @@ describe("ProviderTransform.variants", () => { reasoningConfig: { type: "adaptive", maxReasoningEffort: "max", + display: "summarized", }, }) }) @@ -4055,6 +3565,7 @@ describe("ProviderTransform.variants", () => { url: "https://bedrock.amazonaws.com", npm: "@ai-sdk/amazon-bedrock", }, + reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }], }) const result = ProviderTransform.variants(model) expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) @@ -4084,6 +3595,7 @@ describe("ProviderTransform.variants", () => { url: "https://bedrock.amazonaws.com", npm: "@ai-sdk/amazon-bedrock", }, + reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }], }), ) expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) @@ -4106,6 +3618,7 @@ describe("ProviderTransform.variants", () => { url: "https://bedrock.amazonaws.com", npm: "@ai-sdk/amazon-bedrock", }, + reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }], }), ) expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) @@ -4198,6 +3711,9 @@ describe("ProviderTransform.variants", () => { url: provider.url, npm: provider.name, }, + reasoning_options: testCase.expectedMax + ? [{ type: "budget_tokens", max: testCase.expectedMax.thinkingConfig.thinkingBudget }] + : [{ type: "effort", values: testCase.efforts }], }), ) expect(Object.keys(result)).toEqual(testCase.efforts) @@ -4263,7 +3779,7 @@ describe("ProviderTransform.variants", () => { }) describe("@jerome-benoit/sap-ai-provider-v2", () => { - const sapModel = (apiId: string, releaseDate = "2024-01-01") => + const sapModel = (apiId: string, reasoning_options?: any[]) => createMockModel({ id: `sap-ai-core/${apiId}`, providerID: "sap-ai-core", @@ -4272,116 +3788,27 @@ describe("ProviderTransform.variants", () => { url: "https://api.ai.sap", npm: "@jerome-benoit/sap-ai-provider-v2", }, - release_date: releaseDate, + reasoning_options, }) - for (const testCase of [ - { - name: "sonnet 4.6", - apiIds: ["anthropic--claude-sonnet-4-6"], - efforts: ["low", "medium", "high", "max"], - thinking: { type: "adaptive" }, - }, - { - name: "opus 4.6", - apiIds: ["anthropic--claude-4.6-opus", "anthropic--claude-4-6-opus"], - efforts: ["low", "medium", "high", "max"], - thinking: { type: "adaptive" }, - }, - { - name: "opus 4.7", - apiIds: ["anthropic--claude-4.7-opus", "anthropic--claude-4-7-opus"], - efforts: ["low", "medium", "high", "xhigh", "max"], - thinking: { type: "adaptive", display: "summarized" }, - }, - { - name: "opus 4.8", - apiIds: ["anthropic--claude-4.8-opus", "anthropic--claude-4-8-opus"], - efforts: ["low", "medium", "high", "xhigh", "max"], - thinking: { type: "adaptive", display: "summarized" }, - }, - { - name: "sonnet 5", - apiIds: ["anthropic--claude-sonnet-5", "anthropic--claude-5-sonnet"], - efforts: ["low", "medium", "high", "xhigh", "max"], - thinking: { type: "adaptive", display: "summarized" }, - }, - ]) { - for (const apiId of testCase.apiIds) { - test(`${testCase.name} ${apiId} returns adaptive thinking variants under modelParams`, () => { - const result = ProviderTransform.variants(sapModel(apiId)) - expect(Object.keys(result)).toEqual(testCase.efforts) - for (const effort of testCase.efforts) { - expect(result[effort]).toEqual({ - modelParams: { - thinking: testCase.thinking, - output_config: { effort }, - }, - }) - } - }) - } - } + test("falls back to harmonized reasoning_effort variants", () => { + const result = ProviderTransform.variants(sapModel("generic-reasoner")) + expect(Object.keys(result)).toEqual(["low", "medium", "high"]) + expect(result.high).toEqual({ modelParams: { reasoning_effort: "high" } }) + }) - for (const apiId of ["anthropic--claude-sonnet-4", "anthropic--claude-4.5-opus"]) { - test(`${apiId} returns budget_tokens variants under modelParams`, () => { - const result = ProviderTransform.variants(sapModel(apiId)) - expect(Object.keys(result)).toEqual(["high", "max"]) - expect(result.high).toEqual({ - modelParams: { thinking: { type: "enabled", budget_tokens: 16000 } }, - }) - expect(result.max).toEqual({ - modelParams: { thinking: { type: "enabled", budget_tokens: 31999 } }, - }) + test("uses reasoning_options for Anthropic adaptive variants", () => { + const result = ProviderTransform.variants( + sapModel("anthropic--claude-sonnet-5", [{ type: "effort", values: ["low", "high", "max"] }]), + ) + expect(Object.keys(result)).toEqual(["low", "high", "max"]) + expect(result.max).toEqual({ + modelParams: { + thinking: { type: "adaptive", display: "summarized" }, + output_config: { effort: "max" }, + }, }) - } - - for (const testCase of [ - { apiId: "gemini-2.5-pro", maxBudget: 32768 }, - { apiId: "gemini-2.5-flash", maxBudget: 24576 }, - ]) { - test(`${testCase.apiId} returns thinkingConfig variants under modelParams`, () => { - const result = ProviderTransform.variants(sapModel(testCase.apiId)) - expect(Object.keys(result)).toEqual(["high", "max"]) - expect(result.high).toEqual({ - modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } }, - }) - expect(result.max).toEqual({ - modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: testCase.maxBudget } }, - }) - }) - } - - for (const testCase of [ - { apiId: "gpt-5", releaseDate: "2025-08-07", efforts: ["minimal", "low", "medium", "high"] }, - { apiId: "gpt-5-mini", releaseDate: "2025-08-07", efforts: ["minimal", "low", "medium", "high"] }, - { apiId: "gpt-5-nano", releaseDate: "2025-08-07", efforts: ["minimal", "low", "medium", "high"] }, - { apiId: "gpt-5.4", releaseDate: "2026-01-15", efforts: ["none", "low", "medium", "high", "xhigh"] }, - { apiId: "azure-openai--o3-mini", releaseDate: "2024-01-01", efforts: ["low", "medium", "high"] }, - ]) { - test(`${testCase.apiId} returns reasoning_effort variants under modelParams`, () => { - const result = ProviderTransform.variants(sapModel(testCase.apiId, testCase.releaseDate)) - expect(Object.keys(result)).toEqual(testCase.efforts) - for (const effort of testCase.efforts) { - expect(result[effort]).toEqual({ modelParams: { reasoning_effort: effort } }) - } - }) - } - - for (const apiId of [ - "gemini-3.1-flash-lite", - "cohere--command-a-reasoning", - "sonar-deep-research", - "aws--llama-opus-4.7-fake", - ]) { - test(`${apiId} falls through to harmonized reasoning_effort fallback`, () => { - const result = ProviderTransform.variants(sapModel(apiId)) - expect(Object.keys(result)).toEqual(["low", "medium", "high"]) - for (const effort of ["low", "medium", "high"]) { - expect(result[effort]).toEqual({ modelParams: { reasoning_effort: effort } }) - } - }) - } + }) }) describe("ai-gateway-provider (cloudflare-ai-gateway)", () => { @@ -4397,20 +3824,14 @@ describe("ProviderTransform.variants", () => { release_date: releaseDate, }) - 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("falls back to widely-supported OAI efforts", () => { + const result = ProviderTransform.variants(cfModel("openai/gpt-future", "2026-03-05")) + expect(result).toEqual({ + low: { reasoningEffort: "low" }, + medium: { reasoningEffort: "medium" }, + high: { reasoningEffort: "high" }, }) - } + }) test("openai gpt-4o (no reasoning) returns empty", () => { const model = cfModel("openai/gpt-4o") @@ -4449,12 +3870,20 @@ describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => { } for (const testCase of [ - { id: "gpt-5-chat-latest", options: { store: false } }, + { + id: "gpt-5-chat-latest", + options: { + store: false, + reasoningEffort: "none", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + }, + }, { id: "gpt-5.1-chat-latest", options: { store: false, - reasoningEffort: "medium", + reasoningEffort: "none", reasoningSummary: "auto", include: ["reasoning.encrypted_content"], }, @@ -4463,7 +3892,7 @@ describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => { id: "gpt-5.2-chat-latest", options: { store: false, - reasoningEffort: "medium", + reasoningEffort: "none", reasoningSummary: "auto", include: ["reasoning.encrypted_content"], }, @@ -4520,14 +3949,14 @@ describe("ProviderTransform.smallOptions - google thinking controls", () => { for (const testCase of [ { id: "gemini-3-pro-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "low" } } }, - { id: "gemini-3-flash-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "minimal" } } }, + { id: "gemini-3-flash-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "low" } } }, { id: "gemini-3.1-flash-image-preview", - options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "minimal" } }, + options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "low" } }, }, - { id: "gemini-3-pro-image-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } } }, - { id: "gemini-2.5-pro", options: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } }, - { id: "gemini-2.5-flash", options: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } }, + { id: "gemini-3-pro-image-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "low" } } }, + { id: "gemini-2.5-pro", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "low" } } }, + { id: "gemini-2.5-flash", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "low" } } }, ]) { test(`${testCase.id} returns supported small thinking options`, () => { expect(ProviderTransform.smallOptions(createGoogleModel(testCase.id))).toEqual(testCase.options)