feat(provider): use models.dev reasoning options
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ModelsDev.Model["modalities"]>["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<string, Record<string, any>>): Re
|
||||
return Object.fromEntries(Object.entries(variants).map(([k, v]) => [k, { modelParams: v }]))
|
||||
}
|
||||
|
||||
function googleThinkingVariants(model: Provider.Model): Record<string, Record<string, any>> {
|
||||
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<Provider.Model["reasoning_options"]>[number]
|
||||
type ReasoningEffortOption = Extract<ReasoningOption, { type: "effort" }>
|
||||
type ReasoningBudgetOption = Extract<ReasoningOption, { type: "budget_tokens" }>
|
||||
type ReasoningEffortValue = ReasoningEffortOption["values"][number]
|
||||
|
||||
function reasoningOption<T extends ReasoningOption["type"]>(
|
||||
model: Provider.Model,
|
||||
type: T,
|
||||
): Extract<ReasoningOption, { type: T }> | undefined {
|
||||
return model.reasoning_options?.find((option): option is Extract<ReasoningOption, { type: T }> => option.type === type)
|
||||
}
|
||||
|
||||
function reasoningBudgetVariants(
|
||||
budget: ReasoningBudgetOption,
|
||||
make: (budgetTokens: number) => Record<string, any>,
|
||||
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<string, Record<string, any>> | 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<string, any>][] => {
|
||||
// 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<string, Record<string, any>> {
|
||||
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<string, Record<string, a
|
||||
thinking: { thinking: { type: "adaptive" } },
|
||||
}
|
||||
}
|
||||
const adaptiveThinkingOmitted = anthropicOmitsThinking(model.api.id)
|
||||
const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
|
||||
if (glm52 && model.api.npm === "@openrouter/ai-sdk-provider") {
|
||||
// OpenRouter maps xhigh to GLM-5.2's native max effort.
|
||||
return {
|
||||
high: { reasoning: { effort: "high" } },
|
||||
xhigh: { reasoning: { effort: "xhigh" } },
|
||||
}
|
||||
}
|
||||
if (glm52 && model.api.npm === "@ai-sdk/openai-compatible") {
|
||||
return {
|
||||
high: { reasoningEffort: "high" },
|
||||
max: { reasoningEffort: "max" },
|
||||
}
|
||||
}
|
||||
if (glm52 && model.api.npm === "@ai-sdk/anthropic") {
|
||||
return {
|
||||
high: { effort: "high" },
|
||||
max: { effort: "max" },
|
||||
}
|
||||
}
|
||||
if (
|
||||
id.includes("deepseek-chat") ||
|
||||
id.includes("deepseek-reasoner") ||
|
||||
id.includes("deepseek-r1") ||
|
||||
id.includes("deepseek-v3") ||
|
||||
id.includes("minimax") ||
|
||||
(id.includes("glm") && !glm52) ||
|
||||
id.includes("kimi") ||
|
||||
id.includes("k2p") ||
|
||||
id.includes("qwen") ||
|
||||
id.includes("big-pickle")
|
||||
)
|
||||
return {}
|
||||
|
||||
// see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks
|
||||
if (id.includes("grok") && id.includes("grok-3-mini")) {
|
||||
if (model.api.npm === "@openrouter/ai-sdk-provider") {
|
||||
return {
|
||||
low: { reasoning: { effort: "low" } },
|
||||
high: { reasoning: { effort: "high" } },
|
||||
}
|
||||
}
|
||||
return {
|
||||
low: { reasoningEffort: "low" },
|
||||
high: { reasoningEffort: "high" },
|
||||
}
|
||||
}
|
||||
if (id.includes("grok")) return {}
|
||||
const fromReasoningOptions = reasoningOptionVariants(model)
|
||||
if (fromReasoningOptions) return fromReasoningOptions
|
||||
|
||||
switch (model.api.npm) {
|
||||
case "@openrouter/ai-sdk-provider":
|
||||
return Object.fromEntries(
|
||||
(model.api.id.startsWith("openai/") || id.includes("gpt")
|
||||
? openaiCompatibleReasoningEfforts(model.api.id)
|
||||
: WIDELY_SUPPORTED_EFFORTS
|
||||
).map((effort) => [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<string, Record<string, a
|
||||
case "venice-ai-sdk-provider":
|
||||
// https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
|
||||
case "@ai-sdk/openai-compatible":
|
||||
if (model.api.id.toLowerCase().includes("north-mini-code")) {
|
||||
return Object.fromEntries(["none", "high"].map((effort) => [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<string, Record<string, a
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
|
||||
case "@ai-sdk/google":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
|
||||
return googleThinkingVariants(model)
|
||||
return googleThinkingLevelVariants()
|
||||
|
||||
case "@ai-sdk/mistral":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
|
||||
// https://docs.mistral.ai/capabilities/reasoning/adjustable
|
||||
if (!model.capabilities.reasoning) return {}
|
||||
// Only Mistral Small 4 and Medium 3.5 support reasoning
|
||||
const MISTRAL_REASONING_IDS = [
|
||||
"mistral-small-2603",
|
||||
"mistral-small-latest",
|
||||
"mistral-medium-3.5",
|
||||
"mistral-medium-2604",
|
||||
]
|
||||
const mistralId = model.api.id.toLowerCase()
|
||||
if (!MISTRAL_REASONING_IDS.some((id) => mistralId.includes(id))) return {}
|
||||
return {
|
||||
high: { reasoningEffort: "high" },
|
||||
}
|
||||
@@ -1034,33 +921,12 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
return {}
|
||||
|
||||
case "@jerome-benoit/sap-ai-provider-v2": {
|
||||
if (id.includes("anthropic")) {
|
||||
if (adaptiveEfforts) {
|
||||
// Bedrock adaptive splits `effort` out into `output_config` (vs Anthropic
|
||||
// native which inlines it). Opus 4.7+ flipped `display` default to "omitted".
|
||||
return wrapInSapModelParams(
|
||||
Object.fromEntries(
|
||||
adaptiveEfforts.map((effort) => [
|
||||
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 }])),
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user