From 81cf1069a7908e38a31ef27be914c67789f90c46 Mon Sep 17 00:00:00 2001 From: vimtor Date: Thu, 15 Oct 2026 22:46:37 +0200 Subject: [PATCH 1/2] feat(opencode): build variants from configured reasoning options --- packages/core/src/v1/config/provider.ts | 16 +++++++++++ packages/opencode/src/provider/provider.ts | 9 ++++--- packages/opencode/src/provider/transform.ts | 5 +++- .../opencode/test/provider/provider.test.ts | 27 +++++++++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 17 ++++++++++++ packages/web/src/content/docs/models.mdx | 21 +++++++++++++++ 6 files changed, 91 insertions(+), 4 deletions(-) diff --git a/packages/core/src/v1/config/provider.ts b/packages/core/src/v1/config/provider.ts index 5b6a8133c45e..a8fc711fb4e3 100644 --- a/packages/core/src/v1/config/provider.ts +++ b/packages/core/src/v1/config/provider.ts @@ -10,6 +10,17 @@ const InterleavedField = Schema.Union([ Schema.String, ]) +// models.dev's `reasoning_options`: OpenCode builds the model's reasoning variants from them. +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.optional(Schema.String), name: Schema.optional(Schema.String), @@ -17,6 +28,11 @@ export const Model = Schema.Struct({ release_date: Schema.optional(Schema.String), attachment: Schema.optional(Schema.Boolean), reasoning: Schema.optional(Schema.Boolean), + reasoning_options: Schema.optional( + Schema.Array(ReasoningOption).annotate({ + description: "Reasoning controls the model accepts, used to build its variants", + }), + ), temperature: Schema.optional(Schema.Boolean), tool_call: Schema.optional(Schema.Boolean), interleaved: Schema.optional( diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 97f83bb51d1c..42fde1ee0e86 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1616,10 +1616,13 @@ const layer = Layer.effect( release_date: model.release_date ?? existingModel?.release_date ?? "", variants: {}, } + // Configured reasoning options replace the catalog variants; configured variants still apply on top. const variants = - existingModel?.api.npm === parsedModel.api.npm - ? (existingModel.variants ?? ProviderTransform.variants(parsedModel)) - : ProviderTransform.variants(parsedModel) + model.reasoning_options !== undefined + ? (ProviderTransform.reasoningVariants(model, parsedModel) ?? ProviderTransform.variants(parsedModel)) + : 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), diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 8ae8b4d9f16b..117eb4b9c29d 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1714,7 +1714,10 @@ export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7 return schema } -export function reasoningVariants(model: ModelsDev.Model, target: Provider.Model): Provider.Model["variants"] { +export function reasoningVariants( + model: Pick, + target: Provider.Model, +): Provider.Model["variants"] { const options = model.reasoning_options if (options === undefined) return if (options.length === 0) return {} diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 32d4e3a39b10..7a3b3009bcdc 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -584,6 +584,33 @@ it.instance( }, ) +it.instance( + "model config reasoning options replace catalog variants under configured variants", + Effect.gen(function* () { + yield* set("ANTHROPIC_API_KEY", "test-api-key") + const providers = yield* list + const models = providers[ProviderV2.ID.anthropic].models + expect(Object.keys(models["claude-sonnet-4-6"].variants ?? {})).toEqual(["low", "high"]) + expect(models["claude-sonnet-4-6"].variants?.high).toMatchObject({ effort: "high", extra: true }) + expect(models["claude-fixed"].variants).toEqual({}) + }), + { + config: { + provider: { + anthropic: { + models: { + "claude-sonnet-4-6": { + reasoning_options: [{ type: "effort", values: ["low", "high"] }], + variants: { high: { extra: true } }, + }, + "claude-fixed": { id: "claude-sonnet-4-6", reasoning: true, reasoning_options: [] }, + }, + }, + }, + }, + }, +) + it.instance( "model config regenerates variants when overriding the provider package", Effect.gen(function* () { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 7c04dac47346..5ac9bd7f39f9 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1768,6 +1768,23 @@ export type ProviderConfig = { release_date?: string attachment?: boolean reasoning?: boolean + /** + * Reasoning controls the model accepts, used to build its variants + */ + reasoning_options?: Array< + | { + type: "effort" + values: Array + } + | { + type: "toggle" + } + | { + type: "budget_tokens" + min?: number + max?: number + } + > temperature?: boolean tool_call?: boolean interleaved?: diff --git a/packages/web/src/content/docs/models.mdx b/packages/web/src/content/docs/models.mdx index b135e7554d46..af2fa2ffa39e 100644 --- a/packages/web/src/content/docs/models.mdx +++ b/packages/web/src/content/docs/models.mdx @@ -195,6 +195,27 @@ You can override existing variants or add your own: } ``` +### Reasoning options + +To replace a model's built-in reasoning variants, list the reasoning controls it accepts in `reasoning_options`, using the [models.dev](https://models.dev) format. OpenCode builds the variants from them, then applies `variants` on top: + +```jsonc title="opencode.jsonc" +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "openai": { + "models": { + "gpt-5": { + "reasoning_options": [{ "type": "effort", "values": ["low", "medium", "high"] }], + }, + }, + }, + }, +} +``` + +Each option is `{ "type": "effort", "values": [...] }`, `{ "type": "budget_tokens", "min": ..., "max": ... }`, or `{ "type": "toggle" }`. Effort levels win over a budget. An empty list means the model has no reasoning variants. + ### Cycle variants Use the keybind `variant_cycle` to quickly switch between variants. [Learn more](/docs/keybinds). From 7b8d39f8c3f476608a8daaba55583e0c82751df4 Mon Sep 17 00:00:00 2001 From: vimtor Date: Fri, 16 Oct 2026 12:56:33 +0200 Subject: [PATCH 2/2] docs: drop reasoning_options from 1.x model docs --- packages/web/src/content/docs/models.mdx | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/packages/web/src/content/docs/models.mdx b/packages/web/src/content/docs/models.mdx index af2fa2ffa39e..b135e7554d46 100644 --- a/packages/web/src/content/docs/models.mdx +++ b/packages/web/src/content/docs/models.mdx @@ -195,27 +195,6 @@ You can override existing variants or add your own: } ``` -### Reasoning options - -To replace a model's built-in reasoning variants, list the reasoning controls it accepts in `reasoning_options`, using the [models.dev](https://models.dev) format. OpenCode builds the variants from them, then applies `variants` on top: - -```jsonc title="opencode.jsonc" -{ - "$schema": "https://opencode.ai/config.json", - "provider": { - "openai": { - "models": { - "gpt-5": { - "reasoning_options": [{ "type": "effort", "values": ["low", "medium", "high"] }], - }, - }, - }, - }, -} -``` - -Each option is `{ "type": "effort", "values": [...] }`, `{ "type": "budget_tokens", "min": ..., "max": ... }`, or `{ "type": "toggle" }`. Effort levels win over a budget. An empty list means the model has no reasoning variants. - ### Cycle variants Use the keybind `variant_cycle` to quickly switch between variants. [Learn more](/docs/keybinds).