mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 08:19:59 +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,
|
cacheCreationPerMillion: 3.75,
|
||||||
cacheReadPerMillion: 0.3,
|
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 ($15/$75)
|
||||||
'claude-4-opus-20250514': {
|
'claude-4-opus-20250514': {
|
||||||
inputPerMillion: 15.0,
|
inputPerMillion: 15.0,
|
||||||
@@ -202,6 +215,19 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
|
|||||||
cacheCreationPerMillion: 6.25,
|
cacheCreationPerMillion: 6.25,
|
||||||
cacheReadPerMillion: 0.5,
|
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
|
// OpenAI Models - Source: better-ccusage
|
||||||
@@ -708,7 +734,7 @@ const UNKNOWN_MODEL_PRICING: ModelPricing = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize model name for matching
|
* 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 {
|
function normalizeModelName(model: string): string {
|
||||||
// Remove provider prefixes (e.g., "anthropic/claude-..." -> "claude-...")
|
// Remove provider prefixes (e.g., "anthropic/claude-..." -> "claude-...")
|
||||||
@@ -719,6 +745,14 @@ function normalizeModelName(model: string): string {
|
|||||||
return normalized;
|
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(
|
const NORMALIZED_PRICING_REGISTRY: Record<string, ModelPricing> = Object.entries(
|
||||||
PRICING_REGISTRY
|
PRICING_REGISTRY
|
||||||
).reduce<Record<string, ModelPricing>>((acc, [key, pricing]) => {
|
).reduce<Record<string, ModelPricing>>((acc, [key, pricing]) => {
|
||||||
@@ -730,7 +764,22 @@ function getLookupCandidates(model: string): string[] {
|
|||||||
const normalized = normalizeModelName(model);
|
const normalized = normalizeModelName(model);
|
||||||
const baseModel = normalized.split(':')[0];
|
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 {
|
function getDirectOrAliasPricing(model: string): ModelPricing | undefined {
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export interface UsageDiskCache {
|
|||||||
|
|
||||||
// Current cache version - increment to invalidate old caches
|
// Current cache version - increment to invalidate old caches
|
||||||
// v3: Added hourly data to cache
|
// 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
|
* Ensure ~/.ccs/cache directory exists
|
||||||
|
|||||||
@@ -84,6 +84,42 @@ describe('model-pricing', () => {
|
|||||||
expect(opus.inputPerMillion).toBeGreaterThan(sonnet.inputPerMillion);
|
expect(opus.inputPerMillion).toBeGreaterThan(sonnet.inputPerMillion);
|
||||||
expect(sonnet.inputPerMillion).toBeGreaterThan(haiku.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', () => {
|
describe('calculateCost', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user