Merge pull request #666 from theoDep/fix/claude-4-6-models-pricing

feat(pricing): fix claude sonnet 4.6 and claude opus 4.6 pricing
This commit is contained in:
Kai (Tam Nhu) Tran
2026-03-03 11:16:47 -05:00
committed by GitHub
3 changed files with 119 additions and 2 deletions
+54 -1
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
@@ -719,6 +745,18 @@ function normalizeModelName(model: string): string {
return normalized;
}
/**
* Strip trailing date suffix from model name (e.g., "-20260101")
* Claude session IDs can place dates either at the end or before "-thinking".
*/
function stripDateSuffix(model: string): string {
if (!model.startsWith('claude-')) {
return model;
}
return model.replace(/-\d{8}(?=-thinking(?:$|:))/g, '').replace(/-\d{8}(?=$|:)/g, '');
}
const NORMALIZED_PRICING_REGISTRY: Record<string, ModelPricing> = Object.entries(
PRICING_REGISTRY
).reduce<Record<string, ModelPricing>>((acc, [key, pricing]) => {
@@ -730,7 +768,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
+63
View File
@@ -84,6 +84,54 @@ 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);
});
it('should match date-stamped thinking Claude Opus 4.6 to correct pricing', () => {
const opus46 = getModelPricing('claude-opus-4-6-20260101-thinking');
expect(opus46.inputPerMillion).toBe(5.0);
expect(opus46.outputPerMillion).toBe(25.0);
});
it('should match provider-prefixed date-stamped thinking model to correct pricing', () => {
const opus46 = getModelPricing('anthropic/claude-opus-4-6-20260101-thinking');
expect(opus46.inputPerMillion).toBe(5.0);
expect(opus46.outputPerMillion).toBe(25.0);
});
});
describe('calculateCost', () => {
@@ -143,6 +191,17 @@ describe('model-pricing', () => {
const cost = calculateCost(usage, 'gemini-2.0-flash-exp');
expect(cost).toBe(0); // Experimental models are free
});
it('should calculate Claude Opus 4.6 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-opus-4-6');
expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5
});
});
describe('getKnownModels', () => {
@@ -173,6 +232,10 @@ describe('model-pricing', () => {
expect(hasCustomPricing('qwen3-coder')).toBe(true);
});
it('should not treat date-stamped non-Claude IDs as deterministic aliases', () => {
expect(hasCustomPricing('qwen3-32b-20260101')).toBe(false);
});
it('should return false for unknown models', () => {
expect(hasCustomPricing('unknown-model-xyz')).toBe(false);
});