feat(pricing): fix claude sonnet 4.6 and claude opus 4.6 pricing

This commit is contained in:
theodep
2026-03-03 16:27:06 +01:00
parent d1b4c5fbdf
commit 9a7ca3f1c0
3 changed files with 89 additions and 3 deletions
+51 -2
View File
@@ -151,6 +151,19 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
cacheCreationPerMillion: 3.75,
cacheReadPerMillion: 0.3,
},
// Claude 4.6 Sonnet ($3/$15)
'claude-sonnet-4-6': {
inputPerMillion: 3.0,
outputPerMillion: 15.0,
cacheCreationPerMillion: 3.75,
cacheReadPerMillion: 0.3,
},
'claude-sonnet-4-6-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,
@@ -202,6 +215,19 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
cacheCreationPerMillion: 6.25,
cacheReadPerMillion: 0.5,
},
// Claude 4.6 Opus ($5/$25)
'claude-opus-4-6': {
inputPerMillion: 5.0,
outputPerMillion: 25.0,
cacheCreationPerMillion: 6.25,
cacheReadPerMillion: 0.5,
},
'claude-opus-4-6-thinking': {
inputPerMillion: 5.0,
outputPerMillion: 25.0,
cacheCreationPerMillion: 6.25,
cacheReadPerMillion: 0.5,
},
// ---------------------------------------------------------------------------
// OpenAI Models - Source: better-ccusage
@@ -708,7 +734,7 @@ const UNKNOWN_MODEL_PRICING: ModelPricing = {
/**
* Normalize model name for matching
* Handles variations like provider prefixes and case differences
* Handles variations like provider prefixes, case differences, and date suffixes
*/
function normalizeModelName(model: string): string {
// Remove provider prefixes (e.g., "anthropic/claude-..." -> "claude-...")
@@ -719,6 +745,14 @@ function normalizeModelName(model: string): string {
return normalized;
}
/**
* Strip trailing date suffix from model name (e.g., "-20260101")
* Only strips 8-digit YYYYMMDD suffixes at the end of the string.
*/
function stripDateSuffix(model: string): string {
return model.replace(/-\d{8}$/, '');
}
const NORMALIZED_PRICING_REGISTRY: Record<string, ModelPricing> = Object.entries(
PRICING_REGISTRY
).reduce<Record<string, ModelPricing>>((acc, [key, pricing]) => {
@@ -730,7 +764,22 @@ function getLookupCandidates(model: string): string[] {
const normalized = normalizeModelName(model);
const baseModel = normalized.split(':')[0];
return baseModel === normalized ? [normalized] : [normalized, baseModel];
const candidates: string[] = [normalized];
if (baseModel !== normalized) {
candidates.push(baseModel);
}
// Add date-stripped variants (e.g., "claude-opus-4-6-20260101" -> "claude-opus-4-6")
const stripped = stripDateSuffix(normalized);
if (stripped !== normalized && !candidates.includes(stripped)) {
candidates.push(stripped);
}
const baseStripped = stripDateSuffix(baseModel);
if (baseStripped !== baseModel && !candidates.includes(baseStripped)) {
candidates.push(baseStripped);
}
return candidates;
}
function getDirectOrAliasPricing(model: string): ModelPricing | undefined {
+2 -1
View File
@@ -36,7 +36,8 @@ export interface UsageDiskCache {
// Current cache version - increment to invalidate old caches
// v3: Added hourly data to cache
const CACHE_VERSION = 3;
// v4: Pricing fix for Claude 4.6 models (invalidate stale costs)
const CACHE_VERSION = 4;
/**
* Ensure ~/.ccs/cache directory exists
+36
View File
@@ -84,6 +84,42 @@ describe('model-pricing', () => {
expect(opus.inputPerMillion).toBeGreaterThan(sonnet.inputPerMillion);
expect(sonnet.inputPerMillion).toBeGreaterThan(haiku.inputPerMillion);
});
it('should return correct pricing for Claude Opus 4.6 (not 3x Opus 4 rate)', () => {
const opus46 = getModelPricing('claude-opus-4-6');
expect(opus46.inputPerMillion).toBe(5.0);
expect(opus46.outputPerMillion).toBe(25.0);
});
it('should return correct pricing for Claude Opus 4.6 thinking variant', () => {
const opus46t = getModelPricing('claude-opus-4-6-thinking');
expect(opus46t.inputPerMillion).toBe(5.0);
expect(opus46t.outputPerMillion).toBe(25.0);
});
it('should return correct pricing for Claude Sonnet 4.6', () => {
const sonnet46 = getModelPricing('claude-sonnet-4-6');
expect(sonnet46.inputPerMillion).toBe(3.0);
expect(sonnet46.outputPerMillion).toBe(15.0);
});
it('should match date-stamped Claude Opus 4.6 to correct pricing', () => {
const opus46dated = getModelPricing('claude-opus-4-6-20260101');
expect(opus46dated.inputPerMillion).toBe(5.0);
expect(opus46dated.outputPerMillion).toBe(25.0);
});
it('should match date-stamped Claude Sonnet 4.6 to correct pricing', () => {
const sonnet46dated = getModelPricing('claude-sonnet-4-6-20260115');
expect(sonnet46dated.inputPerMillion).toBe(3.0);
expect(sonnet46dated.outputPerMillion).toBe(15.0);
});
it('should match provider-prefixed date-stamped model to correct pricing', () => {
const opus46 = getModelPricing('anthropic/claude-opus-4-6-20260101');
expect(opus46.inputPerMillion).toBe(5.0);
expect(opus46.outputPerMillion).toBe(25.0);
});
});
describe('calculateCost', () => {