From 42fc5281a1e996b3cd0772383187929a610afa34 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 28 Apr 2026 16:26:27 -0400 Subject: [PATCH] fix(usage): clarify static pricing fallback --- src/web-server/model-pricing.ts | 52 +++++++++++++++++++++----------- tests/unit/model-pricing.test.ts | 10 ++++++ 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/web-server/model-pricing.ts b/src/web-server/model-pricing.ts index 4434bb71..8281b42f 100644 --- a/src/web-server/model-pricing.ts +++ b/src/web-server/model-pricing.ts @@ -758,16 +758,25 @@ const UNKNOWN_MODEL_PRICING: ModelPricing = { // ============================================================================ /** - * Normalize model name for matching - * Handles variations like provider prefixes and case differences + * Strip provider prefixes used by routing/catalog metadata. + * The CCS static table remains model-keyed, so static fallback normalizes + * provider-qualified model IDs before checking aliases. + */ +function stripProviderPrefix(model: string): string { + const trimmed = model.trim(); + const slashIndex = trimmed.indexOf('/'); + if (slashIndex <= 0) { + return trimmed; + } + return trimmed.slice(slashIndex + 1); +} + +/** + * Normalize model name for matching. + * Handles variations like provider prefixes and case differences. */ function normalizeModelName(model: string): string { - // Remove provider prefixes (e.g., "anthropic/claude-..." -> "claude-...") - const normalized = model - .trim() - .toLowerCase() - .replace(/^[^/]+\//, ''); - return normalized; + return stripProviderPrefix(model).toLowerCase(); } /** @@ -835,6 +844,20 @@ function getDirectOrAliasPricing(model: string): ModelPricing | undefined { return undefined; } +function getCcsStaticPricing(model: string): ModelPricing | undefined { + const staticPricing = getDirectOrAliasPricing(model); + if (staticPricing !== undefined) { + return staticPricing; + } + + const providerlessModel = stripProviderPrefix(model); + if (providerlessModel !== model.trim()) { + return getDirectOrAliasPricing(providerlessModel); + } + + return undefined; +} + function hasProviderContext(model: string, options: PricingLookupOptions): boolean { return Boolean(options.provider || /^[^/]+\//.test(model.trim())); } @@ -852,14 +875,9 @@ export function getModelPricing(model: string, options: PricingLookupOptions = { } } - const exactPricing = PRICING_REGISTRY[model]; - if (exactPricing !== undefined) { - return exactPricing; - } - - const directOrAliasPricing = getDirectOrAliasPricing(model); - if (directOrAliasPricing !== undefined) { - return directOrAliasPricing; + const ccsStaticPricing = getCcsStaticPricing(model); + if (ccsStaticPricing !== undefined) { + return ccsStaticPricing; } const modelsDevPricing = resolveModelsDevPricing(model, options); @@ -914,7 +932,7 @@ export function getKnownModels(): string[] { */ export function hasCustomPricing(model: string, options: PricingLookupOptions = {}): boolean { return ( - getDirectOrAliasPricing(model) !== undefined || + getCcsStaticPricing(model) !== undefined || resolveModelsDevPricing(model, options) !== undefined ); } diff --git a/tests/unit/model-pricing.test.ts b/tests/unit/model-pricing.test.ts index 62d3a523..c304e7f1 100644 --- a/tests/unit/model-pricing.test.ts +++ b/tests/unit/model-pricing.test.ts @@ -388,6 +388,16 @@ describe('model-pricing', () => { expect(getModelPricing('gpt-4o').inputPerMillion).toBe(2.5); }); + it('falls back to CCS static pricing when provider-aware models.dev lookup misses a known model', () => { + const staticPricing = getModelPricing('claude-sonnet-4-5'); + + expect(getModelPricing('anthropic/claude-sonnet-4-5')).toEqual(staticPricing); + expect(getModelPricing('claude-sonnet-4-5', { provider: 'anthropic' })).toEqual( + staticPricing + ); + expect(hasCustomPricing('anthropic/claude-sonnet-4-5')).toBe(true); + }); + it('does not use ambiguous model-only models.dev matches', () => { const pricing = getModelPricing('gpt-5.5'); expect(pricing).toEqual(getModelPricing('unknown-model-xyz'));