From 779fef5160e188afc7657f26627ccec69d3c1a9b Mon Sep 17 00:00:00 2001 From: pxys-io Date: Fri, 3 Apr 2026 05:37:34 +0200 Subject: [PATCH] feat: support context_tiers for arbitrary tiered pricing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update cost calculation to use the new context_tiers array from models.dev instead of the hardcoded 200K threshold. - Add contextTiers to Model cost schema (provider.ts) - Parse context_tiers from models.dev ModelsDev schema (models.ts) - Replace hardcoded 200_000 check in session/index.ts with dynamic tier selection — picks highest tier where totalInputTokens >= min_context - Backward compatible: falls back to experimentalOver200K for legacy models Companion PR: anomalyco/models.dev#1324 Co-authored-by: Qwen-Coder --- packages/opencode/src/provider/models.ts | 18 ++++++++++++ packages/opencode/src/provider/provider.ts | 34 ++++++++++++++++++++++ packages/opencode/src/session/index.ts | 26 ++++++++++++++--- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index c6ab5d8365c1..8df923d420e8 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -47,6 +47,9 @@ export namespace ModelsDev { output: z.number(), cache_read: z.number().optional(), cache_write: z.number().optional(), + /** + * @deprecated Use `context_tiers` instead. + */ context_over_200k: z .object({ input: z.number(), @@ -55,6 +58,21 @@ export namespace ModelsDev { cache_write: z.number().optional(), }) .optional(), + /** + * Ordered array of pricing tiers that apply when total input tokens + * (input + cache_read) meet or exceed the tier's min_context threshold. + */ + context_tiers: z + .array( + z.object({ + min_context: z.number(), + input: z.number(), + output: z.number(), + cache_read: z.number().optional(), + cache_write: z.number().optional(), + }), + ) + .optional(), }) .optional(), limit: z.object({ diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index afd69f9e7072..bff49dd56701 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -832,6 +832,10 @@ export namespace Provider { read: z.number(), write: z.number(), }), + /** + * @deprecated Use `contextTiers` instead. + * Kept for backward compatibility with older models.dev snapshots. + */ experimentalOver200K: z .object({ input: z.number(), @@ -842,6 +846,25 @@ export namespace Provider { }), }) .optional(), + /** + * Ordered pricing tiers that apply when total input tokens + * (input + cache_read) meet or exceed the tier's min_context threshold. + * Consumers should select the highest tier whose min_context is <= + * the current total input tokens. + */ + contextTiers: z + .array( + z.object({ + min_context: z.number(), + input: z.number(), + output: z.number(), + cache: z.object({ + read: z.number(), + write: z.number(), + }), + }), + ) + .optional(), }), limit: z.object({ context: z.number(), @@ -928,6 +951,17 @@ export namespace Provider { output: model.cost.context_over_200k.output, } : undefined, + contextTiers: model.cost?.context_tiers + ? model.cost.context_tiers.map((tier) => ({ + min_context: tier.min_context, + input: tier.input, + output: tier.output, + cache: { + read: tier.cache_read ?? 0, + write: tier.cache_write ?? 0, + }, + })) + : undefined, }, limit: { context: model.limit.context, diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 41fad1a9d483..54d471f7264c 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -286,10 +286,28 @@ export namespace Session { }, } - const costInfo = - input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000 - ? input.model.cost.experimentalOver200K - : input.model.cost + const costInfo = (() => { + const totalInputTokens = tokens.input + tokens.cache.read; + + // Prefer new context_tiers with arbitrary thresholds + if (input.model.cost?.contextTiers && input.model.cost.contextTiers.length > 0) { + // Find the highest tier whose min_context <= totalInputTokens + let selected = input.model.cost; // base cost (no tier) + for (const tier of input.model.cost.contextTiers) { + if (totalInputTokens >= tier.min_context) { + selected = tier; + } + } + return selected; + } + + // Fall back to legacy experimentalOver200K (hardcoded 200K threshold) + if (input.model.cost?.experimentalOver200K && totalInputTokens > 200_000) { + return input.model.cost.experimentalOver200K; + } + + return input.model.cost; + })(); return { cost: safe( new Decimal(0)