Browse Source

fix(core): derive models.dev reasoning variants (#34726)

Aiden Cline 1 month ago
parent
commit
140224b0fc

+ 5 - 0
packages/client/src/generated/types.ts

@@ -151,6 +151,7 @@ export type AgentListOutput = {
     readonly id: string
     readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
     readonly request: {
+      readonly settings: { readonly [x: string]: JsonValue }
       readonly headers: { readonly [x: string]: string }
       readonly body: { readonly [x: string]: JsonValue }
     }
@@ -2192,12 +2193,14 @@ export type ModelListOutput = {
       readonly output: ReadonlyArray<string>
     }
     readonly request: {
+      readonly settings: { readonly [x: string]: JsonValue }
       readonly headers: { readonly [x: string]: string }
       readonly body: { readonly [x: string]: JsonValue }
       readonly variant?: string
     }
     readonly variants: ReadonlyArray<{
       readonly id: string
+      readonly settings: { readonly [x: string]: JsonValue }
       readonly headers: { readonly [x: string]: string }
       readonly body: { readonly [x: string]: JsonValue }
     }>
@@ -2256,6 +2259,7 @@ export type ProviderListOutput = {
         }
       | { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
     readonly request: {
+      readonly settings: { readonly [x: string]: JsonValue }
       readonly headers: { readonly [x: string]: string }
       readonly body: { readonly [x: string]: JsonValue }
     }
@@ -2289,6 +2293,7 @@ export type ProviderGetOutput = {
         }
       | { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
     readonly request: {
+      readonly settings: { readonly [x: string]: JsonValue }
       readonly headers: { readonly [x: string]: string }
       readonly body: { readonly [x: string]: JsonValue }
     }

+ 1 - 0
packages/core/src/catalog.ts

@@ -85,6 +85,7 @@ const layer = Layer.effect(
               ? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } }
               : model.api
       const request = {
+        settings: { ...provider.request.settings, ...model.request.settings },
         headers: { ...provider.request.headers, ...model.request.headers },
         body: { ...provider.request.body, ...model.request.body },
         variant: model.request.variant,

+ 4 - 1
packages/core/src/config/plugin/provider.ts

@@ -4,7 +4,6 @@ import { define } from "../../plugin/internal"
 import { Effect } from "effect"
 import { Config } from "../../config"
 import { ModelV2 } from "../../model"
-import { ProviderV2 } from "../../provider"
 
 export const Plugin = define({
   id: "config-provider",
@@ -54,6 +53,7 @@ export const Plugin = define({
               if (item.name !== undefined) provider.name = item.name
               if (item.api !== undefined) provider.api = { ...item.api }
               if (item.request !== undefined) {
+                Object.assign(provider.request.settings, item.request.settings)
                 Object.assign(provider.request.headers, item.request.headers)
                 Object.assign(provider.request.body, item.request.body)
               }
@@ -71,6 +71,7 @@ export const Plugin = define({
                   }
                 }
                 if (config.request !== undefined) {
+                  Object.assign(model.request.settings, config.request.settings)
                   Object.assign(model.request.headers, config.request.headers)
                   Object.assign(model.request.body, config.request.body)
                   if (config.request.variant !== undefined) model.request.variant = config.request.variant
@@ -81,11 +82,13 @@ export const Plugin = define({
                     if (!existing) {
                       existing = {
                         id: variant.id,
+                        settings: {},
                         headers: {},
                         body: {},
                       }
                       model.variants.push(existing)
                     }
+                    Object.assign(existing.settings, variant.settings)
                     Object.assign(existing.headers, variant.headers)
                     Object.assign(existing.body, variant.body)
                   }

+ 1 - 0
packages/core/src/config/provider.ts

@@ -5,6 +5,7 @@ import { ProviderV2 } from "../provider"
 import { ModelV2 } from "../model"
 
 export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
+  settings: ProviderV2.Settings.pipe(Schema.optional),
   headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
   body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
 }) {}

+ 6 - 1
packages/core/src/model.ts

@@ -26,8 +26,13 @@ export type Api = Model.Api
 export const Info = Model.Info
 export type Info = Model.Info
 
-export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & {
+export type MutableRequest = ProviderV2.MutableRequest & { variant?: string }
+export type MutableVariant = ProviderV2.MutableRequest & { id: VariantID }
+
+export type MutableInfo = Omit<Types.DeepMutable<Info>, "api" | "request" | "variants"> & {
   api: ProviderV2.MutableApi<Api>
+  request: MutableRequest
+  variants: MutableVariant[]
 }
 
 export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {

+ 66 - 18
packages/core/src/plugin/models-dev.ts

@@ -70,25 +70,73 @@ function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"]
   return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()]
 }
 
-function reasoningVariants(model: ModelsDev.Model, packageName: string | undefined): ModelV2Info["variants"] {
-  const result = new Map<ModelV2.VariantID, ModelV2Info["variants"][number]>()
-  if (packageName === "@ai-sdk/openai" || packageName === "@ai-sdk/openai-compatible") {
-    const option = model.reasoning_options?.find((option) => option.type === "effort")
-    for (const value of option?.values ?? []) {
-      const id = value === null ? "none" : value
-      if (typeof id !== "string") continue
-      const variantID = ModelV2.VariantID.make(id)
-      result.set(variantID, {
-        id: variantID,
-        headers: {},
-        body:
-          packageName === "@ai-sdk/openai"
-            ? { include: ["reasoning.encrypted_content"], reasoning: { effort: id, summary: "auto" } }
-            : { reasoning_effort: id },
-      })
+const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
+
+function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): ModelV2Info["variants"] {
+  const npm = model.provider?.npm ?? provider.npm
+  const options = model.reasoning_options ?? []
+  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, settings, headers: {}, body: {} }] : []
+    })
+  }
+
+  const budget = options.find((option) => option.type === "budget_tokens")
+  if (budget?.type === "budget_tokens") return budgetVariants(npm, budget)
+
+  // Toggle-only reasoning is intentionally left for a follow-up because V1 has
+  // provider/model-specific behavior like MiniMax M3 adaptive thinking and
+  // Qwen/GLM enable_thinking request shapes in packages/opencode.
+  return []
+}
+
+function settingsForEffort(npm: string | undefined, 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/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") {
+    return {
+      reasoningEffort: effort,
+      reasoningSummary: "auto",
+      include: OPENAI_INCLUDE_ENCRYPTED_REASONING,
     }
   }
-  return [...result.values()]
+  if (npm === "@ai-sdk/openai-compatible") return { reasoningEffort: effort }
+}
+
+function budgetVariants(
+  npm: string | undefined,
+  option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>,
+): ModelV2Info["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)
+  return [
+    { id: "high", budget: high },
+    ...(max === undefined || max === high ? [] : [{ id: "max", budget: max }]),
+  ].flatMap((item) => {
+    const settings = settingsForBudget(npm, item.budget)
+    return settings ? [{ id: item.id, settings, headers: {}, body: {} }] : []
+  })
+}
+
+function settingsForBudget(npm: string | undefined, 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 } }
+  }
 }
 
 function modeName(model: ModelsDev.Model, mode: string) {
@@ -193,7 +241,7 @@ export const ModelsDevPlugin = define({
 
           for (const model of Object.values(item.models)) {
             const baseCost = cost(model.cost)
-            const variants = reasoningVariants(model, model.provider?.npm ?? item.npm)
+            const variants = reasoningVariants(item, model)
             catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost, variants }))
             for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
               catalog.model.update(providerID, `${model.id}-${mode}`, (draft) =>

+ 1 - 1
packages/core/src/plugin/provider/opencode.ts

@@ -146,7 +146,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
                 const variantID = ModelV2.VariantID.make(id)
                 let existing = model.variants.find((item) => item.id === variantID)
                 if (!existing) {
-                  existing = { id: variantID, headers: {}, body: {} }
+                  existing = { id: variantID, settings: {}, headers: {}, body: {} }
                   model.variants.push(existing)
                 }
                 Object.assign(existing.headers, options.headers)

+ 2 - 1
packages/core/src/plugin/variant.ts

@@ -33,7 +33,8 @@ export function generate(model: ModelV2Info): ModelV2Info["variants"] {
   if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
   return ["high", "max"].map((id) => ({
     id,
+    settings: { reasoningEffort: id },
     headers: {},
-    body: { reasoning_effort: id },
+    body: {},
   }))
 }

+ 9 - 1
packages/core/src/provider.ts

@@ -19,7 +19,15 @@ export type MutableApi<T extends Api = Api> = T extends Api
 export const Request = Provider.Request
 export type Request = Provider.Request
 
+export const Settings = Provider.Settings
+export type Settings = Provider.Settings
+
 export const Info = Provider.Info
 export type Info = Provider.Info
 
-export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi }
+export type MutableRequest = Types.DeepMutable<Request>
+
+export type MutableInfo = Omit<Types.DeepMutable<Info>, "api" | "request"> & {
+  api: MutableApi
+  request: MutableRequest
+}

+ 10 - 0
packages/core/src/session/runner/model.ts

@@ -97,11 +97,20 @@ const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
     provider: model.providerID,
     endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url },
     headers: model.request.headers,
+    providerOptions: providerOptions(model),
     http: { body: httpBody },
     limits: { context: model.limit.context, output: model.limit.output },
   })
 }
 
+const providerOptions = (model: ModelV2.Info) => {
+  if (Object.keys(model.request.settings).length === 0) return undefined
+  if (model.api.type !== "aisdk") return undefined
+  if (model.api.package === "@ai-sdk/openai") return { openai: model.request.settings }
+  if (model.api.package === "@ai-sdk/anthropic") return { anthropic: model.request.settings }
+  if (model.api.package === "@ai-sdk/openai-compatible") return { openai: model.request.settings }
+}
+
 export const withVariant = (
   model: ModelV2.Info,
   variantID: ModelV2.VariantID | undefined,
@@ -119,6 +128,7 @@ export const withVariant = (
   return Effect.succeed(
     variant
       ? produce(model, (draft) => {
+          Object.assign(draft.request.settings, variant.settings)
           Object.assign(draft.request.headers, variant.headers)
           Object.assign(draft.request.body, variant.body)
         })

+ 67 - 0
packages/core/test/plugin/fixtures/models-dev-reasoning.json

@@ -0,0 +1,67 @@
+{
+  "openai": {
+    "id": "openai",
+    "name": "OpenAI",
+    "env": ["OPENAI_API_KEY"],
+    "npm": "@ai-sdk/openai",
+    "api": "https://api.openai.com/v1",
+    "models": {
+      "gpt-reasoning": {
+        "id": "gpt-reasoning",
+        "name": "GPT Reasoning",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [
+          { "type": "effort", "values": ["low", "high"] },
+          { "type": "budget_tokens", "min": 1024, "max": 64000 },
+          { "type": "toggle" }
+        ],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 8192 },
+        "experimental": {
+          "modes": {
+            "high": {
+              "provider": {
+                "headers": { "x-mode": "high" },
+                "body": { "service_tier": "priority" }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "anthropic": {
+    "id": "anthropic",
+    "name": "Anthropic",
+    "env": ["ANTHROPIC_API_KEY"],
+    "npm": "@ai-sdk/anthropic",
+    "api": "https://api.anthropic.com/v1",
+    "models": {
+      "claude-budget": {
+        "id": "claude-budget",
+        "name": "Claude Budget",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "budget_tokens", "min": 1024, "max": 64000 }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 8192 }
+      },
+      "claude-effort": {
+        "id": "claude-effort",
+        "name": "Claude Effort",
+        "release_date": "2026-01-01",
+        "attachment": false,
+        "reasoning": true,
+        "reasoning_options": [{ "type": "effort", "values": ["low"] }],
+        "temperature": true,
+        "tool_call": true,
+        "limit": { "context": 128000, "output": 8192 }
+      }
+    }
+  }
+}

+ 3 - 1
packages/core/test/plugin/host.ts

@@ -288,7 +288,7 @@ function providerInfo(value: ProviderV2.MutableInfo) {
   return {
     ...value,
     api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
-    request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
+    request: { settings: { ...value.request.settings }, headers: { ...value.request.headers }, body: { ...value.request.body } },
   }
 }
 
@@ -303,11 +303,13 @@ function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
     },
     request: {
       ...value.request,
+      settings: { ...value.request.settings },
       headers: { ...value.request.headers },
       body: { ...value.request.body },
     },
     variants: value.variants.map((variant) => ({
       ...variant,
+      settings: { ...variant.settings },
       headers: { ...variant.headers },
       body: { ...variant.body },
     })),

+ 61 - 48
packages/core/test/plugin/models-dev.test.ts

@@ -168,14 +168,14 @@ describe("ModelsDevPlugin", () => {
     ),
   )
 
-  it.effect("derives OpenAI reasoning variants from models.dev reasoning options", () =>
+  it.effect("converts reasoning options into settings variants", () =>
     Effect.acquireUseRelease(
       Effect.sync(() => {
         const previous = {
           path: Flag.OPENCODE_MODELS_PATH,
           disabled: Flag.OPENCODE_DISABLE_MODELS_FETCH,
         }
-        Flag.OPENCODE_MODELS_PATH = path.join(import.meta.dir, "fixtures", "models-dev.json")
+        Flag.OPENCODE_MODELS_PATH = path.join(import.meta.dir, "fixtures", "models-dev-reasoning.json")
         Flag.OPENCODE_DISABLE_MODELS_FETCH = true
         return previous
       }),
@@ -183,17 +183,6 @@ describe("ModelsDevPlugin", () => {
         Effect.gen(function* () {
           const catalog = yield* Catalog.Service
           const integrations = yield* Integration.Service
-          yield* catalog.transform((catalog) => {
-            catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("gpt-5.5"), (model) => {
-              model.variants = [
-                {
-                  id: ModelV2.VariantID.make("high"),
-                  headers: { custom: "true" },
-                  body: { custom: true },
-                },
-              ]
-            })
-          })
           yield* ModelsDevPlugin.effect(
             host({
               catalog: catalogHost(catalog),
@@ -201,42 +190,67 @@ describe("ModelsDevPlugin", () => {
             }),
           )
 
-          expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("gpt-5.5")))?.variants).toEqual([
-            {
-              id: ModelV2.VariantID.make("none"),
-              headers: {},
-              body: {
-                include: ["reasoning.encrypted_content"],
-                reasoning: { effort: "none", summary: "auto" },
-              },
+          const model = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning"))
+          expect(model?.variants.map((variant) => variant.id)).toEqual([
+            ModelV2.VariantID.make("low"),
+            ModelV2.VariantID.make("high"),
+          ])
+          expect(model?.variants).toContainEqual({
+            id: ModelV2.VariantID.make("low"),
+            settings: {
+              reasoningEffort: "low",
+              reasoningSummary: "auto",
+              include: ["reasoning.encrypted_content"],
             },
-            expect.objectContaining({
-              id: "low",
-              body: {
-                include: ["reasoning.encrypted_content"],
-                reasoning: { effort: "low", summary: "auto" },
-              },
-            }),
-            expect.objectContaining({
-              id: "medium",
-              body: {
-                include: ["reasoning.encrypted_content"],
-                reasoning: { effort: "medium", summary: "auto" },
-              },
-            }),
-            expect.objectContaining({
-              id: "high",
-              headers: { custom: "true" },
-              body: { custom: true },
-            }),
-            expect.objectContaining({
-              id: "xhigh",
-              body: {
-                include: ["reasoning.encrypted_content"],
-                reasoning: { effort: "xhigh", summary: "auto" },
-              },
-            }),
+            headers: {},
+            body: {},
+          })
+          expect(model?.variants).toContainEqual({
+            id: ModelV2.VariantID.make("high"),
+            settings: {
+              reasoningEffort: "high",
+              reasoningSummary: "auto",
+              include: ["reasoning.encrypted_content"],
+            },
+            headers: {},
+            body: {},
+          })
+
+          const mode = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-high"))
+          expect(mode).toMatchObject({
+            id: "gpt-reasoning-high",
+            name: "GPT Reasoning High",
+            request: {
+              headers: { "x-mode": "high" },
+              body: { service_tier: "priority" },
+            },
+          })
+          expect(mode?.variants.map((variant) => variant.id)).toEqual([
+            ModelV2.VariantID.make("low"),
+            ModelV2.VariantID.make("high"),
           ])
+
+          const budgetModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-budget"))
+          expect(budgetModel?.variants).toContainEqual({
+            id: ModelV2.VariantID.make("high"),
+            settings: { thinking: { type: "enabled", budgetTokens: 16000 } },
+            headers: {},
+            body: {},
+          })
+          expect(budgetModel?.variants).toContainEqual({
+            id: ModelV2.VariantID.make("max"),
+            settings: { thinking: { type: "enabled", budgetTokens: 64000 } },
+            headers: {},
+            body: {},
+          })
+
+          const anthropicEffortModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-effort"))
+          expect(anthropicEffortModel?.variants).toContainEqual({
+            id: ModelV2.VariantID.make("low"),
+            settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
+            headers: {},
+            body: {},
+          })
         }).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
       (previous) =>
         Effect.sync(() => {
@@ -245,5 +259,4 @@ describe("ModelsDevPlugin", () => {
         }),
     ),
   )
-
 })

+ 5 - 1
packages/core/test/plugin/provider-opencode.test.ts

@@ -142,6 +142,7 @@ describe("OpencodePlugin", () => {
               model.variants = [
                 {
                   id: ModelV2.VariantID.make("custom"),
+                  settings: {},
                   headers: { "x-custom": "true" },
                   body: { custom: true },
                 },
@@ -177,7 +178,7 @@ describe("OpencodePlugin", () => {
               url: `${server.url.origin}/v1`,
             },
           })
-          expect(provider.request).toEqual({ headers: { "x-org-id": "org" }, body: { custom: "value" } })
+          expect(provider.request).toEqual({ settings: {}, headers: { "x-org-id": "org" }, body: { custom: "value" } })
           expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined()
 
           const model = required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model")))
@@ -192,11 +193,13 @@ describe("OpencodePlugin", () => {
           expect(model.variants).toEqual([
             {
               id: ModelV2.VariantID.make("custom"),
+              settings: {},
               headers: { "x-custom": "true" },
               body: { custom: true },
             },
             {
               id: ModelV2.VariantID.make("high"),
+              settings: {},
               headers: {},
               body: { temperature: 0.2 },
             },
@@ -359,6 +362,7 @@ describe("OpencodePlugin", () => {
             ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
             api: { type: "aisdk", package: "test-provider" },
             request: {
+              settings: {},
               headers: {},
               body: { apiKey: "configured" },
             },

+ 4 - 4
packages/core/test/plugin/variant.test.ts

@@ -37,8 +37,8 @@ describe("VariantPlugin", () => {
       yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
 
       expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2")))?.variants).toEqual([
-        expect.objectContaining({ id: "high", body: { reasoning_effort: "high" } }),
-        expect.objectContaining({ id: "max", body: { reasoning_effort: "max" } }),
+        expect.objectContaining({ id: "high", settings: { reasoningEffort: "high" } }),
+        expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }),
       ])
     }),
   )
@@ -53,14 +53,14 @@ describe("VariantPlugin", () => {
             type: "aisdk",
             package: "@ai-sdk/openai-compatible",
           }
-          model.variants = [{ id: ModelV2.VariantID.make("high"), headers: { custom: "true" }, body: {} }]
+          model.variants = [{ id: ModelV2.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {} }]
         })
       })
       yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
 
       expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2")))?.variants).toEqual([
         expect.objectContaining({ id: "high", headers: { custom: "true" } }),
-        expect.objectContaining({ id: "max", body: { reasoning_effort: "max" } }),
+        expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }),
       ])
     }),
   )

+ 17 - 10
packages/core/test/session-runner-model.test.ts

@@ -30,6 +30,7 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
     api: { id: ModelV2.ID.make("api-test-model"), ...api },
     capabilities: { tools: true, input: ["text"], output: ["text"] },
     request: {
+      settings: {},
       headers: { "x-test": "header" },
       body: { apiKey: "secret", custom_extension: { enabled: true } },
     },
@@ -83,7 +84,7 @@ describe("SessionRunnerModel", () => {
             url: "https://compatible.example/v1",
             settings: { apiKey: "settings-secret", compatibility: "strict" },
           }),
-          request: { headers: {}, body: {} },
+          request: { settings: {}, headers: {}, body: {} },
         }),
       )
       const request = LLM.request({ model: resolved, prompt: "Hello" })
@@ -100,17 +101,17 @@ describe("SessionRunnerModel", () => {
     }),
   )
 
-  it.effect("overlays selected OpenAI Session variant bodies", () =>
+  it.effect("overlays selected OpenAI Session variant settings and bodies", () =>
     Effect.gen(function* () {
       const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [
         {
           id: ModelV2.VariantID.make("high"),
+          settings: { reasoningEffort: "high" },
           headers: { "x-variant": "high" },
           body: {
             store: false,
             service_tier: "priority",
             temperature: 0.2,
-            reasoning: { effort: "high" },
           },
         },
       ])
@@ -137,7 +138,9 @@ describe("SessionRunnerModel", () => {
         store: false,
         service_tier: "priority",
         temperature: 0.2,
-        reasoning: { effort: "high" },
+      })
+      expect(resolved.route.defaults.providerOptions).toEqual({
+        openai: { store: false, reasoningEffort: "high" },
       })
     }),
   )
@@ -149,6 +152,7 @@ describe("SessionRunnerModel", () => {
         [
           {
             id: ModelV2.VariantID.make("high"),
+            settings: {},
             headers: {},
             body: { store: false, reasoning_effort: "high" },
           },
@@ -205,13 +209,14 @@ describe("SessionRunnerModel", () => {
     }),
   )
 
-  it.effect("overlays selected Anthropic Session variant bodies", () =>
+  it.effect("overlays selected Anthropic Session variant settings", () =>
     Effect.gen(function* () {
       const catalog = model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }, [
         {
           id: ModelV2.VariantID.make("high"),
+          settings: { thinking: { type: "enabled", budgetTokens: 12000 } },
           headers: {},
-          body: { thinking: { type: "enabled", budget_tokens: 12000 } },
+          body: {},
         },
       ])
       const session = SessionV2.Info.make({
@@ -229,7 +234,9 @@ describe("SessionRunnerModel", () => {
 
       expect(resolved.route.defaults.http?.body).toEqual({
         custom_extension: { enabled: true },
-        thinking: { type: "enabled", budget_tokens: 12000 },
+      })
+      expect(resolved.route.defaults.providerOptions).toEqual({
+        anthropic: { thinking: { type: "enabled", budgetTokens: 12000 } },
       })
     }),
   )
@@ -252,7 +259,7 @@ describe("SessionRunnerModel", () => {
       const resolved = yield* SessionRunnerModel.fromCatalogModel(
         ModelV2.Info.make({
           ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
-          request: { headers: {}, body: {} },
+          request: { settings: {}, headers: {}, body: {} },
         }),
         Credential.Key.make({ type: "key", key: "secret" }),
       )
@@ -275,7 +282,7 @@ describe("SessionRunnerModel", () => {
       const resolved = yield* SessionRunnerModel.fromCatalogModel(
         ModelV2.Info.make({
           ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
-          request: { headers: {}, body: { apiKey: "configured-secret" } },
+          request: { settings: {}, headers: {}, body: { apiKey: "configured-secret" } },
         }),
         credential,
       )
@@ -297,7 +304,7 @@ describe("SessionRunnerModel", () => {
       const resolved = yield* SessionRunnerModel.fromCatalogModel(
         ModelV2.Info.make({
           ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
-          request: { headers: {}, body: {} },
+          request: { settings: {}, headers: {}, body: {} },
         }),
         Credential.OAuth.make({
           type: "oauth",

+ 33 - 4
packages/llm/src/protocols/anthropic-messages.ts

@@ -148,9 +148,22 @@ const AnthropicToolChoice = Schema.Union([
   Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }),
 ])
 
-const AnthropicThinking = Schema.Struct({
-  type: Schema.tag("enabled"),
-  budget_tokens: Schema.Number,
+const AnthropicThinking = Schema.Union([
+  Schema.Struct({
+    type: Schema.tag("enabled"),
+    budget_tokens: Schema.Number,
+  }),
+  Schema.Struct({
+    type: Schema.tag("adaptive"),
+    display: Schema.optional(Schema.Literals(["summarized", "omitted"])),
+  }),
+  Schema.Struct({
+    type: Schema.tag("disabled"),
+  }),
+])
+
+const AnthropicOutputConfig = Schema.Struct({
+  effort: Schema.optional(Schema.String),
 })
 
 const AnthropicBodyFields = {
@@ -166,6 +179,7 @@ const AnthropicBodyFields = {
   top_k: Schema.optional(Schema.Number),
   stop_sequences: optionalArray(Schema.String),
   thinking: Schema.optional(AnthropicThinking),
+  output_config: Schema.optional(AnthropicOutputConfig),
 }
 const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields)
 export type AnthropicMessagesBody = Schema.Schema.Type<typeof AnthropicMessagesBody>
@@ -492,7 +506,16 @@ const anthropicOptions = (request: LLMRequest) => request.providerOptions?.anthr
 
 const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) {
   const thinking = anthropicOptions(request)?.thinking
-  if (!ProviderShared.isRecord(thinking) || thinking.type !== "enabled") return undefined
+  if (!ProviderShared.isRecord(thinking)) return undefined
+  if (thinking.type === "adaptive") {
+    const display = thinking.display
+    return {
+      type: "adaptive" as const,
+      ...(display === "summarized" || display === "omitted" ? { display } : {}),
+    }
+  }
+  if (thinking.type === "disabled") return { type: "disabled" as const }
+  if (thinking.type !== "enabled") return undefined
   const budget =
     typeof thinking.budgetTokens === "number"
       ? thinking.budgetTokens
@@ -503,6 +526,11 @@ const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (re
   return { type: "enabled" as const, budget_tokens: budget }
 })
 
+const outputConfig = (request: LLMRequest) => {
+  const effort = anthropicOptions(request)?.effort
+  return typeof effort === "string" ? { effort } : undefined
+}
+
 const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
   const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
   const generation = request.generation
@@ -549,6 +577,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
     top_k: generation?.topK,
     stop_sequences: generation?.stop,
     thinking: yield* lowerThinking(request),
+    output_config: outputConfig(request),
   }
 })
 

+ 0 - 4
packages/llm/src/protocols/openai-chat.ts

@@ -168,8 +168,6 @@ interface ParserState {
   readonly lifecycle: Lifecycle.State
 }
 
-const invalid = ProviderShared.invalidRequest
-
 // =============================================================================
 // Request Lowering
 // =============================================================================
@@ -333,8 +331,6 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request:
 const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request: LLMRequest) {
   const store = OpenAIOptions.store(request)
   const reasoningEffort = OpenAIOptions.reasoningEffort(request)
-  if (reasoningEffort && !OpenAIOptions.isReasoningEffort(reasoningEffort))
-    return yield* invalid(`OpenAI Chat does not support reasoning effort ${reasoningEffort}`)
   return {
     ...(store !== undefined ? { store } : {}),
     ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),

+ 0 - 2
packages/llm/src/protocols/openai-responses.ts

@@ -457,8 +457,6 @@ const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (reques
   const store = OpenAIOptions.store(request)
   const promptCacheKey = OpenAIOptions.promptCacheKey(request)
   const effort = OpenAIOptions.reasoningEffort(request)
-  if (effort && !OpenAIOptions.isReasoningEffort(effort))
-    return yield* invalid(`OpenAI Responses does not support reasoning effort ${effort}`)
   const summary = OpenAIOptions.reasoningSummary(request)
   const include = OpenAIOptions.include(request)
   const verbosity = OpenAIOptions.textVerbosity(request)

+ 7 - 15
packages/llm/src/protocols/utils/openai-options.ts

@@ -1,11 +1,9 @@
 import { Schema } from "effect"
-import type { LLMRequest, ReasoningEffort, TextVerbosity as TextVerbosityValue } from "../../schema"
+import type { LLMRequest, TextVerbosity as TextVerbosityValue } from "../../schema"
 import { ReasoningEfforts, TextVerbosity } from "../../schema"
 
-export const OpenAIReasoningEfforts = ReasoningEfforts.filter(
-  (effort): effort is Exclude<ReasoningEffort, "max"> => effort !== "max",
-)
-export type OpenAIReasoningEffort = (typeof OpenAIReasoningEfforts)[number]
+export const OpenAIReasoningEfforts = ReasoningEfforts
+export type OpenAIReasoningEffort = string
 
 // Mirrors OpenAI's `ResponseIncludable` union from the official SDK. Keep this
 // in lockstep with `openai-node/src/resources/responses/responses.ts`.
@@ -23,22 +21,16 @@ export type OpenAIResponseIncludable = (typeof OpenAIResponseIncludables)[number
 export const OpenAIServiceTiers = ["auto", "default", "flex", "priority"] as const
 export type OpenAIServiceTier = (typeof OpenAIServiceTiers)[number]
 
-const REASONING_EFFORTS = new Set<string>(ReasoningEfforts)
-const OPENAI_REASONING_EFFORTS = new Set<string>(OpenAIReasoningEfforts)
 const TEXT_VERBOSITY = new Set<string>(["low", "medium", "high"])
 const INCLUDABLES = new Set<string>(OpenAIResponseIncludables)
 const SERVICE_TIERS = new Set<string>(OpenAIServiceTiers)
 
-export const OpenAIReasoningEffort = Schema.Literals(OpenAIReasoningEfforts)
+export const OpenAIReasoningEffort = Schema.String
 export const OpenAITextVerbosity = TextVerbosity
 export const OpenAIResponseIncludable = Schema.Literals(OpenAIResponseIncludables)
 export const OpenAIServiceTier = Schema.Literals(OpenAIServiceTiers)
 
-const isAnyReasoningEffort = (effort: unknown): effort is ReasoningEffort =>
-  typeof effort === "string" && REASONING_EFFORTS.has(effort)
-
-export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEffort =>
-  typeof effort === "string" && OPENAI_REASONING_EFFORTS.has(effort)
+export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEffort => typeof effort === "string"
 
 const isTextVerbosity = (value: unknown): value is TextVerbosityValue =>
   typeof value === "string" && TEXT_VERBOSITY.has(value)
@@ -50,9 +42,9 @@ export const store = (request: LLMRequest): boolean | undefined => {
   return typeof value === "boolean" ? value : undefined
 }
 
-export const reasoningEffort = (request: LLMRequest): ReasoningEffort | undefined => {
+export const reasoningEffort = (request: LLMRequest): string | undefined => {
   const value = options(request)?.reasoningEffort
-  return isAnyReasoningEffort(value) ? value : undefined
+  return typeof value === "string" ? value : undefined
 }
 
 export const reasoningSummary = (request: LLMRequest): "auto" | undefined =>

+ 1 - 1
packages/llm/src/schema/ids.ts

@@ -27,7 +27,7 @@ export const ToolCallID = Schema.String
 export type ToolCallID = Schema.Schema.Type<typeof ToolCallID>
 
 export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
-export const ReasoningEffort = Schema.Literals(ReasoningEfforts)
+export const ReasoningEffort = Schema.String
 export type ReasoningEffort = Schema.Schema.Type<typeof ReasoningEffort>
 
 export const TextVerbosity = Schema.Literals(["low", "medium", "high"])

+ 17 - 0
packages/llm/test/provider/anthropic-messages.test.ts

@@ -57,6 +57,23 @@ describe("Anthropic Messages route", () => {
     }),
   )
 
+  it.effect("lowers adaptive thinking settings with effort", () =>
+    Effect.gen(function* () {
+      const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
+        LLM.updateRequest(request, {
+          providerOptions: {
+            anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
+          },
+        }),
+      )
+
+      expect(prepared.body).toMatchObject({
+        thinking: { type: "adaptive", display: "summarized" },
+        output_config: { effort: "low" },
+      })
+    }),
+  )
+
   it.effect("lowers chronological system updates natively for Claude Opus 4.8 with cache hints", () =>
     Effect.gen(function* () {
       const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(

+ 16 - 2
packages/llm/test/provider/openai-chat.test.ts

@@ -98,12 +98,26 @@ describe("OpenAI Chat route", () => {
         LLM.request({
           model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).chat("gpt-4o-mini"),
           prompt: "think",
-          providerOptions: { openai: { reasoningEffort: "low" } },
+          providerOptions: { openai: { reasoningEffort: "max" } },
         }),
       )
 
       expect(prepared.body.store).toBe(false)
-      expect(prepared.body.reasoning_effort).toBe("low")
+      expect(prepared.body.reasoning_effort).toBe("max")
+    }),
+  )
+
+  it.effect("passes through custom OpenAI-compatible reasoning effort strings", () =>
+    Effect.gen(function* () {
+      const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
+        LLM.request({
+          model,
+          prompt: "think",
+          providerOptions: { openai: { reasoningEffort: "experimental" } },
+        }),
+      )
+
+      expect(prepared.body.reasoning_effort).toBe("experimental")
     }),
   )
 

+ 10 - 0
packages/llm/test/provider/openai-responses.test.ts

@@ -69,6 +69,16 @@ describe("OpenAI Responses route", () => {
     }),
   )
 
+  it.effect("passes through custom OpenAI reasoning effort strings", () =>
+    Effect.gen(function* () {
+      const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
+        LLM.updateRequest(request, { providerOptions: { openai: { reasoningEffort: "experimental" } } }),
+      )
+
+      expect(prepared.body.reasoning).toEqual({ effort: "experimental" })
+    }),
+  )
+
   it.effect("omits unsupported semantic service tiers", () =>
     Effect.gen(function* () {
       const prepared = yield* LLMClient.prepare(

+ 1 - 1
packages/schema/src/agent.ts

@@ -38,7 +38,7 @@ export const Info = Schema.Struct({
       empty: (id: ID) =>
         schema.make({
           id,
-          request: { headers: {}, body: {} },
+          request: { settings: {}, headers: {}, body: {} },
           mode: "all",
           hidden: false,
           permissions: [

+ 1 - 1
packages/schema/src/model.ts

@@ -94,7 +94,7 @@ export const Info = Schema.Struct({
           name: modelID,
           api: { id: modelID, type: "native", settings: {} },
           capabilities: { tools: false, input: [], output: [] },
-          request: { headers: {}, body: {} },
+          request: { settings: {}, headers: {}, body: {} },
           variants: [],
           time: { released: 0 },
           cost: [],

+ 6 - 2
packages/schema/src/provider.ts

@@ -1,6 +1,6 @@
 export * as Provider from "./provider.js"
 
-import { Schema } from "effect"
+import { Effect, Schema } from "effect"
 import { optional } from "./schema.js"
 import { Integration } from "./integration.js"
 import { statics } from "./schema.js"
@@ -43,8 +43,12 @@ export const Api = Schema.Union([AISDK, Native])
   .annotate({ identifier: "Provider.Api" })
 export type Api = typeof Api.Type
 
+export const Settings = Schema.Record(Schema.String, Schema.Unknown).annotate({ identifier: "Provider.Settings" })
+export type Settings = typeof Settings.Type
+
 export interface Request extends Schema.Schema.Type<typeof Request> {}
 export const Request = Schema.Struct({
+  settings: Settings.pipe(Schema.withConstructorDefault(Effect.succeed({}))),
   headers: Schema.Record(Schema.String, Schema.String),
   body: Schema.Record(Schema.String, Schema.Json),
 }).annotate({ identifier: "Provider.Request" })
@@ -66,7 +70,7 @@ export const Info = Schema.Struct({
           id,
           name: id,
           api: { type: "native", settings: {} },
-          request: { headers: {}, body: {} },
+          request: { settings: {}, headers: {}, body: {} },
         }),
     })),
   )

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

@@ -399,6 +399,8 @@ import type {
   V2SessionSwitchAgentResponses,
   V2SessionSwitchModelErrors,
   V2SessionSwitchModelResponses,
+  V2SessionSyntheticErrors,
+  V2SessionSyntheticResponses,
   V2SessionWaitErrors,
   V2SessionWaitResponses,
   V2ShellCreateErrors,
@@ -5816,6 +5818,47 @@ export class Session3 extends HeyApiClient {
     })
   }
 
+  /**
+   * Add synthetic message
+   *
+   * Append a synthetic message to a session and resume execution.
+   */
+  public synthetic<ThrowOnError extends boolean = false>(
+    parameters: {
+      sessionID: string
+      text?: string
+      description?: string
+      metadata?: {
+        [key: string]: unknown
+      }
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "path", key: "sessionID" },
+            { in: "body", key: "text" },
+            { in: "body", key: "description" },
+            { in: "body", key: "metadata" },
+          ],
+        },
+      ],
+    )
+    return (options?.client ?? this.client).post<V2SessionSyntheticResponses, V2SessionSyntheticErrors, ThrowOnError>({
+      url: "/api/session/{sessionID}/synthetic",
+      ...options,
+      ...params,
+      headers: {
+        "Content-Type": "application/json",
+        ...options?.headers,
+        ...params.headers,
+      },
+    })
+  }
+
   /**
    * Compact session
    *

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

@@ -942,6 +942,9 @@ export type GlobalEvent = {
           messageID: string
           text: string
           description?: string
+          metadata?: {
+            [key: string]: unknown
+          }
         }
       }
     | {
@@ -3618,6 +3621,9 @@ export type SyncEventSessionNextSynthetic = {
       messageID: string
       text: string
       description?: string
+      metadata?: {
+        [key: string]: unknown
+      }
     }
   }
 }
@@ -4091,7 +4097,12 @@ export type LocationInfo = {
   }
 }
 
+export type ProviderSettings = {
+  [key: string]: unknown
+}
+
 export type ProviderRequest = {
+  settings: ProviderSettings
   headers: {
     [key: string]: string
   }
@@ -4583,6 +4594,9 @@ export type SessionNextSynthetic = {
     messageID: string
     text: string
     description?: string
+    metadata?: {
+      [key: string]: unknown
+    }
   }
 }
 
@@ -5120,6 +5134,7 @@ export type ModelV2Info = {
   api: ModelApi
   capabilities: ModelCapabilities
   request: {
+    settings: ProviderSettings
     headers: {
       [key: string]: string
     }
@@ -5130,6 +5145,7 @@ export type ModelV2Info = {
   }
   variants: Array<{
     id: string
+    settings: ProviderSettings
     headers: {
       [key: string]: string
     }
@@ -6806,6 +6822,9 @@ export type EventSessionNextSynthetic = {
     messageID: string
     text: string
     description?: string
+    metadata?: {
+      [key: string]: unknown
+    }
   }
 }
 
@@ -12284,6 +12303,47 @@ export type V2SessionSkillResponses = {
 
 export type V2SessionSkillResponse = V2SessionSkillResponses[keyof V2SessionSkillResponses]
 
+export type V2SessionSyntheticData = {
+  body: {
+    text: string
+    description?: string
+    metadata?: {
+      [key: string]: unknown
+    }
+  }
+  path: {
+    sessionID: string
+  }
+  query?: never
+  url: "/api/session/{sessionID}/synthetic"
+}
+
+export type V2SessionSyntheticErrors = {
+  /**
+   * InvalidRequestError
+   */
+  400: InvalidRequestError
+  /**
+   * UnauthorizedError
+   */
+  401: UnauthorizedError
+  /**
+   * SessionNotFoundError
+   */
+  404: SessionNotFoundError
+}
+
+export type V2SessionSyntheticError = V2SessionSyntheticErrors[keyof V2SessionSyntheticErrors]
+
+export type V2SessionSyntheticResponses = {
+  /**
+   * <No Content>
+   */
+  204: void
+}
+
+export type V2SessionSyntheticResponse = V2SessionSyntheticResponses[keyof V2SessionSyntheticResponses]
+
 export type V2SessionCompactData = {
   body?: never
   path: {