1import { describe, expect, it } from "@effect/vitest";
2
3import { cursorRateModel } from "@t3tools/provider-cursor/server/accountUsage";
4import type { UsageSpeed } from "@t3tools/provider-core/server/usage";
5import {
6 cacheSavingsUsd,
7 createOverrideRateTable,
8 lookupRate,
9 parseRateTable,
10 priceUsage,
11} from "./usagePricing.ts";
12
13const rate = (input: number, cacheRead?: number) => ({
14 input_cost_per_token: input,
15 output_cost_per_token: input * 5,
16 ...(cacheRead === undefined ? {} : { cache_read_input_token_cost: cacheRead }),
17});
18
19describe("usage pricing", () => {
20 const totals = {
21 uncachedInputTokens: 1_000_000,
22 cachedInputTokens: 1_000_000,
23 cacheCreationTokens: 1_000_000,
24 outputTokens: 1_000_000,
25 reasoningTokens: 500_000,
26 };
27 const record = (
28 model: string,
29 reportedCostUsd: number | null = null,
30 speed: UsageSpeed = "standard",
31 ) => ({
32 model,
33 totals,
34 reportedCostUsd,
35 speed,
36 });
37
38 it("uses custom token rates ahead of public and provider-reported costs", () => {
39 const table = parseRateTable({ "example-model": rate(1) });
40 const overrides = createOverrideRateTable({
41 "example-model": {
42 inputCostPerMillionTokens: 2,
43 outputCostPerMillionTokens: 8,
44 cacheReadCostPerMillionTokens: 0.5,
45 cacheWriteCostPerMillionTokens: 3,
46 },
47 });
48
49 for (const reportedCostUsd of [null, 99]) {
50 expect(priceUsage(table, record("example-model", reportedCostUsd), overrides)).toMatchObject({
51 costUsd: 13.5,
52 costSource: "modelPriced",
53 });
54 }
55 expect(cacheSavingsUsd(table, record("example-model"), overrides)).toBe(1.5);
56 });
57
58 it("prices Cursor cache savings at the base model rate", () => {
59 const table = parseRateTable({
60 "claude-fable-5-1": rate(10e-6, 1e-6),
61 "xai/grok-4.7": rate(2e-6, 0.5e-6),
62 "openrouter/x-ai/grok-4.7": rate(3e-6, 0.5e-6),
63 });
64 const cursorRecord = (model: string) => ({
65 ...record(model, 0.25),
66 rateModel: cursorRateModel(model),
67 });
68
69 expect(cacheSavingsUsd(table, cursorRecord("claude-fable-5-1-thinking-high"))).toBeCloseTo(9);
70 expect(cacheSavingsUsd(table, cursorRecord("cursor-grok-4.7-high-fast"))).toBeCloseTo(1.5);
71 expect(cacheSavingsUsd(table, cursorRecord("default"))).toBe(0);
72 expect(priceUsage(table, cursorRecord("grok-4.7-xhigh-fast"))).toMatchObject({
73 costUsd: 0.25,
74 costSource: "providerReported",
75 });
76 });
77
78 it("falls back to the display model when the rate model is unlisted", () => {
79 const table = parseRateTable({ "example-model": rate(1e-6, 0.1e-6) });
80 const unlisted = { ...record("example-model"), rateModel: "reseller/example-model" };
81
82 expect(priceUsage(table, unlisted)).toEqual(priceUsage(table, record("example-model")));
83 expect(cacheSavingsUsd(table, unlisted)).toBeCloseTo(0.9);
84 });
85
86 it("prices unknown models offline and uses input prices for omitted cache rates", () => {
87 const table = parseRateTable({});
88 const overrides = createOverrideRateTable({
89 "example-model": { inputCostPerMillionTokens: 2, outputCostPerMillionTokens: 8 },
90 });
91
92 expect(priceUsage(table, record("example-model"), overrides)).toMatchObject({
93 costUsd: 14,
94 costSource: "modelPriced",
95 });
96 expect(cacheSavingsUsd(table, record("example-model"), overrides)).toBe(0);
97 });
98
99 it("preserves explicit zero rates and matches only the exact trimmed model ID", () => {
100 const table = parseRateTable({});
101 const overrides = createOverrideRateTable({
102 " vendor/example-model[1m] ": {
103 inputCostPerMillionTokens: 0,
104 outputCostPerMillionTokens: 0,
105 },
106 });
107 expect(priceUsage(table, record(" vendor/example-model[1m] ", 99), overrides)).toMatchObject({
108 costUsd: 0,
109 costSource: "modelPriced",
110 });
111 for (const model of [
112 "example-model[1m]",
113 "vendor/example-model",
114 "vendor/Example-model[1m]",
115 "other/example-model[1m]",
116 ]) {
117 expect(priceUsage(table, record(model), overrides).costSource).toBe("unpriced");
118 expect(priceUsage(table, record(model, 99), overrides)).toEqual({
119 costUsd: 99,
120 costSource: "providerReported",
121 categoryCostUsd: null,
122 speedPremiumUsd: 0,
123 });
124 }
125 });
126
127 it("prices fast-mode requests at the model's published fast multiple", () => {
128 const table = parseRateTable({
129 "claude-opus-5-5": { ...rate(4e-6, 2e-7), provider_specific_entry: { fast: 2, us: 1.1 } },
130 "claude-fable-5-1": { ...rate(1e-5, 2.5e-7), provider_specific_entry: { us: 1.1 } },
131 });
132 const overrides = createOverrideRateTable({
133 "claude-opus-5-5": { inputCostPerMillionTokens: 4, outputCostPerMillionTokens: 20 },
134 });
135 const cost = (model: string, speed: UsageSpeed, custom?: typeof overrides) =>
136 priceUsage(table, record(model, null, speed), custom).costUsd;
137
138 expect(cost("claude-opus-5-5", "fast")).toBeCloseTo(2 * cost("claude-opus-5-5", "standard"));
139 expect(cacheSavingsUsd(table, record("claude-opus-5-5", null, "fast"))).toBeCloseTo(
140 2 * cacheSavingsUsd(table, record("claude-opus-5-5")),
141 );
142 expect(cost("claude-fable-5-1", "fast")).toBe(cost("claude-fable-5-1", "standard"));
143 expect(cost("claude-opus-5-5", "fast", overrides)).toBe(
144 cost("claude-opus-5-5", "standard", overrides),
145 );
146 });
147
148 it("splits cost by category and prices the speed premium", () => {
149 const table = parseRateTable({
150 "claude-opus-5-5": {
151 ...rate(4e-6, 4e-7),
152 cache_creation_input_token_cost: 5e-6,
153 provider_specific_entry: { fast: 2 },
154 },
155 });
156 const split = (input: number, cacheRead: number, cacheWrite: number, output: number) => ({
157 input: expect.closeTo(input),
158 cacheRead: expect.closeTo(cacheRead),
159 cacheWrite: expect.closeTo(cacheWrite),
160 output: expect.closeTo(output),
161 });
162
163 expect(priceUsage(table, record("claude-opus-5-5", null, "fast"))).toEqual({
164 costUsd: expect.closeTo(58.8),
165 costSource: "modelPriced",
166 categoryCostUsd: split(8, 0.8, 10, 40),
167 speedPremiumUsd: expect.closeTo(29.4),
168 });
169 expect(priceUsage(table, record("claude-opus-5-5", 29.4, "fast"))).toEqual({
170 costUsd: 29.4,
171 costSource: "providerReported",
172 categoryCostUsd: split(4, 0.4, 5, 20),
173 speedPremiumUsd: expect.closeTo(14.7),
174 });
175 expect(priceUsage(table, record("unknown-model", 29.4, "fast"))).toMatchObject({
176 costUsd: 29.4,
177 categoryCostUsd: null,
178 speedPremiumUsd: 0,
179 });
180 });
181
182 it("prices Codex priority and ultrafast requests at their published tier rates", () => {
183 const table = parseRateTable({
184 "gpt-6-astra": {
185 ...rate(1e-5, 1e-6),
186 input_cost_per_token_priority: 2e-5,
187 output_cost_per_token_priority: 1e-4,
188 cache_read_input_token_cost_priority: 2e-6,
189 input_cost_per_token_ultrafast: 6e-5,
190 output_cost_per_token_ultrafast: 3e-4,
191 },
192 "gpt-6-sol": rate(2e-6, 2e-7),
193 });
194 const cost = (model: string, speed: UsageSpeed) =>
195 priceUsage(table, record(model, null, speed)).costUsd;
196 const standard = cost("gpt-6-astra", "standard");
197
198 expect(cost("gpt-6-astra", "fast")).toBeCloseTo(2 * standard);
199 expect(cost("gpt-6-astra", "ultrafast")).toBeCloseTo(6 * standard);
200 expect(cacheSavingsUsd(table, record("gpt-6-astra", null, "ultrafast"))).toBeCloseTo(
201 6 * cacheSavingsUsd(table, record("gpt-6-astra")),
202 );
203 expect(cost("gpt-6-sol", "ultrafast")).toBe(cost("gpt-6-sol", "standard"));
204 });
205
206 it("prices a request above the model's long-context threshold at its above-tier rates", () => {
207 const table = parseRateTable({
208 "claude-haiku-5-5": {
209 input_cost_per_token: 1e-6,
210 output_cost_per_token: 5e-6,
211 cache_read_input_token_cost: 1e-7,
212 cache_creation_input_token_cost: 1.25e-6,
213 input_cost_per_token_above_100k_tokens: 2e-6,
214 output_cost_per_token_above_100k_tokens: 7.5e-6,
215 cache_read_input_token_cost_above_100k_tokens: 2e-7,
216 cache_creation_input_token_cost_above_100k_tokens: 2.5e-6,
217 },
218 "gpt-6-sol": {
219 ...rate(2e-6, 2e-7),
220 input_cost_per_token_priority: 4e-6,
221 output_cost_per_token_priority: 2e-5,
222 cache_read_input_token_cost_priority: 4e-7,
223 input_cost_per_token_above_272k_tokens_priority: 8e-6,
224 output_cost_per_token_above_272k_tokens_priority: 3e-5,
225 cache_read_input_token_cost_above_272k_tokens_priority: 8e-7,
226 },
227 });
228 const longRequest = (
229 model: string,
230 speed: UsageSpeed,
231 totals: { uncached: number; cacheRead: number; cacheWrite: number; output: number },
232 ) => ({
233 model,
234 speed,
235 reportedCostUsd: null,
236 totals: {
237 uncachedInputTokens: totals.uncached,
238 cachedInputTokens: totals.cacheRead,
239 cacheCreationTokens: totals.cacheWrite,
240 outputTokens: totals.output,
241 reasoningTokens: 0,
242 },
243 });
244
245 // The Claude prompt is input + cache read + cache write: 120K, above the 100K tier.
246 expect
247 .soft(
248 priceUsage(
249 table,
250 longRequest("claude-haiku-5-5", "standard", {
251 uncached: 10_000,
252 cacheRead: 100_000,
253 cacheWrite: 10_000,
254 output: 1_000,
255 }),
256 ).costUsd,
257 )
258 .toBeCloseTo(10_000 * 2e-6 + 100_000 * 2e-7 + 10_000 * 2.5e-6 + 1_000 * 7.5e-6, 12);
259 expect(
260 priceUsage(
261 table,
262 longRequest("gpt-6-sol", "fast", {
263 uncached: 20_000,
264 cacheRead: 280_000,
265 cacheWrite: 0,
266 output: 1_000,
267 }),
268 ).costUsd,
269 ).toBeCloseTo(20_000 * 8e-6 + 280_000 * 8e-7 + 1_000 * 3e-5, 12);
270 });
271
272 it("bills the long-context tier only above its threshold, filling missing tier fields from base", () => {
273 const table = parseRateTable({
274 "gpt-6-sol": {
275 input_cost_per_token: 2e-6,
276 output_cost_per_token: 1e-5,
277 cache_read_input_token_cost: 2e-7,
278 input_cost_per_token_priority: 4e-6,
279 output_cost_per_token_priority: 2e-5,
280 input_cost_per_token_above_272k_tokens: 4e-6,
281 },
282 });
283 const cost = (prompt: number, speed: UsageSpeed = "standard") =>
284 priceUsage(table, {
285 model: "gpt-6-sol",
286 speed,
287 reportedCostUsd: null,
288 totals: {
289 uncachedInputTokens: prompt - 200_000,
290 cachedInputTokens: 200_000,
291 cacheCreationTokens: 0,
292 outputTokens: 1_000,
293 reasoningTokens: 0,
294 },
295 }).costUsd;
296
297 expect(cost(272_000)).toBeCloseTo(72_000 * 2e-6 + 200_000 * 2e-7 + 1_000 * 1e-5, 12);
298 // Output keeps the base rate; cache reads keep the base discount relative to input.
299 expect(cost(272_001)).toBeCloseTo(72_001 * 4e-6 + 200_000 * 4e-7 + 1_000 * 1e-5, 12);
300 // Without a priority long-context rate, priority requests keep the priority rate.
301 expect(cost(272_001, "fast")).toBeCloseTo(72_001 * 4e-6 + 200_000 * 4e-7 + 1_000 * 2e-5, 12);
302 });
303
304 it("keeps the canonical Fable rate separate from DeepInfra in either order", () => {
305 const canonical = ["claude-fable-5", rate(1e-5, 1e-6)] as const;
306 const deepInfra = ["deepinfra/anthropic/claude-fable-5", rate(1e-5)] as const;
307
308 for (const entries of [
309 [canonical, deepInfra],
310 [deepInfra, canonical],
311 ]) {
312 const table = parseRateTable(Object.fromEntries(entries));
313
314 expect(lookupRate(table, "claude-fable-5")?.cacheReadCostPerToken).toBe(1e-6);
315 expect(lookupRate(table, "deepinfra/anthropic/claude-fable-5")?.cacheReadCostPerToken).toBe(
316 1e-5,
317 );
318 expect(lookupRate(table, "other/claude-fable-5")).toBeNull();
319 }
320 });
321
322 it("prices a bracketed context-tier variant at the base model's rate", () => {
323 const table = parseRateTable({ "claude-fable-5-1": rate(1e-5, 2.5e-7) });
324
325 expect(lookupRate(table, "claude-fable-5-1[1m]")).toEqual(
326 lookupRate(table, "claude-fable-5-1"),
327 );
328 expect(lookupRate(table, "anthropic/Claude-Fable-5-1[1m]")).toBeNull();
329 });
330
331 it("adds a bare alias when every qualified entry has the same rate", () => {
332 const table = parseRateTable({
333 "provider-a/example-model": rate(1),
334 "provider-b/example-model": rate(1),
335 });
336
337 expect(lookupRate(table, "example-model")).toEqual(
338 lookupRate(table, "provider-a/example-model"),
339 );
340 });
341
342 it("keeps a bare alias when a reseller entry omits the fast tier", () => {
343 const table = parseRateTable({
344 "provider-a/example-model": {
345 ...rate(1),
346 input_cost_per_token_priority: 2,
347 output_cost_per_token_priority: 10,
348 input_cost_per_token_above_272k_tokens: 2,
349 input_cost_per_token_above_272k_tokens_priority: 4,
350 output_cost_per_token_above_272k_tokens_priority: 20,
351 },
352 "provider-b/example-model": { ...rate(1), input_cost_per_token_above_272k_tokens: 2 },
353 "provider-c/example-model": rate(1),
354 });
355
356 expect(lookupRate(table, "example-model")).toEqual(
357 lookupRate(table, "provider-a/example-model"),
358 );
359 });
360
361 it("leaves an ambiguous bare name unpriced", () => {
362 const table = parseRateTable({
363 "provider-a/example-model": rate(1),
364 "provider-b/example-model": rate(3),
365 });
366
367 expect(lookupRate(table, "provider-a/example-model")?.inputCostPerToken).toBe(1);
368 expect(lookupRate(table, "provider-b/example-model")?.inputCostPerToken).toBe(3);
369 expect(lookupRate(table, "example-model")).toBeNull();
370 });
371
372 it("prices a bare name at the model maker's rate when a reseller marks it up", () => {
373 const reseller = { "kyma/gemini-4-argon": rate(5.4e-6) };
374 const makers = {
375 "gemini/gemini-4-argon": rate(2e-6, 1e-7),
376 "vertex_ai/gemini-4-argon": rate(2e-6, 1e-7),
377 };
378
379 for (const document of [
380 { ...reseller, ...makers },
381 { ...makers, ...reseller },
382 ]) {
383 const table = parseRateTable(document);
384 expect(lookupRate(table, "gemini-4-argon")).toEqual(
385 lookupRate(table, "gemini/gemini-4-argon"),
386 );
387 }
388 });
389
390 it("prices a bare name listed only under Bedrock IDs at the base anthropic rate", () => {
391 const bedrock = (input: number, output: number, cacheRead: number, cacheCreation: number) => ({
392 input_cost_per_token: input,
393 output_cost_per_token: output,
394 cache_read_input_token_cost: cacheRead,
395 cache_creation_input_token_cost: cacheCreation,
396 });
397 const base = bedrock(8e-6, 4e-5, 2e-7, 1e-5);
398 const regional = bedrock(8.8e-6, 4.4e-5, 2.2e-7, 1.1e-5);
399 const govCloud = bedrock(9.6e-6, 4.8e-5, 2.4e-7, 1.2e-5);
400 const rows = [
401 ["anthropic.claude-mythos-5-2", base],
402 ["global.anthropic.claude-mythos-5-2", base],
403 ["us.anthropic.claude-mythos-5-2", regional],
404 ["eu.anthropic.claude-mythos-5-2", regional],
405 ["us-gov.anthropic.claude-mythos-5-2", govCloud],
406 ["bedrock/us-gov-east-1/anthropic.claude-mythos-5-2", govCloud],
407 ["bedrock/us-gov-west-1/anthropic.claude-mythos-5-2", govCloud],
408 ] as const;
409
410 for (const entries of [rows, rows.toReversed()]) {
411 const table = parseRateTable(Object.fromEntries(entries));
412 expect(lookupRate(table, "claude-mythos-5-2")).toMatchObject({
413 inputCostPerToken: 8e-6,
414 outputCostPerToken: 4e-5,
415 cacheReadCostPerToken: 2e-7,
416 cacheCreationCostPerToken: 1e-5,
417 });
418 }
419 });
420
421 it("leaves a bare name unpriced when model makers' own entries disagree", () => {
422 const table = parseRateTable({
423 "openai/example-model": rate(1),
424 "xai/example-model": rate(3),
425 "kyma/example-model": rate(1),
426 });
427
428 expect(lookupRate(table, "example-model")).toBeNull();
429 });
430});