From c8a13619cd715fbc1cc45c3b2638de570dd8abdd Mon Sep 17 00:00:00 2001 From: Aiden Date: Sun, 23 Aug 2026 16:53:55 +0000 Subject: [PATCH] fix(opencode): close tier precedence gaps --- packages/core/src/config/plugin/provider.ts | 20 +++++- packages/core/src/config/provider.ts | 5 ++ packages/core/src/plugin/models-dev.ts | 4 +- packages/core/src/plugin/provider/opencode.ts | 12 ++-- packages/core/src/v1/config/migrate.ts | 21 +++--- packages/core/test/config/provider.test.ts | 65 +++++++++++++++++++ packages/core/test/plugin/models-dev.test.ts | 7 +- packages/sdk/js/src/gen/types.gen.ts | 8 ++- 8 files changed, 121 insertions(+), 21 deletions(-) diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index 6f6e0528da7..bbe5718298f 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -91,7 +91,8 @@ export const Plugin = define({ } } if (config.cost !== undefined) { - model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({ + const input = Array.isArray(config.cost) ? config.cost : [config.cost] + const next = input.map((cost) => ({ tier: cost.tier && { ...cost.tier }, input: cost.input, output: cost.output, @@ -100,6 +101,23 @@ export const Plugin = define({ write: cost.cache?.write ?? 0, }, })) + const explicit = next.filter((cost) => cost.tier?.type === "context") + const existing = model.cost.filter((cost) => cost.tier?.type === "context") + const legacy = input.find((cost) => cost.tier === undefined)?.context_over_200k + model.cost = [ + ...next.filter((cost) => cost.tier === undefined), + ...(explicit.length > 0 ? explicit : existing), + ...(legacy && explicit.length === 0 && existing.length === 0 + ? [ + { + tier: { type: "context" as const, size: 200_000 }, + input: legacy.input, + output: legacy.output, + cache: { read: legacy.cache?.read ?? 0, write: legacy.cache?.write ?? 0 }, + }, + ] + : []), + ] } if (config.disabled !== undefined) model.enabled = !config.disabled if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit } diff --git a/packages/core/src/config/provider.ts b/packages/core/src/config/provider.ts index 1b547570783..00a4a2322f1 100644 --- a/packages/core/src/config/provider.ts +++ b/packages/core/src/config/provider.ts @@ -22,6 +22,11 @@ class Cost extends Schema.Class("ConfigV2.Model.Cost")({ input: Schema.Finite, output: Schema.Finite, cache: Cache.pipe(Schema.optional), + context_over_200k: Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + cache: Cache.pipe(Schema.optional), + }).pipe(Schema.optional), }) {} class Limit extends Schema.Class("ConfigV2.Model.Limit")({ diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index bfb01c261e9..d9ea2507296 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -51,8 +51,10 @@ function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] { function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"] | undefined) { if (!override) return base - const next = cost(override) const [baseDefault, ...baseTiers] = base + const next = cost( + baseTiers.length > 0 && !override.tiers?.length ? { ...override, context_over_200k: undefined } : override, + ) 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]) => ({ diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 781454d584c..5daf29021f5 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -2,7 +2,7 @@ import { Duration, Effect, Schema, Semaphore, Stream } from "effect" import type { Scope } from "effect" import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import type { CredentialValue } from "@opencode-ai/sdk/v2/types" +import type { CredentialValue, ModelV2Info } from "@opencode-ai/sdk/v2/types" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { EventV2 } from "../../event" import { Credential } from "../../credential" @@ -162,7 +162,10 @@ export const OpencodePlugin = define cost.tier?.type === "context"), + ) } model.status = config.status ?? "active" model.enabled = config.status !== "deprecated" @@ -224,7 +227,7 @@ function withoutCredentials(body: Readonly> | undefined) return Object.fromEntries(Object.entries(body ?? {}).filter(([key]) => key !== "apiKey" && key !== "headers")) } -function remoteCost(input: NonNullable<(typeof ConfigProviderV1.Model.Type)["cost"]>) { +function remoteCost(input: NonNullable<(typeof ConfigProviderV1.Model.Type)["cost"]>, tiers: ModelV2Info["cost"] = []) { const legacy = input.context_over_200k return [ { @@ -232,7 +235,8 @@ function remoteCost(input: NonNullable<(typeof ConfigProviderV1.Model.Type)["cos output: input.output, cache: { read: input.cache_read ?? 0, write: input.cache_write ?? 0 }, }, - ...(legacy + ...tiers, + ...(legacy && tiers.length === 0 ? [ { tier: { type: "context" as const, size: 200_000 }, diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index c3283b282cc..fddde8a24a7 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -194,23 +194,20 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st const lowerer = ConfigProviderOptionsV1.get(packageID) const request = info.options && lowerer.request(info.options) const legacy = info.cost?.context_over_200k - const costs = info.cost && [ - { - input: info.cost.input, - output: info.cost.output, - cache: { read: info.cost.cache_read, write: info.cost.cache_write }, - }, + const costs = info.cost && { + input: info.cost.input, + output: info.cost.output, + cache: { read: info.cost.cache_read, write: info.cost.cache_write }, ...(legacy - ? [ - { - tier: { type: "context" as const, size: 200_000 }, + ? { + context_over_200k: { input: legacy.input, output: legacy.output, cache: { read: legacy.cache_read, write: legacy.cache_write }, }, - ] - : []), - ] + } + : {}), + } const capabilities = info.tool_call !== undefined || info.modalities?.input !== undefined || info.modalities?.output !== undefined ? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] } diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 605bcd63cf9..ec5060948a8 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -55,6 +55,71 @@ function request(headers: Record, variant?: string) { const decode = Schema.decodeUnknownSync(Config.Info) describe("ConfigProviderPlugin.Plugin", () => { + it.effect("prefers catalog tiers over legacy config pricing", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const providerID = ProviderV2.ID.opencode + const modelID = ModelV2.ID.make("alpha-gpt-next") + const config = Config.Service.of({ + entries: () => + Effect.succeed([ + new Config.Document({ + type: "document", + info: decode({ + providers: { + opencode: { + models: { + "alpha-gpt-next": { + cost: [ + { input: 1, output: 2 }, + { + tier: { type: "context", size: 272_000 }, + input: 3, + output: 4, + cache: { read: 0.3 }, + }, + ], + }, + }, + }, + }, + }), + }), + new Config.Document({ + type: "document", + info: decode({ + providers: { + opencode: { + models: { + "alpha-gpt-next": { + cost: { + input: 5, + output: 6, + context_over_200k: { input: 100, output: 100 }, + }, + }, + }, + }, + }, + }), + }), + ]), + }) + + yield* addPlugin(config) + + expect(required(yield* catalog.model.get(providerID, modelID)).cost).toEqual([ + { input: 5, output: 6, cache: { read: 0, write: 0 }, tier: undefined }, + { + tier: { type: "context", size: 272_000 }, + input: 3, + output: 4, + cache: { read: 0.3, write: 0 }, + }, + ]) + }), + ) + it.effect("keeps configured model variant bodies unchanged", () => Effect.gen(function* () { const catalog = yield* Catalog.Service diff --git a/packages/core/test/plugin/models-dev.test.ts b/packages/core/test/plugin/models-dev.test.ts index 423be40cbd6..6cef52b0ae7 100644 --- a/packages/core/test/plugin/models-dev.test.ts +++ b/packages/core/test/plugin/models-dev.test.ts @@ -67,7 +67,12 @@ describe("ModelsDevPlugin", () => { experimental: { modes: { fast: { - cost: { input: 5, output: 30, cache_read: 0.5 }, + cost: { + input: 5, + output: 30, + cache_read: 0.5, + context_over_200k: { input: 100, output: 100 }, + }, provider: { headers: { "x-mode": "fast" }, body: { service_tier: "priority" }, diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 5e4fd890615..1ed3f23119f 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1489,14 +1489,18 @@ export type Model = { read: number write: number } - experimentalOver200K?: { + tiers?: Array<{ input: number output: number cache: { read: number write: number } - } + tier: { + type: "context" + size: number + } + }> } limit: { context: number