fix(core): expose models.dev modes as models (#34714)
This commit is contained in:
@@ -11,7 +11,7 @@ function released(date: string) {
|
|||||||
return Number.isFinite(time) ? time : 0
|
return Number.isFinite(time) ? time : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function cost(input: ModelsDev.Model["cost"]) {
|
function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
|
||||||
const base = {
|
const base = {
|
||||||
input: input?.input ?? 0,
|
input: input?.input ?? 0,
|
||||||
output: input?.output ?? 0,
|
output: input?.output ?? 0,
|
||||||
@@ -20,25 +20,57 @@ function cost(input: ModelsDev.Model["cost"]) {
|
|||||||
write: input?.cache_write ?? 0,
|
write: input?.cache_write ?? 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if (!input?.context_over_200k) return [base]
|
|
||||||
return [
|
return [
|
||||||
base,
|
base,
|
||||||
{
|
...(input?.tiers?.map((item) => ({
|
||||||
tier: {
|
tier: item.tier,
|
||||||
type: "context" as const,
|
input: item.input,
|
||||||
size: 200_000,
|
output: item.output,
|
||||||
},
|
|
||||||
input: input.context_over_200k.input,
|
|
||||||
output: input.context_over_200k.output,
|
|
||||||
cache: {
|
cache: {
|
||||||
read: input.context_over_200k.cache_read ?? 0,
|
read: item.cache_read ?? 0,
|
||||||
write: input.context_over_200k.cache_write ?? 0,
|
write: item.cache_write ?? 0,
|
||||||
},
|
},
|
||||||
},
|
})) ?? []),
|
||||||
|
...(input?.context_over_200k
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
tier: {
|
||||||
|
type: "context" as const,
|
||||||
|
size: 200_000,
|
||||||
|
},
|
||||||
|
input: input.context_over_200k.input,
|
||||||
|
output: input.context_over_200k.output,
|
||||||
|
cache: {
|
||||||
|
read: input.context_over_200k.cache_read ?? 0,
|
||||||
|
write: input.context_over_200k.cache_write ?? 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function variants(model: ModelsDev.Model, packageName: string | undefined): ModelV2Info["variants"] {
|
function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"] | undefined) {
|
||||||
|
if (!override) return base
|
||||||
|
const next = cost(override)
|
||||||
|
const [baseDefault, ...baseTiers] = base
|
||||||
|
const [nextDefault, ...nextTiers] = next
|
||||||
|
const tierKey = (item: ModelV2Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
|
||||||
|
const merge = (left: ModelV2Info["cost"][number], right: ModelV2Info["cost"][number]) => ({
|
||||||
|
...left,
|
||||||
|
...right,
|
||||||
|
tier: right.tier ?? left.tier,
|
||||||
|
cache: { ...left.cache, ...right.cache },
|
||||||
|
})
|
||||||
|
const tiers = new Map(baseTiers.map((item) => [tierKey(item), item]))
|
||||||
|
for (const item of nextTiers) {
|
||||||
|
const current = tiers.get(tierKey(item))
|
||||||
|
tiers.set(tierKey(item), current ? merge(current, item) : item)
|
||||||
|
}
|
||||||
|
return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
function reasoningVariants(model: ModelsDev.Model, packageName: string | undefined): ModelV2Info["variants"] {
|
||||||
const result = new Map<ModelV2.VariantID, ModelV2Info["variants"][number]>()
|
const result = new Map<ModelV2.VariantID, ModelV2Info["variants"][number]>()
|
||||||
if (packageName === "@ai-sdk/openai" || packageName === "@ai-sdk/openai-compatible") {
|
if (packageName === "@ai-sdk/openai" || packageName === "@ai-sdk/openai-compatible") {
|
||||||
const option = model.reasoning_options?.find((option) => option.type === "effort")
|
const option = model.reasoning_options?.find((option) => option.type === "effort")
|
||||||
@@ -56,17 +88,13 @@ function variants(model: ModelsDev.Model, packageName: string | undefined): Mode
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const [id, item] of Object.entries(model.experimental?.modes ?? {})) {
|
|
||||||
const variantID = ModelV2.VariantID.make(id)
|
|
||||||
result.set(variantID, {
|
|
||||||
id: variantID,
|
|
||||||
headers: { ...(item.provider?.headers ?? {}) },
|
|
||||||
body: { ...(item.provider?.body ?? {}) },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return [...result.values()]
|
return [...result.values()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function modeName(model: ModelsDev.Model, mode: string) {
|
||||||
|
return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
|
||||||
|
}
|
||||||
|
|
||||||
function mergeVariants(model: ModelV2Info, next: ModelV2Info["variants"]) {
|
function mergeVariants(model: ModelV2Info, next: ModelV2Info["variants"]) {
|
||||||
const existing = new Map(model.variants.map((variant) => [variant.id, variant]))
|
const existing = new Map(model.variants.map((variant) => [variant.id, variant]))
|
||||||
const nextIDs = new Set(next.map((variant) => variant.id))
|
const nextIDs = new Set(next.map((variant) => variant.id))
|
||||||
@@ -76,6 +104,50 @@ function mergeVariants(model: ModelV2Info, next: ModelV2Info["variants"]) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyModel(
|
||||||
|
draft: ModelV2Info,
|
||||||
|
model: ModelsDev.Model,
|
||||||
|
input: {
|
||||||
|
readonly name?: string
|
||||||
|
readonly cost?: ModelV2Info["cost"]
|
||||||
|
readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
|
||||||
|
readonly variants?: ModelV2Info["variants"]
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
draft.name = input.name ?? model.name
|
||||||
|
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
|
||||||
|
draft.api = model.provider?.npm
|
||||||
|
? {
|
||||||
|
id: ModelV2.ID.make(model.id),
|
||||||
|
type: "aisdk",
|
||||||
|
package: model.provider.npm,
|
||||||
|
url: model.provider.api,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
id: ModelV2.ID.make(model.id),
|
||||||
|
type: "native",
|
||||||
|
url: model.provider?.api,
|
||||||
|
settings: {},
|
||||||
|
}
|
||||||
|
draft.capabilities = {
|
||||||
|
tools: model.tool_call,
|
||||||
|
input: [...(model.modalities?.input ?? [])],
|
||||||
|
output: [...(model.modalities?.output ?? [])],
|
||||||
|
}
|
||||||
|
mergeVariants(draft, input.variants ?? [])
|
||||||
|
draft.time.released = released(model.release_date)
|
||||||
|
draft.cost = input.cost ?? cost(model.cost)
|
||||||
|
draft.status = model.status ?? "active"
|
||||||
|
draft.enabled = true
|
||||||
|
draft.limit = {
|
||||||
|
context: model.limit.context,
|
||||||
|
input: model.limit.input,
|
||||||
|
output: model.limit.output,
|
||||||
|
}
|
||||||
|
Object.assign(draft.request.headers, input.request?.headers ?? {})
|
||||||
|
Object.assign(draft.request.body, input.request?.body ?? {})
|
||||||
|
}
|
||||||
|
|
||||||
export const ModelsDevPlugin = define({
|
export const ModelsDevPlugin = define({
|
||||||
id: "models-dev",
|
id: "models-dev",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
@@ -120,39 +192,19 @@ export const ModelsDevPlugin = define({
|
|||||||
})
|
})
|
||||||
|
|
||||||
for (const model of Object.values(item.models)) {
|
for (const model of Object.values(item.models)) {
|
||||||
const modelID = ModelV2.ID.make(model.id)
|
const baseCost = cost(model.cost)
|
||||||
catalog.model.update(providerID, modelID, (draft) => {
|
const variants = reasoningVariants(model, model.provider?.npm ?? item.npm)
|
||||||
draft.name = model.name
|
catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost, variants }))
|
||||||
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
|
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
|
||||||
draft.api = model.provider?.npm
|
catalog.model.update(providerID, `${model.id}-${mode}`, (draft) =>
|
||||||
? {
|
applyModel(draft, model, {
|
||||||
id: draft.api.id,
|
name: modeName(model, mode),
|
||||||
type: "aisdk",
|
cost: mergeCost(baseCost, options.cost),
|
||||||
package: model.provider?.npm,
|
request: options.provider,
|
||||||
url: model.provider.api,
|
variants,
|
||||||
}
|
}),
|
||||||
: {
|
)
|
||||||
id: draft.api.id,
|
}
|
||||||
type: "native",
|
|
||||||
url: model.provider?.api,
|
|
||||||
settings: {},
|
|
||||||
}
|
|
||||||
draft.capabilities = {
|
|
||||||
tools: model.tool_call,
|
|
||||||
input: [...(model.modalities?.input ?? [])],
|
|
||||||
output: [...(model.modalities?.output ?? [])],
|
|
||||||
}
|
|
||||||
mergeVariants(draft, variants(model, model.provider?.npm ?? item.npm))
|
|
||||||
draft.time.released = released(model.release_date)
|
|
||||||
draft.cost = cost(model.cost)
|
|
||||||
draft.status = model.status ?? "active"
|
|
||||||
draft.enabled = true
|
|
||||||
draft.limit = {
|
|
||||||
context: model.limit.context,
|
|
||||||
input: model.limit.input,
|
|
||||||
output: model.limit.output,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -32,6 +32,103 @@ const layer = Layer.mergeAll(catalog.pipe(Layer.provide(connections)), integrati
|
|||||||
const it = testEffect(layer)
|
const it = testEffect(layer)
|
||||||
|
|
||||||
describe("ModelsDevPlugin", () => {
|
describe("ModelsDevPlugin", () => {
|
||||||
|
it.effect("projects models.dev modes as separate models instead of variants", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const models = ModelsDev.Service.of({
|
||||||
|
get: () =>
|
||||||
|
Effect.succeed({
|
||||||
|
acme: {
|
||||||
|
id: "acme",
|
||||||
|
name: "Acme",
|
||||||
|
env: [],
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
api: "https://api.acme.test/v1",
|
||||||
|
models: {
|
||||||
|
"gpt-5.4": {
|
||||||
|
id: "gpt-5.4",
|
||||||
|
name: "GPT-5.4",
|
||||||
|
family: "gpt",
|
||||||
|
release_date: "2026-01-01",
|
||||||
|
attachment: false,
|
||||||
|
reasoning: true,
|
||||||
|
temperature: true,
|
||||||
|
tool_call: true,
|
||||||
|
cost: {
|
||||||
|
input: 2.5,
|
||||||
|
output: 15,
|
||||||
|
tiers: [
|
||||||
|
{
|
||||||
|
tier: { type: "context", size: 272_000 },
|
||||||
|
input: 3,
|
||||||
|
output: 18,
|
||||||
|
cache_read: 0.25,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 },
|
||||||
|
},
|
||||||
|
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
|
||||||
|
experimental: {
|
||||||
|
modes: {
|
||||||
|
fast: {
|
||||||
|
cost: { input: 5, output: 30, cache_read: 0.5 },
|
||||||
|
provider: {
|
||||||
|
headers: { "x-mode": "fast" },
|
||||||
|
body: { service_tier: "priority" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Record<string, ModelsDev.Provider>),
|
||||||
|
refresh: () => Effect.void,
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* ModelsDevPlugin.effect(
|
||||||
|
host({
|
||||||
|
catalog: catalogHost(catalog),
|
||||||
|
integration: integrationHost(integrations),
|
||||||
|
}),
|
||||||
|
).pipe(Effect.provideService(ModelsDev.Service, models))
|
||||||
|
|
||||||
|
const providerID = ProviderV2.ID.make("acme")
|
||||||
|
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(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" },
|
||||||
|
},
|
||||||
|
variants: [],
|
||||||
|
})
|
||||||
|
expect(fast?.cost).toEqual([
|
||||||
|
{ input: 5, output: 30, cache: { read: 0.5, write: 0 } },
|
||||||
|
{
|
||||||
|
tier: { type: "context", size: 272_000 },
|
||||||
|
input: 3,
|
||||||
|
output: 18,
|
||||||
|
cache: { read: 0.25, write: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tier: { type: "context", size: 200_000 },
|
||||||
|
input: 5,
|
||||||
|
output: 22.5,
|
||||||
|
cache: { read: 0.5, write: 0 },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("registers key methods for providers with environment variables", () =>
|
it.effect("registers key methods for providers with environment variables", () =>
|
||||||
Effect.acquireUseRelease(
|
Effect.acquireUseRelease(
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user