Prechádzať zdrojové kódy

fix(provider): derive variants from reasoning metadata (#36624)

Aiden Cline 1 mesiac pred
rodič
commit
a8062ea314

+ 16 - 3
packages/core/src/models-dev.ts

@@ -44,6 +44,21 @@ const Cost = Schema.Struct({
   ),
 })
 
+const ReasoningOption = Schema.Union([
+  Schema.Struct({
+    type: Schema.Literal("effort"),
+    values: Schema.Array(Schema.NullOr(Schema.String)),
+  }),
+  Schema.Struct({
+    type: Schema.Literal("toggle"),
+  }),
+  Schema.Struct({
+    type: Schema.Literal("budget_tokens"),
+    min: Schema.optional(Schema.Finite),
+    max: Schema.optional(Schema.Finite),
+  }),
+])
+
 export const Model = Schema.Struct({
   id: Schema.String,
   name: Schema.String,
@@ -51,11 +66,9 @@ export const Model = Schema.Struct({
   release_date: Schema.String,
   attachment: Schema.Boolean,
   reasoning: Schema.Boolean,
-  // models.dev is external metadata and reasoning controls are expected to evolve.
-  // Provider normalization extracts the subset understood by this client.
-  reasoning_options: Schema.optional(Schema.Unknown),
   temperature: Schema.Boolean,
   tool_call: Schema.Boolean,
+  reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
   interleaved: Schema.optional(
     Schema.Union([
       Schema.Literal(true),

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

@@ -1,5 +1,5 @@
-import { describe, expect, beforeAll, beforeEach, afterAll, test } from "bun:test"
-import { Effect, Layer, Ref, Schema } from "effect"
+import { describe, expect, beforeAll, beforeEach, afterAll } from "bun:test"
+import { Effect, Layer, Ref } from "effect"
 import { HttpClient, HttpClientResponse } from "effect/unstable/http"
 import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
@@ -126,21 +126,6 @@ const initialState: MockState = {
   calls: [],
 }
 
-test("models.dev model schema keeps reasoning options permissive", () => {
-  const model = Schema.decodeUnknownSync(ModelsDev.Model)({
-    id: "acme-1",
-    name: "Acme One",
-    release_date: "2026-01-01",
-    attachment: false,
-    reasoning: true,
-    reasoning_options: [{ type: "future_control", value: { nested: true } }, "future-shape"],
-    temperature: true,
-    tool_call: true,
-    limit: { context: 128000, output: 8192 },
-  })
-  expect(model.reasoning_options).toEqual([{ type: "future_control", value: { nested: true } }, "future-shape"])
-})
-
 describe("ModelsDev Service", () => {
   it.live("get() returns providers from disk when cache file exists", () =>
     Effect.gen(function* () {

+ 9 - 51
packages/opencode/src/provider/provider.ts

@@ -981,21 +981,6 @@ const ProviderInterleaved = Schema.Union([
   }),
 ])
 
-const ProviderReasoningOption = Schema.Union([
-  Schema.Struct({
-    type: Schema.Literal("effort"),
-    values: Schema.Array(Schema.NullOr(Schema.String)),
-  }),
-  Schema.Struct({
-    type: Schema.Literal("toggle"),
-  }),
-  Schema.Struct({
-    type: Schema.Literal("budget_tokens"),
-    min: optional(Schema.Finite),
-    max: optional(Schema.Finite),
-  }),
-])
-
 const ProviderCapabilities = Schema.Struct({
   temperature: Schema.Boolean,
   reasoning: Schema.Boolean,
@@ -1054,7 +1039,6 @@ export const Model = Schema.Struct({
   options: Schema.Record(Schema.String, Schema.Any),
   headers: Schema.Record(Schema.String, Schema.String),
   release_date: Schema.String,
-  reasoning_options: optional(Schema.Array(ProviderReasoningOption)),
   variants: optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))),
 }).annotate({ identifier: "Model" })
 export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
@@ -1218,36 +1202,6 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
   return result
 }
 
-type ReasoningOption = NonNullable<Model["reasoning_options"]>[number]
-
-function reasoningOptions(input: unknown): Model["reasoning_options"] {
-  if (!Array.isArray(input)) return []
-  return input.flatMap((option) => {
-    const normalized = normalizeReasoningOption(option)
-    return normalized ? [normalized] : []
-  })
-}
-
-function normalizeReasoningOption(option: unknown): ReasoningOption | undefined {
-  if (!isRecord(option)) return
-  if (option.type === "effort") {
-    if (!Array.isArray(option.values)) return
-    return {
-      type: "effort",
-      values: option.values.filter((value): value is string | null => value === null || typeof value === "string"),
-    }
-  }
-  if (option.type === "toggle") return { type: "toggle" }
-  if (option.type !== "budget_tokens") return
-  const min = typeof option.min === "number" && Number.isFinite(option.min) ? option.min : undefined
-  const max = typeof option.max === "number" && Number.isFinite(option.max) ? option.max : undefined
-  return {
-    type: "budget_tokens",
-    ...(min === undefined ? {} : { min }),
-    ...(max === undefined ? {} : { max }),
-  }
-}
-
 function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
   const base: Model = {
     id: ModelV2.ID.make(model.id),
@@ -1290,13 +1244,14 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
       interleaved: model.interleaved ?? false,
     },
     release_date: model.release_date ?? "",
-    reasoning_options: reasoningOptions(model.reasoning_options),
     variants: {},
   }
 
+  const variants = ProviderTransform.reasoningVariants(model, base) ?? ProviderTransform.variants(base)
+
   return {
     ...base,
-    variants: mapValues(ProviderTransform.variants(base), (v) => v),
+    variants: mapValues(variants, (v) => v),
   }
 }
 
@@ -1537,10 +1492,13 @@ const layer = Layer.effect(
               headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}),
               family: model.family ?? existingModel?.family ?? "",
               release_date: model.release_date ?? existingModel?.release_date ?? "",
-              reasoning_options: existingModel?.reasoning_options,
               variants: {},
             }
-            const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {})
+            const variants =
+              existingModel?.api.npm === parsedModel.api.npm
+                ? (existingModel.variants ?? ProviderTransform.variants(parsedModel))
+                : ProviderTransform.variants(parsedModel)
+            const merged = mergeDeep(variants, model.variants ?? {})
             parsedModel.variants = mapValues(
               pickBy(merged, (v) => !v.disabled),
               (v) => omit(v, ["disabled"]),
@@ -1668,7 +1626,7 @@ const layer = Layer.effect(
             )
               delete provider.models[modelID]
 
-            if (!model.variants || Object.keys(model.variants).length === 0) {
+            if (model.variants === undefined) {
               model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
             }
 

+ 185 - 0
packages/opencode/src/provider/transform.ts

@@ -1578,4 +1578,189 @@ export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7
   return schema
 }
 
+export function reasoningVariants(model: ModelsDev.Model, target: Provider.Model): Provider.Model["variants"] {
+  const options = model.reasoning_options
+  if (options === undefined) return
+  if (options.length === 0) return {}
+
+  const effort = options.find((option) => option.type === "effort")
+  if (effort) return nonEmptyVariants(effortVariants(target, effort.values))
+
+  const toggle = options.some((option) => option.type === "toggle")
+  const budget = options.find((option) => option.type === "budget_tokens")
+  if (!budget) return toggle ? nonEmptyVariants(reasoningToggle(target)) : undefined
+
+  return nonEmptyVariants({
+    ...(toggle ? reasoningToggle(target) : {}),
+    ...budgetVariants(target, budget.min, budget.max),
+  })
+}
+
+function effortVariants(model: Provider.Model, values: readonly unknown[]) {
+  return Object.fromEntries(
+    values.flatMap((value) => {
+      const id = (() => {
+        if (value === null) return "none"
+        if (typeof value === "string") return value
+      })()
+      if (id === undefined) return []
+      const settings = reasoningEffort(model, id)
+      return settings ? [[id, settings]] : []
+    }),
+  )
+}
+
+function budgetVariants(model: Provider.Model, min?: number, max?: number) {
+  const limit = model.limit.output - 1
+  if (limit <= 0) return {}
+  const high = Math.min(
+    max === undefined ? Math.max(min ?? 0, 16_000) : Math.min(Math.max(min ?? 0, 16_000), max),
+    limit,
+  )
+  const maximum = max === undefined ? undefined : Math.min(max, limit)
+  return Object.fromEntries(
+    [
+      { id: "high", budget: high },
+      ...(maximum === undefined || maximum === high ? [] : [{ id: "max", budget: maximum }]),
+    ].flatMap((item) => {
+      const settings = reasoningBudget(model, item.budget)
+      return settings ? [[item.id, settings]] : []
+    }),
+  )
+}
+
+function nonEmptyVariants(variants: NonNullable<Provider.Model["variants"]>): Provider.Model["variants"] {
+  return Object.keys(variants).length > 0 ? variants : undefined
+}
+
+function reasoningToggle(model: Provider.Model): NonNullable<Provider.Model["variants"]> {
+  if (model.api.npm === "@ai-sdk/alibaba")
+    return {
+      none: { enableThinking: false },
+      high: { enableThinking: true },
+    }
+  if (model.api.npm === "@ai-sdk/cohere")
+    return {
+      none: { thinking: { type: "disabled" } },
+      high: { thinking: { type: "enabled" } },
+    }
+  return {}
+}
+
+function reasoningEffort(model: Provider.Model, effort: string) {
+  switch (model.api.npm) {
+    case "@openrouter/ai-sdk-provider":
+      return { reasoning: { effort } }
+    case "@ai-sdk/anthropic":
+    case "@ai-sdk/google-vertex/anthropic":
+      return anthropicEffort(model, effort)
+    case "@ai-sdk/google":
+    case "@ai-sdk/google-vertex":
+      return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
+    case "@ai-sdk/amazon-bedrock":
+      if (anthropicAdaptiveEfforts(model.api.id))
+        return {
+          reasoningConfig: {
+            type: "adaptive",
+            maxReasoningEffort: effort,
+            ...(anthropicOmitsThinking(model.api.id) ? { display: "summarized" } : {}),
+          },
+        }
+      if (model.api.id.includes("anthropic")) return
+      return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }
+    case "@ai-sdk/gateway":
+      if (model.id.includes("anthropic")) return { thinking: { type: "adaptive", display: "summarized" }, effort }
+      if (model.id.includes("google")) return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
+      return { reasoningEffort: effort }
+    case "@ai-sdk/github-copilot":
+      // OAuth discovery replaces these with variants from Copilot's /models capabilities.
+      if (model.id.includes("gemini")) return
+      if (model.id.includes("claude")) return { reasoningEffort: effort }
+      return { reasoningEffort: effort, reasoningSummary: "auto", include: INCLUDE_ENCRYPTED_REASONING }
+    case "@ai-sdk/openai":
+    case "@ai-sdk/amazon-bedrock/mantle":
+      return { reasoningEffort: effort, reasoningSummary: "auto", include: INCLUDE_ENCRYPTED_REASONING }
+    case "@ai-sdk/azure":
+      return { reasoningEffort: effort, reasoningSummary: "auto", include: INCLUDE_ENCRYPTED_REASONING }
+    case "@jerome-benoit/sap-ai-provider-v2":
+      if (model.id.includes("anthropic"))
+        return { modelParams: { thinking: { type: "adaptive", display: "summarized" }, output_config: { effort } } }
+      return { modelParams: { reasoning_effort: effort } }
+    case "@ai-sdk/openai-compatible":
+    case "@ai-sdk/xai":
+    case "@ai-sdk/mistral":
+    case "@ai-sdk/groq":
+    case "@ai-sdk/cerebras":
+    case "@ai-sdk/deepinfra":
+    case "@ai-sdk/togetherai":
+    case "venice-ai-sdk-provider":
+    case "ai-gateway-provider":
+      return { reasoningEffort: effort }
+    case "@ai-sdk/cohere":
+    case "@ai-sdk/perplexity":
+    case "@ai-sdk/vercel":
+    case "@ai-sdk/alibaba":
+    case "gitlab-ai-provider":
+      return
+  }
+}
+
+function anthropicEffort(model: Provider.Model, effort: string) {
+  if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) return { effort }
+  if (!anthropicAdaptiveEfforts(model.api.id)) return
+  return {
+    thinking: {
+      type: "adaptive",
+      ...(anthropicOmitsThinking(model.api.id) ? { display: "summarized" } : {}),
+    },
+    effort,
+  }
+}
+
+function reasoningBudget(model: Provider.Model, budget: number) {
+  switch (model.api.npm) {
+    case "@openrouter/ai-sdk-provider":
+      return { reasoning: { max_tokens: budget } }
+    case "@ai-sdk/anthropic":
+    case "@ai-sdk/google-vertex/anthropic":
+      return { thinking: { type: "enabled", budgetTokens: budget } }
+    case "@ai-sdk/google":
+    case "@ai-sdk/google-vertex":
+      return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
+    case "@ai-sdk/amazon-bedrock":
+      return { reasoningConfig: { type: "enabled", budgetTokens: budget } }
+    case "@ai-sdk/gateway":
+      if (model.id.includes("anthropic")) return { thinking: { type: "enabled", budgetTokens: budget } }
+      if (model.id.includes("google")) return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
+      return
+    case "@ai-sdk/cohere":
+      return { thinking: { type: "enabled", tokenBudget: budget } }
+    case "@ai-sdk/alibaba":
+      return { enableThinking: true, thinkingBudget: budget }
+    case "@jerome-benoit/sap-ai-provider-v2":
+      if (model.id.includes("anthropic"))
+        return { modelParams: { thinking: { type: "enabled", budget_tokens: budget } } }
+      if (model.id.includes("gemini"))
+        return { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } } }
+      return
+    case "@ai-sdk/amazon-bedrock/mantle":
+    case "@ai-sdk/azure":
+    case "@ai-sdk/cerebras":
+    case "@ai-sdk/deepinfra":
+    case "@ai-sdk/github-copilot":
+    case "@ai-sdk/groq":
+    case "@ai-sdk/mistral":
+    case "@ai-sdk/openai":
+    case "@ai-sdk/openai-compatible":
+    case "@ai-sdk/perplexity":
+    case "@ai-sdk/togetherai":
+    case "@ai-sdk/vercel":
+    case "@ai-sdk/xai":
+    case "ai-gateway-provider":
+    case "gitlab-ai-provider":
+    case "venice-ai-sdk-provider":
+      return
+  }
+}
+
 export * as ProviderTransform from "./transform"

+ 83 - 36
packages/opencode/test/provider/provider.test.ts

@@ -562,6 +562,45 @@ it.instance(
   },
 )
 
+it.instance(
+  "model config preserves explicitly empty models.dev variants",
+  Effect.gen(function* () {
+    yield* set("OPENAI_API_KEY", "test-api-key")
+    const providers = yield* list
+    const model = providers[ProviderV2.ID.openai].models["custom-gpt-chat"]
+    expect(model.name).toBe("Custom GPT Chat")
+    expect(model.variants).toEqual({})
+  }),
+  {
+    config: {
+      provider: {
+        openai: { models: { "custom-gpt-chat": { id: "gpt-5-chat-latest", name: "Custom GPT Chat" } } },
+      },
+    },
+  },
+)
+
+it.instance(
+  "model config regenerates variants when overriding the provider package",
+  Effect.gen(function* () {
+    yield* set("ANTHROPIC_API_KEY", "test-api-key")
+    const providers = yield* list
+    const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
+    expect(model.variants?.low).toEqual({ reasoningEffort: "low" })
+    expect(model.variants?.max).toBeUndefined()
+  }),
+  {
+    config: {
+      provider: {
+        anthropic: {
+          npm: "@ai-sdk/openai-compatible",
+          models: { "claude-sonnet-4-6": { name: "Claude via OpenAI" } },
+        },
+      },
+    },
+  },
+)
+
 it.instance(
   "disabled_providers prevents loading even with env var",
   Effect.gen(function* () {
@@ -1426,52 +1465,60 @@ test("models.dev normalization fills required response fields", () => {
   expect(model.release_date).toBe("")
 })
 
-test("models.dev reasoning options normalize to known shapes", () => {
-  const provider = Provider.fromModelsDevProvider({
-    id: "openai",
-    name: "OpenAI",
+test("models.dev reasoning options replace generated variants and unsupported options fall back", () => {
+  const provider = {
+    id: "reasoning",
+    name: "Reasoning",
     env: [],
     npm: "@ai-sdk/openai",
     models: {
-      reasoner: {
-        id: "reasoner",
-        name: "Reasoner",
+      explicit: {
+        id: "gpt-5.4",
+        name: "Explicit",
         reasoning: true,
-        reasoning_options: [
-          { type: "future_control", value: true },
-          { type: "effort", values: ["high", null, 42] },
-          { type: "toggle" },
-          { type: "budget_tokens", min: 1024, max: "invalid" },
-        ],
-        limit: { context: 128_000, output: 16_000 },
+        reasoning_options: [{ type: "effort", values: ["low"] }],
+        limit: { context: 128_000, output: 64_000 },
       },
-    },
-  } as unknown as ModelsDev.Provider)
-
-  expect(provider.models.reasoner.reasoning_options).toEqual([
-    { type: "effort", values: ["high", null] },
-    { type: "toggle" },
-    { type: "budget_tokens", min: 1024 },
-  ])
-})
-
-test("models.dev models without reasoning options normalize to an empty list", () => {
-  const provider = Provider.fromModelsDevProvider({
-    id: "openai",
-    name: "OpenAI",
-    env: [],
-    npm: "@ai-sdk/openai",
-    models: {
-      reasoner: {
+      empty: {
         id: "gpt-5.4",
-        name: "Reasoner",
+        name: "Empty",
         reasoning: true,
-        limit: { context: 128_000, output: 16_000 },
+        reasoning_options: [],
+        limit: { context: 128_000, output: 64_000 },
+      },
+      fallback: {
+        id: "gpt-5.4",
+        name: "Fallback",
+        reasoning: true,
+        reasoning_options: [{ type: "toggle" }],
+        limit: { context: 128_000, output: 64_000 },
+      },
+      override: {
+        id: "gemini-3-pro",
+        name: "Override",
+        reasoning: true,
+        reasoning_options: [{ type: "effort", values: ["high"] }],
+        provider: { npm: "@ai-sdk/google" },
+        limit: { context: 128_000, output: 64_000 },
+        experimental: { modes: { fast: {} } },
       },
     },
-  } as unknown as ModelsDev.Provider)
+  } as unknown as ModelsDev.Provider
 
-  expect(provider.models.reasoner.reasoning_options).toEqual([])
+  const models = Provider.fromModelsDevProvider(provider).models
+  expect(models.explicit.variants).toEqual({
+    low: {
+      reasoningEffort: "low",
+      reasoningSummary: "auto",
+      include: ["reasoning.encrypted_content"],
+    },
+  })
+  expect(models.empty.variants).toEqual({})
+  expect(Object.keys(models.fallback.variants ?? {})).toEqual(["none", "low", "medium", "high", "xhigh"])
+  expect(models.override.variants).toEqual({
+    high: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
+  })
+  expect(models["gemini-3-pro-fast"].variants).toEqual(models.override.variants)
 })
 
 test("public provider info omits invalid models", () => {

+ 237 - 0
packages/opencode/test/provider/transform.test.ts

@@ -4,6 +4,7 @@ import { ProviderTransform } from "@/provider/transform"
 import { LLMRequestPrep } from "@/session/llm/request"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
+import { ModelsDev } from "@opencode-ai/core/models-dev"
 import { jsonSchema } from "ai"
 
 describe("ProviderTransform.options - setCacheKey", () => {
@@ -2988,6 +2989,242 @@ describe("ProviderTransform.temperature - Cohere North", () => {
   })
 })
 
+describe("ProviderTransform.reasoningVariants", () => {
+  const model = (reasoning_options: ModelsDev.Model["reasoning_options"]) => ({ reasoning_options }) as ModelsDev.Model
+  const target = (npm: string, id = "test-model") =>
+    ({ id, api: { id, npm, url: "" }, capabilities: { reasoning: true }, limit: { output: 64_000 } }) as any
+
+  test("respects explicitly empty reasoning options", () => {
+    expect(ProviderTransform.reasoningVariants(model([]), target("@ai-sdk/openai"))).toEqual({})
+  })
+
+  test.each([
+    ["@openrouter/ai-sdk-provider", { reasoning: { effort: "high" } }],
+    ["@ai-sdk/anthropic", { thinking: { type: "adaptive" }, effort: "high" }, "claude-opus-4-6"],
+    [
+      "@ai-sdk/google-vertex/anthropic",
+      { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
+      "claude-opus-4-7",
+    ],
+    ["@ai-sdk/google", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }],
+    ["@ai-sdk/google-vertex", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }],
+    [
+      "@ai-sdk/azure",
+      {
+        reasoningEffort: "high",
+        reasoningSummary: "auto",
+        include: ["reasoning.encrypted_content"],
+      },
+    ],
+    [
+      "@ai-sdk/openai",
+      {
+        reasoningEffort: "high",
+        reasoningSummary: "auto",
+        include: ["reasoning.encrypted_content"],
+      },
+    ],
+    [
+      "@ai-sdk/amazon-bedrock/mantle",
+      {
+        reasoningEffort: "high",
+        reasoningSummary: "auto",
+        include: ["reasoning.encrypted_content"],
+      },
+    ],
+    [
+      "@ai-sdk/github-copilot",
+      {
+        reasoningEffort: "high",
+        reasoningSummary: "auto",
+        include: ["reasoning.encrypted_content"],
+      },
+    ],
+    ["@ai-sdk/openai-compatible", { reasoningEffort: "high" }],
+    ["@ai-sdk/xai", { reasoningEffort: "high" }],
+    ["@ai-sdk/mistral", { reasoningEffort: "high" }],
+    ["@ai-sdk/groq", { reasoningEffort: "high" }],
+    ["@ai-sdk/cerebras", { reasoningEffort: "high" }],
+    ["@ai-sdk/deepinfra", { reasoningEffort: "high" }],
+    ["@ai-sdk/togetherai", { reasoningEffort: "high" }],
+    ["venice-ai-sdk-provider", { reasoningEffort: "high" }],
+    ["ai-gateway-provider", { reasoningEffort: "high" }],
+    ["@ai-sdk/amazon-bedrock", { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }],
+  ])("converts effort for %s", (npm, expected, ...args) => {
+    const id = args[0] as string | undefined
+    expect(ProviderTransform.reasoningVariants(model([{ type: "effort", values: ["high"] }]), target(npm, id))).toEqual(
+      { high: expected },
+    )
+  })
+
+  test("uses bare effort for Claude Opus 4.5", () => {
+    expect(
+      ProviderTransform.reasoningVariants(
+        model([{ type: "effort", values: ["high"] }]),
+        target("@ai-sdk/anthropic", "claude-opus-4-5"),
+      ),
+    ).toEqual({ high: { effort: "high" } })
+  })
+
+  test("leaves legacy Anthropic effort options to budget fallback", () => {
+    expect(
+      ProviderTransform.reasoningVariants(
+        model([{ type: "effort", values: ["high"] }]),
+        target("@ai-sdk/anthropic", "claude-sonnet-4"),
+      ),
+    ).toBeUndefined()
+  })
+
+  test("uses adaptive reasoning config for Anthropic models on Bedrock", () => {
+    expect(
+      ProviderTransform.reasoningVariants(
+        model([{ type: "effort", values: ["high"] }]),
+        target("@ai-sdk/amazon-bedrock", "anthropic.claude-opus-4-7-v1:0"),
+      ),
+    ).toEqual({
+      high: {
+        reasoningConfig: {
+          type: "adaptive",
+          maxReasoningEffort: "high",
+          display: "summarized",
+        },
+      },
+    })
+  })
+
+  test("leaves legacy Anthropic Bedrock effort options to budget fallback", () => {
+    expect(
+      ProviderTransform.reasoningVariants(
+        model([{ type: "effort", values: ["high"] }]),
+        target("@ai-sdk/amazon-bedrock", "anthropic.claude-sonnet-4-v1:0"),
+      ),
+    ).toBeUndefined()
+  })
+
+  test.each([
+    ["@openrouter/ai-sdk-provider", { reasoning: { max_tokens: 16_000 } }],
+    ["@ai-sdk/anthropic", { thinking: { type: "enabled", budgetTokens: 16_000 } }],
+    ["@ai-sdk/google-vertex/anthropic", { thinking: { type: "enabled", budgetTokens: 16_000 } }],
+    ["@ai-sdk/google", { thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } }],
+    ["@ai-sdk/google-vertex", { thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } }],
+    ["@ai-sdk/amazon-bedrock", { reasoningConfig: { type: "enabled", budgetTokens: 16_000 } }],
+    ["@ai-sdk/cohere", { thinking: { type: "enabled", tokenBudget: 16_000 } }],
+    ["@ai-sdk/alibaba", { enableThinking: true, thinkingBudget: 16_000 }],
+  ])("converts token budgets for %s", (npm, high) => {
+    expect(
+      ProviderTransform.reasoningVariants(model([{ type: "budget_tokens", min: 1_024, max: 16_000 }]), target(npm)),
+    ).toEqual({ high })
+  })
+
+  test("maps null effort to none", () => {
+    expect(
+      ProviderTransform.reasoningVariants(model([{ type: "effort", values: [null] }]), target("@ai-sdk/openai")),
+    ).toEqual({
+      none: {
+        reasoningEffort: "none",
+        reasoningSummary: "auto",
+        include: ["reasoning.encrypted_content"],
+      },
+    })
+  })
+
+  test.each([
+    ["@ai-sdk/alibaba", { none: { enableThinking: false }, high: { enableThinking: true } }],
+    [
+      "@ai-sdk/cohere",
+      {
+        none: { thinking: { type: "disabled" } },
+        high: { thinking: { type: "enabled" } },
+      },
+    ],
+  ])("converts toggle options for %s", (npm, expected) => {
+    expect(ProviderTransform.reasoningVariants(model([{ type: "toggle" }]), target(npm))).toEqual(expected)
+  })
+
+  test("combines Cohere toggle and budget options", () => {
+    expect(
+      ProviderTransform.reasoningVariants(
+        model([{ type: "toggle" }, { type: "budget_tokens", min: 1 }]),
+        target("@ai-sdk/cohere"),
+      ),
+    ).toEqual({
+      none: { thinking: { type: "disabled" } },
+      high: { thinking: { type: "enabled", tokenBudget: 16_000 } },
+    })
+  })
+
+  test("generates bounded high and max token budgets", () => {
+    expect(
+      ProviderTransform.reasoningVariants(
+        model([{ type: "budget_tokens", min: 1_024, max: 64_000 }]),
+        target("@ai-sdk/anthropic"),
+      ),
+    ).toEqual({
+      high: { thinking: { type: "enabled", budgetTokens: 16_000 } },
+      max: { thinking: { type: "enabled", budgetTokens: 63_999 } },
+    })
+  })
+
+  test("caps token budgets below the model output limit", () => {
+    const anthropic = target("@ai-sdk/anthropic")
+    anthropic.limit.output = 5_000
+    expect(
+      ProviderTransform.reasoningVariants(model([{ type: "budget_tokens", min: 1_024, max: 64_000 }]), anthropic),
+    ).toEqual({
+      high: { thinking: { type: "enabled", budgetTokens: 4_999 } },
+    })
+  })
+
+  test("prefers effort options over token budgets", () => {
+    expect(
+      ProviderTransform.reasoningVariants(
+        model([
+          { type: "budget_tokens", min: 1_024, max: 64_000 },
+          { type: "effort", values: ["low"] },
+        ]),
+        target("@ai-sdk/openai"),
+      ),
+    ).toEqual({
+      low: {
+        reasoningEffort: "low",
+        reasoningSummary: "auto",
+        include: ["reasoning.encrypted_content"],
+      },
+    })
+  })
+
+  test("leaves unsupported options for heuristic fallback", () => {
+    expect(
+      ProviderTransform.reasoningVariants(model([{ type: "effort", values: ["high"] }]), target("@ai-sdk/perplexity")),
+    ).toBeUndefined()
+    expect(ProviderTransform.reasoningVariants(model([{ type: "toggle" }]), target("@ai-sdk/openai"))).toBeUndefined()
+  })
+
+  test("uses model-family options for gateway and GitHub Copilot", () => {
+    const effort = model([{ type: "effort", values: ["high"] }])
+    expect(ProviderTransform.reasoningVariants(effort, target("@ai-sdk/gateway", "anthropic/claude-sonnet-4"))).toEqual(
+      {
+        high: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
+      },
+    )
+    expect(ProviderTransform.reasoningVariants(effort, target("@ai-sdk/gateway", "google/gemini-3-pro"))).toEqual({
+      high: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
+    })
+    expect(
+      ProviderTransform.reasoningVariants(effort, target("@ai-sdk/github-copilot", "gemini-3-pro")),
+    ).toBeUndefined()
+  })
+
+  test.each(["@ai-sdk/cohere", "@ai-sdk/perplexity", "@ai-sdk/vercel", "@ai-sdk/alibaba", "gitlab-ai-provider"])(
+    "does not invent effort controls for %s",
+    (npm) => {
+      expect(
+        ProviderTransform.reasoningVariants(model([{ type: "effort", values: ["high"] }]), target(npm)),
+      ).toBeUndefined()
+    },
+  )
+})
+
 describe("ProviderTransform.variants", () => {
   const createMockModel = (overrides: Partial<any> = {}): any => ({
     id: "test/test-model",

+ 0 - 14
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -2100,20 +2100,6 @@ export type Model = {
     [key: string]: string
   }
   release_date: string
-  reasoning_options?: Array<
-    | {
-        type: "effort"
-        values: Array<string>
-      }
-    | {
-        type: "toggle"
-      }
-    | {
-        type: "budget_tokens"
-        min?: number
-        max?: number
-      }
-  >
   variants?: {
     [key: string]: {
       [key: string]: unknown

+ 0 - 52
packages/sdk/openapi.json

@@ -21777,58 +21777,6 @@
           "release_date": {
             "type": "string"
           },
-          "reasoning_options": {
-            "type": "array",
-            "items": {
-              "anyOf": [
-                {
-                  "type": "object",
-                  "properties": {
-                    "type": {
-                      "type": "string",
-                      "enum": ["effort"]
-                    },
-                    "values": {
-                      "type": "array",
-                      "items": {
-                        "type": "string"
-                      }
-                    }
-                  },
-                  "required": ["type", "values"],
-                  "additionalProperties": false
-                },
-                {
-                  "type": "object",
-                  "properties": {
-                    "type": {
-                      "type": "string",
-                      "enum": ["toggle"]
-                    }
-                  },
-                  "required": ["type"],
-                  "additionalProperties": false
-                },
-                {
-                  "type": "object",
-                  "properties": {
-                    "type": {
-                      "type": "string",
-                      "enum": ["budget_tokens"]
-                    },
-                    "min": {
-                      "type": "number"
-                    },
-                    "max": {
-                      "type": "number"
-                    }
-                  },
-                  "required": ["type"],
-                  "additionalProperties": false
-                }
-              ]
-            }
-          },
           "variants": {
             "type": "object",
             "additionalProperties": {