Parse the curated reasoning_options field from models.dev api.json and use its effort values to generate reasoning variants instead of the hardcoded per-package tables, in both the v1 provider catalog and the v2 catalog plugin. - core: ModelsDev.ReasoningOption discriminated union (toggle | effort | budget_tokens); effort values stay open strings and unknown option types are tolerated since api.json is cast, not decoded - core: ReasoningVariants shared per-package effort encoder used by v1 ProviderTransform.variants and the v2 ModelsDevPlugin - core: v2 catalog generates effort variants from reasoning_options; curated experimental modes win id collisions; anthropic profile gains the effort semantic - llm: anthropic protocol supports adaptive thinking, lowers effort to output_config.effort, and sends the effort-2025-11-24 beta header - opencode: resolved Provider.Model carries capabilities.reasoningOptions (unknown types and null effort values dropped at the mapping boundary); config models accept reasoning_options; models without usable effort data fall back to the hardcoded tables unchanged Catalog-wide audit vs live api.json: 2975 models byte-identical, 38 diffs, all data correcting stale hardcoded effort lists.
68 lines
2.7 KiB
TypeScript
68 lines
2.7 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { Schema } from "effect"
|
|
import { ConfigProviderV1 } from "@opencode-ai/core/v1/config/provider"
|
|
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
|
import { Provider } from "@/provider/provider"
|
|
|
|
const options = [
|
|
{ type: "toggle" },
|
|
{ type: "effort", values: [null, "low", "medium", "high", "xhigh", "max", "ultrathink"] },
|
|
{ type: "budget_tokens", min: 1024 },
|
|
{ type: "budget_tokens", min: 0, max: 24_576 },
|
|
]
|
|
|
|
describe("reasoning_options schemas", () => {
|
|
test("models.dev model schema decodes all known option shapes", () => {
|
|
const model = Schema.decodeUnknownSync(ModelsDev.Model)({
|
|
id: "test-model",
|
|
name: "Test Model",
|
|
release_date: "2026-01-01",
|
|
attachment: false,
|
|
reasoning: true,
|
|
reasoning_options: options,
|
|
temperature: true,
|
|
tool_call: true,
|
|
limit: { context: 128000, output: 8192 },
|
|
})
|
|
expect(model.reasoning_options).toEqual(options as typeof model.reasoning_options)
|
|
})
|
|
|
|
test("config model schema decodes reasoning_options", () => {
|
|
const model = Schema.decodeUnknownSync(ConfigProviderV1.Model)({ reasoning_options: options })
|
|
expect(model.reasoning_options).toEqual(options as typeof model.reasoning_options)
|
|
})
|
|
|
|
test("provider capabilities decode reasoningOptions", () => {
|
|
// The resolved catalog never carries null effort values; they are stripped
|
|
// when mapping models.dev data, so the resolved schema rejects them.
|
|
const resolved = [
|
|
{ type: "toggle" },
|
|
{ type: "effort", values: ["low", "medium", "high", "xhigh", "max", "ultrathink"] },
|
|
{ type: "budget_tokens", min: 1024 },
|
|
{ type: "budget_tokens", min: 0, max: 24_576 },
|
|
]
|
|
const capabilities = Schema.decodeUnknownSync(Provider.Model.fields.capabilities)({
|
|
temperature: true,
|
|
reasoning: true,
|
|
reasoningOptions: resolved,
|
|
attachment: false,
|
|
toolcall: true,
|
|
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
interleaved: false,
|
|
})
|
|
expect(capabilities.reasoningOptions).toEqual(resolved as typeof capabilities.reasoningOptions)
|
|
expect(() =>
|
|
Schema.decodeUnknownSync(Provider.Model.fields.capabilities)({
|
|
temperature: true,
|
|
reasoning: true,
|
|
reasoningOptions: [{ type: "effort", values: [null, "low"] }],
|
|
attachment: false,
|
|
toolcall: true,
|
|
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
interleaved: false,
|
|
}),
|
|
).toThrow()
|
|
})
|
|
})
|