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 +21 -0
- package/README.md +560 -0
- package/dist/index.cjs +2173 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +420 -0
- package/dist/index.d.ts +420 -0
- package/dist/index.js +2150 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
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
|
+
[](https://www.npmjs.com/package/usage-tab)
|
|
6
|
+
[](https://github.com/SergeevDmitry/llm-kit/actions/workflows/ci.yml)
|
|
7
|
+
[](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).
|