usage-tab 1.0.0 → 1.2.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/README.md CHANGED
@@ -112,7 +112,10 @@ result.warnings; // readonly PriceWarning[] — empty here, never silently dropp
112
112
  version) proves two calculations used byte-identical pricing data.
113
113
  - **Historical lookups are exact and reproducible.** A cost computed for a
114
114
  specific `at` date always resolves the same pricing period, regardless of
115
- when you run it.
115
+ when — or on which machine — you run it: every ISO `at` string is read as
116
+ UTC, including the offset-less form `Date.parse` would otherwise read in
117
+ the host's local timezone. See
118
+ [Historical lookup](#historical-lookup-and-the-data-freshnesseffective-date-policy).
116
119
  - **Zero runtime dependencies, browser-safe.** No `node:` import in `src/`.
117
120
 
118
121
  ## API
@@ -127,7 +130,7 @@ interface PriceRequest {
127
130
  provider?: string; // qualifies resolution to one provider — see "two channels, one rule" below
128
131
  usage: LlmUsage | unknown; // normalize a raw provider response first — see below
129
132
  mode?: 'standard' | 'batch';
130
- at?: Date | string; // defaults to `new Date()`
133
+ at?: Date | string; // defaults to `new Date()`; an ISO string is read as UTC
131
134
  }
132
135
 
133
136
  interface CostBreakdown {
@@ -429,27 +432,80 @@ result.totalUsdExact; // "0.666666" — exact
429
432
  result.totalUsd; // 0.666666 — the same value, as a number
430
433
  ```
431
434
 
435
+ ### Totalling many requests exactly
436
+
437
+ `sum += breakdown.totalUsd` puts binary floating point back exactly where
438
+ `totalUsdExact` removed it. `createCostAggregator` keeps the running totals
439
+ on the same exact `bigint` path, overall and per model:
440
+
441
+ ```ts
442
+ import { calculateCost, createCostAggregator, sumExactUsd } from 'usage-tab';
443
+
444
+ const aggregator = createCostAggregator();
445
+ for (const call of [
446
+ { model: 'gpt-4o', provider: 'openai', usage: { inputTokens: 40_000, outputTokens: 4_000 } },
447
+ {
448
+ model: 'claude-sonnet-5',
449
+ provider: 'anthropic',
450
+ usage: { inputTokens: 40_000, outputTokens: 4_000 },
451
+ },
452
+ ]) {
453
+ aggregator.add(calculateCost({ ...call, at: '2026-08-15' }));
454
+ }
455
+
456
+ aggregator.total().totalUsdExact; // exact decimal string — the value to store
457
+ aggregator.total().count; // 2
458
+ aggregator.byModel(); // Map keyed "openai:gpt-4o", "anthropic:claude-sonnet-5"
459
+
460
+ // Already have the strings — from a database column, say:
461
+ sumExactUsd(['0.10', '0.20']); // "0.30", where 0.1 + 0.2 gives 0.30000000000000004
462
+ ```
463
+
464
+ `total().registryVersions` lists every pricing snapshot the aggregate drew
465
+ on. More than one is expected for a report spanning a registry update, and a
466
+ bug for a total meant to be reproducible against a single one — this package
467
+ reports it rather than guessing which you meant.
468
+
469
+ Both throw `InvalidRateError` on a value that is not a non-negative decimal
470
+ string, rather than coercing it: a stringified `NaN` or an exponent-notation
471
+ `"1e-7"` silently summing to something wrong is the failure this exists to
472
+ prevent.
473
+
432
474
  ### Historical lookup and the data-freshness/effective-date policy
433
475
 
434
476
  Every price carries an `effectiveFrom` (and, when superseded, an
435
477
  `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:
478
+ date - `gemini-3.6-flash`'s real promotional rate, whose start and end
479
+ Google publishes, is the golden fixture for this behavior:
438
480
 
439
481
  ```ts
440
482
  import { calculateCost } from 'usage-tab';
441
483
 
442
484
  const usage = { inputTokens: 1_000_000, outputTokens: 1_000_000 };
443
485
 
444
- calculateCost({ model: 'claude-sonnet-5', provider: 'anthropic', usage, at: '2026-08-15' })
486
+ calculateCost({ model: 'gemini-3.6-flash', provider: 'google', usage, at: '2026-11-01' })
445
487
  .totalUsdExact;
446
- // "12.00" the introductory rate ($2.00/$10.00), active through 2026-08-31
488
+ // "4.50" - the promotional rate ($0.75/$3.75), published as running through 2026-12-31
447
489
 
448
- calculateCost({ model: 'claude-sonnet-5', provider: 'anthropic', usage, at: '2026-09-15' })
490
+ calculateCost({ model: 'gemini-3.6-flash', provider: 'google', usage, at: '2027-01-15' })
449
491
  .totalUsdExact;
450
- // "18.00" the standard rate ($3.00/$15.00), effective 2026-09-01
492
+ // "9.00" - the standard rate ($1.50/$7.50), resuming 2027-01-01
451
493
  ```
452
494
 
495
+ Period boundaries are UTC midnights, so **every ISO `at` string is read as
496
+ UTC**. An ISO date (`'2026-09-01'`) and an offset-carrying datetime
497
+ (`'2026-09-01T05:00:00Z'`, `'...+02:00'`) already are by specification; one
498
+ _without_ an offset — `'2026-09-01T05:00:00'`, the shape a log timestamp, a
499
+ `datetime-local` input, and several DB drivers produce — is `Date.parse`'s
500
+ local-time case, and is read as UTC here too. Otherwise the same lookup
501
+ prices at $12.00 in one deployment and $18.00 in another, purely from the
502
+ host's `TZ`.
503
+
504
+ A non-ISO string (`'2026/09/01'`, `'September 1, 2026'`) is
505
+ implementation-defined rather than specified, so there is no single reading
506
+ to normalize it to and it keeps whatever `Date.parse` does with it — local
507
+ time, in practice. Pass a `Date` or an ISO form.
508
+
453
509
  Pricing data is committed, not fetched — there is no runtime network call,
454
510
  ever. That means it can go stale between releases: a provider can change a
455
511
  price the day after `usage-tab` ships, and calculations will use the old