Skip to content

Commit 0f52a75

Browse files
committed
fix(cost): account cache writes per TTL class instead of one aggregate rate
What: - Add `cacheWritesPrice1h` / `cacheWritesPrice12h` to the ModelInfo schema plus `deriveCacheWritesPrice1h` / `deriveCacheWritesPrice12h`, so the vendor multiplier relationship is stated once in model.ts. - Bill three write classes in `calculateApiCostInternal`: the 5m remainder is derived as total - 1h - 12h (clamped at 0), so the classes always sum to the aggregate and no token is billed twice or dropped. - Scale the TTL rates with `cacheWritesPriceMultiplier` in long-context pricing. - Carry `cacheWrite1hTokens` / `cacheWrite12hTokens` on `ApiStreamUsageChunk` and read them from Anthropic `usage.cache_creation.ephemeral_1h_input_tokens` (anthropic, minimax, anthropic-vertex) and OpenAI Responses `input_tokens_details.cache_write_tokens_details.ttl_12h_tokens` (openai-native `normalizeUsage` only). - Thread the split through the Task usage accumulator, including the cancel-path cost recomputation that runs when a provider yields no totalCost. - Populate the rates: 22 Anthropic entries, 19 Bedrock Anthropic-family entries, 7 OpenAI GPT-5.6/6-class entries. Why: Anthropic bills a 1h cache write at 2x input versus 1.25x for 5m, and OpenAI bills a 12h write at 2.5x versus 1.25x for 30m. The repo read only the aggregate `cache_creation_input_tokens` and priced every write at the short rate, so a 1h write under-reported by 37.5% and a 12h write by 50%. The vendors already report the per-TTL split; it was being discarded. Impact: Reporting only. No request shape changes: the repo emits no `ttl` field, so live traffic still reports 0/absent long-lived writes and pricing is byte-identical to before (asserted against the historical cost literals). When the TTL knob lands, a 1h write can no longer be billed at the 5m rate. 18 new tests fail on the base blob; the new cost code has no surviving mutants.
1 parent 6fd6fad commit 0f52a75

16 files changed

Lines changed: 788 additions & 4 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// npx vitest run src/__tests__/cache-write-ttl-pricing.test.ts
2+
3+
import { describe, it, expect } from "vitest"
4+
5+
import { anthropicModels } from "../providers/anthropic.js"
6+
import { bedrockModels } from "../providers/bedrock.js"
7+
import { openAiNativeModels } from "../providers/openai.js"
8+
import {
9+
CACHE_WRITE_MULTIPLIER_12H,
10+
CACHE_WRITE_MULTIPLIER_1H,
11+
deriveCacheWritesPrice12h,
12+
deriveCacheWritesPrice1h,
13+
type ModelInfo,
14+
} from "../model.js"
15+
16+
/**
17+
* Catalog invariant for the TTL accounting program (ADR-0006 phase 1).
18+
*
19+
* A model that advertises prompt caching on a provider which can report a
20+
* long-lived cache write must carry the matching per-TTL write rate. Without it
21+
* the cost layer can only fall back to the aggregate write rate, which
22+
* under-reports every long-lived write (37.5% for Anthropic 1h, 50% for OpenAI 12h).
23+
*/
24+
describe("cache-write TTL pricing catalog invariant", () => {
25+
const anthropicFamilyEntries = Object.entries(anthropicModels as Record<string, ModelInfo>)
26+
27+
const bedrockAnthropicEntries = Object.entries(bedrockModels as Record<string, ModelInfo>).filter(([id]) =>
28+
id.startsWith("anthropic."),
29+
)
30+
31+
const openAiLongLivedEntries = Object.entries(openAiNativeModels as Record<string, ModelInfo>).filter(
32+
([id]) => id.startsWith("gpt-5.6") || id.startsWith("gpt-6"),
33+
)
34+
35+
it("every Anthropic catalog entry with prompt caching defines cacheWritesPrice1h", () => {
36+
const missing = anthropicFamilyEntries
37+
.filter(([, info]) => info.supportsPromptCache && info.cacheWritesPrice1h === undefined)
38+
.map(([id]) => id)
39+
40+
expect(missing).toEqual([])
41+
})
42+
43+
it("every Bedrock Anthropic-family entry with prompt caching defines cacheWritesPrice1h", () => {
44+
const missing = bedrockAnthropicEntries
45+
.filter(([, info]) => info.supportsPromptCache && info.cacheWritesPrice1h === undefined)
46+
.map(([id]) => id)
47+
48+
expect(missing).toEqual([])
49+
})
50+
51+
it("every OpenAI 5.6/6-class entry defines cacheWritesPrice12h", () => {
52+
expect(openAiLongLivedEntries.length).toBeGreaterThan(0)
53+
54+
const missing = openAiLongLivedEntries
55+
.filter(([, info]) => info.supportsPromptCache && info.cacheWritesPrice12h === undefined)
56+
.map(([id]) => id)
57+
58+
expect(missing).toEqual([])
59+
})
60+
61+
it("Anthropic 1h rates match the documented 2x input-price relationship", () => {
62+
for (const [id, info] of anthropicFamilyEntries) {
63+
if (info.cacheWritesPrice1h === undefined || info.inputPrice === undefined) continue
64+
expect(info.cacheWritesPrice1h, id).toBeCloseTo(info.inputPrice * CACHE_WRITE_MULTIPLIER_1H, 9)
65+
}
66+
})
67+
68+
it("OpenAI 12h rates match the documented 2.5x input-price relationship", () => {
69+
for (const [id, info] of openAiLongLivedEntries) {
70+
if (info.cacheWritesPrice12h === undefined || info.inputPrice === undefined) continue
71+
expect(info.cacheWritesPrice12h, id).toBeCloseTo(info.inputPrice * CACHE_WRITE_MULTIPLIER_12H, 9)
72+
}
73+
})
74+
75+
it("deriveCacheWritesPrice1h scales inputPrice by 2x and stays undefined without one", () => {
76+
expect(deriveCacheWritesPrice1h(3.0)).toBe(6.0)
77+
expect(deriveCacheWritesPrice1h(undefined)).toBeUndefined()
78+
})
79+
80+
it("deriveCacheWritesPrice12h scales inputPrice by 2.5x and stays undefined without one", () => {
81+
expect(deriveCacheWritesPrice12h(4.0)).toBe(10.0)
82+
expect(deriveCacheWritesPrice12h(undefined)).toBeUndefined()
83+
})
84+
})

‎packages/types/src/__tests__/openai-models.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ describe("OpenAI native models", () => {
122122
inputPrice: 2,
123123
outputPrice: 10,
124124
cacheWritesPrice: 2.5,
125+
cacheWritesPrice12h: 5,
125126
cacheReadsPrice: 0.2,
126127
longContextPricing: {
127128
thresholdTokens: 272_000,
@@ -166,6 +167,7 @@ describe("OpenAI native models", () => {
166167
inputPrice: 0.1,
167168
outputPrice: 0.5,
168169
cacheWritesPrice: 0.125,
170+
cacheWritesPrice12h: 0.25,
169171
cacheReadsPrice: 0.01,
170172
longContextPricing: {
171173
thresholdTokens: 272_000,

‎packages/types/src/model.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ export const modelInfoSchema = z.object({
136136
inputPrice: z.number().optional(),
137137
outputPrice: z.number().optional(),
138138
cacheWritesPrice: z.number().optional(),
139+
// Per-TTL cache-write rates. Vendors bill a longer cache lifetime at a higher
140+
// write rate than the default 5m/30m class, so a single `cacheWritesPrice`
141+
// under-reports whenever the response reports a long-lived write. Omitted
142+
// means "no long-lived writes are expected on this model"; the cost layer
143+
// derives a fail-closed rate from `inputPrice` if one still arrives.
144+
cacheWritesPrice1h: z.number().optional(),
145+
cacheWritesPrice12h: z.number().optional(),
139146
cacheReadsPrice: z.number().optional(),
140147
longContextPricing: z
141148
.object({
@@ -188,6 +195,34 @@ export const modelInfoSchema = z.object({
188195

189196
export type ModelInfo = z.infer<typeof modelInfoSchema>
190197

198+
/**
199+
* Multipliers vendors apply to the base input price for a cache write, by TTL class.
200+
* Anthropic: 5m = 1.25x, 1h = 2x. OpenAI: 30m = 1.25x, 12h = 2.5x.
201+
*/
202+
export const CACHE_WRITE_MULTIPLIER_5M = 1.25
203+
export const CACHE_WRITE_MULTIPLIER_1H = 2.0
204+
export const CACHE_WRITE_MULTIPLIER_12H = 2.5
205+
206+
/**
207+
* Derive the 1h cache-write rate from the base input price.
208+
*
209+
* Stated once here so every Anthropic-family catalog entry and every runtime
210+
* fallback agrees on the 2x relationship. Returns `undefined` when the model
211+
* has no input price to scale, which keeps the cost layer fail-closed: it will
212+
* fall back to `cacheWritesPrice` rather than invent a rate.
213+
*/
214+
export function deriveCacheWritesPrice1h(inputPrice: number | undefined): number | undefined {
215+
return inputPrice === undefined ? undefined : inputPrice * CACHE_WRITE_MULTIPLIER_1H
216+
}
217+
218+
/**
219+
* Derive the 12h cache-write rate from the base input price (OpenAI 12h class).
220+
* Same fail-closed contract as {@link deriveCacheWritesPrice1h}.
221+
*/
222+
export function deriveCacheWritesPrice12h(inputPrice: number | undefined): number | undefined {
223+
return inputPrice === undefined ? undefined : inputPrice * CACHE_WRITE_MULTIPLIER_12H
224+
}
225+
191226
export type ModelRecord = Record<string, ModelInfo>
192227

193228
export type RouterModels = Record<DynamicProvider | LocalProvider, ModelRecord>

‎packages/types/src/providers/anthropic.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const anthropicModels = {
1616
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
1717
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
1818
cacheWritesPrice: 3.75, // $3.75 per million tokens
19+
cacheWritesPrice1h: 6, // $6.00 per million tokens (2x input price, 1h cache writes)
1920
cacheReadsPrice: 0.3, // $0.30 per million tokens
2021
supportsReasoningBudget: true,
2122
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
@@ -37,6 +38,7 @@ export const anthropicModels = {
3738
inputPrice: 2.0, // $2 per million input tokens (introductory pricing through Aug 31, 2026)
3839
outputPrice: 10.0, // $10 per million output tokens (introductory pricing through Aug 31, 2026)
3940
cacheWritesPrice: 2.5, // $2.50 per million tokens (introductory pricing through Aug 31, 2026)
41+
cacheWritesPrice1h: 4, // $4.00 per million tokens (2x input price, 1h cache writes)
4042
cacheReadsPrice: 0.2, // $0.20 per million tokens (introductory pricing through Aug 31, 2026)
4143
// Sonnet 5 uses the same adaptive-thinking / binary-toggle convention as
4244
// Opus 4.7+ and Fable 5 on the direct Anthropic provider path. Manual
@@ -56,6 +58,7 @@ export const anthropicModels = {
5658
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
5759
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
5860
cacheWritesPrice: 3.75, // $3.75 per million tokens
61+
cacheWritesPrice1h: 6, // $6.00 per million tokens (2x input price, 1h cache writes)
5962
cacheReadsPrice: 0.3, // $0.30 per million tokens
6063
supportsReasoningBudget: true,
6164
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
@@ -77,6 +80,7 @@ export const anthropicModels = {
7780
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
7881
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
7982
cacheWritesPrice: 3.75, // $3.75 per million tokens
83+
cacheWritesPrice1h: 6, // $6.00 per million tokens (2x input price, 1h cache writes)
8084
cacheReadsPrice: 0.3, // $0.30 per million tokens
8185
supportsReasoningBudget: true,
8286
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
@@ -98,6 +102,7 @@ export const anthropicModels = {
98102
inputPrice: 5.0, // $5 per million input tokens (≤200K context)
99103
outputPrice: 25.0, // $25 per million output tokens (≤200K context)
100104
cacheWritesPrice: 6.25, // $6.25 per million tokens
105+
cacheWritesPrice1h: 10, // $10.00 per million tokens (2x input price, 1h cache writes)
101106
cacheReadsPrice: 0.5, // $0.50 per million tokens
102107
supportsReasoningBudget: true,
103108
// Tiered pricing for extended context (requires beta flag)
@@ -119,6 +124,7 @@ export const anthropicModels = {
119124
inputPrice: 5.0, // $5 per million input tokens
120125
outputPrice: 25.0, // $25 per million output tokens
121126
cacheWritesPrice: 6.25, // $6.25 per million tokens
127+
cacheWritesPrice1h: 10, // $10.00 per million tokens (2x input price, 1h cache writes)
122128
cacheReadsPrice: 0.5, // $0.50 per million tokens
123129
// Keep the hybrid-reasoning capability so Anthropic token-cap handling and
124130
// stored max-token overrides behave the same as before.
@@ -136,6 +142,7 @@ export const anthropicModels = {
136142
inputPrice: 5.0, // $5 per million input tokens (regular tier)
137143
outputPrice: 25.0, // $25 per million output tokens (regular tier)
138144
cacheWritesPrice: 6.25, // $6.25 per million tokens
145+
cacheWritesPrice1h: 10, // $10.00 per million tokens (2x input price, 1h cache writes)
139146
cacheReadsPrice: 0.5, // $0.50 per million tokens
140147
// 4.8 inherits the adaptive-thinking model introduced in 4.7 — no breaking
141148
// API changes. supportsReasoningBudget is kept true so the existing token-cap
@@ -154,6 +161,7 @@ export const anthropicModels = {
154161
inputPrice: 5.0, // $5 per million input tokens
155162
outputPrice: 25.0, // $25 per million output tokens
156163
cacheWritesPrice: 6.25, // $6.25 per million tokens
164+
cacheWritesPrice1h: 10, // $10.00 per million tokens (2x input price, 1h cache writes)
157165
cacheReadsPrice: 0.5, // $0.50 per million tokens
158166
// Opus 5 uses the same adaptive-thinking / binary-toggle convention as
159167
// Opus 4.7+ and Sonnet 5 on the direct Anthropic provider path. Manual
@@ -172,6 +180,7 @@ export const anthropicModels = {
172180
inputPrice: 4.0, // $4 per million input tokens
173181
outputPrice: 20.0, // $20 per million output tokens
174182
cacheWritesPrice: 5.0, // $5 per million tokens
183+
cacheWritesPrice1h: 8, // $8.00 per million tokens (2x input price, 1h cache writes)
175184
cacheReadsPrice: 0.2, // $0.20 per million tokens
176185
// Opus 5.5 always uses adaptive thinking and rejects manual budget_tokens.
177186
supportsReasoningBinary: true,
@@ -192,6 +201,7 @@ export const anthropicModels = {
192201
inputPrice: 0.1, // $0.10 per million input tokens
193202
outputPrice: 0.5, // $0.50 per million output tokens
194203
cacheWritesPrice: 0.125, // $0.125 per million tokens (5m cache writes)
204+
cacheWritesPrice1h: 0.2, // $0.20 per million tokens (2x input price, 1h cache writes)
195205
cacheReadsPrice: 0.01, // $0.01 per million tokens (cache hits / refreshes)
196206
// Haiku 5.5 uses the same adaptive-thinking / binary-toggle convention as
197207
// Sonnet 5 and Opus 5 on the direct Anthropic provider path. Manual
@@ -211,6 +221,7 @@ export const anthropicModels = {
211221
inputPrice: 10.0, // $10 per million input tokens
212222
outputPrice: 50.0, // $50 per million output tokens
213223
cacheWritesPrice: 12.5, // $12.50 per million tokens (5-minute cache)
224+
cacheWritesPrice1h: 20, // $20.00 per million tokens (2x input price, 1h cache writes)
214225
cacheReadsPrice: 0.25, // $0.25 per million tokens (2.5% of base input price)
215226
// Fable 5.1 only accepts adaptive thinking (or an omitted thinking field).
216227
// Manual budget_tokens and non-default sampling parameters return a 400.
@@ -228,6 +239,7 @@ export const anthropicModels = {
228239
inputPrice: 10.0, // $10 per million input tokens
229240
outputPrice: 50.0, // $50 per million output tokens
230241
cacheWritesPrice: 12.5, // $12.50 per million tokens
242+
cacheWritesPrice1h: 20, // $20.00 per million tokens (2x input price, 1h cache writes)
231243
cacheReadsPrice: 1.0, // $1.00 per million tokens
232244
// Fable 5 uses the same adaptive-thinking / binary-toggle convention as
233245
// Opus 4.7+ on the direct Anthropic provider path.
@@ -245,6 +257,7 @@ export const anthropicModels = {
245257
inputPrice: 5.0, // $5 per million input tokens
246258
outputPrice: 25.0, // $25 per million output tokens
247259
cacheWritesPrice: 6.25, // $6.25 per million tokens
260+
cacheWritesPrice1h: 10, // $10.00 per million tokens (2x input price, 1h cache writes)
248261
cacheReadsPrice: 0.5, // $0.50 per million tokens
249262
supportsReasoningBudget: true,
250263
},
@@ -256,6 +269,7 @@ export const anthropicModels = {
256269
inputPrice: 15.0, // $15 per million input tokens
257270
outputPrice: 75.0, // $75 per million output tokens
258271
cacheWritesPrice: 18.75, // $18.75 per million tokens
272+
cacheWritesPrice1h: 30, // $30.00 per million tokens (2x input price, 1h cache writes)
259273
cacheReadsPrice: 1.5, // $1.50 per million tokens
260274
supportsReasoningBudget: true,
261275
},
@@ -267,6 +281,7 @@ export const anthropicModels = {
267281
inputPrice: 15.0, // $15 per million input tokens
268282
outputPrice: 75.0, // $75 per million output tokens
269283
cacheWritesPrice: 18.75, // $18.75 per million tokens
284+
cacheWritesPrice1h: 30, // $30.00 per million tokens (2x input price, 1h cache writes)
270285
cacheReadsPrice: 1.5, // $1.50 per million tokens
271286
supportsReasoningBudget: true,
272287
},
@@ -278,6 +293,7 @@ export const anthropicModels = {
278293
inputPrice: 3.0, // $3 per million input tokens
279294
outputPrice: 15.0, // $15 per million output tokens
280295
cacheWritesPrice: 3.75, // $3.75 per million tokens
296+
cacheWritesPrice1h: 6, // $6.00 per million tokens (2x input price, 1h cache writes)
281297
cacheReadsPrice: 0.3, // $0.30 per million tokens
282298
supportsReasoningBudget: true,
283299
requiredReasoningBudget: true,
@@ -290,6 +306,7 @@ export const anthropicModels = {
290306
inputPrice: 3.0, // $3 per million input tokens
291307
outputPrice: 15.0, // $15 per million output tokens
292308
cacheWritesPrice: 3.75, // $3.75 per million tokens
309+
cacheWritesPrice1h: 6, // $6.00 per million tokens (2x input price, 1h cache writes)
293310
cacheReadsPrice: 0.3, // $0.30 per million tokens
294311
},
295312
"claude-3-5-sonnet-20241022": {
@@ -300,6 +317,7 @@ export const anthropicModels = {
300317
inputPrice: 3.0, // $3 per million input tokens
301318
outputPrice: 15.0, // $15 per million output tokens
302319
cacheWritesPrice: 3.75, // $3.75 per million tokens
320+
cacheWritesPrice1h: 6, // $6.00 per million tokens (2x input price, 1h cache writes)
303321
cacheReadsPrice: 0.3, // $0.30 per million tokens
304322
},
305323
"claude-3-5-haiku-20241022": {
@@ -310,6 +328,7 @@ export const anthropicModels = {
310328
inputPrice: 1.0,
311329
outputPrice: 5.0,
312330
cacheWritesPrice: 1.25,
331+
cacheWritesPrice1h: 2, // $2.00 per million tokens (2x input price, 1h cache writes)
313332
cacheReadsPrice: 0.1,
314333
},
315334
"claude-3-opus-20240229": {
@@ -320,6 +339,7 @@ export const anthropicModels = {
320339
inputPrice: 15.0,
321340
outputPrice: 75.0,
322341
cacheWritesPrice: 18.75,
342+
cacheWritesPrice1h: 30, // $30.00 per million tokens (2x input price, 1h cache writes)
323343
cacheReadsPrice: 1.5,
324344
},
325345
"claude-3-haiku-20240307": {
@@ -330,6 +350,7 @@ export const anthropicModels = {
330350
inputPrice: 0.25,
331351
outputPrice: 1.25,
332352
cacheWritesPrice: 0.3,
353+
cacheWritesPrice1h: 0.5, // $0.50 per million tokens (2x input price, 1h cache writes)
333354
cacheReadsPrice: 0.03,
334355
},
335356
"claude-haiku-4-5-20251001": {
@@ -340,6 +361,7 @@ export const anthropicModels = {
340361
inputPrice: 1.0,
341362
outputPrice: 5.0,
342363
cacheWritesPrice: 1.25,
364+
cacheWritesPrice1h: 2, // $2.00 per million tokens (2x input price, 1h cache writes)
343365
cacheReadsPrice: 0.1,
344366
supportsReasoningBudget: true,
345367
description:

0 commit comments

Comments
 (0)