mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 14:16:43 +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:
@@ -17,13 +17,23 @@ import {
|
||||
// TYPE DEFINITIONS
|
||||
// ============================================================================
|
||||
|
||||
export interface ModelPricing {
|
||||
export interface PricingRates {
|
||||
inputPerMillion: number;
|
||||
outputPerMillion: number;
|
||||
cacheCreationPerMillion: 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 {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
@@ -31,7 +41,14 @@ export interface TokenUsage {
|
||||
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
|
||||
@@ -223,12 +240,20 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
|
||||
cacheCreationPerMillion: 6.25,
|
||||
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': {
|
||||
inputPerMillion: 5.0,
|
||||
outputPerMillion: 25.0,
|
||||
cacheCreationPerMillion: 6.25,
|
||||
cacheReadPerMillion: 0.5,
|
||||
serviceTiers: {
|
||||
fast: {
|
||||
inputPerMillion: 30.0,
|
||||
outputPerMillion: 150.0,
|
||||
cacheCreationPerMillion: 37.5,
|
||||
cacheReadPerMillion: 3.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
'claude-opus-4-6-thinking': {
|
||||
inputPerMillion: 5.0,
|
||||
@@ -236,19 +261,35 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
|
||||
cacheCreationPerMillion: 6.25,
|
||||
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': {
|
||||
inputPerMillion: 5.0,
|
||||
outputPerMillion: 25.0,
|
||||
cacheCreationPerMillion: 6.25,
|
||||
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': {
|
||||
inputPerMillion: 5.0,
|
||||
outputPerMillion: 25.0,
|
||||
cacheCreationPerMillion: 6.25,
|
||||
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()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Unknown future model families should fall back instead of inheriting the
|
||||
* first known family tier that happens to share a prefix.
|
||||
*/
|
||||
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)) {
|
||||
const ccsOverridePricing = getCcsPolicyOverridePricing(model);
|
||||
if (ccsOverridePricing !== undefined) {
|
||||
|
||||
@@ -176,6 +176,46 @@ describe('model-pricing', () => {
|
||||
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', () => {
|
||||
const fallback = getModelPricing('unknown-model-xyz');
|
||||
|
||||
@@ -285,6 +325,17 @@ describe('model-pricing', () => {
|
||||
const cost = calculateCost(usage, 'claude-opus-4-8');
|
||||
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user