From c72dc99635d19bfdb6ec19988a3050556b315c9a Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 4 Aug 2026 00:41:27 +0900 Subject: [PATCH] fix(usage): price long-context requests at the published long rate (#908) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several vendors reprice the entire request once the prompt crosses a token threshold, and a flat Cost4 could not express it — so every request billed at the short rate, including the long ones, which are the expensive ones. The threshold reads raw usage.inputTokens, not normalized billable input: a 280k prompt with a 200k cache read has 80k billable input and still crosses OpenAI's 272k boundary. Deciding after normalization would have under-billed exactly the cache-heavy long requests. Long context and Fast are mutually exclusive, not composable. OpenAI does not serve long context in Fast mode, so exclusivity keys on the response-confirmed tier: a >272k request merely tagged priority was necessarily downgraded and bills long. That needed tier provenance at all four estimator call sites instead of the collapsed scalar. Also adds base prices for the three -pro virtual aliases, which resolved to null and rendered no cost estimate at all. Fixes #908 --- src/server/management/shared.ts | 4 +- src/usage/cost.ts | Bin 14400 -> 18688 bytes src/usage/expected-prices.ts | 111 +++++++++++++++++++++ src/usage/summary.ts | 8 +- tests/usage-cost.test.ts | 171 ++++++++++++++++++++++++++++---- 5 files changed, 268 insertions(+), 26 deletions(-) diff --git a/src/server/management/shared.ts b/src/server/management/shared.ts index 29cdde2e08..5880b03400 100644 --- a/src/server/management/shared.ts +++ b/src/server/management/shared.ts @@ -51,7 +51,7 @@ import type { OcxClaudeCodeConfig, OcxClaudeDesktopProfile, OcxConfig, OcxCustom import type { DesktopProfileModel } from "../../claude/desktop-profile"; import { drainAndShutdown } from "../lifecycle"; import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log"; -import { estimateComboCost, estimateRequestCost, effectiveServiceTier, normalizeCostTokens, tokensPerSecond } from "../../usage/cost"; +import { estimateComboCost, estimateRequestCost, serviceTierContext, normalizeCostTokens, tokensPerSecond } from "../../usage/cost"; import type { PersistedUsageAttempt } from "../../usage/log"; import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors"; import { applySystemEnvToggle } from "../system-env"; @@ -124,7 +124,7 @@ export function unavailableCostReason(entry: MetricSource): MetricUnavailableRea } export function costResult(entry: MetricSource): CostResult { - const tier = effectiveServiceTier(entry); + const tier = serviceTierContext(entry); const estimate = entry.attempts?.length ? estimateComboCost(entry.attempts, undefined, tier) : estimateRequestCost({ provider: entry.provider, model: entry.model, usage: entry.usage, usageStatus: entry.usageStatus, serviceTier: tier }); diff --git a/src/usage/cost.ts b/src/usage/cost.ts index 5deaa0026bb149a9021451f4eb5901163d3f9d47..45f78c62645b9573d651bdc259edd0ade27b505d 100644 GIT binary patch delta 3842 zcmbVP&u<$=6jo3wkR>D#YD<5?OK8eEbv96%0Yu9ZN7t7w+nR)Mf-}k-e zFOzpZnE33aNooa=eby9sxHmWSLDqkR))-WBB^x#MbVbDNYi}A^yx|kCs$~eR;Ahs zJUM3&0*;KgWF#U_y0k7^E!p)RPMVhn43k8U~iSU|>Bl}f6g4B#uEdhW^RVd1ueRFgp zdt}f4-WQX9-Y1EeEUOZUZpz38m5gT&hiUT_1>`06P(K!~_ELS6LD@rl4rX87|5tb9 zHW6(PFfp>77)muVW@zpf#ecu~bmdm1(n=#RGLESC^#i{h?s}d~j3x;Jg1vlKpPRUu z39UU53Po|g*h*2n0mo4xFIGO|P0Nsx0tt;`NqIu4z|yVsx+PX%uoy87QPnP&)fTmm zr(q}+OCsk4GyTpwfC4}w+>Q`HTc%bbv=vk-sy+*HEtl$lK1!gn#cn^|oiXGXNcQ+*7=)o~B$ZTPs7CRLIBntgY4 z@8k6xY`1R-rBbr(XTKlaH&M@S%p97uKOdgikDu?)9A4m`w}zR|MOwAAcK9nKS~|@V zBRe@WRb~5+TX{(r7vcLkwWnz*e}<}HHyt~ceSUObm%mr-J?tS3l}aE4ba$QPwacrG zD=W(v=*r6K!qmZlM3myuql3$Lv<*a6m4EjFYZu%7#Cj){%1KGnaq zT0>`o{5WPlE{auKgb2r{Ci;JMfp%qXQ;Mzayph2K`0`GihCa1e%1s%1_j!(v#msc$N$K`h&mox(T0ZQ%otjjHucNws&2zpRK^tC< zriUiM=Rx=p&TA*Jw$QcH&;&etD4qA-U6yix+%9~ek^Oq?16i*S!45-IcY|2!i(y z%Pdz}^&b0xH2Q%`L8`^kA$7vZcJ>~6qh=?q@VpPt$S<0*@9*r;)X)s;@Z%b=FQays z0J4VToZ~(2peVUI7*?V<- zuUc{7zFW0oTwd#aGV|WVL$?|2;*&@AbFB+ME;Wy8Pd@qaeUFUp00XkeAK05cHv7j3 z-YD@8z_OvBcLUGX0Q(T}@XWEn0kD39~(H=5@Sn-GVu-R#hU z^K=PDi`Wuf8-n@iHRQlvqWNN?dNz0b!2QK5y$i?FiN}as*L6wh;dnVmX8{Xb}2%j(c(Jg66p+6^PSacXZi`OFWuk*oLfGiN9M4@s^qJXIY- l4%;ByQHo>-7pngcwFifQ&3-(2Bzt$^?|Z=4i%xw%@h_I0L23X1 delta 189 zcmZpe#CV`!gA2>#43?zLKI|OKldU;KCZFf}z1f!Mx6Ed7<9f!)|Jc|xbhtFF6r!E; zi%U#&6!J=QlTwRf6{@)u6f)BkG>TJ;$}*EvLo!o~Hs3Ot%>>fH4Ae1M-cEG#T=P$p z+g#-)$6HBEK4_u0d8w5j<7Ne$nP78S!RAh$XfCt)m2D-<M1K^G~>TC0B#Va&Dg{E7> = { export function resolvePriorityMultiplier(modelId: string): number { return PRIORITY_MULTIPLIERS[modelId] ?? 1; } + +/** + * Long-context pricing tiers (#908). Several vendors reprice the ENTIRE request + * once the prompt crosses a published input-token threshold, so a flat Cost4 + * cannot express it. + * + * The threshold is measured on RAW `usage.inputTokens` (total prompt size, + * including cache reads/writes) — never on normalized billable input, which has + * already had cache tokens subtracted. A 280k prompt with a 200k cache read has + * 80k billable input and still crosses OpenAI's 272k boundary; deciding after + * normalization would under-bill exactly the cache-heavy long requests. + * + * Rules are exact provider+model matches. No case folding: the jawcode bundle + * carries BOTH `minimax-m3` and `MiniMax-M3` at different rates, so folding + * would select the wrong base row. No model-level fallback: routed resellers + * (Cursor, OpenRouter) share model slugs but price independently. + */ +export interface ContextTier { + provider: string; + modelId: string; + /** Long rates apply once raw input tokens pass this boundary. */ + thresholdInputTokens: number; + /** true = `>=` threshold (xAI), false = `>` threshold (OpenAI, MiniMax). */ + inclusive: boolean; + /** Per-field factor from the short rate to the published long rate. */ + multiplier: Cost4; + source: string; + verifiedAt: string; +} + +/** + * OpenAI GPT-5.6: "Prompts with >272K input tokens are priced at 2x input and + * 1.5x output for the full request." Cached input and cache writes also double, + * per the published short/long columns. + */ +const OPENAI_LONG_CONTEXT: Cost4 = { input: 2, output: 1.5, cacheRead: 2, cacheWrite: 2 }; +/** xAI and MiniMax double every rate uniformly past their thresholds. */ +const UNIFORM_DOUBLE: Cost4 = { input: 2, output: 2, cacheRead: 2, cacheWrite: 2 }; + +const OPENAI_PRICING_DOC = "https://developers.openai.com/api/docs/pricing"; +const OPENAI_GPT56_CONTEXT_MODELS = [ + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + // Virtual `-pro` selections keep their own id in usage logs (the wire model is + // recorded separately), so they need their own rows or they silently skip the tier. + "gpt-5.6-sol-pro", + "gpt-5.6-terra-pro", + "gpt-5.6-luna-pro", +]; + +export const CONTEXT_TIERS: readonly ContextTier[] = [ + ...["openai", "openai-apikey"].flatMap(provider => + OPENAI_GPT56_CONTEXT_MODELS.map((modelId): ContextTier => ({ + provider, + modelId, + thresholdInputTokens: 272_000, + inclusive: false, + multiplier: OPENAI_LONG_CONTEXT, + source: OPENAI_PRICING_DOC, + verifiedAt: "2026-08-03", + })), + ), + { + provider: "xai", + modelId: "grok-4.5", + thresholdInputTokens: 200_000, + inclusive: true, + multiplier: UNIFORM_DOUBLE, + source: "https://docs.x.ai/developers/pricing", + verifiedAt: "2026-08-03", + }, + ...["minimax", "minimax-cn"].map((provider): ContextTier => ({ + provider, + modelId: "MiniMax-M3", + thresholdInputTokens: 512_000, + inclusive: false, + multiplier: UNIFORM_DOUBLE, + source: "https://platform.minimax.io/docs/guides/pricing-paygo", + verifiedAt: "2026-08-03", + })), +]; + +/** Exact provider+model context-tier lookup. No fuzzy matching, no case folding. */ +export function findContextTier( + provider: string, + modelId: string, + tiers: readonly ContextTier[] = CONTEXT_TIERS, +): ContextTier | undefined { + return tiers.find(tier => tier.provider === provider && tier.modelId === modelId); +} + +/** Whether a raw input-token count crosses the tier's published boundary. */ +export function isLongContext(tier: ContextTier, rawInputTokens: number): boolean { + if (!Number.isFinite(rawInputTokens)) return false; + return tier.inclusive + ? rawInputTokens >= tier.thresholdInputTokens + : rawInputTokens > tier.thresholdInputTokens; +} diff --git a/src/usage/summary.ts b/src/usage/summary.ts index 45e077bf42..8337505ed5 100644 --- a/src/usage/summary.ts +++ b/src/usage/summary.ts @@ -2,7 +2,7 @@ import { baseProviderLabel } from "../providers/label"; import { canonicalAntigravityUsageModel } from "../providers/antigravity-models"; import { usageDisplayTotalTokens } from "./totals"; import type { PersistedUsageEntry, UsageStatus } from "./log"; -import { estimateComboCost, estimateRequestCost, effectiveServiceTier } from "./cost"; +import { estimateComboCost, estimateRequestCost, serviceTierContext } from "./cost"; export type UsageRange = "7d" | "30d" | "all"; export type UsageSurface = "all" | "codex" | "claude" | "grok"; @@ -286,7 +286,7 @@ function addEstimatedCost( totals.unmeteredRequests += 1; return; } - const tier = effectiveServiceTier(entry); + const tier = serviceTierContext(entry); const estimate = entry.attempts?.length ? estimateComboCost(entry.attempts, undefined, tier) : estimateRequestCost({ provider: entry.provider, model: entry.model, usage: entry.usage, usageStatus: entry.usageStatus, serviceTier: tier }); @@ -415,7 +415,7 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage } // Accumulate per-model estimated cost for (const entry of entries) { - const tier = effectiveServiceTier(entry); + const tier = serviceTierContext(entry); const estimate = entry.attempts?.length ? estimateComboCost(entry.attempts, undefined, tier) : estimateRequestCost({ provider: entry.provider, model: entry.model, usage: entry.usage, usageStatus: entry.usageStatus, serviceTier: tier }); @@ -524,7 +524,7 @@ function buildProviders(entries: PersistedUsageEntry[], totalTokens: number): Us } } for (const entry of entries) { - const tier = effectiveServiceTier(entry); + const tier = serviceTierContext(entry); const estimate = entry.attempts?.length ? estimateComboCost(entry.attempts, undefined, tier) : estimateRequestCost({ provider: entry.provider, model: entry.model, usage: entry.usage, usageStatus: entry.usageStatus, serviceTier: tier }); diff --git a/tests/usage-cost.test.ts b/tests/usage-cost.test.ts index d77a5c329d..f4e0d114fa 100644 --- a/tests/usage-cost.test.ts +++ b/tests/usage-cost.test.ts @@ -12,6 +12,7 @@ import { import { EXPECTED_PRICE_OVERLAYS, PRIORITY_MULTIPLIERS, + CONTEXT_TIERS, findExpectedPriceOverlay, resolvePriorityMultiplier, type ExpectedPriceOverlay, @@ -229,8 +230,8 @@ describe("resolveMatchedPrice", () => { expect(resolveMatchedPrice("openrouter", "anthropic-claude-3.5-sonnet")).toBeNull(); }); - test("16. shipped overlay membership: 48 keys, including Opus 5 and compatibility prices", () => { - expect(EXPECTED_PRICE_OVERLAYS.length).toBe(48); + test("16. shipped overlay membership: 51 keys, including Opus 5 and compatibility prices", () => { + expect(EXPECTED_PRICE_OVERLAYS.length).toBe(51); expect(EXPECTED_PRICE_OVERLAYS.some(row => row.status === "unverified")).toBe(false); const keys = new Set(EXPECTED_PRICE_OVERLAYS.map(row => `${row.provider}/${row.modelId}`)); for (const expected of [ @@ -405,17 +406,19 @@ describe("priority (Fast) service tier multiplier", () => { { provider: "openai", modelId: "gpt-5.5", cost4: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, source: "test", verifiedAt: "2026-07-24", status: "verified" }, { provider: "openai", modelId: "gpt-5.3-codex-spark", cost4: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }, source: "test", verifiedAt: "2026-07-24", status: "verified" }, ]; - const usage = { inputTokens: 1_000_000, outputTokens: 100_000 }; + // Below OpenAI's 272k long-context boundary on purpose: these cases isolate the Fast + // multiplier, and a 1M-token prompt would silently also trip the context tier (#908). + const usage = { inputTokens: 200_000, outputTokens: 20_000 }; test("P1. priority tier applies 2x multiplier for gpt-5.6-sol", () => { const base = estimateRequestCost({ provider: "openai", model: "gpt-5.6-sol", usageStatus: "reported", usage }, overlays); const fast = estimateRequestCost({ provider: "openai", model: "gpt-5.6-sol", usageStatus: "reported", usage, serviceTier: "priority" }, overlays); expect(base).not.toBeNull(); expect(fast).not.toBeNull(); - // base: 5*1 + 30*0.1 = 8.0 - expect(base!.cost.total).toBeCloseTo(8.0, 9); - // fast: 2x => 16.0 - expect(fast!.cost.total).toBeCloseTo(16.0, 9); + // base: 5*0.2 + 30*0.02 = 1.6 + expect(base!.cost.total).toBeCloseTo(1.6, 9); + // fast: 2x => 3.2 + expect(fast!.cost.total).toBeCloseTo(3.2, 9); expect(fast!.priorityMultiplier).toBe(2); expect(base!.priorityMultiplier).toBeUndefined(); }); @@ -425,32 +428,32 @@ describe("priority (Fast) service tier multiplier", () => { const fast = estimateRequestCost({ provider: "openai", model: "gpt-5.6-luna", usageStatus: "reported", usage, serviceTier: "priority" }); expect(base).not.toBeNull(); expect(fast).not.toBeNull(); - // Standard: $1 input + $0.60 output = $1.60. Fast: $0.40 + $0.24 = $0.64. - expect(base!.cost.total).toBeCloseTo(1.6, 9); - expect(fast!.cost.total).toBeCloseTo(0.64, 9); + // Standard: $0.20 input + $0.12 output = $0.32. Fast (0.4x): $0.128. + expect(base!.cost.total).toBeCloseTo(0.32, 9); + expect(fast!.cost.total).toBeCloseTo(0.128, 9); expect(fast!.priorityMultiplier).toBe(0.4); }); test("P2. priority tier applies 2.5x multiplier for gpt-5.5", () => { const base = estimateRequestCost({ provider: "openai", model: "gpt-5.5", usageStatus: "reported", usage }, overlays); const fast = estimateRequestCost({ provider: "openai", model: "gpt-5.5", usageStatus: "reported", usage, serviceTier: "priority" }, overlays); - expect(base!.cost.total).toBeCloseTo(8.0, 9); - // 2.5x => 20.0 - expect(fast!.cost.total).toBeCloseTo(20.0, 9); + expect(base!.cost.total).toBeCloseTo(1.6, 9); + // 2.5x => 4.0 + expect(fast!.cost.total).toBeCloseTo(4.0, 9); expect(fast!.priorityMultiplier).toBe(2.5); }); test("P3. no service tier => base price unchanged (regression)", () => { const est = estimateRequestCost({ provider: "openai", model: "gpt-5.6-sol", usageStatus: "reported", usage }, overlays); - expect(est!.cost.total).toBeCloseTo(8.0, 9); + expect(est!.cost.total).toBeCloseTo(1.6, 9); expect(est!.priorityMultiplier).toBeUndefined(); }); test("P4. unknown model + priority => multiplier 1 (fallback)", () => { const est = estimateRequestCost({ provider: "openai", model: "gpt-5.3-codex-spark", usageStatus: "reported", usage, serviceTier: "priority" }, overlays); expect(est).not.toBeNull(); - // base: 1.75*1 + 14*0.1 = 3.15; no multiplier listed => stays 3.15 - expect(est!.cost.total).toBeCloseTo(3.15, 9); + // base: 1.75*0.2 + 14*0.02 = 0.63; no multiplier listed => stays 0.63 + expect(est!.cost.total).toBeCloseTo(0.63, 9); expect(est!.priorityMultiplier).toBeUndefined(); }); @@ -461,7 +464,7 @@ describe("priority (Fast) service tier multiplier", () => { const est = estimateRequestCost({ provider: "openrouter", model: "gpt-5.6-sol", usageStatus: "reported", usage, serviceTier: "priority" }, customOverlays); expect(est).not.toBeNull(); // provider gate: openrouter is not openai => no multiplier - expect(est!.cost.total).toBeCloseTo(8.0, 9); + expect(est!.cost.total).toBeCloseTo(1.6, 9); expect(est!.priorityMultiplier).toBeUndefined(); }); @@ -470,7 +473,7 @@ describe("priority (Fast) service tier multiplier", () => { { ordinal: 1, provider: "openai", model: "gpt-5.6-sol", usageStatus: "reported", usage }, ], overlays, "priority"); expect(combo).not.toBeNull(); - expect(combo!.cost.total).toBeCloseTo(16.0, 9); + expect(combo!.cost.total).toBeCloseTo(3.2, 9); expect(combo!.priorityMultiplier).toBe(2); }); @@ -506,8 +509,136 @@ describe("priority (Fast) service tier multiplier", () => { test("P10. attempt cost with priority tier", () => { const base = estimateAttemptCost({ ordinal: 1, provider: "openai", model: "gpt-5.6-sol", usageStatus: "reported", usage }, overlays); const fast = estimateAttemptCost({ ordinal: 1, provider: "openai", model: "gpt-5.6-sol", usageStatus: "reported", usage }, overlays, "priority"); - expect(base!.cost.total).toBeCloseTo(8.0, 9); - expect(fast!.cost.total).toBeCloseTo(16.0, 9); + expect(base!.cost.total).toBeCloseTo(1.6, 9); + expect(fast!.cost.total).toBeCloseTo(3.2, 9); expect(fast!.priorityMultiplier).toBe(2); }); }); + +describe("long-context pricing tiers (#908)", () => { + const SOL: ExpectedPriceOverlay[] = [ + { provider: "openai", modelId: "gpt-5.6-sol", cost4: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, source: "test", verifiedAt: "2026-08-03", status: "verified" }, + { provider: "openai", modelId: "gpt-5.3-codex-spark", cost4: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }, source: "test", verifiedAt: "2026-08-03", status: "verified" }, + ]; + const sol = (usage: Record, serviceTier?: Parameters[0]["serviceTier"]) => + estimateRequestCost({ provider: "openai", model: "gpt-5.6-sol", usageStatus: "reported", usage, serviceTier }, SOL); + + test("L1. OpenAI boundary is exclusive: 272,000 is standard, 272,001 is long", () => { + const at = sol({ inputTokens: 272_000, outputTokens: 10_000 }); + const over = sol({ inputTokens: 272_001, outputTokens: 10_000 }); + expect(at!.contextTier).toBeUndefined(); + expect(over!.contextTier).toBe("long"); + // 2x input, 1.5x output — not a uniform doubling. Compare at equal token + // counts so the one-token difference across the boundary does not skew it. + const overSameTokens = sol({ inputTokens: 272_001, outputTokens: 10_000 })!; + expect(overSameTokens.cost.output).toBeCloseTo(at!.cost.output * 1.5, 9); + expect(overSameTokens.cost.input / (272_001 / 1e6)).toBeCloseTo(10, 9); + expect(at!.cost.input / (272_000 / 1e6)).toBeCloseTo(5, 9); + }); + + test("L2. worked example: 300k in + 20k out is $2.10 standard, $3.90 long", () => { + // The tier is what makes this $3.90; without it the estimator reports $2.10. + const est = sol({ inputTokens: 300_000, outputTokens: 20_000 }); + expect(est!.contextTier).toBe("long"); + expect(est!.cost.total).toBeCloseTo(3.9, 9); + const short = 300_000 / 1e6 * 5 + 20_000 / 1e6 * 30; + expect(short).toBeCloseTo(2.1, 9); + }); + + test("L3. threshold reads RAW input, not cache-normalized billable input", () => { + // 280k prompt with a 200k cache read: 80k billable input, but the vendor + // threshold is measured on the whole prompt. Deciding after normalization + // would under-bill exactly the cache-heavy long requests. + const est = sol({ inputTokens: 280_000, outputTokens: 1_000, cachedInputTokens: 200_000 }); + expect(est!.tokens.input).toBe(80_000); + expect(est!.contextTier).toBe("long"); + expect(est!.cost.cacheRead).toBeCloseTo(200_000 / 1e6 * 0.5 * 2, 9); + }); + + test("L4. xAI boundary is inclusive: 199,999 standard, 200,000 long", () => { + const overlays: ExpectedPriceOverlay[] = [ + { provider: "xai", modelId: "grok-4.5", cost4: { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0 }, source: "test", verifiedAt: "2026-08-03", status: "verified" }, + ]; + const at = (n: number) => estimateRequestCost({ provider: "xai", model: "grok-4.5", usageStatus: "reported", usage: { inputTokens: n, outputTokens: 1_000 } }, overlays); + expect(at(199_999)!.contextTier).toBeUndefined(); + expect(at(200_000)!.contextTier).toBe("long"); + }); + + test("L5. MiniMax casing is exact: MiniMax-M3 tiers, minimax-m3 does not", () => { + const overlays: ExpectedPriceOverlay[] = [ + { provider: "minimax", modelId: "MiniMax-M3", cost4: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 }, source: "test", verifiedAt: "2026-08-03", status: "verified" }, + { provider: "minimax", modelId: "minimax-m3", cost4: { input: 0.6, output: 2.4, cacheRead: 0.12, cacheWrite: 0 }, source: "test", verifiedAt: "2026-08-03", status: "verified" }, + ]; + const at = (model: string, n: number) => estimateRequestCost({ provider: "minimax", model, usageStatus: "reported", usage: { inputTokens: n, outputTokens: 1_000 } }, overlays); + expect(at("MiniMax-M3", 512_000)!.contextTier).toBeUndefined(); + expect(at("MiniMax-M3", 512_001)!.contextTier).toBe("long"); + // The bundle carries both ids at different rates; case-folding would pick the wrong row. + expect(at("minimax-m3", 512_001)!.contextTier).toBeUndefined(); + }); + + test("L6. routed resellers price independently: cursor/openrouter never tier", () => { + const overlays: ExpectedPriceOverlay[] = [ + { provider: "cursor", modelId: "gpt-5.6-sol", cost4: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, source: "test", verifiedAt: "2026-08-03", status: "verified" }, + { provider: "openrouter", modelId: "gpt-5.6-sol", cost4: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, source: "test", verifiedAt: "2026-08-03", status: "verified" }, + ]; + for (const provider of ["cursor", "openrouter"]) { + const est = estimateRequestCost({ provider, model: "gpt-5.6-sol", usageStatus: "reported", usage: { inputTokens: 1_000_000, outputTokens: 10_000 } }, overlays); + expect(est!.contextTier).toBeUndefined(); + } + }); + + test("L7. a model with no published tier is unaffected at any size", () => { + const est = estimateRequestCost({ provider: "openai", model: "gpt-5.3-codex-spark", usageStatus: "reported", usage: { inputTokens: 5_000_000, outputTokens: 10_000 } }, SOL); + expect(est!.contextTier).toBeUndefined(); + expect(est!.cost.total).toBeCloseTo(5_000_000 / 1e6 * 1.75 + 10_000 / 1e6 * 14, 9); + }); + + test("L8. Fast and long context are mutually exclusive, by PROVENANCE", () => { + const usage = { inputTokens: 300_000, outputTokens: 20_000 }; + // Response-confirmed Fast: OpenAI does not serve long context in Fast mode, + // so the request really was Fast and the context tier must not apply. + const confirmed = sol(usage, { responseServiceTier: "priority" }); + expect(confirmed!.contextTier).toBeUndefined(); + expect(confirmed!.priorityMultiplier).toBe(2); + expect(confirmed!.cost.total).toBeCloseTo(4.2, 9); + + // Requested/configured only, with no response confirmation: a >272k request + // cannot have been served as Fast, so it was downgraded and bills long. + // Suppressing the tier here would under-bill exactly the downgraded request. + for (const tier of [{ requestedServiceTier: "priority" }, { configuredServiceTier: "priority" }]) { + const downgraded = sol(usage, tier); + expect(downgraded!.contextTier).toBe("long"); + expect(downgraded!.cost.total).toBeCloseTo(3.9, 9); + } + }); + + test("L9. combo carries the tier when any attempt is long", () => { + const combo = estimateComboCost([ + { ordinal: 1, provider: "openai", model: "gpt-5.6-sol", usageStatus: "reported", usage: { inputTokens: 300_000, outputTokens: 10_000 } }, + { ordinal: 2, provider: "openai", model: "gpt-5.6-sol", usageStatus: "reported", usage: { inputTokens: 1_000, outputTokens: 10_000 } }, + ], SOL); + expect(combo!.attempts![0]!.contextTier).toBe("long"); + expect(combo!.attempts![1]!.contextTier).toBeUndefined(); + expect(combo!.contextTier).toBe("long"); + }); + + test("L10. -pro virtual aliases are priceable AND tier (regression: returned null)", () => { + for (const model of ["gpt-5.6-sol-pro", "gpt-5.6-terra-pro", "gpt-5.6-luna-pro"]) { + // Shipped overlays on purpose: these ids had no base price at all, so the + // estimator returned null and the dashboard showed no cost for them. + const priced = estimateRequestCost({ provider: "openai-apikey", model, usageStatus: "reported", usage: { inputTokens: 1_000, outputTokens: 1_000 } }); + expect(priced).not.toBeNull(); + const long = estimateRequestCost({ provider: "openai-apikey", model, usageStatus: "reported", usage: { inputTokens: 272_001, outputTokens: 1_000 } }); + expect(long!.contextTier).toBe("long"); + } + }); + + test("L11. every tier rule records a source and a verification date", () => { + expect(CONTEXT_TIERS.length).toBeGreaterThan(0); + for (const tier of CONTEXT_TIERS) { + expect(tier.source).toMatch(/^https:\/\//); + expect(tier.verifiedAt).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(tier.thresholdInputTokens).toBeGreaterThan(0); + } + }); +});