Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions packages/types/src/__tests__/cache-write-ttl-pricing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// npx vitest run src/__tests__/cache-write-ttl-pricing.test.ts

import { describe, it, expect } from "vitest"

import { anthropicModels } from "../providers/anthropic.js"
import { bedrockModels } from "../providers/bedrock.js"
import { openAiNativeModels } from "../providers/openai.js"
import {
CACHE_WRITE_MULTIPLIER_12H,
CACHE_WRITE_MULTIPLIER_1H,
deriveCacheWritesPrice12h,
deriveCacheWritesPrice1h,
type ModelInfo,
} from "../model.js"

/**
* Catalog invariant for the TTL accounting program (ADR-0006 phase 1).
*
* A model that advertises prompt caching on a provider which can report a
* long-lived cache write must carry the matching per-TTL write rate. Without it
* the cost layer can only fall back to the aggregate write rate, which
* under-reports every long-lived write (37.5% for Anthropic 1h, 50% for OpenAI 12h).
*/
describe("cache-write TTL pricing catalog invariant", () => {
const anthropicFamilyEntries = Object.entries(anthropicModels as Record<string, ModelInfo>)

const bedrockAnthropicEntries = Object.entries(bedrockModels as Record<string, ModelInfo>).filter(([id]) =>
id.startsWith("anthropic."),
)

const openAiLongLivedEntries = Object.entries(openAiNativeModels as Record<string, ModelInfo>).filter(
([id]) => id.startsWith("gpt-5.6") || id.startsWith("gpt-6"),
)

it("every Anthropic catalog entry with prompt caching defines cacheWritesPrice1h", () => {
const missing = anthropicFamilyEntries
.filter(([, info]) => info.supportsPromptCache && info.cacheWritesPrice1h === undefined)
.map(([id]) => id)

expect(missing).toEqual([])
})

it("every Bedrock Anthropic-family entry with prompt caching defines cacheWritesPrice1h", () => {
const missing = bedrockAnthropicEntries
.filter(([, info]) => info.supportsPromptCache && info.cacheWritesPrice1h === undefined)
.map(([id]) => id)

expect(missing).toEqual([])
})

it("every OpenAI 5.6/6-class entry defines cacheWritesPrice12h", () => {
expect(openAiLongLivedEntries.length).toBeGreaterThan(0)

const missing = openAiLongLivedEntries
.filter(([, info]) => info.supportsPromptCache && info.cacheWritesPrice12h === undefined)
.map(([id]) => id)

expect(missing).toEqual([])
})

it("Anthropic 1h rates match the documented 2x input-price relationship", () => {
for (const [id, info] of anthropicFamilyEntries) {
if (info.cacheWritesPrice1h === undefined || info.inputPrice === undefined) continue
expect(info.cacheWritesPrice1h, id).toBeCloseTo(info.inputPrice * CACHE_WRITE_MULTIPLIER_1H, 9)
}
})

it("OpenAI 12h rates match the documented 2.5x input-price relationship", () => {
for (const [id, info] of openAiLongLivedEntries) {
if (info.cacheWritesPrice12h === undefined || info.inputPrice === undefined) continue
expect(info.cacheWritesPrice12h, id).toBeCloseTo(info.inputPrice * CACHE_WRITE_MULTIPLIER_12H, 9)
}
})

it("deriveCacheWritesPrice1h scales inputPrice by 2x and stays undefined without one", () => {
expect(deriveCacheWritesPrice1h(3.0)).toBe(6.0)
expect(deriveCacheWritesPrice1h(undefined)).toBeUndefined()
})

it("deriveCacheWritesPrice12h scales inputPrice by 2.5x and stays undefined without one", () => {
expect(deriveCacheWritesPrice12h(4.0)).toBe(10.0)
expect(deriveCacheWritesPrice12h(undefined)).toBeUndefined()
})
})
2 changes: 2 additions & 0 deletions packages/types/src/__tests__/openai-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ describe("OpenAI native models", () => {
inputPrice: 2,
outputPrice: 10,
cacheWritesPrice: 2.5,
cacheWritesPrice12h: 5,
cacheReadsPrice: 0.2,
longContextPricing: {
thresholdTokens: 272_000,
Expand Down Expand Up @@ -166,6 +167,7 @@ describe("OpenAI native models", () => {
inputPrice: 0.1,
outputPrice: 0.5,
cacheWritesPrice: 0.125,
cacheWritesPrice12h: 0.25,
cacheReadsPrice: 0.01,
longContextPricing: {
thresholdTokens: 272_000,
Expand Down
40 changes: 40 additions & 0 deletions packages/types/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ export const modelInfoSchema = z.object({
inputPrice: z.number().optional(),
outputPrice: z.number().optional(),
cacheWritesPrice: z.number().optional(),
// Per-TTL cache-write rates. Vendors bill a longer cache lifetime at a higher
// write rate than the default 5m/30m class, so a single `cacheWritesPrice`
// under-reports whenever the response reports a long-lived write. Omitted
// means "no long-lived writes are expected on this model"; the cost layer
// derives a fail-closed rate from `inputPrice` if one still arrives.
cacheWritesPrice1h: z.number().optional(),
cacheWritesPrice12h: z.number().optional(),
cacheReadsPrice: z.number().optional(),
longContextPricing: z
.object({
Expand Down Expand Up @@ -180,6 +187,11 @@ export const modelInfoSchema = z.object({
inputPrice: z.number().optional(),
outputPrice: z.number().optional(),
cacheWritesPrice: z.number().optional(),
// Per-TTL write rates for this tier. A tier that overrides inputPrice
// must also override these, otherwise a long-context or flex/priority
// write bills at the base tier's TTL rate.
cacheWritesPrice1h: z.number().optional(),
cacheWritesPrice12h: z.number().optional(),
cacheReadsPrice: z.number().optional(),
}),
)
Expand All @@ -188,6 +200,34 @@ export const modelInfoSchema = z.object({

export type ModelInfo = z.infer<typeof modelInfoSchema>

/**
* Multipliers vendors apply to the base input price for a cache write, by TTL class.
* Anthropic: 5m = 1.25x, 1h = 2x. OpenAI: 30m = 1.25x, 12h = 2.5x.
*/
export const CACHE_WRITE_MULTIPLIER_5M = 1.25
export const CACHE_WRITE_MULTIPLIER_1H = 2.0
export const CACHE_WRITE_MULTIPLIER_12H = 2.5

/**
* Derive the 1h cache-write rate from the base input price.
*
* Stated once here so every Anthropic-family catalog entry and every runtime
* fallback agrees on the 2x relationship. Returns `undefined` when the model
* has no input price to scale, which keeps the cost layer fail-closed: it will
* fall back to `cacheWritesPrice` rather than invent a rate.
*/
export function deriveCacheWritesPrice1h(inputPrice: number | undefined): number | undefined {
return inputPrice === undefined ? undefined : inputPrice * CACHE_WRITE_MULTIPLIER_1H
}

/**
* Derive the 12h cache-write rate from the base input price (OpenAI 12h class).
* Same fail-closed contract as {@link deriveCacheWritesPrice1h}.
*/
export function deriveCacheWritesPrice12h(inputPrice: number | undefined): number | undefined {
return inputPrice === undefined ? undefined : inputPrice * CACHE_WRITE_MULTIPLIER_12H
}

export type ModelRecord = Record<string, ModelInfo>

export type RouterModels = Record<DynamicProvider | LocalProvider, ModelRecord>
Expand Down
26 changes: 26 additions & 0 deletions packages/types/src/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const anthropicModels = {
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
cacheWritesPrice: 3.75, // $3.75 per million tokens
cacheWritesPrice1h: 6, // $6.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.3, // $0.30 per million tokens
supportsReasoningBudget: true,
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
Expand All @@ -25,6 +26,7 @@ export const anthropicModels = {
inputPrice: 6.0, // $6 per million input tokens (>200K context)
outputPrice: 22.5, // $22.50 per million output tokens (>200K context)
cacheWritesPrice: 7.5, // $7.50 per million tokens (>200K context)
cacheWritesPrice1h: 12, // $12.00 per million tokens (2x tier input price, 1h cache writes)
cacheReadsPrice: 0.6, // $0.60 per million tokens (>200K context)
},
],
Expand All @@ -37,6 +39,7 @@ export const anthropicModels = {
inputPrice: 2.0, // $2 per million input tokens (introductory pricing through Aug 31, 2026)
outputPrice: 10.0, // $10 per million output tokens (introductory pricing through Aug 31, 2026)
cacheWritesPrice: 2.5, // $2.50 per million tokens (introductory pricing through Aug 31, 2026)
cacheWritesPrice1h: 4, // $4.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.2, // $0.20 per million tokens (introductory pricing through Aug 31, 2026)
// Sonnet 5 uses the same adaptive-thinking / binary-toggle convention as
// Opus 4.7+ and Fable 5 on the direct Anthropic provider path. Manual
Expand All @@ -56,6 +59,7 @@ export const anthropicModels = {
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
cacheWritesPrice: 3.75, // $3.75 per million tokens
cacheWritesPrice1h: 6, // $6.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.3, // $0.30 per million tokens
supportsReasoningBudget: true,
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
Expand All @@ -65,6 +69,7 @@ export const anthropicModels = {
inputPrice: 6.0, // $6 per million input tokens (>200K context)
outputPrice: 22.5, // $22.50 per million output tokens (>200K context)
cacheWritesPrice: 7.5, // $7.50 per million tokens (>200K context)
cacheWritesPrice1h: 12, // $12.00 per million tokens (2x tier input price, 1h cache writes)
cacheReadsPrice: 0.6, // $0.60 per million tokens (>200K context)
},
],
Expand All @@ -77,6 +82,7 @@ export const anthropicModels = {
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
cacheWritesPrice: 3.75, // $3.75 per million tokens
cacheWritesPrice1h: 6, // $6.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.3, // $0.30 per million tokens
supportsReasoningBudget: true,
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
Expand All @@ -86,6 +92,7 @@ export const anthropicModels = {
inputPrice: 6.0, // $6 per million input tokens (>200K context)
outputPrice: 22.5, // $22.50 per million output tokens (>200K context)
cacheWritesPrice: 7.5, // $7.50 per million tokens (>200K context)
cacheWritesPrice1h: 12, // $12.00 per million tokens (2x tier input price, 1h cache writes)
cacheReadsPrice: 0.6, // $0.60 per million tokens (>200K context)
},
],
Expand All @@ -98,6 +105,7 @@ export const anthropicModels = {
inputPrice: 5.0, // $5 per million input tokens (≤200K context)
outputPrice: 25.0, // $25 per million output tokens (≤200K context)
cacheWritesPrice: 6.25, // $6.25 per million tokens
cacheWritesPrice1h: 10, // $10.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.5, // $0.50 per million tokens
supportsReasoningBudget: true,
// Tiered pricing for extended context (requires beta flag)
Expand All @@ -107,6 +115,7 @@ export const anthropicModels = {
inputPrice: 10.0, // $10 per million input tokens (>200K context)
outputPrice: 37.5, // $37.50 per million output tokens (>200K context)
cacheWritesPrice: 12.5, // $12.50 per million tokens (>200K context)
cacheWritesPrice1h: 20, // $20.00 per million tokens (2x tier input price, 1h cache writes)
cacheReadsPrice: 1.0, // $1.00 per million tokens (>200K context)
},
],
Expand All @@ -119,6 +128,7 @@ export const anthropicModels = {
inputPrice: 5.0, // $5 per million input tokens
outputPrice: 25.0, // $25 per million output tokens
cacheWritesPrice: 6.25, // $6.25 per million tokens
cacheWritesPrice1h: 10, // $10.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.5, // $0.50 per million tokens
// Keep the hybrid-reasoning capability so Anthropic token-cap handling and
// stored max-token overrides behave the same as before.
Expand All @@ -136,6 +146,7 @@ export const anthropicModels = {
inputPrice: 5.0, // $5 per million input tokens (regular tier)
outputPrice: 25.0, // $25 per million output tokens (regular tier)
cacheWritesPrice: 6.25, // $6.25 per million tokens
cacheWritesPrice1h: 10, // $10.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.5, // $0.50 per million tokens
// 4.8 inherits the adaptive-thinking model introduced in 4.7 — no breaking
// API changes. supportsReasoningBudget is kept true so the existing token-cap
Expand All @@ -154,6 +165,7 @@ export const anthropicModels = {
inputPrice: 5.0, // $5 per million input tokens
outputPrice: 25.0, // $25 per million output tokens
cacheWritesPrice: 6.25, // $6.25 per million tokens
cacheWritesPrice1h: 10, // $10.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.5, // $0.50 per million tokens
// Opus 5 uses the same adaptive-thinking / binary-toggle convention as
// Opus 4.7+ and Sonnet 5 on the direct Anthropic provider path. Manual
Expand All @@ -172,6 +184,7 @@ export const anthropicModels = {
inputPrice: 4.0, // $4 per million input tokens
outputPrice: 20.0, // $20 per million output tokens
cacheWritesPrice: 5.0, // $5 per million tokens
cacheWritesPrice1h: 8, // $8.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.2, // $0.20 per million tokens
// Opus 5.5 always uses adaptive thinking and rejects manual budget_tokens.
supportsReasoningBinary: true,
Expand All @@ -192,6 +205,7 @@ export const anthropicModels = {
inputPrice: 0.1, // $0.10 per million input tokens
outputPrice: 0.5, // $0.50 per million output tokens
cacheWritesPrice: 0.125, // $0.125 per million tokens (5m cache writes)
cacheWritesPrice1h: 0.2, // $0.20 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.01, // $0.01 per million tokens (cache hits / refreshes)
// Haiku 5.5 uses the same adaptive-thinking / binary-toggle convention as
// Sonnet 5 and Opus 5 on the direct Anthropic provider path. Manual
Expand All @@ -211,6 +225,7 @@ export const anthropicModels = {
inputPrice: 10.0, // $10 per million input tokens
outputPrice: 50.0, // $50 per million output tokens
cacheWritesPrice: 12.5, // $12.50 per million tokens (5-minute cache)
cacheWritesPrice1h: 20, // $20.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.25, // $0.25 per million tokens (2.5% of base input price)
// Fable 5.1 only accepts adaptive thinking (or an omitted thinking field).
// Manual budget_tokens and non-default sampling parameters return a 400.
Expand All @@ -228,6 +243,7 @@ export const anthropicModels = {
inputPrice: 10.0, // $10 per million input tokens
outputPrice: 50.0, // $50 per million output tokens
cacheWritesPrice: 12.5, // $12.50 per million tokens
cacheWritesPrice1h: 20, // $20.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 1.0, // $1.00 per million tokens
// Fable 5 uses the same adaptive-thinking / binary-toggle convention as
// Opus 4.7+ on the direct Anthropic provider path.
Expand All @@ -245,6 +261,7 @@ export const anthropicModels = {
inputPrice: 5.0, // $5 per million input tokens
outputPrice: 25.0, // $25 per million output tokens
cacheWritesPrice: 6.25, // $6.25 per million tokens
cacheWritesPrice1h: 10, // $10.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.5, // $0.50 per million tokens
supportsReasoningBudget: true,
},
Expand All @@ -256,6 +273,7 @@ export const anthropicModels = {
inputPrice: 15.0, // $15 per million input tokens
outputPrice: 75.0, // $75 per million output tokens
cacheWritesPrice: 18.75, // $18.75 per million tokens
cacheWritesPrice1h: 30, // $30.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 1.5, // $1.50 per million tokens
supportsReasoningBudget: true,
},
Expand All @@ -267,6 +285,7 @@ export const anthropicModels = {
inputPrice: 15.0, // $15 per million input tokens
outputPrice: 75.0, // $75 per million output tokens
cacheWritesPrice: 18.75, // $18.75 per million tokens
cacheWritesPrice1h: 30, // $30.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 1.5, // $1.50 per million tokens
supportsReasoningBudget: true,
},
Expand All @@ -278,6 +297,7 @@ export const anthropicModels = {
inputPrice: 3.0, // $3 per million input tokens
outputPrice: 15.0, // $15 per million output tokens
cacheWritesPrice: 3.75, // $3.75 per million tokens
cacheWritesPrice1h: 6, // $6.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.3, // $0.30 per million tokens
supportsReasoningBudget: true,
requiredReasoningBudget: true,
Expand All @@ -290,6 +310,7 @@ export const anthropicModels = {
inputPrice: 3.0, // $3 per million input tokens
outputPrice: 15.0, // $15 per million output tokens
cacheWritesPrice: 3.75, // $3.75 per million tokens
cacheWritesPrice1h: 6, // $6.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.3, // $0.30 per million tokens
},
"claude-3-5-sonnet-20241022": {
Expand All @@ -300,6 +321,7 @@ export const anthropicModels = {
inputPrice: 3.0, // $3 per million input tokens
outputPrice: 15.0, // $15 per million output tokens
cacheWritesPrice: 3.75, // $3.75 per million tokens
cacheWritesPrice1h: 6, // $6.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.3, // $0.30 per million tokens
},
"claude-3-5-haiku-20241022": {
Expand All @@ -310,6 +332,7 @@ export const anthropicModels = {
inputPrice: 1.0,
outputPrice: 5.0,
cacheWritesPrice: 1.25,
cacheWritesPrice1h: 2, // $2.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.1,
},
"claude-3-opus-20240229": {
Expand All @@ -320,6 +343,7 @@ export const anthropicModels = {
inputPrice: 15.0,
outputPrice: 75.0,
cacheWritesPrice: 18.75,
cacheWritesPrice1h: 30, // $30.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 1.5,
},
"claude-3-haiku-20240307": {
Expand All @@ -330,6 +354,7 @@ export const anthropicModels = {
inputPrice: 0.25,
outputPrice: 1.25,
cacheWritesPrice: 0.3,
cacheWritesPrice1h: 0.5, // $0.50 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.03,
},
"claude-haiku-4-5-20251001": {
Expand All @@ -340,6 +365,7 @@ export const anthropicModels = {
inputPrice: 1.0,
outputPrice: 5.0,
cacheWritesPrice: 1.25,
cacheWritesPrice1h: 2, // $2.00 per million tokens (2x input price, 1h cache writes)
cacheReadsPrice: 0.1,
supportsReasoningBudget: true,
description:
Expand Down
Loading
Loading