diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index 964e4b1ac7..3a17468a19 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -46,7 +46,7 @@ const Cost = Schema.Struct({ const ReasoningEffortValue = Schema.Union([Schema.Null, Schema.String]) -export const ReasoningOption = Schema.Union([ +export const KnownReasoningOption = Schema.Union([ Schema.Struct({ type: Schema.Literal("effort"), values: Schema.Array(ReasoningEffortValue), @@ -60,6 +60,14 @@ export const ReasoningOption = Schema.Union([ max: Schema.optional(Schema.Finite), }), ]) +export type KnownReasoningOption = typeof KnownReasoningOption.Type + +export const ReasoningOption = Schema.Union([ + KnownReasoningOption, + Schema.Struct({ + type: Schema.String, + }), +]) export type ReasoningOption = typeof ReasoningOption.Type export const Model = Schema.Struct({ @@ -69,9 +77,7 @@ 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. - // Keep the fetched value opaque here; provider normalization extracts the known subset. - reasoning_options: Schema.optional(Schema.Unknown), + reasoning_options: Schema.optional(Schema.Array(ReasoningOption)), temperature: Schema.Boolean, tool_call: Schema.Boolean, interleaved: Schema.optional( diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index b76ea6f9c7..220b0144e1 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1022,7 +1022,7 @@ export const Model = Schema.Struct({ name: Schema.String, family: optional(Schema.String), capabilities: ProviderCapabilities, - reasoning_options: optional(Schema.Array(ModelsDev.ReasoningOption)), + reasoning_options: optional(Schema.Array(ModelsDev.KnownReasoningOption)), cost: ProviderCost, limit: ProviderLimit, status: ModelStatus, diff --git a/packages/opencode/test/provider/model-status.test.ts b/packages/opencode/test/provider/model-status.test.ts index ba6f360b15..56bfd8693d 100644 --- a/packages/opencode/test/provider/model-status.test.ts +++ b/packages/opencode/test/provider/model-status.test.ts @@ -59,7 +59,7 @@ describe("provider model status schemas", () => { ).toBe("active") }) - test("keeps models.dev reasoning options opaque for forward compatibility", () => { + test("accepts future models.dev reasoning options without rejecting known structure", () => { expect(() => Schema.decodeUnknownSync(ModelsDev.Model)({ id: "test-model", @@ -67,10 +67,11 @@ describe("provider model status schemas", () => { release_date: "2026-01-01", attachment: false, reasoning: true, - reasoning_options: { - future: "shape", - values: [{ type: "new_control", values: ["turbo"] }], - }, + reasoning_options: [ + { type: "effort", values: ["turbo"] }, + { type: "toggle", modes: ["auto", "off"] }, + { type: "new_control", values: ["turbo"] }, + ], temperature: true, tool_call: true, limit: { context: 128000, output: 8192 }, diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index f56f1c96f3..5c8414b1ab 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1463,21 +1463,12 @@ test("models.dev normalization ignores unknown reasoning options and passes new reasoning: true, reasoning_options: [ { type: "future_control", values: ["turbo"] }, - { type: "effort", values: ["turbo", 123, null] }, - { type: "budget_tokens", min: "1024", max: 16_000 }, - "invalid", + { type: "effort", values: ["turbo", null] }, + { type: "budget_tokens", max: 16_000 }, ], cost: { input: 1, output: 2 }, limit: { context: 128_000, output: 32_000 }, }, - changed: { - id: "changed", - name: "Changed", - reasoning: true, - reasoning_options: { type: "effort", values: ["turbo"] }, - cost: { input: 1, output: 2 }, - limit: { context: 128_000, output: 32_000 }, - }, future: { id: "future", name: "Future", @@ -1495,7 +1486,6 @@ test("models.dev normalization ignores unknown reasoning options and passes new { type: "budget_tokens", max: 16_000 }, ]) expect(models.reasoner.variants).toEqual({ turbo: { reasoningEffort: "turbo" } }) - expect(models.changed.reasoning_options).toBeUndefined() expect(models.future.reasoning_options).toBeUndefined() })