| 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | |
| 3 | import { cursorRateModel } from "@t3tools/provider-cursor/server/accountUsage"; |
| 4 | import type { UsageSpeed } from "@t3tools/provider-core/server/usage"; |
| 5 | import { |
| 6 | cacheSavingsUsd, |
| 7 | createOverrideRateTable, |
| 8 | lookupRate, |
| 9 | parseRateTable, |
| 10 | priceUsage, |
| 11 | } from "./usagePricing.ts"; |
| 12 | |
| 13 | const 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 | |
| 19 | describe("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("prices unknown models offline and uses input prices for omitted cache rates", () => { |
| 79 | const table = parseRateTable({}); |
| 80 | const overrides = createOverrideRateTable({ |
| 81 | "example-model": { inputCostPerMillionTokens: 2, outputCostPerMillionTokens: 8 }, |
| 82 | }); |
| 83 | |
| 84 | expect(priceUsage(table, record("example-model"), overrides)).toMatchObject({ |
| 85 | costUsd: 14, |
| 86 | costSource: "modelPriced", |
| 87 | }); |
| 88 | expect(cacheSavingsUsd(table, record("example-model"), overrides)).toBe(0); |
| 89 | }); |
| 90 | |
| 91 | it("preserves explicit zero rates and matches only the exact trimmed model ID", () => { |
| 92 | const table = parseRateTable({}); |
| 93 | const overrides = createOverrideRateTable({ |
| 94 | " vendor/example-model[1m] ": { |
| 95 | inputCostPerMillionTokens: 0, |
| 96 | outputCostPerMillionTokens: 0, |
| 97 | }, |
| 98 | }); |
| 99 | expect(priceUsage(table, record(" vendor/example-model[1m] ", 99), overrides)).toMatchObject({ |
| 100 | costUsd: 0, |
| 101 | costSource: "modelPriced", |
| 102 | }); |
| 103 | for (const model of [ |
| 104 | "example-model[1m]", |
| 105 | "vendor/example-model", |
| 106 | "vendor/Example-model[1m]", |
| 107 | "other/example-model[1m]", |
| 108 | ]) { |
| 109 | expect(priceUsage(table, record(model), overrides).costSource).toBe("unpriced"); |
| 110 | expect(priceUsage(table, record(model, 99), overrides)).toEqual({ |
| 111 | costUsd: 99, |
| 112 | costSource: "providerReported", |
| 113 | categoryCostUsd: null, |
| 114 | speedPremiumUsd: 0, |
| 115 | }); |
| 116 | } |
| 117 | }); |
| 118 | |
| 119 | it("prices fast-mode requests at the model's published fast multiple", () => { |
| 120 | const table = parseRateTable({ |
| 121 | "claude-opus-5-5": { ...rate(4e-6, 2e-7), provider_specific_entry: { fast: 2, us: 1.1 } }, |
| 122 | "claude-fable-5-1": { ...rate(1e-5, 2.5e-7), provider_specific_entry: { us: 1.1 } }, |
| 123 | }); |
| 124 | const overrides = createOverrideRateTable({ |
| 125 | "claude-opus-5-5": { inputCostPerMillionTokens: 4, outputCostPerMillionTokens: 20 }, |
| 126 | }); |
| 127 | const cost = (model: string, speed: UsageSpeed, custom?: typeof overrides) => |
| 128 | priceUsage(table, record(model, null, speed), custom).costUsd; |
| 129 | |
| 130 | expect(cost("claude-opus-5-5", "fast")).toBeCloseTo(2 * cost("claude-opus-5-5", "standard")); |
| 131 | expect(cacheSavingsUsd(table, record("claude-opus-5-5", null, "fast"))).toBeCloseTo( |
| 132 | 2 * cacheSavingsUsd(table, record("claude-opus-5-5")), |
| 133 | ); |
| 134 | // No published fast tier, and custom prices, both stay at the standard rate. |
| 135 | expect(cost("claude-fable-5-1", "fast")).toBe(cost("claude-fable-5-1", "standard")); |
| 136 | expect(cost("claude-opus-5-5", "fast", overrides)).toBe( |
| 137 | cost("claude-opus-5-5", "standard", overrides), |
| 138 | ); |
| 139 | }); |
| 140 | |
| 141 | it("splits cost by category and prices the speed premium", () => { |
| 142 | const table = parseRateTable({ |
| 143 | "claude-opus-5-5": { |
| 144 | ...rate(4e-6, 4e-7), |
| 145 | cache_creation_input_token_cost: 5e-6, |
| 146 | provider_specific_entry: { fast: 2 }, |
| 147 | }, |
| 148 | }); |
| 149 | const split = (input: number, cacheRead: number, cacheWrite: number, output: number) => ({ |
| 150 | input: expect.closeTo(input), |
| 151 | cacheRead: expect.closeTo(cacheRead), |
| 152 | cacheWrite: expect.closeTo(cacheWrite), |
| 153 | output: expect.closeTo(output), |
| 154 | }); |
| 155 | |
| 156 | expect(priceUsage(table, record("claude-opus-5-5", null, "fast"))).toEqual({ |
| 157 | costUsd: expect.closeTo(58.8), |
| 158 | costSource: "modelPriced", |
| 159 | categoryCostUsd: split(8, 0.8, 10, 40), |
| 160 | speedPremiumUsd: expect.closeTo(29.4), |
| 161 | }); |
| 162 | // A reported cost keeps its total and splits in proportion to list rates. |
| 163 | expect(priceUsage(table, record("claude-opus-5-5", 29.4, "fast"))).toEqual({ |
| 164 | costUsd: 29.4, |
| 165 | costSource: "providerReported", |
| 166 | categoryCostUsd: split(4, 0.4, 5, 20), |
| 167 | speedPremiumUsd: expect.closeTo(14.7), |
| 168 | }); |
| 169 | // Without rates there is nothing to split it by. |
| 170 | expect(priceUsage(table, record("unknown-model", 29.4, "fast"))).toMatchObject({ |
| 171 | costUsd: 29.4, |
| 172 | categoryCostUsd: null, |
| 173 | speedPremiumUsd: 0, |
| 174 | }); |
| 175 | }); |
| 176 | |
| 177 | it("prices Codex priority and ultrafast requests at their published tier rates", () => { |
| 178 | const table = parseRateTable({ |
| 179 | "gpt-6-astra": { |
| 180 | ...rate(1e-5, 1e-6), |
| 181 | input_cost_per_token_priority: 2e-5, |
| 182 | output_cost_per_token_priority: 1e-4, |
| 183 | cache_read_input_token_cost_priority: 2e-6, |
| 184 | input_cost_per_token_ultrafast: 6e-5, |
| 185 | output_cost_per_token_ultrafast: 3e-4, |
| 186 | // No ultrafast cache rate: keeps the standard 10:1 input-to-cache ratio. |
| 187 | }, |
| 188 | "gpt-6-sol": rate(2e-6, 2e-7), |
| 189 | }); |
| 190 | const cost = (model: string, speed: UsageSpeed) => |
| 191 | priceUsage(table, record(model, null, speed)).costUsd; |
| 192 | const standard = cost("gpt-6-astra", "standard"); |
| 193 | |
| 194 | expect(cost("gpt-6-astra", "fast")).toBeCloseTo(2 * standard); |
| 195 | expect(cost("gpt-6-astra", "ultrafast")).toBeCloseTo(6 * standard); |
| 196 | expect(cacheSavingsUsd(table, record("gpt-6-astra", null, "ultrafast"))).toBeCloseTo( |
| 197 | 6 * cacheSavingsUsd(table, record("gpt-6-astra")), |
| 198 | ); |
| 199 | // A tier the model does not publish bills at the standard rate. |
| 200 | expect(cost("gpt-6-sol", "ultrafast")).toBe(cost("gpt-6-sol", "standard")); |
| 201 | }); |
| 202 | |
| 203 | it("keeps the canonical Fable rate separate from DeepInfra in either order", () => { |
| 204 | const canonical = ["claude-fable-5", rate(1e-5, 1e-6)] as const; |
| 205 | const deepInfra = ["deepinfra/anthropic/claude-fable-5", rate(1e-5)] as const; |
| 206 | |
| 207 | for (const entries of [ |
| 208 | [canonical, deepInfra], |
| 209 | [deepInfra, canonical], |
| 210 | ]) { |
| 211 | const table = parseRateTable(Object.fromEntries(entries)); |
| 212 | |
| 213 | expect(lookupRate(table, "claude-fable-5")?.cacheReadCostPerToken).toBe(1e-6); |
| 214 | expect(lookupRate(table, "deepinfra/anthropic/claude-fable-5")?.cacheReadCostPerToken).toBe( |
| 215 | 1e-5, |
| 216 | ); |
| 217 | expect(lookupRate(table, "other/claude-fable-5")).toBeNull(); |
| 218 | } |
| 219 | }); |
| 220 | |
| 221 | it("prices a bracketed context-tier variant at the base model's rate", () => { |
| 222 | const table = parseRateTable({ "claude-fable-5-1": rate(1e-5, 2.5e-7) }); |
| 223 | |
| 224 | expect(lookupRate(table, "claude-fable-5-1[1m]")).toEqual( |
| 225 | lookupRate(table, "claude-fable-5-1"), |
| 226 | ); |
| 227 | expect(lookupRate(table, "anthropic/Claude-Fable-5-1[1m]")).toBeNull(); |
| 228 | }); |
| 229 | |
| 230 | it("adds a bare alias when every qualified entry has the same rate", () => { |
| 231 | const table = parseRateTable({ |
| 232 | "provider-a/example-model": rate(1), |
| 233 | "provider-b/example-model": rate(1), |
| 234 | }); |
| 235 | |
| 236 | expect(lookupRate(table, "example-model")).toEqual( |
| 237 | lookupRate(table, "provider-a/example-model"), |
| 238 | ); |
| 239 | }); |
| 240 | |
| 241 | it("leaves an ambiguous bare name unpriced", () => { |
| 242 | const table = parseRateTable({ |
| 243 | "provider-a/example-model": rate(1), |
| 244 | "provider-b/example-model": rate(3), |
| 245 | }); |
| 246 | |
| 247 | expect(lookupRate(table, "provider-a/example-model")?.inputCostPerToken).toBe(1); |
| 248 | expect(lookupRate(table, "provider-b/example-model")?.inputCostPerToken).toBe(3); |
| 249 | expect(lookupRate(table, "example-model")).toBeNull(); |
| 250 | }); |
| 251 | }); |