diff --git a/src/web-server/model-pricing.ts b/src/web-server/model-pricing.ts index 9a176882..c07f4e3a 100644 --- a/src/web-server/model-pricing.ts +++ b/src/web-server/model-pricing.ts @@ -216,6 +216,21 @@ const PRICING_REGISTRY: Record = { cacheCreationPerMillion: 3.75, cacheReadPerMillion: 0.3, }, + // Claude Sonnet 5 ($3/$15) — latest Sonnet; shares the standard Sonnet rates. + // Registered explicitly so it is a known model rather than relying on the + // unknown-model fallback that coincidentally matches these rates. + 'claude-sonnet-5': { + inputPerMillion: 3.0, + outputPerMillion: 15.0, + cacheCreationPerMillion: 3.75, + cacheReadPerMillion: 0.3, + }, + 'claude-sonnet-5-thinking': { + inputPerMillion: 3.0, + outputPerMillion: 15.0, + cacheCreationPerMillion: 3.75, + cacheReadPerMillion: 0.3, + }, // Claude 4 Opus ($15/$75) 'claude-4-opus-20250514': { inputPerMillion: 15.0, diff --git a/tests/unit/model-pricing.test.ts b/tests/unit/model-pricing.test.ts index 7502f674..9ce51248 100644 --- a/tests/unit/model-pricing.test.ts +++ b/tests/unit/model-pricing.test.ts @@ -277,6 +277,14 @@ describe('model-pricing', () => { expect(fable5.cacheReadPerMillion).toBe(1.0); }); + it('should return correct pricing for Claude Sonnet 5', () => { + const sonnet5 = getModelPricing('claude-sonnet-5'); + expect(sonnet5.inputPerMillion).toBe(3.0); + expect(sonnet5.outputPerMillion).toBe(15.0); + expect(sonnet5.cacheCreationPerMillion).toBe(3.75); + expect(sonnet5.cacheReadPerMillion).toBe(0.3); + }); + it('should not map unknown future model families onto known family pricing', () => { const fallback = getModelPricing('unknown-model-xyz'); @@ -398,6 +406,17 @@ describe('model-pricing', () => { expect(cost).toBe(73.5); // 10 + 50 + 12.5 + 1.0 }); + it('should calculate Claude Sonnet 5 cost including cache token rates', () => { + const usage: TokenUsage = { + inputTokens: 1_000_000, + outputTokens: 1_000_000, + cacheCreationTokens: 1_000_000, + cacheReadTokens: 1_000_000, + }; + const cost = calculateCost(usage, 'claude-sonnet-5'); + expect(cost).toBe(22.05); // 3 + 15 + 3.75 + 0.3 + }); + it('should calculate fast-tier Claude Opus 4.8 cost (2x premium)', () => { const usage: TokenUsage = { inputTokens: 1_000_000, @@ -426,6 +445,13 @@ describe('model-pricing', () => { const models = getKnownModels(); expect(models.some((m) => m.startsWith('glm-'))).toBe(true); }); + + it('should include Claude Sonnet 5 as an explicitly registered model', () => { + // Sonnet 5 must be a first-class known model rather than relying on the + // unknown-model fallback that happens to share Sonnet's $3/$15 rates. + const models = getKnownModels(); + expect(models).toContain('claude-sonnet-5'); + }); }); describe('hasCustomPricing', () => { @@ -434,6 +460,14 @@ describe('model-pricing', () => { expect(hasCustomPricing('glm-4.6')).toBe(true); }); + it('should treat Claude Sonnet 5 as a known model with explicit pricing', () => { + // Without an explicit registry entry Sonnet 5 falls through to + // UNKNOWN_MODEL_PRICING (which coincidentally matches Sonnet rates); + // register it so cost accounting is intentional, not accidental. + expect(hasCustomPricing('claude-sonnet-5')).toBe(true); + expect(hasCustomPricing('claude-sonnet-5-thinking')).toBe(true); + }); + it('should return true for deterministic qwen3-coder alias', () => { expect(hasCustomPricing('qwen3-coder')).toBe(true); });