Browse Source

fix(core): expand reasoning option variants (#36894)

Aiden Cline 1 month ago
parent
commit
7eb7fe0763

+ 43 - 11
packages/core/src/aisdk.ts

@@ -103,7 +103,7 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
 }
 
 function prepareOptions(model: ModelV2.Info, pkg: string) {
-  const projected = mapBodyToProviderOptions(model)
+  const projected = mapBodyToProviderOptions(model, pkg)
   const options: Record<string, any> = {
     name: model.providerID,
     ...(model.settings ?? {}),
@@ -265,7 +265,7 @@ export const locationLayer = Layer.effect(
             cause: new Error(`Unsupported package ${model.package}`),
           })
 
-        const packageName = ProviderV2.packageName(model.package) ?? ""
+        const packageName = ProviderV2.packageName(model.package)
         const options = prepareOptions(model, packageName)
         const sdkKey = cacheKey({
           providerID: model.providerID,
@@ -301,11 +301,17 @@ export const locationLayer = Layer.effect(
 export const defaultLayer = locationLayer
 
 function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
-  const packageName = ProviderV2.packageName(info.package)
-  const projected = mapBodyToProviderOptions(info)
+  const packageName = ProviderV2.packageName(info.package!)
+  const projected = mapBodyToProviderOptions(info, packageName)
   const optionKey = providerOptionKey(packageName, info.providerID)
+  const providerOptions = (() => {
+    if (projected.settings === undefined) return
+    if (packageName === "@ai-sdk/gateway") return gatewayProviderOptions(info.modelID ?? info.id, projected.settings)
+    if (packageName === "@ai-sdk/azure") return { openai: projected.settings, azure: projected.settings }
+    return { [optionKey]: projected.settings }
+  })()
   const route: AnyRoute = {
-    id: `ai-sdk:${ProviderV2.packageName(info.package) ?? "unknown"}`,
+    id: `ai-sdk:${packageName}`,
     provider: ProviderID.make(info.providerID),
     providerMetadataKey: optionKey,
     protocol: "ai-sdk",
@@ -326,7 +332,7 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
               headers: info.headers,
             },
       limits: { context: info.limit.context, output: info.limit.output },
-      providerOptions: projected.settings === undefined ? undefined : { [optionKey]: projected.settings },
+      providerOptions,
     },
     body: {
       schema: Schema.Unknown,
@@ -340,13 +346,35 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
   return Model.make({ id: info.modelID ?? info.id, provider: info.providerID, route })
 }
 
+function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly<Record<string, unknown>>) {
+  const gateway =
+    typeof settings.gateway === "object" && settings.gateway !== null && !Array.isArray(settings.gateway)
+      ? Object.fromEntries(Object.entries(settings.gateway))
+      : undefined
+  const model = Object.fromEntries(Object.entries(settings).filter(([key]) => key !== "gateway"))
+  if (Object.keys(model).length === 0) return gateway === undefined ? undefined : { gateway }
+
+  const separator = modelID.indexOf("/")
+  const prefix = separator > 0 ? modelID.slice(0, separator) : undefined
+  if (prefix)
+    return { ...(gateway === undefined ? {} : { gateway }), [prefix === "amazon" ? "bedrock" : prefix]: model }
+  if (typeof gateway === "object" && gateway !== null && !Array.isArray(gateway))
+    return { gateway: { ...gateway, ...model } }
+  return { gateway: model }
+}
+
 function providerOptionKey(packageName: string | undefined, providerID: ProviderV2.ID) {
   if (packageName === "@ai-sdk/google") return "google"
   if (packageName === "@ai-sdk/google-vertex") return "vertex"
   if (packageName === "@ai-sdk/google-vertex/anthropic") return "anthropic"
-  if (packageName === "@ai-sdk/amazon-bedrock" || packageName === "@ai-sdk/amazon-bedrock/mantle") return "bedrock"
+  if (packageName === "@ai-sdk/amazon-bedrock") return "bedrock"
+  if (packageName === "@ai-sdk/amazon-bedrock/mantle") return "openai"
   if (packageName === "@ai-sdk/azure") return "azure"
+  if (packageName === "@ai-sdk/github-copilot") return "copilot"
+  if (packageName === "@jerome-benoit/sap-ai-provider-v2") return "sap-ai"
+  if (packageName === "@ai-sdk/openai-compatible") return providerID.split(".")[0]
   if (packageName === "@openrouter/ai-sdk-provider") return "openrouter"
+  if (packageName === "ai-gateway-provider") return "openaiCompatible"
   if (packageName?.startsWith("@ai-sdk/")) return packageName.slice("@ai-sdk/".length)
   return providerID
 }
@@ -361,14 +389,18 @@ function requestSettings(settings: Readonly<Record<string, unknown>> | undefined
   return Object.keys(result).length === 0 ? undefined : result
 }
 
-function mapBodyToProviderOptions(model: ModelV2.Info) {
+function mapBodyToProviderOptions(model: ModelV2.Info, packageName: string) {
   const settings = requestSettings(model.settings)
-  if (!Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(model.body?.reasoning))
-    return { settings, body: model.body }
+  const pro = Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(model.body?.reasoning)
+  const forceReasoning =
+    ["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle"].includes(packageName) &&
+    (pro || settings?.reasoningEffort !== undefined || settings?.reasoningSummary !== undefined)
+  const normalized = forceReasoning ? ProviderV2.mergeOverlay(settings, { forceReasoning: true }) : settings
+  if (!pro) return { settings: normalized, body: model.body }
   const body = { ...model.body }
   delete body.reasoning
   return {
-    settings: ProviderV2.mergeOverlay(settings, { reasoningMode: "pro" }),
+    settings: ProviderV2.mergeOverlay(normalized, { reasoningMode: "pro" }),
     body: Object.keys(body).length === 0 ? undefined : body,
   }
 }

+ 263 - 27
packages/core/src/models-dev.ts

@@ -56,7 +56,10 @@ type SourceModel = {
         string,
         {
           readonly cost?: Cost
-          readonly provider?: { readonly body?: ProviderV2.Settings; readonly headers?: Readonly<Record<string, string>> }
+          readonly provider?: {
+            readonly body?: ProviderV2.Settings
+            readonly headers?: Readonly<Record<string, string>>
+          }
         }
       >
     >
@@ -70,7 +73,7 @@ type SourceProvider = {
   readonly name: string
   readonly env: readonly string[]
   readonly id: string
-  readonly npm?: string
+  readonly npm: string
   readonly models: Readonly<Record<string, SourceModel>>
 }
 
@@ -87,7 +90,7 @@ function normalize(input: Record<string, SourceProvider>): readonly Snapshot[] {
     const info = {
       id: providerID,
       name: item.name,
-      package: item.npm ? ProviderV2.aisdk(item.npm) : "",
+      package: ProviderV2.aisdk(item.npm),
       ...(item.api ? { settings: { baseURL: item.api } } : {}),
     } satisfies ProviderV2.Info
     const models: ModelV2.Info[] = []
@@ -185,61 +188,294 @@ function mergeCost(base: ModelV2.Info["cost"], override: SourceModel["cost"] | u
 }
 
 const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
+const OUTPUT_TOKEN_MAX = 32_000
 
 function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable<ModelV2.Info["variants"]> {
   const npm = model.provider?.npm ?? provider.npm
-  const options = model.reasoning_options ?? []
+  const options = model.reasoning_options
+  if (!options?.length) return []
+  const toggle = options.some((option) => option.type === "toggle")
   const effort = options.find((option) => option.type === "effort")
   if (effort?.type === "effort") {
-    return effort.values.flatMap((value) => {
-      const raw: unknown = value
-      const id = raw === null ? "none" : typeof raw === "string" ? raw : undefined
-      if (id === undefined) return []
-      const settings = settingsForEffort(npm, id)
-      return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : []
-    })
+    const off = toggle ? toggleVariants(npm, model.id).filter((variant) => variant.id === "none") : []
+    const variants = [
+      ...off,
+      ...effort.values.flatMap((value) => {
+        const raw: unknown = value
+        const id = typeof raw === "string" && raw !== "null" ? raw : undefined
+        if (id === undefined) return []
+        if (id === "none" && off.length > 0) return []
+        const settings = settingsForEffort(npm, model.id, id)
+        return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : []
+      }),
+    ]
+    return [...new Map(variants.map((variant) => [variant.id, variant])).values()]
   }
   const budget = options.find((option) => option.type === "budget_tokens")
-  if (budget?.type === "budget_tokens") return budgetVariants(npm, budget)
+  if (budget?.type === "budget_tokens")
+    return [
+      ...(toggle ? toggleVariants(npm, model.id).filter((variant) => variant.id === "none") : []),
+      ...budgetVariants(npm, model, budget),
+    ]
+  if (toggle) return toggleVariants(npm, model.id)
   return []
 }
 
-function settingsForEffort(npm: string | undefined, effort: string): ProviderV2.Settings | undefined {
+function settingsForEffort(npm: string, modelID: string, effort: string): ProviderV2.Settings | undefined {
   if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } }
-  if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
-    return { thinking: { type: "adaptive", display: "summarized" }, effort }
+  if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") {
+    if (anthropicManualThinking(modelID)) return { effort }
+    return {
+      thinking: { type: "adaptive", display: "summarized" },
+      effort,
+    }
+  }
   if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
     return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
-  if (npm === "@ai-sdk/azure") return { reasoningEffort: effort }
-  if (npm === "@ai-sdk/openai")
+  if (npm === "@ai-sdk/amazon-bedrock") {
+    if (modelID.includes("anthropic"))
+      return {
+        reasoningConfig: {
+          ...(anthropicManualThinking(modelID) ? {} : { type: "adaptive", display: "summarized" }),
+          maxReasoningEffort: effort,
+        },
+      }
+    return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }
+  }
+  if (npm === "@ai-sdk/gateway") {
+    const upstream = gatewayPackage(modelID)
+    if (upstream) return settingsForEffort(upstream, modelID, effort)
+    return { reasoningEffort: effort }
+  }
+  if (npm === "@ai-sdk/github-copilot") {
+    if (modelID.includes("gemini")) return
+    if (modelID.includes("claude")) return { reasoningEffort: effort }
+    return { reasoningEffort: effort, reasoningSummary: "auto", include: OPENAI_INCLUDE_ENCRYPTED_REASONING }
+  }
+  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/amazon-bedrock/mantle" || npm === "@ai-sdk/azure")
     return { reasoningEffort: effort, reasoningSummary: "auto", include: OPENAI_INCLUDE_ENCRYPTED_REASONING }
-  if (npm === "@ai-sdk/openai-compatible") return { reasoningEffort: effort }
+  if (npm === "@jerome-benoit/sap-ai-provider-v2") {
+    if (modelID.includes("anthropic"))
+      return {
+        modelParams: {
+          additionalModelRequestFields: {
+            ...(anthropicManualThinking(modelID) ? {} : { thinking: { type: "adaptive", display: "summarized" } }),
+            output_config: { effort },
+          },
+        },
+      }
+    if (modelID.includes("gemini"))
+      return { modelParams: { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } } }
+    if (modelID.includes("amazon--nova"))
+      return { modelParams: { additionalModelRequestFields: { output_config: { effort } } } }
+    return { modelParams: { reasoning_effort: effort } }
+  }
+  if (
+    [
+      "@ai-sdk/openai-compatible",
+      "@ai-sdk/xai",
+      "@ai-sdk/mistral",
+      "@ai-sdk/groq",
+      "@ai-sdk/cerebras",
+      "@ai-sdk/deepinfra",
+      "@ai-sdk/togetherai",
+      "venice-ai-sdk-provider",
+      "ai-gateway-provider",
+    ].includes(npm)
+  )
+    return { reasoningEffort: effort }
 }
 
 function budgetVariants(
-  npm: string | undefined,
+  npm: string,
+  model: SourceModel,
   option: Extract<NonNullable<SourceModel["reasoning_options"]>[number], { type: "budget_tokens" }>,
 ): NonNullable<ModelV2.Info["variants"]> {
-  const max = option.max
-  const high =
-    option.max === undefined
-      ? Math.max(option.min ?? 0, 16_000)
-      : Math.min(Math.max(option.min ?? 0, 16_000), option.max)
+  const maximum = Math.min(option.max ?? OUTPUT_TOKEN_MAX - 1, model.limit.output - 1, OUTPUT_TOKEN_MAX - 1)
+  if (maximum <= 0) return []
+  const high = Math.min(Math.max(option.min ?? 0, Math.floor((maximum + 1) / 2)), maximum)
   return [
     { id: "high", budget: high },
-    ...(max === undefined || max === high ? [] : [{ id: "max", budget: max }]),
+    { id: "max", budget: maximum },
   ].flatMap((item) => {
-    const settings = settingsForBudget(npm, item.budget)
+    const settings = settingsForBudget(npm, model.id, item.budget)
     return settings ? [{ id: ModelV2.VariantID.make(item.id), settings }] : []
   })
 }
 
-function settingsForBudget(npm: string | undefined, budget: number): ProviderV2.Settings | undefined {
+function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info["variants"]> {
+  if (npm === "@ai-sdk/gateway") {
+    const upstream = gatewayPackage(modelID)
+    if (upstream) return toggleVariants(upstream, modelID)
+    return [
+      {
+        id: ModelV2.VariantID.make("none"),
+        settings: { reasoning: { enabled: false } },
+      },
+      {
+        id: ModelV2.VariantID.make("thinking"),
+        settings: { reasoning: { enabled: true } },
+      },
+    ]
+  }
+  if (npm === "@openrouter/ai-sdk-provider")
+    return [
+      { id: ModelV2.VariantID.make("none"), settings: { reasoning: { enabled: false } } },
+      { id: ModelV2.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } },
+    ]
+  if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
+    return [
+      { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
+      {
+        id: ModelV2.VariantID.make("thinking"),
+        settings: {
+          thinking: { type: "adaptive", display: "summarized" },
+        },
+      },
+    ]
+  if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
+    return [
+      {
+        id: ModelV2.VariantID.make("none"),
+        settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
+      },
+      {
+        id: ModelV2.VariantID.make("thinking"),
+        settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } },
+      },
+    ]
+  if (npm === "@ai-sdk/amazon-bedrock") {
+    const anthropic = modelID.includes("anthropic")
+    return [
+      {
+        id: ModelV2.VariantID.make("none"),
+        settings: {
+          additionalModelRequestFields: anthropic
+            ? { thinking: { type: "disabled" } }
+            : { reasoningConfig: { type: "disabled" } },
+        },
+      },
+      {
+        id: ModelV2.VariantID.make("thinking"),
+        settings: {
+          additionalModelRequestFields: anthropic
+            ? { thinking: { type: "adaptive", display: "summarized" } }
+            : { reasoningConfig: { type: "enabled" } },
+        },
+      },
+    ]
+  }
+  if (npm === "@ai-sdk/alibaba")
+    return [
+      { id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
+      { id: ModelV2.VariantID.make("thinking"), settings: { enableThinking: true } },
+    ]
+  if (npm === "@ai-sdk/cohere")
+    return [
+      { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
+      { id: ModelV2.VariantID.make("thinking"), settings: { thinking: { type: "enabled" } } },
+    ]
+  if (npm === "@jerome-benoit/sap-ai-provider-v2") {
+    if (modelID.includes("gemini"))
+      return [
+        {
+          id: ModelV2.VariantID.make("none"),
+          settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
+        },
+        {
+          id: ModelV2.VariantID.make("thinking"),
+          settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } },
+        },
+      ]
+    if (modelID.includes("cohere"))
+      return [
+        {
+          id: ModelV2.VariantID.make("none"),
+          settings: { modelParams: { thinking: { type: "disabled" } } },
+        },
+        {
+          id: ModelV2.VariantID.make("thinking"),
+          settings: { modelParams: { thinking: { type: "enabled" } } },
+        },
+      ]
+    if (modelID.includes("amazon--nova"))
+      return [
+        {
+          id: ModelV2.VariantID.make("none"),
+          settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } },
+        },
+        {
+          id: ModelV2.VariantID.make("thinking"),
+          settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "enabled" } } } },
+        },
+      ]
+    if (modelID.includes("anthropic"))
+      return [
+        {
+          id: ModelV2.VariantID.make("none"),
+          settings: {
+            modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } },
+          },
+        },
+        {
+          id: ModelV2.VariantID.make("thinking"),
+          settings: {
+            modelParams: {
+              additionalModelRequestFields: {
+                thinking: { type: "adaptive", display: "summarized" },
+              },
+            },
+          },
+        },
+      ]
+  }
+  return []
+}
+
+function settingsForBudget(npm: string, modelID: string, budget: number): ProviderV2.Settings | undefined {
   if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { max_tokens: budget } }
   if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
     return { thinking: { type: "enabled", budgetTokens: budget } }
   if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
     return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
+  if (npm === "@ai-sdk/amazon-bedrock") return { reasoningConfig: { type: "enabled", budgetTokens: budget } }
+  if (npm === "@ai-sdk/gateway") {
+    const upstream = gatewayPackage(modelID)
+    return upstream ? settingsForBudget(upstream, modelID, budget) : { reasoning: { max_tokens: budget } }
+  }
+  if (npm === "@ai-sdk/cohere") return { thinking: { type: "enabled", tokenBudget: budget } }
+  if (npm === "@ai-sdk/alibaba") return { enableThinking: true, thinkingBudget: budget }
+  if (npm === "@jerome-benoit/sap-ai-provider-v2") {
+    if (modelID.includes("anthropic"))
+      return {
+        modelParams: {
+          additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: budget } },
+        },
+      }
+    if (modelID.includes("gemini"))
+      return { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } } }
+    if (modelID.includes("cohere")) return { modelParams: { thinking: { type: "enabled", token_budget: budget } } }
+  }
+}
+
+function gatewayPackage(modelID: string) {
+  const separator = modelID.indexOf("/")
+  if (separator <= 0) return
+  const prefix = modelID.slice(0, separator)
+  if (prefix === "anthropic") return "@ai-sdk/anthropic"
+  if (prefix === "google") return "@ai-sdk/google"
+  if (prefix === "amazon") return "@ai-sdk/amazon-bedrock"
+  if (prefix === "alibaba") return "@ai-sdk/alibaba"
+}
+
+function anthropicManualThinking(modelID: string) {
+  const familyFirst = /(?:claude-)?(?:opus|sonnet|haiku)-(\d+)(?:[.-](\d+))?/i.exec(modelID)
+  const versionFirst = /claude-(\d+)(?:[.-](\d+))?-(?:opus|sonnet|haiku)/i.exec(modelID)
+  const major = Number(familyFirst?.[1] ?? versionFirst?.[1])
+  const rawMinor = Number(familyFirst?.[2] ?? versionFirst?.[2] ?? 0)
+  if (!Number.isFinite(major)) return false
+  const minor = rawMinor > 9 ? 0 : rawMinor
+  return major < 4 || (major === 4 && minor < 6)
 }
 
 function modeName(model: SourceModel, mode: string) {

+ 5 - 2
packages/core/src/provider.ts

@@ -11,9 +11,12 @@ export const ID = Provider.ID
 export type ID = typeof ID.Type
 
 export const AISDK_PREFIX = "aisdk:"
-export const isAISDK = (value: string | undefined) => value?.startsWith(AISDK_PREFIX) ?? false
+export const isAISDK = (value: string | undefined): value is string => value?.startsWith(AISDK_PREFIX) ?? false
 export const aisdk = (value: string) => (isAISDK(value) ? value : `${AISDK_PREFIX}${value}`)
-export const packageName = (value: string | undefined) => {
+export function packageName(value: string): string
+export function packageName(value: undefined): undefined
+export function packageName(value: string | undefined): string | undefined
+export function packageName(value: string | undefined) {
   if (value === undefined || !isAISDK(value)) return value
   return value.slice(AISDK_PREFIX.length)
 }

+ 99 - 1
packages/core/test/aisdk.test.ts

@@ -86,7 +86,105 @@ it.effect("maps pro reasoning bodies to AI SDK provider options", () =>
     )
 
     expect(body).toBeUndefined()
-    expect(prepared.body.providerOptions).toEqual({ openai: { reasoningMode: "pro" } })
+    expect(prepared.body.providerOptions).toEqual({
+      openai: { forceReasoning: true, reasoningMode: "pro" },
+    })
+  }),
+)
+
+it.effect("maps package-specific AI SDK provider option keys", () =>
+  Effect.gen(function* () {
+    const aisdk = yield* AISDK.Service
+    yield* aisdk.hook.sdk((event) => {
+      event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
+    })
+
+    const cases = [
+      ["@ai-sdk/github-copilot", "copilot", { reasoningEffort: "high" }],
+      ["@ai-sdk/amazon-bedrock/mantle", "openai", { reasoningEffort: "high", forceReasoning: true }],
+      ["@ai-sdk/openai-compatible", "test-provider", { reasoningEffort: "high" }],
+      ["@jerome-benoit/sap-ai-provider-v2", "sap-ai", { reasoningEffort: "high" }],
+      ["ai-gateway-provider", "openaiCompatible", { reasoningEffort: "high" }],
+    ] as const
+    for (const [packageName, key, settings] of cases) {
+      const resolved = yield* aisdk.model(model(packageName, { reasoningEffort: "high" }))
+      const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
+        LLM.request({ model: resolved, prompt: "Hello" }),
+      )
+      expect(prepared.body.providerOptions).toEqual({ [key]: settings })
+    }
+  }),
+)
+
+it.effect("forces reasoning and projects both Azure AI SDK namespaces", () =>
+  Effect.gen(function* () {
+    const aisdk = yield* AISDK.Service
+    yield* aisdk.hook.sdk((event) => {
+      event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
+    })
+
+    const openai = yield* aisdk.model(model("@ai-sdk/openai", { reasoningEffort: "high" }))
+    const openaiPrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
+      LLM.request({ model: openai, prompt: "Hello" }),
+    )
+    expect(openaiPrepared.body.providerOptions).toEqual({
+      openai: { reasoningEffort: "high", forceReasoning: true },
+    })
+
+    const azure = yield* aisdk.model(model("@ai-sdk/azure", { reasoningEffort: "high" }))
+    const azurePrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
+      LLM.request({ model: azure, prompt: "Hello" }),
+    )
+    expect(azurePrepared.body.providerOptions).toEqual({
+      openai: { reasoningEffort: "high", forceReasoning: true },
+      azure: { reasoningEffort: "high", forceReasoning: true },
+    })
+  }),
+)
+
+it.effect("routes AI Gateway model options by upstream prefix", () =>
+  Effect.gen(function* () {
+    const aisdk = yield* AISDK.Service
+    yield* aisdk.hook.sdk((event) => {
+      event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
+    })
+
+    const anthropic = yield* aisdk.model({
+      ...model("@ai-sdk/gateway", {
+        gateway: { order: ["anthropic"] },
+        thinking: { type: "adaptive" },
+      }),
+      modelID: ModelV2.ID.make("anthropic/claude-sonnet-5"),
+    })
+    const anthropicPrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
+      LLM.request({ model: anthropic, prompt: "Hello" }),
+    )
+    expect(anthropicPrepared.body.providerOptions).toEqual({
+      gateway: { order: ["anthropic"] },
+      anthropic: { thinking: { type: "adaptive" } },
+    })
+
+    const bedrock = yield* aisdk.model({
+      ...model("@ai-sdk/gateway", { reasoningConfig: { type: "enabled" } }),
+      modelID: ModelV2.ID.make("amazon/nova-2-lite"),
+    })
+    const bedrockPrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
+      LLM.request({ model: bedrock, prompt: "Hello" }),
+    )
+    expect(bedrockPrepared.body.providerOptions).toEqual({
+      bedrock: { reasoningConfig: { type: "enabled" } },
+    })
+
+    const fallback = yield* aisdk.model({
+      ...model("@ai-sdk/gateway", { reasoningEffort: "high" }),
+      modelID: ModelV2.ID.make("deepseek/deepseek-v4"),
+    })
+    const fallbackPrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
+      LLM.request({ model: fallback, prompt: "Hello" }),
+    )
+    expect(fallbackPrepared.body.providerOptions).toEqual({
+      deepseek: { reasoningEffort: "high" },
+    })
   }),
 )
 

+ 4 - 2
packages/core/test/models.test.ts

@@ -37,6 +37,7 @@ const fixture = {
     id: "acme",
     name: "Acme",
     env: ["ACME_API_KEY"],
+    npm: "@ai-sdk/openai-compatible",
     models: {
       "acme-1": {
         id: "acme-1",
@@ -57,7 +58,7 @@ const fixtureSnapshot = [
     info: {
       id: ProviderV2.ID.make("acme"),
       name: "Acme",
-      package: "",
+      package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
     },
     models: [
       {
@@ -97,6 +98,7 @@ const fixture2 = {
     id: "beta",
     name: "Beta",
     env: ["BETA_API_KEY"],
+    npm: "@ai-sdk/openai-compatible",
     models: {
       "beta-1": {
         id: "beta-1",
@@ -117,7 +119,7 @@ const fixture2Snapshot = [
     info: {
       id: ProviderV2.ID.make("beta"),
       name: "Beta",
-      package: "",
+      package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
     },
     models: [
       {

+ 287 - 4
packages/core/test/plugin/fixtures/models-dev-reasoning.json

@@ -13,7 +13,7 @@
         "attachment": false,
         "reasoning": true,
         "reasoning_options": [
-          { "type": "effort", "values": ["low", "high"] },
+          { "type": "effort", "values": [null, "null", "low", "high"] },
           { "type": "budget_tokens", "min": 1024, "max": 64000 },
           { "type": "toggle" }
         ],
@@ -27,6 +27,11 @@
                 "headers": { "x-mode": "high" },
                 "body": { "service_tier": "priority" }
               }
+            },
+            "pro": {
+              "provider": {
+                "body": { "reasoning": { "mode": "pro" } }
+              }
             }
           }
         }
@@ -49,18 +54,296 @@
         "reasoning_options": [{ "type": "budget_tokens", "min": 1024, "max": 64000 }],
         "temperature": true,
         "tool_call": true,
-        "limit": { "context": 128000, "output": 8192 }
+        "limit": { "context": 128000, "output": 64000 }
       },
       "claude-effort": {
-        "id": "claude-effort",
+        "id": "claude-opus-4.7",
         "name": "Claude Effort",
         "release_date": "2026-01-01",
         "attachment": false,
         "reasoning": true,
-        "reasoning_options": [{ "type": "effort", "values": ["low"] }],
+        "reasoning_options": [{ "type": "toggle" }, { "type": "effort", "values": ["low"] }],
         "temperature": true,
         "tool_call": true,
         "limit": { "context": 128000, "output": 8192 }
+      },
+      "claude-toggle": {
+        "id": "claude-toggle",
+        "name": "Claude Toggle",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 8192 }
+      },
+      "claude-opus-4-5": {
+        "id": "claude-opus-4-5",
+        "name": "Claude Opus 4.5",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [
+          { "type": "effort", "values": ["low", "high"] },
+          { "type": "budget_tokens", "min": 1024 }
+        ],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 8192 }
+      }
+    }
+  },
+  "xai": {
+    "id": "xai",
+    "name": "xAI",
+    "env": ["XAI_API_KEY"],
+    "npm": "@ai-sdk/xai",
+    "models": {
+      "grok-4.5": {
+        "id": "grok-4.5",
+        "name": "Grok 4.5",
+        "release_date": "2026-07-08",
+        "attachment": true,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "effort", "values": ["low", "medium", "high"] }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 500000, "output": 500000 }
+      }
+    }
+  },
+  "opencode-go": {
+    "id": "opencode-go",
+    "name": "OpenCode Go",
+    "env": ["OPENCODE_API_KEY"],
+    "npm": "@ai-sdk/openai-compatible",
+    "models": {
+      "minimax-m3": {
+        "id": "minimax-m3",
+        "name": "MiniMax-M3",
+        "release_date": "2026-05-31",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 1000000, "output": 131072 },
+        "provider": { "npm": "@ai-sdk/anthropic" }
+      }
+    }
+  },
+  "alibaba": {
+    "id": "alibaba",
+    "name": "Alibaba",
+    "env": ["ALIBABA_API_KEY"],
+    "npm": "@ai-sdk/alibaba",
+    "models": {
+      "toggle-only": {
+        "id": "toggle-only",
+        "name": "Toggle Only",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      },
+      "toggle-budget": {
+        "id": "toggle-budget",
+        "name": "Toggle Budget",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }, { "type": "budget_tokens", "max": 16000 }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      }
+    }
+  },
+  "vercel": {
+    "id": "vercel",
+    "name": "Vercel AI Gateway",
+    "env": ["AI_GATEWAY_API_KEY"],
+    "npm": "@ai-sdk/gateway",
+    "models": {
+      "alibaba/qwen-toggle": {
+        "id": "alibaba/qwen-toggle",
+        "name": "Gateway Alibaba Toggle",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }, { "type": "budget_tokens", "max": 16000 }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      },
+      "amazon/nova-2-lite": {
+        "id": "amazon/nova-2-lite",
+        "name": "Gateway Nova 2 Lite",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }, { "type": "effort", "values": ["low", "high"] }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      },
+      "deepseek/deepseek-toggle": {
+        "id": "deepseek/deepseek-toggle",
+        "name": "Gateway DeepSeek Toggle",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }, { "type": "effort", "values": ["low", "high"] }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      }
+    }
+  },
+  "openrouter": {
+    "id": "openrouter",
+    "name": "OpenRouter",
+    "env": ["OPENROUTER_API_KEY"],
+    "npm": "@openrouter/ai-sdk-provider",
+    "models": {
+      "openrouter-toggle": {
+        "id": "openrouter-toggle",
+        "name": "OpenRouter Toggle",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      }
+    }
+  },
+  "google": {
+    "id": "google",
+    "name": "Google",
+    "env": ["GOOGLE_GENERATIVE_AI_API_KEY"],
+    "npm": "@ai-sdk/google",
+    "models": {
+      "gemini-2.5-flash": {
+        "id": "gemini-2.5-flash",
+        "name": "Gemini 2.5 Flash",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }, { "type": "budget_tokens", "min": 0, "max": 16000 }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      }
+    }
+  },
+  "google-vertex": {
+    "id": "google-vertex",
+    "name": "Google Vertex",
+    "env": ["GOOGLE_VERTEX_PROJECT"],
+    "npm": "@ai-sdk/google-vertex",
+    "models": {
+      "gemini-2.5-flash-lite": {
+        "id": "gemini-2.5-flash-lite",
+        "name": "Gemini 2.5 Flash Lite",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }, { "type": "budget_tokens", "min": 512, "max": 16000 }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      }
+    }
+  },
+  "amazon-bedrock": {
+    "id": "amazon-bedrock",
+    "name": "Amazon Bedrock",
+    "env": ["AWS_ACCESS_KEY_ID"],
+    "npm": "@ai-sdk/amazon-bedrock",
+    "models": {
+      "amazon.nova-2-lite-v1:0": {
+        "id": "amazon.nova-2-lite-v1:0",
+        "name": "Nova 2 Lite",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }, { "type": "effort", "values": ["low", "high"] }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      }
+    }
+  },
+  "sap-ai-core": {
+    "id": "sap-ai-core",
+    "name": "SAP AI Core",
+    "env": ["AICORE_SERVICE_KEY"],
+    "npm": "@jerome-benoit/sap-ai-provider-v2",
+    "models": {
+      "gemini-2.5-flash": {
+        "id": "gemini-2.5-flash",
+        "name": "Gemini 2.5 Flash",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }, { "type": "budget_tokens", "min": 0, "max": 16000 }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      },
+      "amazon--nova-lite": {
+        "id": "amazon--nova-lite",
+        "name": "Nova Lite",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "toggle" }, { "type": "effort", "values": ["low", "high"] }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      },
+      "cohere--command-a-reasoning": {
+        "id": "cohere--command-a-reasoning",
+        "name": "Command A Reasoning",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [
+          { "type": "toggle" },
+          { "type": "effort", "values": ["low", "high"] },
+          { "type": "budget_tokens", "min": 1 }
+        ],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      },
+      "anthropic--claude-4.7-opus": {
+        "id": "anthropic--claude-4.7-opus",
+        "name": "Claude 4.7 Opus",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "effort", "values": ["low"] }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
+      },
+      "anthropic--claude-4-sonnet": {
+        "id": "anthropic--claude-4-sonnet",
+        "name": "Claude 4 Sonnet",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "budget_tokens", "min": 1024, "max": 16000 }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 20000 }
       }
     }
   }

+ 287 - 6
packages/core/test/plugin/models-dev.test.ts

@@ -292,6 +292,12 @@ describe("ModelsDevPlugin", () => {
             ModelV2.VariantID.make("high"),
           ])
 
+          const pro = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-pro"))
+          expect(pro).toMatchObject({
+            id: "gpt-reasoning-pro",
+            body: { reasoning: { mode: "pro" } },
+          })
+
           const budgetModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-budget"))
           expect(budgetModel?.variants).toContainEqual({
             id: ModelV2.VariantID.make("high"),
@@ -299,17 +305,292 @@ describe("ModelsDevPlugin", () => {
           })
           expect(budgetModel?.variants).toContainEqual({
             id: ModelV2.VariantID.make("max"),
-            settings: { thinking: { type: "enabled", budgetTokens: 64000 } },
+            settings: { thinking: { type: "enabled", budgetTokens: 31999 } },
           })
 
           const anthropicEffortModel = yield* catalog.model.get(
             ProviderV2.ID.anthropic,
-            ModelV2.ID.make("claude-effort"),
+            ModelV2.ID.make("claude-opus-4.7"),
           )
-          expect(anthropicEffortModel?.variants).toContainEqual({
-            id: ModelV2.VariantID.make("low"),
-            settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
-          })
+          expect(anthropicEffortModel?.variants).toEqual([
+            { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
+            {
+              id: ModelV2.VariantID.make("low"),
+              settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
+            },
+          ])
+
+          const anthropicToggleModel = yield* catalog.model.get(
+            ProviderV2.ID.anthropic,
+            ModelV2.ID.make("claude-toggle"),
+          )
+          expect(anthropicToggleModel?.variants).toEqual([
+            { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
+            {
+              id: ModelV2.VariantID.make("thinking"),
+              settings: { thinking: { type: "adaptive", display: "summarized" } },
+            },
+          ])
+
+          const opus45 = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-opus-4-5"))
+          expect(opus45?.variants).toEqual([
+            { id: ModelV2.VariantID.make("low"), settings: { effort: "low" } },
+            { id: ModelV2.VariantID.make("high"), settings: { effort: "high" } },
+          ])
+
+          const grok = yield* catalog.model.get(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4.5"))
+          expect(grok?.variants).toEqual(
+            ["low", "medium", "high"].map((id) => ({
+              id: ModelV2.VariantID.make(id),
+              settings: { reasoningEffort: id },
+            })),
+          )
+
+          const minimax = yield* catalog.model.get(ProviderV2.ID.make("opencode-go"), ModelV2.ID.make("minimax-m3"))
+          expect(minimax?.variants).toEqual([
+            { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
+            {
+              id: ModelV2.VariantID.make("thinking"),
+              settings: { thinking: { type: "adaptive", display: "summarized" } },
+            },
+          ])
+
+          const toggle = yield* catalog.model.get(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("toggle-only"))
+          expect(toggle?.variants).toEqual([
+            { id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
+            { id: ModelV2.VariantID.make("thinking"), settings: { enableThinking: true } },
+          ])
+
+          const combined = yield* catalog.model.get(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("toggle-budget"))
+          expect(combined?.variants).toEqual([
+            { id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
+            {
+              id: ModelV2.VariantID.make("high"),
+              settings: { enableThinking: true, thinkingBudget: 8000 },
+            },
+            {
+              id: ModelV2.VariantID.make("max"),
+              settings: { enableThinking: true, thinkingBudget: 16000 },
+            },
+          ])
+
+          const gateway = yield* catalog.model.get(ProviderV2.ID.make("vercel"), ModelV2.ID.make("alibaba/qwen-toggle"))
+          expect(gateway?.variants).toEqual([
+            { id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } },
+            {
+              id: ModelV2.VariantID.make("high"),
+              settings: { enableThinking: true, thinkingBudget: 8000 },
+            },
+            {
+              id: ModelV2.VariantID.make("max"),
+              settings: { enableThinking: true, thinkingBudget: 16000 },
+            },
+          ])
+
+          const gatewayNova = yield* catalog.model.get(
+            ProviderV2.ID.make("vercel"),
+            ModelV2.ID.make("amazon/nova-2-lite"),
+          )
+          expect(gatewayNova?.variants).toEqual([
+            {
+              id: ModelV2.VariantID.make("none"),
+              settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
+            },
+            {
+              id: ModelV2.VariantID.make("low"),
+              settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
+            },
+            {
+              id: ModelV2.VariantID.make("high"),
+              settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
+            },
+          ])
+
+          const gatewayFallback = yield* catalog.model.get(
+            ProviderV2.ID.make("vercel"),
+            ModelV2.ID.make("deepseek/deepseek-toggle"),
+          )
+          expect(gatewayFallback?.variants).toEqual([
+            {
+              id: ModelV2.VariantID.make("none"),
+              settings: { reasoning: { enabled: false } },
+            },
+            {
+              id: ModelV2.VariantID.make("low"),
+              settings: { reasoningEffort: "low" },
+            },
+            {
+              id: ModelV2.VariantID.make("high"),
+              settings: { reasoningEffort: "high" },
+            },
+          ])
+
+          const openrouter = yield* catalog.model.get(
+            ProviderV2.ID.make("openrouter"),
+            ModelV2.ID.make("openrouter-toggle"),
+          )
+          expect(openrouter?.variants).toEqual([
+            { id: ModelV2.VariantID.make("none"), settings: { reasoning: { enabled: false } } },
+            { id: ModelV2.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } },
+          ])
+
+          const google = yield* catalog.model.get(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini-2.5-flash"))
+          expect(google?.variants).toEqual([
+            {
+              id: ModelV2.VariantID.make("none"),
+              settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
+            },
+            {
+              id: ModelV2.VariantID.make("high"),
+              settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } },
+            },
+            {
+              id: ModelV2.VariantID.make("max"),
+              settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
+            },
+          ])
+
+          const vertex = yield* catalog.model.get(
+            ProviderV2.ID.make("google-vertex"),
+            ModelV2.ID.make("gemini-2.5-flash-lite"),
+          )
+          expect(vertex?.variants).toEqual([
+            {
+              id: ModelV2.VariantID.make("none"),
+              settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
+            },
+            {
+              id: ModelV2.VariantID.make("high"),
+              settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } },
+            },
+            {
+              id: ModelV2.VariantID.make("max"),
+              settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
+            },
+          ])
+
+          const bedrock = yield* catalog.model.get(
+            ProviderV2.ID.make("amazon-bedrock"),
+            ModelV2.ID.make("amazon.nova-2-lite-v1:0"),
+          )
+          expect(bedrock?.variants).toEqual([
+            {
+              id: ModelV2.VariantID.make("none"),
+              settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
+            },
+            {
+              id: ModelV2.VariantID.make("low"),
+              settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
+            },
+            {
+              id: ModelV2.VariantID.make("high"),
+              settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
+            },
+          ])
+
+          const sapGemini = yield* catalog.model.get(
+            ProviderV2.ID.make("sap-ai-core"),
+            ModelV2.ID.make("gemini-2.5-flash"),
+          )
+          expect(sapGemini?.variants).toEqual([
+            {
+              id: ModelV2.VariantID.make("none"),
+              settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
+            },
+            {
+              id: ModelV2.VariantID.make("high"),
+              settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } } },
+            },
+            {
+              id: ModelV2.VariantID.make("max"),
+              settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } },
+            },
+          ])
+
+          const sapNova = yield* catalog.model.get(
+            ProviderV2.ID.make("sap-ai-core"),
+            ModelV2.ID.make("amazon--nova-lite"),
+          )
+          expect(sapNova?.variants).toEqual([
+            {
+              id: ModelV2.VariantID.make("none"),
+              settings: {
+                modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } },
+              },
+            },
+            {
+              id: ModelV2.VariantID.make("low"),
+              settings: {
+                modelParams: { additionalModelRequestFields: { output_config: { effort: "low" } } },
+              },
+            },
+            {
+              id: ModelV2.VariantID.make("high"),
+              settings: {
+                modelParams: { additionalModelRequestFields: { output_config: { effort: "high" } } },
+              },
+            },
+          ])
+
+          const sapCohere = yield* catalog.model.get(
+            ProviderV2.ID.make("sap-ai-core"),
+            ModelV2.ID.make("cohere--command-a-reasoning"),
+          )
+          expect(sapCohere?.variants).toEqual([
+            {
+              id: ModelV2.VariantID.make("none"),
+              settings: { modelParams: { thinking: { type: "disabled" } } },
+            },
+            {
+              id: ModelV2.VariantID.make("low"),
+              settings: { modelParams: { reasoning_effort: "low" } },
+            },
+            {
+              id: ModelV2.VariantID.make("high"),
+              settings: { modelParams: { reasoning_effort: "high" } },
+            },
+          ])
+
+          const sapAnthropicEffort = yield* catalog.model.get(
+            ProviderV2.ID.make("sap-ai-core"),
+            ModelV2.ID.make("anthropic--claude-4.7-opus"),
+          )
+          expect(sapAnthropicEffort?.variants).toEqual([
+            {
+              id: ModelV2.VariantID.make("low"),
+              settings: {
+                modelParams: {
+                  additionalModelRequestFields: {
+                    thinking: { type: "adaptive", display: "summarized" },
+                    output_config: { effort: "low" },
+                  },
+                },
+              },
+            },
+          ])
+
+          const sapAnthropicBudget = yield* catalog.model.get(
+            ProviderV2.ID.make("sap-ai-core"),
+            ModelV2.ID.make("anthropic--claude-4-sonnet"),
+          )
+          expect(sapAnthropicBudget?.variants).toEqual([
+            {
+              id: ModelV2.VariantID.make("high"),
+              settings: {
+                modelParams: {
+                  additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 8000 } },
+                },
+              },
+            },
+            {
+              id: ModelV2.VariantID.make("max"),
+              settings: {
+                modelParams: {
+                  additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 16000 } },
+                },
+              },
+            },
+          ])
         }).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
       (previous) =>
         Effect.sync(() => {