From 254fc88f717dd5f649aa6ffa30b1939ac3f9078f Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sun, 31 May 2026 10:34:50 +0200 Subject: [PATCH] refactor(pricing): derive fast-tier cache rates via buildRates helper Address review feedback on the per-service-tier pricing: - Extract `buildRates(input, output)` plus named CACHE_5M_WRITE_MULTIPLIER / CACHE_READ_MULTIPLIER constants so fast-tier cache rates are derived from Anthropic's documented multipliers instead of hand-computed numbers. This removes the inconsistency where Opus 4.6 fast-tier lacked the explanatory inline comments that 4.7/4.8 carried (presto suggestion #3). - Document in `applyServiceTier` that no production caller passes `serviceTier` yet (Anthropic's service_tier is not captured on CliproxyRequestDetail), so fast-mode usage is currently billed at the standard rate until the usage pipeline records the tier. - Add a combined date-suffix + fast-tier lookup test to prove the two resolution stages compose. Built [OnSteroids](https://onsteroids.ai) --- src/web-server/model-pricing.ts | 48 ++++++++++++++++++++------------ tests/unit/model-pricing.test.ts | 10 +++++++ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/web-server/model-pricing.ts b/src/web-server/model-pricing.ts index a37e084f..ab423c07 100644 --- a/src/web-server/model-pricing.ts +++ b/src/web-server/model-pricing.ts @@ -50,6 +50,26 @@ export interface PricingLookupOptions extends ModelsDevPricingLookupOptions { serviceTier?: string; } +// Anthropic prompt-caching multipliers, expressed relative to the base input +// rate (see https://platform.claude.com/docs/en/about-claude/pricing#prompt-caching). +// Keeping them as named constants avoids drift when deriving per-tier cache rates. +const CACHE_5M_WRITE_MULTIPLIER = 1.25; +const CACHE_READ_MULTIPLIER = 0.1; + +/** + * Build a full rate set from input/output rates, deriving cache rates from the + * documented Anthropic multipliers so fast-tier entries stay in sync with the + * base-rate math instead of carrying hand-computed cache numbers. + */ +function buildRates(inputPerMillion: number, outputPerMillion: number): PricingRates { + return { + inputPerMillion, + outputPerMillion, + cacheCreationPerMillion: inputPerMillion * CACHE_5M_WRITE_MULTIPLIER, + cacheReadPerMillion: inputPerMillion * CACHE_READ_MULTIPLIER, + }; +} + // ============================================================================ // USER-EDITABLE PRICING TABLE // Update rates below (per million tokens in USD) @@ -247,12 +267,7 @@ const PRICING_REGISTRY: Record = { cacheCreationPerMillion: 6.25, cacheReadPerMillion: 0.5, serviceTiers: { - fast: { - inputPerMillion: 30.0, - outputPerMillion: 150.0, - cacheCreationPerMillion: 37.5, - cacheReadPerMillion: 3.0, - }, + fast: buildRates(30.0, 150.0), }, }, 'claude-opus-4-6-thinking': { @@ -268,12 +283,7 @@ const PRICING_REGISTRY: Record = { cacheCreationPerMillion: 6.25, cacheReadPerMillion: 0.5, serviceTiers: { - fast: { - inputPerMillion: 30.0, - outputPerMillion: 150.0, - cacheCreationPerMillion: 37.5, // 30 * 1.25 (5m cache write multiplier) - cacheReadPerMillion: 3.0, // 30 * 0.1 (cache read multiplier) - }, + fast: buildRates(30.0, 150.0), }, }, // Claude 4.8 Opus ($5/$25) — fast mode ($10/$50, 2x premium per Anthropic docs) @@ -283,12 +293,7 @@ const PRICING_REGISTRY: Record = { cacheCreationPerMillion: 6.25, cacheReadPerMillion: 0.5, serviceTiers: { - fast: { - inputPerMillion: 10.0, - outputPerMillion: 50.0, - cacheCreationPerMillion: 12.5, // 10 * 1.25 (5m cache write multiplier) - cacheReadPerMillion: 1.0, // 10 * 0.1 (cache read multiplier) - }, + fast: buildRates(10.0, 50.0), }, }, 'claude-opus-4-7-thinking': { @@ -944,6 +949,13 @@ function hasProviderContext(model: string, options: PricingLookupOptions): boole * Apply per-service-tier rates if the resolved model declares them and the * caller requested a matching tier. Unknown tiers transparently fall through * to the base rates so existing callers stay unaffected. + * + * TODO(opus-fast-mode): No production caller currently passes `serviceTier`. + * Anthropic's `service_tier` is not yet captured on CliproxyRequestDetail, so + * usage transformers (cliproxy-usage-transformer.ts, data-aggregator.ts) bill + * fast-mode requests at the standard rate — i.e. fast Opus is under-reported by + * the tier premium ($10/$50 vs $5/$25). Wire `serviceTier` through once the + * usage pipeline records it. The schema below is ready for that integration. */ function applyServiceTier(pricing: ModelPricing, tier: string | undefined): ModelPricing { if (!tier) return pricing; diff --git a/tests/unit/model-pricing.test.ts b/tests/unit/model-pricing.test.ts index 59eb0523..653b5664 100644 --- a/tests/unit/model-pricing.test.ts +++ b/tests/unit/model-pricing.test.ts @@ -202,6 +202,16 @@ describe('model-pricing', () => { expect(opus47fast.outputPerMillion).toBe(150.0); }); + it('should apply fast-tier pricing together with date-suffix stripping', () => { + // stripDateSuffix (base lookup) and applyServiceTier run independently; + // confirm they compose for a date-stamped id requesting the fast tier. + const opus48fastDated = getModelPricing('claude-opus-4-8-20260530', { serviceTier: 'fast' }); + expect(opus48fastDated.inputPerMillion).toBe(10.0); + expect(opus48fastDated.outputPerMillion).toBe(50.0); + expect(opus48fastDated.cacheCreationPerMillion).toBe(12.5); + expect(opus48fastDated.cacheReadPerMillion).toBe(1.0); + }); + it('should fall back to standard rates when serviceTier is unknown', () => { // Unknown tier names must not throw; revert to base pricing transparently. const opus48 = getModelPricing('claude-opus-4-8', { serviceTier: 'enterprise-mythos' });