mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 08:15:17 +00:00
feat(pricing): support per-service-tier rates; wire Opus fast mode
Extend `ModelPricing` with an optional `serviceTiers` map keyed by
Anthropic's `service_tier` request parameter. `getModelPricing(model,
{ serviceTier })` returns the matching tier rates when present and
transparently falls through to base rates otherwise — existing call
sites keep current behavior.
Registry entries added (per Anthropic Fast mode pricing docs):
- claude-opus-4-8 fast: $10/$50 input/output (2x premium)
- claude-opus-4-7 fast: $30/$150 (6x premium)
- claude-opus-4-6 fast: $30/$150 (6x premium)
Cache rates derived from the documented Prompt caching multipliers
(1.25x for 5-min cache write, 0.1x for cache read).
Note: tier tracking through CliproxyRequestDetail is not wired yet —
that's a follow-up so usage transformers can pass `serviceTier` when
Anthropic returns it on the response. Schema is in place; integration
can land independently.
Also adds a defensive `claude-opus-4-8-20260530` pricing test to exercise
`stripDateSuffix` even though Anthropic no longer issues date-stamped IDs
for the 4.6+ generation (addresses presto-review suggestion #1).
Built [OnSteroids](https://onsteroids.ai)
This commit is contained in:
1 parent
357ce8b07a
commit
878775f5b6
2 files changed
+113
-5
No files matched your search
@@ -17,13 +17,23 @@ import {
|
|||||||
// TYPE DEFINITIONS
|
// TYPE DEFINITIONS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export interface ModelPricing {
|
export interface PricingRates {
|
||||||
inputPerMillion: number;
|
inputPerMillion: number;
|
||||||
outputPerMillion: number;
|
outputPerMillion: number;
|
||||||
cacheCreationPerMillion: number;
|
cacheCreationPerMillion: number;
|
||||||
cacheReadPerMillion: number;
|
cacheReadPerMillion: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModelPricing extends PricingRates {
|
||||||
|
/**
|
||||||
|
* Optional per-service-tier rate overrides keyed by Anthropic's
|
||||||
|
* `service_tier` request parameter (e.g. `'fast'`). When the lookup is
|
||||||
|
* called with a known tier, these rates replace the base ones; otherwise
|
||||||
|
* the base rates apply.
|
||||||
|
*/
|
||||||
|
serviceTiers?: Record<string, PricingRates>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TokenUsage {
|
export interface TokenUsage {
|
||||||
inputTokens: number;
|
inputTokens: number;
|
||||||
outputTokens: number;
|
outputTokens: number;
|
||||||
@@ -31,7 +41,14 @@ export interface TokenUsage {
|
|||||||
cacheReadTokens: number;
|
cacheReadTokens: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PricingLookupOptions = ModelsDevPricingLookupOptions;
|
export interface PricingLookupOptions extends ModelsDevPricingLookupOptions {
|
||||||
|
/**
|
||||||
|
* Anthropic `service_tier` (e.g. `'fast'`). When set and the resolved model
|
||||||
|
* has matching `serviceTiers` rates, those are returned instead of the
|
||||||
|
* base rates. Unknown tiers transparently fall through to base.
|
||||||
|
*/
|
||||||
|
serviceTier?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// USER-EDITABLE PRICING TABLE
|
// USER-EDITABLE PRICING TABLE
|
||||||
@@ -223,12 +240,20 @@ 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 4.6 Opus ($5/$25) — fast mode ($30/$150, 6x premium per Anthropic docs)
|
||||||
'claude-opus-4-6': {
|
'claude-opus-4-6': {
|
||||||
inputPerMillion: 5.0,
|
inputPerMillion: 5.0,
|
||||||
outputPerMillion: 25.0,
|
outputPerMillion: 25.0,
|
||||||
cacheCreationPerMillion: 6.25,
|
cacheCreationPerMillion: 6.25,
|
||||||
cacheReadPerMillion: 0.5,
|
cacheReadPerMillion: 0.5,
|
||||||
|
serviceTiers: {
|
||||||
|
fast: {
|
||||||
|
inputPerMillion: 30.0,
|
||||||
|
outputPerMillion: 150.0,
|
||||||
|
cacheCreationPerMillion: 37.5,
|
||||||
|
cacheReadPerMillion: 3.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'claude-opus-4-6-thinking': {
|
'claude-opus-4-6-thinking': {
|
||||||
inputPerMillion: 5.0,
|
inputPerMillion: 5.0,
|
||||||
@@ -236,19 +261,35 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
|
|||||||
cacheCreationPerMillion: 6.25,
|
cacheCreationPerMillion: 6.25,
|
||||||
cacheReadPerMillion: 0.5,
|
cacheReadPerMillion: 0.5,
|
||||||
},
|
},
|
||||||
// Claude 4.7 Opus ($5/$25)
|
// Claude 4.7 Opus ($5/$25) — fast mode ($30/$150, 6x premium per Anthropic docs)
|
||||||
'claude-opus-4-7': {
|
'claude-opus-4-7': {
|
||||||
inputPerMillion: 5.0,
|
inputPerMillion: 5.0,
|
||||||
outputPerMillion: 25.0,
|
outputPerMillion: 25.0,
|
||||||
cacheCreationPerMillion: 6.25,
|
cacheCreationPerMillion: 6.25,
|
||||||
cacheReadPerMillion: 0.5,
|
cacheReadPerMillion: 0.5,
|
||||||
|
serviceTiers: {
|
||||||
|
fast: {
|
||||||
|
inputPerMillion: 30.0,
|
||||||
|
outputPerMillion: 150.0,
|
||||||
|
cacheCreationPerMillion: 37.5, // 30 * 1.25 (5m cache write multiplier)
|
||||||
|
cacheReadPerMillion: 3.0, // 30 * 0.1 (cache read multiplier)
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Claude 4.8 Opus ($5/$25)
|
// Claude 4.8 Opus ($5/$25) — fast mode ($10/$50, 2x premium per Anthropic docs)
|
||||||
'claude-opus-4-8': {
|
'claude-opus-4-8': {
|
||||||
inputPerMillion: 5.0,
|
inputPerMillion: 5.0,
|
||||||
outputPerMillion: 25.0,
|
outputPerMillion: 25.0,
|
||||||
cacheCreationPerMillion: 6.25,
|
cacheCreationPerMillion: 6.25,
|
||||||
cacheReadPerMillion: 0.5,
|
cacheReadPerMillion: 0.5,
|
||||||
|
serviceTiers: {
|
||||||
|
fast: {
|
||||||
|
inputPerMillion: 10.0,
|
||||||
|
outputPerMillion: 50.0,
|
||||||
|
cacheCreationPerMillion: 12.5, // 10 * 1.25 (5m cache write multiplier)
|
||||||
|
cacheReadPerMillion: 1.0, // 10 * 0.1 (cache read multiplier)
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -893,12 +934,28 @@ function hasProviderContext(model: string, options: PricingLookupOptions): boole
|
|||||||
return Boolean(options.provider || /^[^/]+\//.test(model.trim()));
|
return Boolean(options.provider || /^[^/]+\//.test(model.trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply per-service-tier rates if the resolved model declares them and the
|
||||||
|
* caller requested a matching tier. Unknown tiers transparently fall through
|
||||||
|
* to the base rates so existing callers stay unaffected.
|
||||||
|
*/
|
||||||
|
function applyServiceTier(pricing: ModelPricing, tier: string | undefined): ModelPricing {
|
||||||
|
if (!tier) return pricing;
|
||||||
|
const tierRates = pricing.serviceTiers?.[tier];
|
||||||
|
if (!tierRates) return pricing;
|
||||||
|
return { ...tierRates, serviceTiers: pricing.serviceTiers };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get pricing for a model with narrow fuzzy matching fallback.
|
* Get pricing for a model with narrow fuzzy matching fallback.
|
||||||
* Unknown future model families should fall back instead of inheriting the
|
* Unknown future model families should fall back instead of inheriting the
|
||||||
* first known family tier that happens to share a prefix.
|
* first known family tier that happens to share a prefix.
|
||||||
*/
|
*/
|
||||||
export function getModelPricing(model: string, options: PricingLookupOptions = {}): ModelPricing {
|
export function getModelPricing(model: string, options: PricingLookupOptions = {}): ModelPricing {
|
||||||
|
return applyServiceTier(resolveBasePricing(model, options), options.serviceTier);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBasePricing(model: string, options: PricingLookupOptions): ModelPricing {
|
||||||
if (hasProviderContext(model, options)) {
|
if (hasProviderContext(model, options)) {
|
||||||
const ccsOverridePricing = getCcsPolicyOverridePricing(model);
|
const ccsOverridePricing = getCcsPolicyOverridePricing(model);
|
||||||
if (ccsOverridePricing !== undefined) {
|
if (ccsOverridePricing !== undefined) {
|
||||||
|
|||||||
@@ -176,6 +176,46 @@ describe('model-pricing', () => {
|
|||||||
expect(opus48.cacheReadPerMillion).toBe(0.5);
|
expect(opus48.cacheReadPerMillion).toBe(0.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should match date-stamped Claude Opus 4.8 to correct pricing', () => {
|
||||||
|
// Anthropic stopped issuing date-stamped Opus IDs starting with the 4.6
|
||||||
|
// generation, so this is a defensive guard for stripDateSuffix in case
|
||||||
|
// a third-party emits a synthesized dated alias.
|
||||||
|
const opus48dated = getModelPricing('claude-opus-4-8-20260530');
|
||||||
|
expect(opus48dated.inputPerMillion).toBe(5.0);
|
||||||
|
expect(opus48dated.outputPerMillion).toBe(25.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return fast-tier pricing for Claude Opus 4.8 when serviceTier=fast', () => {
|
||||||
|
// Anthropic's "fast mode" charges 2x for Opus 4.8 ($10/$50 vs $5/$25).
|
||||||
|
// Cache rates scale by the same standard multipliers (1.25x write, 0.1x read).
|
||||||
|
const opus48fast = getModelPricing('claude-opus-4-8', { serviceTier: 'fast' });
|
||||||
|
expect(opus48fast.inputPerMillion).toBe(10.0);
|
||||||
|
expect(opus48fast.outputPerMillion).toBe(50.0);
|
||||||
|
expect(opus48fast.cacheCreationPerMillion).toBe(12.5);
|
||||||
|
expect(opus48fast.cacheReadPerMillion).toBe(1.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return fast-tier pricing for Claude Opus 4.7 when serviceTier=fast', () => {
|
||||||
|
// Fast mode on 4.7 carries a 6x premium ($30/$150 per Anthropic docs).
|
||||||
|
const opus47fast = getModelPricing('claude-opus-4-7', { serviceTier: 'fast' });
|
||||||
|
expect(opus47fast.inputPerMillion).toBe(30.0);
|
||||||
|
expect(opus47fast.outputPerMillion).toBe(150.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to standard rates when serviceTier is unknown', () => {
|
||||||
|
// Unknown tier names must not throw; revert to base pricing transparently.
|
||||||
|
const opus48 = getModelPricing('claude-opus-4-8', { serviceTier: 'enterprise-mythos' });
|
||||||
|
expect(opus48.inputPerMillion).toBe(5.0);
|
||||||
|
expect(opus48.outputPerMillion).toBe(25.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve standard rates for Opus 4.8 when serviceTier omitted', () => {
|
||||||
|
// Regression guard: existing callers must keep current behavior.
|
||||||
|
const opus48 = getModelPricing('claude-opus-4-8');
|
||||||
|
expect(opus48.inputPerMillion).toBe(5.0);
|
||||||
|
expect(opus48.outputPerMillion).toBe(25.0);
|
||||||
|
});
|
||||||
|
|
||||||
it('should not map unknown future model families onto known family pricing', () => {
|
it('should not map unknown future model families onto known family pricing', () => {
|
||||||
const fallback = getModelPricing('unknown-model-xyz');
|
const fallback = getModelPricing('unknown-model-xyz');
|
||||||
|
|
||||||
@@ -285,6 +325,17 @@ describe('model-pricing', () => {
|
|||||||
const cost = calculateCost(usage, 'claude-opus-4-8');
|
const cost = calculateCost(usage, 'claude-opus-4-8');
|
||||||
expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5
|
expect(cost).toBe(36.75); // 5 + 25 + 6.25 + 0.5
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should calculate fast-tier Claude Opus 4.8 cost (2x premium)', () => {
|
||||||
|
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-8', { serviceTier: 'fast' });
|
||||||
|
expect(cost).toBe(73.5); // 10 + 50 + 12.5 + 1.0
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getKnownModels', () => {
|
describe('getKnownModels', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user