mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
feat(pricing): fix claude sonnet 4.6 and claude opus 4.6 pricing
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user