feat: drive reasoning variants from models.dev reasoning_options
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.
This commit is contained in:
parent
c939aa04fe
commit
f7dcfc2680
15 changed files with 954 additions and 62 deletions
116
packages/core/test/models-dev-plugin.test.ts
Normal file
116
packages/core/test/models-dev-plugin.test.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { location } from "./fixture/location"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const model = (input: Partial<ModelsDev.Model> & { id: string }): ModelsDev.Model => ({
|
||||
name: input.id,
|
||||
release_date: "2026-01-01",
|
||||
attachment: false,
|
||||
reasoning: true,
|
||||
temperature: true,
|
||||
tool_call: true,
|
||||
limit: { context: 200_000, output: 64_000 },
|
||||
...input,
|
||||
})
|
||||
|
||||
const fixture: Record<string, ModelsDev.Provider> = {
|
||||
anthropic: {
|
||||
id: "anthropic",
|
||||
name: "Anthropic",
|
||||
env: [],
|
||||
npm: "@ai-sdk/anthropic",
|
||||
models: {
|
||||
"claude-sonnet-4-6": model({
|
||||
id: "claude-sonnet-4-6",
|
||||
reasoning_options: [
|
||||
{ type: "effort", values: ["low", "medium", "high", "max"] },
|
||||
{ type: "budget_tokens", min: 1024 },
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
compat: {
|
||||
id: "compat",
|
||||
name: "Compat",
|
||||
env: [],
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
models: {
|
||||
"deepseek-v4": model({
|
||||
id: "deepseek-v4",
|
||||
reasoning_options: [{ type: "toggle" }, { type: "effort", values: [null, "high", "max"] }],
|
||||
experimental: {
|
||||
modes: {
|
||||
high: { provider: { body: { reasoning_effort: "high", custom: true } } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
|
||||
)
|
||||
const modelsDevLayer = Layer.succeed(
|
||||
ModelsDev.Service,
|
||||
ModelsDev.Service.of({
|
||||
get: () => Effect.succeed(fixture),
|
||||
refresh: () => Effect.void,
|
||||
}),
|
||||
)
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(modelsDevLayer, Catalog.locationLayer).pipe(
|
||||
Layer.provideMerge(EventV2.defaultLayer),
|
||||
Layer.provideMerge(locationLayer),
|
||||
),
|
||||
)
|
||||
|
||||
describe("ModelsDevPlugin reasoning_options", () => {
|
||||
it.effect("generates anthropic effort variants as semantic thinking + effort options", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* ModelsDevPlugin.effect
|
||||
const catalog = yield* Catalog.Service
|
||||
const info = yield* catalog.model.get(ProviderV2.ID.make("anthropic"), ModelV2.ID.make("claude-sonnet-4-6"))
|
||||
expect(info.variants.map((variant) => variant.id)).toEqual(
|
||||
["low", "medium", "high", "max"].map((id) => ModelV2.VariantID.make(id)),
|
||||
)
|
||||
expect(info.variants[2]).toMatchObject({
|
||||
id: "high",
|
||||
headers: {},
|
||||
body: {},
|
||||
options: { thinking: { type: "adaptive" }, effort: "high" },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("merges effort variants after curated experimental modes, skipping null values and collisions", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* ModelsDevPlugin.effect
|
||||
const catalog = yield* Catalog.Service
|
||||
const info = yield* catalog.model.get(ProviderV2.ID.make("compat"), ModelV2.ID.make("deepseek-v4"))
|
||||
expect(info.variants.map((variant) => variant.id)).toEqual(["high", "max"].map((id) => ModelV2.VariantID.make(id)))
|
||||
// curated mode wins the "high" id; its body keys survive partitioning
|
||||
expect(info.variants[0]).toMatchObject({
|
||||
id: "high",
|
||||
body: { custom: true },
|
||||
options: { reasoningEffort: "high" },
|
||||
})
|
||||
// data-driven effort variant for "max" uses the openai-compatible encoding
|
||||
expect(info.variants[1]).toMatchObject({
|
||||
id: "max",
|
||||
body: {},
|
||||
options: { reasoningEffort: "max" },
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue