feat(provider): load catalog request variants

This commit is contained in:
Aiden Cline 2026-07-21 22:58:11 -05:00
commit 75ab5ceae9
11 changed files with 207 additions and 243 deletions

View file

@ -69,6 +69,7 @@ export const Model = Schema.Struct({
temperature: Schema.Boolean,
tool_call: Schema.Boolean,
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
variants: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.MutableJson))),
interleaved: Schema.optional(
Schema.Union([
Schema.Literal(true),
@ -109,7 +110,13 @@ export const Model = Schema.Struct({
),
status: Schema.optional(CatalogModelStatus),
provider: Schema.optional(
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
Schema.Struct({
npm: Schema.optional(Schema.String),
api: Schema.optional(Schema.String),
variant: Schema.optional(Schema.String),
body: Schema.optional(Schema.Record(Schema.String, Schema.MutableJson)),
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
}),
),
})
export type Model = Schema.Schema.Type<typeof Model>

View file

@ -3,6 +3,7 @@ import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect"
import { EventV2 } from "../event"
import { ModelsDev } from "../models-dev"
import { ModelV2 } from "../model"
import { ProviderV2 } from "../provider"
function released(date: string) {
@ -102,7 +103,11 @@ function applyModel(
input: [...(model.modalities?.input ?? [])],
output: [...(model.modalities?.output ?? [])],
}
draft.variants = []
draft.variants = Object.entries(model.variants ?? {}).map(([id, body]) => ({
id: ModelV2.VariantID.make(id),
headers: {},
body: { ...body },
}))
draft.time.released = released(model.release_date)
draft.cost = input.cost ?? cost(model.cost)
draft.status = model.status ?? "active"
@ -112,6 +117,9 @@ function applyModel(
input: model.limit.input,
output: model.limit.output,
}
if (model.provider?.variant !== undefined) draft.request.variant = ModelV2.VariantID.make(model.provider.variant)
Object.assign(draft.request.headers, model.provider?.headers ?? {})
Object.assign(draft.request.body, model.provider?.body ?? {})
Object.assign(draft.request.headers, input.request?.headers ?? {})
Object.assign(draft.request.body, input.request?.body ?? {})
}

View file

@ -48,8 +48,13 @@ describe("ModelsDevPlugin", () => {
release_date: "2026-01-01",
attachment: false,
reasoning: true,
reasoning_options: [{ type: "toggle" }],
temperature: true,
tool_call: true,
variants: {
none: { thinking: { type: "disabled" } },
thinking: { thinking: { type: "adaptive" } },
},
cost: {
input: 2.5,
output: 15,
@ -64,6 +69,11 @@ describe("ModelsDevPlugin", () => {
context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 },
},
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
provider: {
variant: "thinking",
body: { thinking: { type: "adaptive" } },
headers: { "x-model": "gpt-5.4" },
},
experimental: {
modes: {
fast: {
@ -93,18 +103,29 @@ describe("ModelsDevPlugin", () => {
const base = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4"))
const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast"))
expect(base?.variants).toEqual([])
expect(base?.request.body).toEqual({})
expect(base?.variants).toEqual([
{ id: ModelV2.VariantID.make("none"), headers: {}, body: { thinking: { type: "disabled" } } },
{ id: ModelV2.VariantID.make("thinking"), headers: {}, body: { thinking: { type: "adaptive" } } },
])
expect(base?.request).toEqual({
variant: ModelV2.VariantID.make("thinking"),
headers: { "x-model": "gpt-5.4" },
body: { thinking: { type: "adaptive" } },
})
expect(fast).toMatchObject({
id: "gpt-5.4-fast",
providerID: "acme",
name: "GPT-5.4 Fast",
api: { id: "gpt-5.4" },
request: {
headers: { "x-mode": "fast" },
body: { service_tier: "priority" },
variant: ModelV2.VariantID.make("thinking"),
headers: { "x-model": "gpt-5.4", "x-mode": "fast" },
body: { thinking: { type: "adaptive" }, service_tier: "priority" },
},
variants: [],
variants: [
{ id: ModelV2.VariantID.make("none"), headers: {}, body: { thinking: { type: "disabled" } } },
{ id: ModelV2.VariantID.make("thinking"), headers: {}, body: { thinking: { type: "adaptive" } } },
],
})
expect(fast?.cost).toEqual([
{ input: 5, output: 30, cache: { read: 0.5, write: 0 } },

View file

@ -22,7 +22,7 @@ type Api =
}
| { readonly type: "native"; readonly url?: string; readonly settings: Record<string, unknown> }
const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
const model = (api: Api, variants: ModelV2.Info["variants"] = [], variant?: ModelV2.VariantID) =>
ModelV2.Info.make({
id: ModelV2.ID.make("test-model"),
providerID: ProviderV2.ID.make("test-provider"),
@ -32,6 +32,7 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
request: {
headers: { "x-test": "header" },
body: { apiKey: "secret", custom_extension: { enabled: true } },
...(variant ? { variant } : {}),
},
variants,
time: { released: 0 },
@ -175,6 +176,44 @@ describe("SessionRunnerModel", () => {
}),
)
it.effect("uses the catalog default Session variant", () =>
Effect.gen(function* () {
const catalog = model(
{ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" },
[
{
id: ModelV2.VariantID.make("none"),
headers: {},
body: { thinking: { type: "disabled" } },
},
{
id: ModelV2.VariantID.make("thinking"),
headers: {},
body: { thinking: { type: "adaptive" } },
},
],
ModelV2.VariantID.make("thinking"),
)
const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_default_variant"),
projectID: ProjectV2.ID.global,
title: "test",
model: { id: catalog.id, providerID: catalog.providerID },
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location: { directory: AbsolutePath.make("/project") },
})
const resolved = yield* SessionRunnerModel.resolve(session, catalog)
expect(resolved.route.defaults.http?.body).toEqual({
custom_extension: { enabled: true },
thinking: { type: "adaptive" },
})
}),
)
it.effect("rejects an explicit unavailable Session variant during model resolution", () =>
Effect.gen(function* () {
const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" })