usage-tab 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 llm-kit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,560 @@
1
+ # usage-tab
2
+
3
+ Turn provider usage objects into a reproducible cost breakdown, from committed pricing data — never a guess.
4
+
5
+ [![npm](https://img.shields.io/npm/v/usage-tab.svg)](https://www.npmjs.com/package/usage-tab)
6
+ [![CI](https://github.com/SergeevDmitry/llm-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/SergeevDmitry/llm-kit/actions/workflows/ci.yml)
7
+ [![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](https://www.npmjs.com/package/usage-tab?activeTab=dependencies)
8
+
9
+ ## The problem
10
+
11
+ Providers report token usage in incompatible shapes and price input, output,
12
+ cached, and batch tokens differently — and the same model name can mean two
13
+ different prices depending on who resells it. Pricing `"gpt-5.6-luna"`
14
+ without saying which provider you mean is not a rounding error: it is a 5x
15
+ swing, and the obvious fix (just pick one) is exactly the bug this package
16
+ exists to prevent.
17
+
18
+ ## Before / after
19
+
20
+ ```ts
21
+ import { calculateCost, AmbiguousAliasError } from 'usage-tab';
22
+
23
+ const usage = { inputTokens: 1_000_000, outputTokens: 1_000_000 };
24
+
25
+ // Azure genuinely resells this OpenAI model at a different price — not a
26
+ // typo, independently confirmed against Microsoft's live Retail Prices API.
27
+ const onAzure = calculateCost({ model: 'gpt-5.6-luna', provider: 'azure-openai', usage });
28
+ const onOpenAI = calculateCost({ model: 'gpt-5.6-luna', provider: 'openai', usage });
29
+
30
+ console.log(onAzure.totalUsdExact); // "7.00" ($1.00/$6.00 per million tokens)
31
+ console.log(onOpenAI.totalUsdExact); // "1.40" ($0.20/$1.20 per million tokens)
32
+ // Azure is 5x OpenAI's first-party rate for the identical model name.
33
+
34
+ // The naive fix — price "gpt-5.6-luna" without saying which provider —
35
+ // doesn't silently pick one. It throws.
36
+ try {
37
+ calculateCost({ model: 'gpt-5.6-luna', usage });
38
+ } catch (error) {
39
+ console.log(error instanceof AmbiguousAliasError); // true
40
+ console.log((error as AmbiguousAliasError).code); // "AMBIGUOUS_ALIAS"
41
+ }
42
+ ```
43
+
44
+ ## Install
45
+
46
+ ```text
47
+ npm install usage-tab
48
+ ```
49
+
50
+ ## Minimal usage
51
+
52
+ ```ts
53
+ import { calculateCost } from 'usage-tab';
54
+
55
+ const result = calculateCost({
56
+ model: 'gpt-5',
57
+ provider: 'openai',
58
+ usage: { inputTokens: 12_400, outputTokens: 850, cachedInputTokens: 9_600 },
59
+ });
60
+
61
+ result.totalUsd; // 0.0132 — ergonomic number, for display/dashboards
62
+ result.totalUsdExact; // "0.0132" — exact decimal string, for accounting
63
+ result.input; // { tokens: 2800, rate: "1.25", costUsd: 0.0035, costUsdExact: "0.0035" }
64
+ result.warnings; // readonly PriceWarning[] — empty here, never silently dropped when non-empty
65
+ ```
66
+
67
+ > **These numbers are estimates, not invoices.** `usage-tab` computes what a
68
+ > request costs against pricing data this package committed and cited on a
69
+ > specific date (see [Data freshness](#data-freshness-and-effective-dates)
70
+ > below) — not what a provider actually billed you. For your real bill, use
71
+ > your provider's own invoice or usage dashboard. Treat every `CostBreakdown`
72
+ > as an estimate for budgeting, attribution, and comparison, never as a
73
+ > reconciliation source.
74
+
75
+ ## Guarantees
76
+
77
+ - **Money is never binary floating point on the authoritative path.**
78
+ Rates are parsed from decimal strings
79
+ exactly, token counts are multiplied as integers, and the entire
80
+ calculation — including summing input, output, cached, cache-write, and
81
+ reasoning lines — is exact `bigint` arithmetic with a power-of-ten
82
+ denominator, so it never needs to round. `totalUsd` is an ergonomic
83
+ `number`; `totalUsdExact` is the authoritative decimal string.
84
+ - **An alias that matches more than one model always throws, never guesses.**
85
+ The registry deliberately contains cross-provider collisions (the same
86
+ open-weight model hosted by several providers, Azure reselling OpenAI
87
+ models) — `AmbiguousAliasError` (`AMBIGUOUS_ALIAS`) is how you find out you
88
+ need a provider qualifier, not a silently wrong bill.
89
+ - **Cached and cache-write tokens are never double-counted.** Ordinary
90
+ billable input is `inputTokens` minus the reported cached/cache-write
91
+ subsets, clamped at zero rather than going negative — the same rule
92
+ applies to reasoning tokens against `outputTokens`.
93
+ - **Under-reporting cost never happens silently.** A model whose recorded
94
+ rate is the cheapest of several published pricing tiers (Google's
95
+ prompt-size tiers, Azure's deployment/context-length/service-tier
96
+ dimensions) always carries a `PARTIAL_TIER_PRICING` warning. An unpriced
97
+ token class (reasoning with no dedicated rate, cache-write with none) is
98
+ billed at a conservative fallback rate and warned about — never dropped.
99
+ - **A known usage field with a malformed value throws, it is never priced as
100
+ though the field were absent.** A provider or gateway that stringifies a
101
+ number (`"1000000"` instead of `1000000`), or sends `NaN`, `Infinity`, a
102
+ negative count, or a fractional count for a billable field like
103
+ `cache_read_input_tokens` or `reasoning_tokens`, throws `InvalidUsageError`
104
+ (`INVALID_USAGE`) instead of silently being treated as "field not present"
105
+ and priced at zero. The one exception is `null`, which real provider
106
+ responses use for "not applicable" (Anthropic's own OpenAPI schema
107
+ documents `cache_creation_input_tokens`/`cache_read_input_tokens` as
108
+ `integer | null`) — `null` is treated the same as an absent field, not as
109
+ malformed. See [Provider usage adapters](#provider-usage-adapters).
110
+ - **No runtime network fetch, ever.** Pricing is committed, versioned data.
111
+ `registryVersion` (a content hash, independent of this package's own npm
112
+ version) proves two calculations used byte-identical pricing data.
113
+ - **Historical lookups are exact and reproducible.** A cost computed for a
114
+ specific `at` date always resolves the same pricing period, regardless of
115
+ when you run it.
116
+ - **Zero runtime dependencies, browser-safe.** No `node:` import in `src/`.
117
+
118
+ ## API
119
+
120
+ ### `calculateCost(request, options?): CostBreakdown`
121
+
122
+ ```ts no-check
123
+ function calculateCost(request: PriceRequest, options?: PriceOptions): CostBreakdown;
124
+
125
+ interface PriceRequest {
126
+ model: string; // a canonical id or alias, from any provider
127
+ provider?: string; // qualifies resolution to one provider — see "two channels, one rule" below
128
+ usage: LlmUsage | unknown; // normalize a raw provider response first — see below
129
+ mode?: 'standard' | 'batch';
130
+ at?: Date | string; // defaults to `new Date()`
131
+ }
132
+
133
+ interface CostBreakdown {
134
+ model: string; // exactly as requested
135
+ canonicalModel: string; // the resolved registry id (may differ if `model` was an alias)
136
+ provider: string;
137
+ matchedBy: ModelMatchKind; // how `model` was resolved — see resolveModel below
138
+ requestedProvider?: string; // `provider` exactly as requested, when supplied
139
+ currency: 'USD';
140
+ input: CostLine;
141
+ output: CostLine;
142
+ cachedInput?: CostLine;
143
+ cacheWrite?: CostLine;
144
+ reasoning?: CostLine;
145
+ totalUsd: number;
146
+ totalUsdExact: string; // authoritative — see "Exact vs. numeric totals" below
147
+ registryVersion: string;
148
+ pricingEffectiveFrom: string;
149
+ warnings: readonly PriceWarning[];
150
+ }
151
+
152
+ interface CostLine {
153
+ tokens: number;
154
+ rate: string; // decimal USD-per-million-tokens rate actually applied
155
+ costUsd: number;
156
+ costUsdExact: string;
157
+ }
158
+ ```
159
+
160
+ #### Provider qualifier: two channels, one rule
161
+
162
+ A provider qualifier can travel on either of two channels — `request.provider`
163
+ or `options.provider` (`PriceOptions` is `ResolveModelOptions`, which declares
164
+ `provider`, and `createPriceCalculator` forwards its own `options.provider`
165
+ straight through). **They are equivalent**: qualifying on `options` behaves
166
+ identically to qualifying on `request`, for a match, a qualified miss, an
167
+ ambiguous id, and an unrecognized provider string alike. When both are
168
+ supplied, `request.provider` wins:
169
+
170
+ ```ts
171
+ import { calculateCost, UnknownModelError } from 'usage-tab';
172
+
173
+ const usage = { inputTokens: 1_000_000, outputTokens: 1_000_000 };
174
+
175
+ // identical results — same provider, same resolution, same total
176
+ const viaRequest = calculateCost({ model: 'gpt-5.6-luna', provider: 'azure-openai', usage });
177
+ const viaOptions = calculateCost({ model: 'gpt-5.6-luna', usage }, { provider: 'azure-openai' });
178
+ viaRequest.totalUsdExact === viaOptions.totalUsdExact; // true
179
+
180
+ // options.provider is exactly as hard a constraint as request.provider — a
181
+ // qualified miss throws UnknownModelError on either channel, never a silent
182
+ // fall-through to a different provider's price
183
+ try {
184
+ calculateCost({ model: 'gpt-5.6-luna', usage }, { provider: 'not-a-provider' });
185
+ } catch (error) {
186
+ console.log(error instanceof UnknownModelError); // true
187
+ }
188
+ ```
189
+
190
+ `options.provider` exists because `createPriceCalculator` threads its own
191
+ `options` bag through to every call — useful when a calculator built with
192
+ shared overrides also needs a default provider qualifier per call, without
193
+ repeating it on `request` each time. Either channel populates
194
+ `CostBreakdown.requestedProvider`, so that field tells you a qualifier was
195
+ supplied, not which channel carried it.
196
+
197
+ ### `resolveModel(model, options?): ResolvedModel`
198
+
199
+ Resolution order (never guesses; ambiguity always throws):
200
+
201
+ 1. exact custom override
202
+ 2. exact canonical id, qualified by `provider`
203
+ 3. exact alias, qualified by `provider`
204
+ 4. globally unambiguous alias
205
+ 5. explicit configured `fallback`
206
+ 6. throws `UnknownModelError` (`UNKNOWN_MODEL`)
207
+
208
+ ```ts
209
+ import { resolveModel } from 'usage-tab';
210
+
211
+ const resolved = resolveModel('claude-haiku-4-5', { provider: 'anthropic' });
212
+ resolved.descriptor.canonicalId; // "claude-haiku-4-5-20251001" — alias resolved to the dated snapshot id
213
+ resolved.matchedBy; // "alias-scoped"
214
+ ```
215
+
216
+ ### `createPriceCalculator(options?): PriceCalculator`
217
+
218
+ Bundles a set of default overrides/fallback/registry once, for pricing many
219
+ requests against the same negotiated rates without repeating `options`:
220
+
221
+ ```ts
222
+ import { createPriceCalculator, createPriceOverride } from 'usage-tab';
223
+
224
+ const calculator = createPriceCalculator({
225
+ overrides: [
226
+ createPriceOverride({
227
+ canonicalId: 'gpt-5',
228
+ provider: 'openai',
229
+ input: '0.90',
230
+ output: '7.50',
231
+ }),
232
+ ],
233
+ });
234
+
235
+ calculator.calculateCost({
236
+ model: 'gpt-5',
237
+ provider: 'openai',
238
+ usage: { inputTokens: 1_000_000, outputTokens: 1_000_000 },
239
+ }).totalUsdExact; // "8.40" — the negotiated rate, not the $11.25 list rate
240
+ ```
241
+
242
+ ### Provider usage adapters
243
+
244
+ ```ts no-check
245
+ normalizeOpenAIUsage(value: unknown): { usage: LlmUsage; warnings: readonly PriceWarning[] };
246
+ normalizeAnthropicUsage(value: unknown): { usage: LlmUsage; warnings: readonly PriceWarning[] };
247
+ normalizeGoogleUsage(value: unknown): { usage: LlmUsage; warnings: readonly PriceWarning[] };
248
+ normalizeOpenAICompatibleUsage(value: unknown): { usage: LlmUsage; warnings: readonly PriceWarning[] };
249
+ ```
250
+
251
+ Structural adapters — field names only, never a provider SDK import. See
252
+ [Provider usage examples](#provider-usage-examples) below.
253
+
254
+ Every field each adapter recognizes has exactly two valid states: **absent**
255
+ (the key is missing, or explicitly `null` — provider responses use `null` for
256
+ "not applicable"; see the field-by-field citations below) or **present as a
257
+ non-negative integer**. Anything else present under a recognized key — a
258
+ string, a boolean, `NaN`, `Infinity`, a negative number, a fractional number —
259
+ throws `InvalidUsageError` rather than being silently treated as absent and
260
+ priced at zero. This applies to every recognized field, top-level and nested:
261
+
262
+ | Adapter | Required fields | Optional fields (absent-vs-invalid rule applies) |
263
+ | -------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
264
+ | `normalizeAnthropicUsage` | `input_tokens`, `output_tokens` | `cache_read_input_tokens`, `cache_creation_input_tokens`, `cache_creation` (object) → `.ephemeral_5m_input_tokens`, `.ephemeral_1h_input_tokens` |
265
+ | `normalizeOpenAIUsage` | `prompt_tokens`/`input_tokens`, `completion_tokens`/`output_tokens` | `prompt_tokens_details`/`input_tokens_details` (object) → `.cached_tokens`; `completion_tokens_details`/`output_tokens_details` (object) → `.reasoning_tokens` |
266
+ | `normalizeGoogleUsage` | `promptTokenCount`, `candidatesTokenCount` | `cachedContentTokenCount`, `thoughtsTokenCount` |
267
+ | `normalizeOpenAICompatibleUsage` | `prompt_tokens`, `completion_tokens` | `prompt_tokens_details` (object) → `.cached_tokens`; flat `cached_tokens`; `prompt_cache_hit_tokens`; `completion_tokens_details` (object) → `.reasoning_tokens` |
268
+ | Direct `LlmUsage` (no adapter) | `inputTokens`, `outputTokens` | `cachedInputTokens`, `cacheWriteTokens`, `reasoningTokens` |
269
+
270
+ A nested parent (e.g. `cache_creation`, `prompt_tokens_details`) follows the
271
+ same rule one level up: absent/`null` is absent, but present-and-not-an-object
272
+ throws rather than silently skipping the fields inside it.
273
+
274
+ `null` really is documented provider behavior for cache fields, not a
275
+ hypothetical: Anthropic's Messages API OpenAPI schema types
276
+ `cache_creation_input_tokens` and `cache_read_input_tokens` as `integer | null`
277
+ (`input_tokens`/`output_tokens` are plain, non-nullable `integer`). OpenAI's
278
+ own prompt-caching guide instead documents `cached_tokens` as always present
279
+ and `0` — never `null` — for requests below the caching threshold; this
280
+ package treats `null` as absent there too, since a stricter reading would
281
+ reject a hypothetical gateway response without buying any real protection.
282
+
283
+ ### Errors
284
+
285
+ ```ts no-check
286
+ class AmbiguousAliasError extends Error {
287
+ code: 'AMBIGUOUS_ALIAS';
288
+ } // an id matches more than one model; pass `provider` or an override
289
+ class UnknownModelError extends Error {
290
+ code: 'UNKNOWN_MODEL';
291
+ } // nothing matched, not even a fallback
292
+ class InvalidLookupDateError extends Error {
293
+ code: 'INVALID_LOOKUP_DATE';
294
+ } // `at` did not parse as a date
295
+ class NoPricingPeriodError extends Error {
296
+ code: 'NO_PRICING_PERIOD';
297
+ } // `at` precedes every known period for this model
298
+ class InvalidUsageError extends Error {
299
+ code: 'INVALID_USAGE';
300
+ } // `usage` isn't LlmUsage-shaped and wasn't normalized first, OR a
301
+ // recognized usage field is present with a malformed value (wrong type,
302
+ // NaN, Infinity, negative, or fractional) — see "Provider usage adapters"
303
+ class InvalidTokenCountError extends Error {
304
+ code: 'INVALID_TOKEN_COUNT';
305
+ } // negative, fractional, or unsafely large token count
306
+ class InvalidRateError extends Error {
307
+ code: 'INVALID_RATE';
308
+ } // a custom override's rate string doesn't parse
309
+ ```
310
+
311
+ Every error extends `Error`, sets a stable `code` string, and carries an
312
+ actionable message. Branch on `.code`, not on message text. This holds even
313
+ for the value that landed in the offending field: a `bigint` — routine from a
314
+ `BIGINT` database column via node-postgres or mysql2 — is rendered safely in
315
+ the message rather than crashing the error-message-building code itself, which
316
+ would otherwise surface as an uncoded `TypeError` instead of `InvalidUsageError`.
317
+
318
+ ## Advanced usage
319
+
320
+ ### Provider usage examples
321
+
322
+ ```ts
323
+ import { calculateCost, normalizeOpenAIUsage } from 'usage-tab';
324
+
325
+ // Exactly what `chat.completions.create(...).usage` (or the Responses API's
326
+ // `response.usage`) returns — this adapter accepts either shape.
327
+ const { usage, warnings } = normalizeOpenAIUsage({
328
+ prompt_tokens: 12_400,
329
+ completion_tokens: 850,
330
+ prompt_tokens_details: { cached_tokens: 9_600 },
331
+ completion_tokens_details: { reasoning_tokens: 120 },
332
+ });
333
+
334
+ warnings; // [] here — every recognized field was priced
335
+ calculateCost({ model: 'gpt-5', provider: 'openai', usage }).totalUsdExact;
336
+ ```
337
+
338
+ ```ts
339
+ import { normalizeAnthropicUsage } from 'usage-tab';
340
+
341
+ // Anthropic reports cache tokens *additively*, not as a subset of
342
+ // input_tokens — this adapter sums them so `LlmUsage.inputTokens` means the
343
+ // same thing (a total) regardless of which provider it came from.
344
+ const { usage } = normalizeAnthropicUsage({
345
+ input_tokens: 700,
346
+ output_tokens: 300,
347
+ cache_read_input_tokens: 200,
348
+ cache_creation_input_tokens: 100,
349
+ });
350
+ usage.inputTokens; // 1000 (700 + 200 + 100)
351
+ ```
352
+
353
+ ```ts
354
+ import { normalizeGoogleUsage } from 'usage-tab';
355
+
356
+ // Gemini's usageMetadata: thoughtsTokenCount is reported *additively* to
357
+ // candidatesTokenCount, not as a subset — this adapter adds it into
358
+ // outputTokens for the same reason the Anthropic adapter sums cache tokens.
359
+ const { usage } = normalizeGoogleUsage({
360
+ promptTokenCount: 1000,
361
+ candidatesTokenCount: 400,
362
+ cachedContentTokenCount: 100,
363
+ thoughtsTokenCount: 50,
364
+ });
365
+ usage.outputTokens; // 450 (400 + 50)
366
+ ```
367
+
368
+ ```ts
369
+ import { normalizeOpenAICompatibleUsage } from 'usage-tab';
370
+
371
+ // Groq, Together AI, and similar OpenAI-compatible chat-completions APIs —
372
+ // leniently accepts a flat `cached_tokens` or DeepSeek-style
373
+ // `prompt_cache_hit_tokens` in addition to OpenAI's nested detail shape.
374
+ const { usage } = normalizeOpenAICompatibleUsage({
375
+ prompt_tokens: 500,
376
+ completion_tokens: 120,
377
+ cached_tokens: 50,
378
+ });
379
+ ```
380
+
381
+ A gateway or proxy that stringifies a numeric field is a malformed known
382
+ field, not an absent one — it throws rather than being priced at zero:
383
+
384
+ ```ts
385
+ import { InvalidUsageError, normalizeAnthropicUsage } from 'usage-tab';
386
+
387
+ try {
388
+ normalizeAnthropicUsage({
389
+ input_tokens: 700,
390
+ output_tokens: 300,
391
+ cache_creation_input_tokens: '1000000', // a gateway stringified this
392
+ });
393
+ } catch (error) {
394
+ console.log(error instanceof InvalidUsageError); // true
395
+ console.log((error as InvalidUsageError).code); // "INVALID_USAGE"
396
+ }
397
+
398
+ // `null`, by contrast, is real provider behavior for "not applicable" and is
399
+ // treated as absent, not malformed:
400
+ normalizeAnthropicUsage({
401
+ input_tokens: 700,
402
+ output_tokens: 300,
403
+ cache_read_input_tokens: null,
404
+ }).usage.cachedInputTokens; // undefined — no error, no warning
405
+ ```
406
+
407
+ ### Exact vs. numeric totals, and why both exist
408
+
409
+ `totalUsd`/`costUsd` are `number`s — convenient for a dashboard, a log line,
410
+ or a quick comparison, but subject to ordinary IEEE 754 float representation
411
+ once they leave this package. `totalUsdExact`/`costUsdExact` are decimal
412
+ strings computed with exact `bigint` arithmetic the entire way through — the
413
+ value to store, sum across many requests, or hand to an accounting system.
414
+ This package never derives `totalUsd` from a second, independent float
415
+ calculation; it is `Number(totalUsdExact)`, so it is always the closest
416
+ double to the true exact value, not a compounded rounding error:
417
+
418
+ ```ts
419
+ import { calculateCost } from 'usage-tab';
420
+
421
+ const result = calculateCost({
422
+ model: 'claude-sonnet-5',
423
+ provider: 'anthropic',
424
+ usage: { inputTokens: 333_333, outputTokens: 0 },
425
+ at: '2026-08-15',
426
+ });
427
+
428
+ result.totalUsdExact; // "0.666666" — exact
429
+ result.totalUsd; // 0.666666 — the same value, as a number
430
+ ```
431
+
432
+ ### Historical lookup and the data-freshness/effective-date policy
433
+
434
+ Every price carries an `effectiveFrom` (and, when superseded, an
435
+ `effectiveTo`) date. `calculateCost`'s `at` picks the period active on that
436
+ date — `claude-sonnet-5`'s real introductory rate is the mandated golden
437
+ fixture for this behavior:
438
+
439
+ ```ts
440
+ import { calculateCost } from 'usage-tab';
441
+
442
+ const usage = { inputTokens: 1_000_000, outputTokens: 1_000_000 };
443
+
444
+ calculateCost({ model: 'claude-sonnet-5', provider: 'anthropic', usage, at: '2026-08-15' })
445
+ .totalUsdExact;
446
+ // "12.00" — the introductory rate ($2.00/$10.00), active through 2026-08-31
447
+
448
+ calculateCost({ model: 'claude-sonnet-5', provider: 'anthropic', usage, at: '2026-09-15' })
449
+ .totalUsdExact;
450
+ // "18.00" — the standard rate ($3.00/$15.00), effective 2026-09-01
451
+ ```
452
+
453
+ Pricing data is committed, not fetched — there is no runtime network call,
454
+ ever. That means it can go stale between releases: a provider can change a
455
+ price the day after `usage-tab` ships, and calculations will use the old
456
+ price until the next release. This is a deliberate trade-off: a
457
+ silently-updating price is worse than a stale, visible, reproducible one. A
458
+ data-only correction always ships as a real release — check `registryVersion`
459
+ (a content hash of the pricing data, independent of this package's own npm
460
+ version) to confirm which snapshot a calculation used.
461
+
462
+ ### Custom price overrides
463
+
464
+ Negotiated or enterprise rates take precedence over the registry
465
+ (resolution's highest-precedence step):
466
+
467
+ ```ts
468
+ import { calculateCost, createPriceOverride } from 'usage-tab';
469
+
470
+ const negotiated = createPriceOverride({
471
+ canonicalId: 'gpt-5',
472
+ provider: 'openai',
473
+ input: '0.90', // decimal strings — see the fixed-point guarantee above
474
+ output: '7.50',
475
+ cachedInput: '0.09',
476
+ batchMultiplier: '0.5',
477
+ effectiveFrom: '2026-01-01',
478
+ });
479
+
480
+ calculateCost(
481
+ {
482
+ model: 'gpt-5',
483
+ provider: 'openai',
484
+ usage: { inputTokens: 1_000_000, outputTokens: 1_000_000 },
485
+ },
486
+ { overrides: [negotiated] },
487
+ ).totalUsdExact; // "8.40" — the negotiated rate, not the registry's $11.25
488
+ ```
489
+
490
+ ### Supported providers
491
+
492
+ The bundled registry ([`@llm-kit/model-registry`](../../internal/model-registry),
493
+ committed and versioned — see `registryVersion` above) covers all ten
494
+ baseline providers: `openai`, `anthropic`, `google`, `azure-openai`,
495
+ `aws-bedrock`, `groq`, `mistral`, `cohere`, `together`, `openrouter`. Pass
496
+ `provider` to qualify a lookup to one of these.
497
+
498
+ ## Edge cases and limitations
499
+
500
+ | Case | Behavior |
501
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
502
+ | An alias matches more than one provider's model | Throws `AmbiguousAliasError` (`AMBIGUOUS_ALIAS`). Pass `provider` or a custom override — never guessed. |
503
+ | No model matches at all | Throws `UnknownModelError` (`UNKNOWN_MODEL`). |
504
+ | `provider` is supplied but the id only exists under a _different_ provider (e.g. `{ model: 'gpt-5.5-pro', provider: 'azure-openai' }` — OpenAI-only) | Throws `UnknownModelError` (`UNKNOWN_MODEL`) — never silently priced at the other provider's rate. A provider qualifier is a constraint, not a hint; the error's `otherProviders` names where the id does exist. Unqualified lookup is unaffected. |
505
+ | `at` precedes every known pricing period for a model | Throws `NoPricingPeriodError` (`NO_PRICING_PERIOD`) — never a zero or guessed cost. |
506
+ | A model's recorded rate is the cheapest of several published tiers | `PARTIAL_TIER_PRICING` warning on every calculation that uses it (Google prompt-size tiers, Azure deployment/context/service tiers). |
507
+ | `mode: 'batch'` requested but the model publishes no batch rate | Falls back to standard pricing (the _higher_ of the two, so it never under-reports) and warns with `BATCH_PRICING_UNAVAILABLE`. |
508
+ | `cachedInputTokens + cacheWriteTokens` exceeds `inputTokens` | Ordinary input clamped to 0 and warns (`CACHED_EXCEEDS_INPUT`); the reported cached/write amounts are still billed in full. |
509
+ | `reasoningTokens` exceeds `outputTokens` | Ordinary output clamped to 0 and warns (`REASONING_EXCEEDS_OUTPUT`); the reported reasoning amount is still billed in full. |
510
+ | Reasoning tokens reported, no dedicated `reasoning` rate | Billed at the output rate and warns (`REASONING_PRICED_AS_OUTPUT`) — never dropped. |
511
+ | Cached/cache-write tokens reported, no dedicated rate | Billed at the input rate and warns (`CACHED_INPUT_PRICED_AS_INPUT` / `CACHE_WRITE_PRICED_AS_INPUT`) — never dropped. |
512
+ | An unrecognized usage field (current or future) | Surfaced as a `UNSUPPORTED_USAGE_FIELD` warning naming the field — never silently discarded. |
513
+ | A recognized usage field present with a malformed value (wrong type, `NaN`, `Infinity`, negative, fractional) | Throws `InvalidUsageError` (`INVALID_USAGE`) — never priced as though the field were absent. |
514
+ | A recognized usage field present as `null` | Treated the same as an absent field — not an error, not priced. See [Provider usage adapters](#provider-usage-adapters). |
515
+ | Zero-token request | Returns an exact zero total; never throws. |
516
+ | A very large aggregate token count | Stays exact up to `Number.MAX_SAFE_INTEGER`; beyond that (or negative, or fractional) throws `InvalidTokenCountError` (`INVALID_TOKEN_COUNT`). |
517
+ | A pricing correction with an earlier `effectiveFrom` than an existing period | Resolved correctly regardless of array order — the period with the latest `effectiveFrom` that still covers `at` always wins. |
518
+
519
+ What this package deliberately does not do: fetch prices
520
+ at runtime, reconcile against a provider invoice, convert currency, or model
521
+ taxes, credits, or negotiated enterprise commitments beyond what you supply
522
+ as an override.
523
+
524
+ ## Runtime compatibility
525
+
526
+ Universal (browser-safe): `src/` contains no `node:` import and no provider
527
+ SDK import, verified directly by `test/no-network-calls.test.ts`. Published
528
+ as ESM and CommonJS from one build (`dist/index.js` and `dist/index.cjs`),
529
+ both proven by installing the packed tarball into a clean project and
530
+ importing it both ways. Node 20+ is the tested baseline (`engines.node:
531
+ ">=20"`).
532
+
533
+ ## Performance
534
+
535
+ `calculateCost` does a registry lookup, an effective-date period selection,
536
+ and a handful of exact `bigint` operations — no I/O, no allocation-heavy
537
+ data structures. Benchmarked (`pnpm run bench`) on the CI reference machine:
538
+
539
+ | Scenario | Throughput |
540
+ | --------------------------------------------------- | ----------------------------- |
541
+ | Single calculation (input/output only) | ~220,000 calculations/second |
542
+ | Single calculation (input/output/cached/cacheWrite) | ~126,000 calculations/second |
543
+ | 100,000-request aggregate | ~500 ms total (~5 μs/request) |
544
+ | Registry lookup, provider-qualified canonical id | ~3.3M lookups/second |
545
+ | Registry lookup, globally unambiguous alias | ~1.4M lookups/second |
546
+
547
+ Run `pnpm run bench` for numbers on your own hardware.
548
+
549
+ ## Security and privacy
550
+
551
+ **No runtime network call, ever** — checked directly by
552
+ `test/no-network-calls.test.ts`, not just claimed here. Pricing is committed,
553
+ versioned data; this package never fetches, phones home, or logs your usage.
554
+ No telemetry, no `eval`. Error messages never embed token content — only
555
+ field names, counts, and model/provider identifiers.
556
+
557
+ ## Contributing and license
558
+
559
+ Part of the [llm-kit](../../README.md) monorepo. MIT licensed — see
560
+ [`LICENSE`](./LICENSE).