diff --git a/packages/__tests__/cost/modelCostFromRegistry.test.ts b/packages/__tests__/cost/modelCostFromRegistry.test.ts index 73aa4f6602..dfaac9b114 100644 --- a/packages/__tests__/cost/modelCostFromRegistry.test.ts +++ b/packages/__tests__/cost/modelCostFromRegistry.test.ts @@ -317,11 +317,40 @@ describe("modelCostBreakdownFromRegistry", () => { // Higher tier: $6/M input, $22.50/M output (multipliers inherited from base tier) expect(breakdown.inputCost).toBe(250000 * 0.000006); expect(breakdown.outputCost).toBe(50000 * 0.0000225); - expect(breakdown.cachedInputCost).toBe(10000 * 0.000003 * 0.1); + // The cache read belongs to a prompt that is over the threshold, so it + // bills at the same higher tier as the input - matching how the Gemini + // case below prices its cache read. + expect(breakdown.cachedInputCost).toBe(10000 * 0.000006 * 0.1); + // Cache writes still use the base tier: calculateModelCostBreakdown + // prices those from basePricing directly, for every provider. expect(breakdown.cacheWrite5mCost).toBe(5000 * 0.000003 * 1.25); } }); + it("should tier a Claude cache read by the whole prompt size", () => { + // Regression: the cache read was priced from tier 0 regardless of prompt + // size, because the anthropic threshold function returned 0 for + // cachedInputCost. A mostly-cached long prompt was billed at half rate. + const modelUsage: ModelUsage = { + input: 10000, + output: 100, + cacheDetails: { + cachedInput: 240000, // prompt totals 250K, over the 200K threshold + }, + }; + + const breakdown = modelCostBreakdownFromRegistry({ + modelUsage, + providerModelId: "claude-sonnet-4-20250514", + provider: "anthropic" as ModelProviderName, + }); + + expect(breakdown).not.toBeNull(); + if (breakdown) { + expect(breakdown.cachedInputCost).toBe(240000 * 0.000006 * 0.1); + } + }); + it("should use base tier pricing for Gemini 3 Pro Preview under 200K tokens", () => { const modelUsage: ModelUsage = { input: 150000, // 150K tokens - under threshold diff --git a/packages/cost/models/calculate-cost.ts b/packages/cost/models/calculate-cost.ts index 860396a924..4b102e24a2 100644 --- a/packages/cost/models/calculate-cost.ts +++ b/packages/cost/models/calculate-cost.ts @@ -142,9 +142,13 @@ function getThresholdValueFunction(provider: ModelProviderName): (usage: ModelUs switch (field) { case "inputCost": case "outputCost": - return usage.input + - (usage.cacheDetails?.cachedInput ?? 0) + - (usage.cacheDetails?.write5m ?? 0) + + // Anthropic's long-context tier is chosen by the size of the whole + // prompt, so a cache read on a >threshold request is billed at the + // same higher tier as the input it belongs to. + case "cachedInputCost": + return usage.input + + (usage.cacheDetails?.cachedInput ?? 0) + + (usage.cacheDetails?.write5m ?? 0) + (usage.cacheDetails?.write1h ?? 0); default: return 0;