From e8c63dbf83f2882cdc104a6e7b3a2b231662b41e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 9 Jun 2026 19:39:42 -0500 Subject: [PATCH] fix(core): type models.dev reasoning options --- packages/core/src/config/plugin/provider.ts | 4 +- packages/core/src/model.ts | 3 +- packages/core/src/models-dev.ts | 57 ++++++++++++++++++- packages/core/src/plugin/models-dev.ts | 2 +- packages/core/test/plugin/models-dev.test.ts | 20 +++---- packages/opencode/src/provider/provider.ts | 6 +- .../opencode/test/provider/provider.test.ts | 8 ++- packages/sdk/js/src/v2/gen/types.gen.ts | 51 ++++++++++++++--- 8 files changed, 122 insertions(+), 29 deletions(-) diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index c7951b9954..3c33131131 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -48,7 +48,9 @@ export const Plugin = PluginV2.define({ if (config.capabilities !== undefined) { model.capabilities = { tools: config.capabilities.tools, - reasoningOptions: config.capabilities.reasoningOptions?.map((option) => ({ ...option })), + reasoningOptions: config.capabilities.reasoningOptions?.map((option) => + option.type === "effort" ? { ...option, values: [...option.values] } : { ...option }, + ), input: [...config.capabilities.input], output: [...config.capabilities.output], } diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index e847b79dda..17a6e56ac1 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -2,6 +2,7 @@ import { DateTime, Schema } from "effect" import { DateTimeUtcFromMillis } from "effect/Schema" import { ProviderV2 } from "./provider" import { ModelRequest } from "./model-request" +import { ModelsDev } from "./models-dev" export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID")) export type ID = typeof ID.Type @@ -15,7 +16,7 @@ export type Family = typeof Family.Type export const Capabilities = Schema.Struct({ tools: Schema.Boolean, - reasoningOptions: Schema.Array(Schema.Record(Schema.String, Schema.Any)).pipe(Schema.optional), + reasoningOptions: Schema.Array(ModelsDev.ResolvedReasoningOption).pipe(Schema.optional), // mime patterns, image, audio, video/*, text/* input: Schema.String.pipe(Schema.Array), output: Schema.String.pipe(Schema.Array), diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index 0097d7930c..e0c248c7b3 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -43,11 +43,64 @@ const Cost = Schema.Struct({ ), }) -export const ReasoningOption = Schema.StructWithRest(Schema.Struct({ type: Schema.String }), [ - Schema.Record(Schema.String, Schema.MutableJson), +const reasoningOption = (fields: Fields) => + Schema.StructWithRest(Schema.Struct(fields), [Schema.Record(Schema.String, Schema.MutableJson)]) + +export const ReasoningOption = Schema.Union([ + reasoningOption({ type: Schema.Literal("toggle") }), + reasoningOption({ + type: Schema.Literal("effort"), + values: Schema.Array(Schema.NullOr(Schema.String)), + }), + reasoningOption({ + type: Schema.Literal("budget_tokens"), + min: Schema.optional(Schema.Finite), + max: Schema.optional(Schema.Finite), + }), + reasoningOption({ type: Schema.String }), ]) export type ReasoningOption = typeof ReasoningOption.Type +export const ResolvedReasoningOption = Schema.Union([ + Schema.Struct({ type: Schema.Literal("toggle") }), + Schema.Struct({ + type: Schema.Literal("effort"), + values: Schema.Array(Schema.String), + }), + Schema.Struct({ + type: Schema.Literal("budget_tokens"), + min: Schema.optional(Schema.Finite), + max: Schema.optional(Schema.Finite), + }), +]) +export type ResolvedReasoningOption = + | { type: "toggle" } + | { type: "effort"; values: string[] } + | { type: "budget_tokens"; min?: number; max?: number } + +export function resolveReasoningOptions( + options: readonly ReasoningOption[] | undefined, +): ResolvedReasoningOption[] | undefined { + if (!options) return + return options + .map((option): ResolvedReasoningOption | undefined => { + if (option.type === "toggle") return { type: "toggle" } + if (option.type === "effort" && Array.isArray(option.values)) { + return { + type: "effort", + values: option.values.filter((value): value is string => typeof value === "string"), + } + } + if (option.type !== "budget_tokens") return + return { + type: "budget_tokens", + ...(typeof option.min === "number" && Number.isFinite(option.min) ? { min: option.min } : {}), + ...(typeof option.max === "number" && Number.isFinite(option.max) ? { max: option.max } : {}), + } + }) + .filter((option): option is ResolvedReasoningOption => option !== undefined) +} + export const Model = Schema.Struct({ id: Schema.String, name: Schema.String, diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index c6e06ef735..3da810a4a2 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -99,7 +99,7 @@ export const ModelsDevPlugin = PluginV2.define({ } draft.capabilities = { tools: model.tool_call, - reasoningOptions: model.reasoning_options?.map((option) => ({ ...option })), + reasoningOptions: ModelsDev.resolveReasoningOptions(model.reasoning_options), input: [...(model.modalities?.input ?? [])], output: [...(model.modalities?.output ?? [])], } diff --git a/packages/core/test/plugin/models-dev.test.ts b/packages/core/test/plugin/models-dev.test.ts index db0603ade3..81b7363384 100644 --- a/packages/core/test/plugin/models-dev.test.ts +++ b/packages/core/test/plugin/models-dev.test.ts @@ -12,7 +12,9 @@ import { location } from "../fixture/location" import { testEffect } from "../lib/effect" const reasoningOptions: ModelsDev.ReasoningOption[] = [ + { type: "toggle" }, { type: "effort", values: [null, "low", "ultrathink"], default: "low" }, + { type: "budget_tokens", min: 1024, future: true }, { type: "future_dynamic_budget", curve: { min: 1, max: 10 }, enabled: true }, ] @@ -47,22 +49,20 @@ const locationLayer = Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") })), ) -const catalog = Catalog.locationLayer.pipe( - Layer.provideMerge(EventV2.defaultLayer), - Layer.provideMerge(locationLayer), -) +const catalog = Catalog.locationLayer.pipe(Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge(locationLayer)) const it = testEffect(Layer.merge(catalog, modelsDev)) describe("ModelsDevPlugin", () => { - it.effect("preserves reasoning options in V2 model capabilities", () => + it.effect("maps known reasoning options into typed V2 capabilities", () => Effect.gen(function* () { yield* ModelsDevPlugin.effect - const model = yield* (yield* Catalog.Service).model.get( - ProviderV2.ID.make("acme"), - ModelV2.ID.make("acme-1"), - ) + const model = yield* (yield* Catalog.Service).model.get(ProviderV2.ID.make("acme"), ModelV2.ID.make("acme-1")) - expect(model.capabilities.reasoningOptions).toEqual(reasoningOptions) + expect(model.capabilities.reasoningOptions).toEqual([ + { type: "toggle" }, + { type: "effort", values: ["low", "ultrathink"] }, + { type: "budget_tokens", min: 1024 }, + ]) }), ) }) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index a4909559af..a13c4e76d7 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -957,12 +957,10 @@ const ProviderInterleaved = Schema.Union([ }), ]) -const ProviderReasoningOption = Schema.Record(Schema.String, Schema.Any) - const ProviderCapabilities = Schema.Struct({ temperature: Schema.Boolean, reasoning: Schema.Boolean, - reasoningOptions: optionalOmitUndefined(Schema.Array(ProviderReasoningOption)), + reasoningOptions: optionalOmitUndefined(Schema.Array(ModelsDev.ResolvedReasoningOption)), attachment: Schema.Boolean, toolcall: Schema.Boolean, input: ProviderModalities, @@ -1181,7 +1179,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model capabilities: { temperature: model.temperature ?? false, reasoning: model.reasoning ?? false, - reasoningOptions: model.reasoning_options?.map((option) => ({ ...option })), + reasoningOptions: ModelsDev.resolveReasoningOptions(model.reasoning_options), attachment: model.attachment ?? false, toolcall: model.tool_call ?? true, input: { diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index d8bbcfee11..6f5a15cf70 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1287,7 +1287,9 @@ test("mode cost preserves over-200k pricing from base model", () => { test("models.dev normalization fills required response fields", () => { const reasoningOptions: ModelsDev.ReasoningOption[] = [ + { type: "toggle" }, { type: "effort", values: [null, "low", "ultrathink"], default: "low" }, + { type: "budget_tokens", min: 1024, future: true }, { type: "future_dynamic_budget", curve: { min: 1, max: 10 } }, ] const provider = { @@ -1310,7 +1312,11 @@ test("models.dev normalization fills required response fields", () => { expect(model.api.url).toBe("") expect(model.capabilities.temperature).toBe(false) expect(model.capabilities.reasoning).toBe(false) - expect(model.capabilities.reasoningOptions).toEqual(reasoningOptions) + expect(model.capabilities.reasoningOptions).toEqual([ + { type: "toggle" }, + { type: "effort", values: ["low", "ultrathink"] }, + { type: "budget_tokens", min: 1024 }, + ]) expect(model.capabilities.attachment).toBe(false) expect(model.capabilities.toolcall).toBe(true) expect(model.release_date).toBe("") diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index b7ada8a938..2bded90cca 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2082,9 +2082,20 @@ export type Model = { capabilities: { temperature: boolean reasoning: boolean - reasoningOptions?: Array<{ - [key: string]: unknown - }> + reasoningOptions?: Array< + | { + type: "toggle" + } + | { + type: "effort" + values: Array + } + | { + type: "budget_tokens" + min?: number + max?: number + } + > attachment: boolean toolcall: boolean input: { @@ -2881,9 +2892,20 @@ export type ModelV2Info = { } capabilities: { tools: boolean - reasoningOptions?: Array<{ - [key: string]: unknown - }> + reasoningOptions?: Array< + | { + type: "toggle" + } + | { + type: "effort" + values: Array + } + | { + type: "budget_tokens" + min?: number + max?: number + } + > input: Array output: Array } @@ -4236,9 +4258,20 @@ export type ModelV2Info1 = { } capabilities: { tools: boolean - reasoningOptions?: Array<{ - [key: string]: unknown - }> + reasoningOptions?: Array< + | { + type: "toggle" + } + | { + type: "effort" + values: Array + } + | { + type: "budget_tokens" + min?: number + max?: number + } + > input: Array output: Array }