llm-relay 0.37.0 → 0.38.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.
Files changed (68) hide show
  1. package/README.md +2 -1
  2. package/config.example.json +2 -1
  3. package/dist/accounting-store-schema.d.ts +105 -5
  4. package/dist/accounting-store-schema.js +171 -43
  5. package/dist/accounting-store-schema.js.map +1 -1
  6. package/dist/accounting-store.d.ts +30 -0
  7. package/dist/accounting-store.js +252 -6
  8. package/dist/accounting-store.js.map +1 -1
  9. package/dist/accounting.d.ts +111 -4
  10. package/dist/accounting.js +197 -2
  11. package/dist/accounting.js.map +1 -1
  12. package/dist/anthropic.d.ts +10 -0
  13. package/dist/anthropic.js.map +1 -1
  14. package/dist/availability-snapshot.d.ts +45 -0
  15. package/dist/availability-snapshot.js +259 -0
  16. package/dist/availability-snapshot.js.map +1 -0
  17. package/dist/availability.d.ts +119 -0
  18. package/dist/availability.js +225 -0
  19. package/dist/availability.js.map +1 -0
  20. package/dist/backend.d.ts +10 -0
  21. package/dist/backend.js +77 -4
  22. package/dist/backend.js.map +1 -1
  23. package/dist/candidates.d.ts +39 -0
  24. package/dist/candidates.js +89 -1
  25. package/dist/candidates.js.map +1 -1
  26. package/dist/catalog.d.ts +31 -0
  27. package/dist/catalog.js +101 -2
  28. package/dist/catalog.js.map +1 -1
  29. package/dist/circuit-breaker.d.ts +14 -1
  30. package/dist/circuit-breaker.js +21 -0
  31. package/dist/circuit-breaker.js.map +1 -1
  32. package/dist/cli.d.ts +26 -1
  33. package/dist/cli.js +227 -5
  34. package/dist/cli.js.map +1 -1
  35. package/dist/config.d.ts +32 -0
  36. package/dist/config.js +45 -0
  37. package/dist/config.js.map +1 -1
  38. package/dist/configured-limits.d.ts +85 -0
  39. package/dist/configured-limits.js +143 -0
  40. package/dist/configured-limits.js.map +1 -0
  41. package/dist/credential-fleet.d.ts +7 -0
  42. package/dist/credential-fleet.js.map +1 -1
  43. package/dist/dashboard/.vite/manifest.json +2 -2
  44. package/dist/dashboard/assets/{index-RtLgb7Cg.js → index-B2fuSM77.js} +14 -14
  45. package/dist/dashboard/assets/index-B_lGhkan.css +1 -0
  46. package/dist/dashboard/index.html +2 -2
  47. package/dist/dashboard-contract.d.ts +88 -4
  48. package/dist/dashboard-contract.js +75 -4
  49. package/dist/dashboard-contract.js.map +1 -1
  50. package/dist/dashboard-snapshot.d.ts +17 -2
  51. package/dist/dashboard-snapshot.js +423 -17
  52. package/dist/dashboard-snapshot.js.map +1 -1
  53. package/dist/emitSse.js +15 -1
  54. package/dist/emitSse.js.map +1 -1
  55. package/dist/quota-demotion.d.ts +65 -0
  56. package/dist/quota-demotion.js +148 -0
  57. package/dist/quota-demotion.js.map +1 -0
  58. package/dist/rate-limits.d.ts +84 -0
  59. package/dist/rate-limits.js +259 -0
  60. package/dist/rate-limits.js.map +1 -0
  61. package/dist/server.d.ts +3 -1
  62. package/dist/server.js +221 -17
  63. package/dist/server.js.map +1 -1
  64. package/dist/target-facts.d.ts +27 -4
  65. package/dist/target-facts.js +39 -6
  66. package/dist/target-facts.js.map +1 -1
  67. package/package.json +1 -1
  68. package/dist/dashboard/assets/index-BeZxN4Lx.css +0 -1
package/README.md CHANGED
@@ -49,7 +49,8 @@ at login.
49
49
  - **Both API fronts** — Anthropic `/v1/messages` plus OpenAI `/v1/chat/completions` and
50
50
  `/v1/responses`, translated in either direction, streaming included.
51
51
  - **Local analytics dashboard** — `llm-relay dashboard` opens bounded request/attempt, token,
52
- latency, provider/model/client/credential, quota, and cooldown views. Unknown or unavailable
52
+ spend, latency, provider/model/client/credential, quota, and cooldown views (spend priced only
53
+ from published prices; `llm-relay cost` rolls it up in a terminal). Unknown or unavailable
53
54
  accounting remains explicit; the dashboard never guesses a value or starts provider probes.
54
55
  - **Honest metadata** — per-deployment limits and prices with provenance, capability scores
55
56
  synced from four leaderboards, metadata-only logging, loopback-only binding.
@@ -5,7 +5,7 @@
5
5
  "base": "https://integrate.api.nvidia.com/v1",
6
6
  "kind": "openai",
7
7
  "credentials": [
8
- { "label": "personal", "authEnv": "NVIDIA_API_KEY" },
8
+ { "label": "personal", "authEnv": "NVIDIA_API_KEY", "limits": { "rpm": 40, "rpd": 1000, "tpd": 150000 } },
9
9
  { "label": "work", "authEnv": "NVIDIA_WORK_API_KEY" }
10
10
  ],
11
11
  "tierType": "free"
@@ -81,6 +81,7 @@
81
81
  "claude": { "enabled": false, "scope": "subagents" },
82
82
  "codex": { "enabled": false, "scope": "subagents" }
83
83
  },
84
+ "quota": { "enforce": true },
84
85
  "sticky": false
85
86
  },
86
87
  "mode": "repair",
@@ -1,4 +1,15 @@
1
- /** The persisted accounting format is deliberately separate from the dashboard wire format. */
1
+ /**
2
+ * The persisted accounting format is deliberately separate from the dashboard wire format.
3
+ *
4
+ * SCHEMA NOTE (2026-08-22, Stage 4 spend): `spend` grew from a hard-typed `null` into real
5
+ * aggregate cells, and `requestSpend`/`partiallyPricedRequests` were added beside it as an
6
+ * optional pair. This is an ADDITIVE change and the schema constant stays `accounting.day.v1`:
7
+ * the guards below accept BOTH shapes — a pre-spend shard's `spend: null` loads as empty cells
8
+ * rather than quarantining a day of real traffic. Nothing rewrites old shards on read: the
9
+ * guards tolerate absence and every reader defaults it (`?? 0`, `mergeSpend(undefined)`), so a
10
+ * legacy shard reads correctly and gains the new fields only when new facts fold into it. A
11
+ * bump would have discarded every existing day shard for no gain.
12
+ */
2
13
  export declare const ACCOUNTING_STORE_VERSION: 1;
3
14
  export declare const ACCOUNTING_DAY_SCHEMA: "accounting.day.v1";
4
15
  export declare const ACCOUNTING_MINUTE_SCHEMA: "accounting.minute.v1";
@@ -71,6 +82,63 @@ export interface AccountingMetricCellV1 {
71
82
  readonly samplesDropped: number;
72
83
  readonly observedAt: string | null;
73
84
  }
85
+ /**
86
+ * PER-MILLION prices actually used to compute one spend figure, carried so every
87
+ * amount stays re-derivable from its own record. Dollars-per-million-tokens equals
88
+ * micro-dollars-per-token, so `tokens x perMillionIn` IS the micro-USD amount.
89
+ */
90
+ export interface AccountingSpendPricesV1 {
91
+ readonly perMillionIn: number | null;
92
+ readonly perMillionOut: number | null;
93
+ }
94
+ /**
95
+ * Coverage of ONE priced spend: which token kinds went into the amount and which
96
+ * rode beside it unpriced.
97
+ * - "full": every reported token kind was priced at a published price.
98
+ * - "input_only": estimated-basis pricing — estimated OUTPUT has no producer today,
99
+ * so the amount covers input alone BY CONSTRUCTION and says so.
100
+ * - "partial": at least one present token kind was left out (cache kinds, or one of
101
+ * in/out unpublished). The amount is a lower bound.
102
+ */
103
+ export type AccountingSpendCoverage = "full" | "input_only" | "partial";
104
+ /** Token kinds observed but NOT priced, per kind; null when the kind itself was absent. */
105
+ export interface AccountingUnpricedTokensV1 {
106
+ readonly cacheRead: number | null;
107
+ readonly cacheCreation: number | null;
108
+ readonly cachedInput: number | null;
109
+ }
110
+ /**
111
+ * One attempt's spend in exact integer micro-USD with full provenance. `null` spend
112
+ * means UNPRICED (no published price resolved, or nothing to price) — never $0.
113
+ * Amounts are integer micro-USD, rounded half-up once per token kind, summed as
114
+ * integers, so no floating-point error can accumulate across requests.
115
+ */
116
+ export interface AccountingSpendV1 {
117
+ readonly amountMicrousd: number;
118
+ readonly priceSource: "provider_published" | "reference";
119
+ readonly tokenBasis: "reported" | "estimated";
120
+ readonly source: "provider_reported" | "relay_estimated";
121
+ readonly coverage: AccountingSpendCoverage;
122
+ readonly unpricedTokens: AccountingUnpricedTokensV1;
123
+ readonly pricesUsed: AccountingSpendPricesV1;
124
+ readonly observedAt: string;
125
+ }
126
+ /** Summing accumulator behind one wire spend cell inside an aggregate. */
127
+ export interface AccountingAggregateSpendCellV1 {
128
+ /** Sum of integer micro-USD contributions; null once any contributor is uncertain or it overflows. */
129
+ readonly amountMicrousd: number | null;
130
+ /** How many spends were summed into this cell. */
131
+ readonly known: number;
132
+ /** Latest observation across contributors; non-null exactly when known > 0. */
133
+ readonly observedAt: string | null;
134
+ }
135
+ /** The four price-source x token-basis cells one aggregate carries for its scope. */
136
+ export interface AccountingAggregateSpendV1 {
137
+ readonly providerPublishedReported: AccountingAggregateSpendCellV1;
138
+ readonly providerPublishedEstimated: AccountingAggregateSpendCellV1;
139
+ readonly referenceReported: AccountingAggregateSpendCellV1;
140
+ readonly referenceEstimated: AccountingAggregateSpendCellV1;
141
+ }
74
142
  export interface AccountingAggregateV1 {
75
143
  readonly requests: number;
76
144
  readonly attempts: number;
@@ -82,8 +150,26 @@ export interface AccountingAggregateV1 {
82
150
  readonly requestTokens: AccountingAggregateTokenTotalsV1;
83
151
  readonly latency: AccountingMetricCellV1;
84
152
  readonly commit: AccountingMetricCellV1;
85
- /** Pricing is intentionally not guessed by the accounting layer. */
86
- readonly spend: null;
153
+ /**
154
+ * Attempt-side spend cells: every completed attempt priced at PUBLISHED prices,
155
+ * summed as integers. `null` is the LEGACY pre-spend shape, tolerated on read and
156
+ * defaulted to empty cells by readers — it never means "zero spend".
157
+ */
158
+ readonly spend: AccountingAggregateSpendV1 | null;
159
+ /**
160
+ * Request-side spend cells (winning serve attempt only), kept apart from
161
+ * `spend` for the same reason `requestTokens` is kept apart from `tokens`:
162
+ * a retried-elsewhere request must not double-count its failed attempts.
163
+ * Absent entirely on legacy shards.
164
+ */
165
+ readonly requestSpend?: AccountingAggregateSpendV1 | null;
166
+ /**
167
+ * Requests whose spend figure exists but left present token kinds unpriced
168
+ * (cache kinds, or one of in/out unpublished) — i.e. every amount above is a
169
+ * lower bound while this is > 0. Absent (= 0) on legacy shards.
170
+ */
171
+ readonly partiallyPricedRequests?: number;
172
+ /** Requests with NO spend figure at all: unserved, or a deployment publishing no price. */
87
173
  readonly unpricedRequests: number;
88
174
  }
89
175
  interface AccountingDimensionRowBaseV1 extends AccountingAggregateV1 {
@@ -198,7 +284,8 @@ export interface AccountingAttemptPacketV1 {
198
284
  readonly model: string | null;
199
285
  readonly credentialId: string | null;
200
286
  readonly tokens: AccountingAggregateTokenTotalsV1;
201
- readonly spend: null;
287
+ /** This attempt's priced spend, or LEGACY/absent. `null` = unpriced, never $0. */
288
+ readonly spend: AccountingSpendV1 | null;
202
289
  }
203
290
  export type AccountingAttemptPacket = AccountingAttemptPacketV1;
204
291
  export interface AccountingDetailAttemptMetadataV1 {
@@ -224,7 +311,12 @@ export interface AccountingRequestPacketV1 {
224
311
  readonly model: string | null;
225
312
  readonly credentialId: string | null;
226
313
  readonly tokens: AccountingAggregateTokenTotalsV1;
227
- readonly spend: null;
314
+ /**
315
+ * The WINNING SERVE attempt's spend, mirroring `tokens`. Repair spend stays on
316
+ * its own attempt rows (C1) so a later `--include-repair` roll-up can add it
317
+ * back without double-counting the serve.
318
+ */
319
+ readonly spend: AccountingSpendV1 | null;
228
320
  readonly attempts: readonly AccountingAttemptPacketV1[];
229
321
  readonly attemptMetadata: AccountingDetailAttemptMetadataV1;
230
322
  }
@@ -284,6 +376,14 @@ export declare const parseAccountingRequestPacket: (value: unknown) => Accountin
284
376
  export declare const parseAccountingRecent: (value: unknown) => AccountingParseResult<AccountingRecentV1>;
285
377
  /** Checked addition for counters. null means the exact safe-integer domain overflowed. */
286
378
  export declare function checkedAddAccountingCounter(left: number, right: number): number | null;
379
+ /**
380
+ * Sum two aggregate SPEND cells: integer micro-USD amounts, contributor counts,
381
+ * latest observation. Amounts stay null once any side is uncertain (overflow),
382
+ * mirroring how token cells degrade — a lost sum is never silently re-guessed.
383
+ */
384
+ export declare function mergeAccountingSpendCells(left: AccountingAggregateSpendCellV1, right: AccountingAggregateSpendCellV1): AccountingAggregateSpendCellV1 | null;
385
+ export declare function emptyAccountingSpendCell(): AccountingAggregateSpendCellV1;
386
+ export declare function emptyAccountingAggregateSpend(): AccountingAggregateSpendV1;
287
387
  export declare function mergeAccountingTokenCells(left: AccountingAggregateTokenCellV1, right: AccountingAggregateTokenCellV1): AccountingAggregateTokenCellV1 | null;
288
388
  export declare function mergeAccountingEstimatedTokenCells(left: AccountingEstimatedTokenCellV1, right: AccountingEstimatedTokenCellV1): AccountingEstimatedTokenCellV1 | null;
289
389
  export declare function mergeAccountingMetricCells(left: AccountingMetricCellV1, right: AccountingMetricCellV1): AccountingMetricCellV1 | null;
@@ -1,5 +1,16 @@
1
1
  import { DASHBOARD_ATTEMPT_ID_MAX_BYTES, DASHBOARD_MAX_DETAIL_ATTEMPTS, DASHBOARD_REQUEST_ID_PATTERN, isDashboardAttemptId, isDashboardSafeId, isDashboardUtcTimestamp, } from "./dashboard-contract.js";
2
- /** The persisted accounting format is deliberately separate from the dashboard wire format. */
2
+ /**
3
+ * The persisted accounting format is deliberately separate from the dashboard wire format.
4
+ *
5
+ * SCHEMA NOTE (2026-08-22, Stage 4 spend): `spend` grew from a hard-typed `null` into real
6
+ * aggregate cells, and `requestSpend`/`partiallyPricedRequests` were added beside it as an
7
+ * optional pair. This is an ADDITIVE change and the schema constant stays `accounting.day.v1`:
8
+ * the guards below accept BOTH shapes — a pre-spend shard's `spend: null` loads as empty cells
9
+ * rather than quarantining a day of real traffic. Nothing rewrites old shards on read: the
10
+ * guards tolerate absence and every reader defaults it (`?? 0`, `mergeSpend(undefined)`), so a
11
+ * legacy shard reads correctly and gains the new fields only when new facts fold into it. A
12
+ * bump would have discarded every existing day shard for no gain.
13
+ */
3
14
  export const ACCOUNTING_STORE_VERSION = 1;
4
15
  export const ACCOUNTING_DAY_SCHEMA = "accounting.day.v1";
5
16
  export const ACCOUNTING_MINUTE_SCHEMA = "accounting.minute.v1";
@@ -61,6 +72,28 @@ function hasExactArray(value, max, guard) {
61
72
  }
62
73
  return true;
63
74
  }
75
+ /**
76
+ * Exact-key check tolerating a CLOSED set of optional keys, used by every owner of
77
+ * an aggregate: pre-spend shards carry neither `requestSpend` nor
78
+ * `partiallyPricedRequests`, and quarantining a whole day of real traffic over
79
+ * their absence would trade one additive field for the ledger itself.
80
+ */
81
+ function hasExactKeysWithOptional(value, keys, optional) {
82
+ if (!isPlainRecord(value) || Object.getOwnPropertySymbols(value).length !== 0)
83
+ return false;
84
+ const names = Object.getOwnPropertyNames(value);
85
+ const enumerableNames = Object.keys(value);
86
+ if (names.length !== enumerableNames.length)
87
+ return false;
88
+ // Any SUBSET of the closed optional set is accepted, not only all-or-none: a
89
+ // fully-priced request attaches `requestSpend` without ever creating
90
+ // `partiallyPricedRequests`, and a writer may legitimately persist either
91
+ // alone. Unknown names are still rejected.
92
+ if (names.length < keys.length || names.length > keys.length + optional.length)
93
+ return false;
94
+ return keys.every((key) => Object.prototype.hasOwnProperty.call(value, key))
95
+ && names.every((name) => keys.includes(name) || optional.includes(name));
96
+ }
64
97
  function hasOnlyEnumerableStringKeys(value) {
65
98
  return Object.getOwnPropertySymbols(value).length === 0 && Object.getOwnPropertyNames(value).length === Object.keys(value).length;
66
99
  }
@@ -279,6 +312,39 @@ function isMetric(value) {
279
312
  return false;
280
313
  return true;
281
314
  }
315
+ /**
316
+ * One summed spend cell inside an aggregate. `known === 0` ⇔ amount and timestamp are
317
+ * null; an amount can be null with known > 0 only when a contributor overflowed, which
318
+ * marks the owning aggregate partial through its own coverage path.
319
+ */
320
+ function isAggregateSpendCell(value) {
321
+ return (hasExactKeys(value, ["amountMicrousd", "known", "observedAt"]) &&
322
+ isNullableCounter(value.amountMicrousd) &&
323
+ isCounter(value.known) &&
324
+ isNullableTimestamp(value.observedAt) &&
325
+ ((value.known === 0 && value.amountMicrousd === null && value.observedAt === null) ||
326
+ (value.known > 0 && value.observedAt !== null)));
327
+ }
328
+ const SPEND_CELL_KEYS = Object.freeze(["providerPublishedReported", "providerPublishedEstimated", "referenceReported", "referenceEstimated"]);
329
+ function isAggregateSpend(value) {
330
+ if (!hasExactKeys(value, SPEND_CELL_KEYS))
331
+ return false;
332
+ for (const key of SPEND_CELL_KEYS) {
333
+ const cell = value[key];
334
+ // hasExactKeys guarantees every key present; the undefined case cannot occur,
335
+ // but the guard keeps the record access total.
336
+ if (cell === undefined || !isAggregateSpendCell(cell))
337
+ return false;
338
+ }
339
+ return true;
340
+ }
341
+ /** The legacy pre-spend shape (`spend: null`) is accepted beside the full cells. */
342
+ function isAggregateSpendOrNull(value) {
343
+ return value === null || isAggregateSpend(value);
344
+ }
345
+ function isEmptyAggregateSpendCell(cell) {
346
+ return cell.known === 0 && cell.amountMicrousd === null && cell.observedAt === null;
347
+ }
282
348
  function isAggregateFields(value, owner = "general") {
283
349
  if (!isCounter(value.requests) ||
284
350
  !isCounter(value.attempts) ||
@@ -289,7 +355,9 @@ function isAggregateFields(value, owner = "general") {
289
355
  !isAggregateTokens(value.requestTokens) ||
290
356
  !isMetric(value.latency) ||
291
357
  !isMetric(value.commit) ||
292
- value.spend !== null ||
358
+ !isAggregateSpendOrNull(value.spend) ||
359
+ !(value.requestSpend === undefined || isAggregateSpendOrNull(value.requestSpend)) ||
360
+ !(value.partiallyPricedRequests === undefined || isCounter(value.partiallyPricedRequests)) ||
293
361
  !isCounter(value.unpricedRequests))
294
362
  return false;
295
363
  if (owner === "request") {
@@ -297,12 +365,17 @@ function isAggregateFields(value, owner = "general") {
297
365
  return false;
298
366
  if (value.served + value.errored + value.cancelled > value.requests)
299
367
  return false;
368
+ if (value.spend !== null && !isEmptyAggregateSpend(value.spend))
369
+ return false;
300
370
  }
301
371
  else if (owner === "attempt") {
302
372
  if (value.requests !== 0 || !isEmptyAggregateTokens(value.requestTokens))
303
373
  return false;
304
374
  if (value.served + value.errored + value.cancelled > value.attempts)
305
375
  return false;
376
+ // Attempt-side rows carry no REQUEST-scoped facts.
377
+ if (value.requestSpend !== undefined || value.partiallyPricedRequests !== undefined)
378
+ return false;
306
379
  }
307
380
  else {
308
381
  if (value.served + value.errored + value.cancelled > value.requests)
@@ -321,46 +394,41 @@ function isAggregateFields(value, owner = "general") {
321
394
  if (value.outcome === "unknown" && (value.served !== 0 || value.errored !== 0 || value.cancelled !== 0))
322
395
  return false;
323
396
  }
324
- return value.unpricedRequests <= value.requests;
397
+ return (value.unpricedRequests <= value.requests &&
398
+ (value.partiallyPricedRequests ?? 0) <= value.requests &&
399
+ (value.unpricedRequests + (value.partiallyPricedRequests ?? 0) <= value.requests));
400
+ }
401
+ function isEmptyAggregateSpend(spend) {
402
+ return (typeof spend === "object" &&
403
+ spend !== null &&
404
+ SPEND_CELL_KEYS.every((key) => {
405
+ const cell = spend[key];
406
+ return cell !== undefined && typeof cell === "object" && cell !== null && isEmptyAggregateSpendCell(cell);
407
+ }));
325
408
  }
409
+ /** Aggregate keys; `requestSpend`/`partiallyPricedRequests` are the additive optional pair. */
410
+ const AGGREGATE_KEYS = Object.freeze([
411
+ "requests",
412
+ "attempts",
413
+ "served",
414
+ "errored",
415
+ "cancelled",
416
+ "tokens",
417
+ "requestTokens",
418
+ "latency",
419
+ "commit",
420
+ "spend",
421
+ "unpricedRequests",
422
+ ]);
423
+ const AGGREGATE_OPTIONAL_KEYS = Object.freeze(["requestSpend", "partiallyPricedRequests"]);
326
424
  function isAggregate(value) {
327
- return (hasExactKeys(value, [
328
- "requests",
329
- "attempts",
330
- "served",
331
- "errored",
332
- "cancelled",
333
- "tokens",
334
- "requestTokens",
335
- "latency",
336
- "commit",
337
- "spend",
338
- "unpricedRequests",
339
- ]) && isAggregateFields(value));
425
+ return (hasExactKeysWithOptional(value, AGGREGATE_KEYS, AGGREGATE_OPTIONAL_KEYS) &&
426
+ isAggregateFields(value));
340
427
  }
341
428
  function isDimensionRow(value) {
342
- if (!hasExactKeys(value, [
343
- "requests",
344
- "attempts",
345
- "served",
346
- "errored",
347
- "cancelled",
348
- "tokens",
349
- "requestTokens",
350
- "latency",
351
- "commit",
352
- "spend",
353
- "unpricedRequests",
354
- "kind",
355
- "role",
356
- "provider",
357
- "model",
358
- "client",
359
- "credentialId",
360
- "attribution",
361
- "outcome",
362
- "failureKind",
363
- ]) ||
429
+ const keys = [...AGGREGATE_KEYS, "kind", "role", "provider", "model", "client", "credentialId", "attribution", "outcome", "failureKind"];
430
+ const optional = [...AGGREGATE_OPTIONAL_KEYS];
431
+ if (!hasExactKeysWithOptional(value, keys, optional) ||
364
432
  (value.kind !== "request" && value.kind !== "attempt") ||
365
433
  (value.kind === "request" && value.role !== "request") ||
366
434
  (value.kind === "attempt" && !isRole(value.role)) ||
@@ -373,9 +441,7 @@ function isDimensionRow(value) {
373
441
  (value.failureKind !== null && !isFailure(value.failureKind)) ||
374
442
  !isFailureCoherent(value.outcome, value.failureKind))
375
443
  return false;
376
- if (!isAggregateFields(value, value.kind === "request" ? "request" : "attempt"))
377
- return false;
378
- return true;
444
+ return isAggregateFields(value, value.kind === "request" ? "request" : "attempt");
379
445
  }
380
446
  function isLossMarker(value) {
381
447
  return (hasExactKeys(value, ["kind", "count", "field"]) &&
@@ -513,6 +579,36 @@ function isLifetime(value) {
513
579
  return false;
514
580
  return withinFileCeiling(value);
515
581
  }
582
+ /** One attempt's priced spend, or LEGACY `null`. Guards every provenance field. */
583
+ function isSpend(value) {
584
+ return (hasExactKeys(value, [
585
+ "amountMicrousd",
586
+ "priceSource",
587
+ "tokenBasis",
588
+ "source",
589
+ "coverage",
590
+ "unpricedTokens",
591
+ "pricesUsed",
592
+ "observedAt",
593
+ ]) &&
594
+ isCounter(value.amountMicrousd) &&
595
+ (value.priceSource === "provider_published" || value.priceSource === "reference") &&
596
+ (value.tokenBasis === "reported" || value.tokenBasis === "estimated") &&
597
+ (value.source === "provider_reported" || value.source === "relay_estimated") &&
598
+ (value.coverage === "full" || value.coverage === "input_only" || value.coverage === "partial") &&
599
+ hasExactKeys(value.unpricedTokens, ["cacheRead", "cacheCreation", "cachedInput"]) &&
600
+ isNullableCounter(value.unpricedTokens.cacheRead) &&
601
+ isNullableCounter(value.unpricedTokens.cacheCreation) &&
602
+ isNullableCounter(value.unpricedTokens.cachedInput) &&
603
+ hasExactKeys(value.pricesUsed, ["perMillionIn", "perMillionOut"]) &&
604
+ isNullableNumber(value.pricesUsed.perMillionIn) &&
605
+ isNullableNumber(value.pricesUsed.perMillionOut) &&
606
+ isTimestamp(value.observedAt));
607
+ }
608
+ /** A finite non-negative number; prices are per-token decimals, not counters. */
609
+ function isNullableNumber(value) {
610
+ return value === null || (typeof value === "number" && Number.isFinite(value) && value >= 0);
611
+ }
516
612
  function isAttemptPacket(value) {
517
613
  if (!hasExactKeys(value, [
518
614
  "requestId",
@@ -547,7 +643,7 @@ function isAttemptPacket(value) {
547
643
  !isNullableId(value.model) ||
548
644
  !isNullableId(value.credentialId) ||
549
645
  !isAggregateTokens(value.tokens) ||
550
- value.spend !== null)
646
+ !(value.spend === null || isSpend(value.spend)))
551
647
  return false;
552
648
  if (value.commitMs !== null && value.role !== "serve")
553
649
  return false;
@@ -601,7 +697,7 @@ function isRequestPacket(value) {
601
697
  !isNullableId(value.model) ||
602
698
  !isNullableId(value.credentialId) ||
603
699
  !isAggregateTokens(value.tokens) ||
604
- value.spend !== null ||
700
+ !(value.spend === null || isSpend(value.spend)) ||
605
701
  !hasExactArray(value.attempts, ACCOUNTING_MAX_DETAIL_ATTEMPTS, isAttemptPacket) ||
606
702
  !isAttemptMetadata(value.attemptMetadata))
607
703
  return false;
@@ -885,6 +981,38 @@ export function checkedAddAccountingCounter(left, right) {
885
981
  return null;
886
982
  return left + right;
887
983
  }
984
+ /**
985
+ * Sum two aggregate SPEND cells: integer micro-USD amounts, contributor counts,
986
+ * latest observation. Amounts stay null once any side is uncertain (overflow),
987
+ * mirroring how token cells degrade — a lost sum is never silently re-guessed.
988
+ */
989
+ export function mergeAccountingSpendCells(left, right) {
990
+ const known = checkedAddAccountingCounter(left.known, right.known);
991
+ if (known === null)
992
+ return null;
993
+ let amountMicrousd = null;
994
+ if (left.amountMicrousd !== null &&
995
+ right.amountMicrousd !== null &&
996
+ left.amountMicrousd <= ACCOUNTING_MAX_COUNTER - right.amountMicrousd) {
997
+ amountMicrousd = left.amountMicrousd + right.amountMicrousd;
998
+ }
999
+ return freezeDeep({
1000
+ amountMicrousd: known > 0 ? amountMicrousd : null,
1001
+ known,
1002
+ observedAt: latestTimestamp(left.observedAt, right.observedAt),
1003
+ });
1004
+ }
1005
+ export function emptyAccountingSpendCell() {
1006
+ return { amountMicrousd: null, known: 0, observedAt: null };
1007
+ }
1008
+ export function emptyAccountingAggregateSpend() {
1009
+ return {
1010
+ providerPublishedReported: emptyAccountingSpendCell(),
1011
+ providerPublishedEstimated: emptyAccountingSpendCell(),
1012
+ referenceReported: emptyAccountingSpendCell(),
1013
+ referenceEstimated: emptyAccountingSpendCell(),
1014
+ };
1015
+ }
888
1016
  function latestTimestamp(a, b) {
889
1017
  if (a === null)
890
1018
  return b;