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.
- package/README.md +2 -1
- package/config.example.json +2 -1
- package/dist/accounting-store-schema.d.ts +105 -5
- package/dist/accounting-store-schema.js +171 -43
- package/dist/accounting-store-schema.js.map +1 -1
- package/dist/accounting-store.d.ts +30 -0
- package/dist/accounting-store.js +252 -6
- package/dist/accounting-store.js.map +1 -1
- package/dist/accounting.d.ts +111 -4
- package/dist/accounting.js +197 -2
- package/dist/accounting.js.map +1 -1
- package/dist/anthropic.d.ts +10 -0
- package/dist/anthropic.js.map +1 -1
- package/dist/availability-snapshot.d.ts +45 -0
- package/dist/availability-snapshot.js +259 -0
- package/dist/availability-snapshot.js.map +1 -0
- package/dist/availability.d.ts +119 -0
- package/dist/availability.js +225 -0
- package/dist/availability.js.map +1 -0
- package/dist/backend.d.ts +10 -0
- package/dist/backend.js +77 -4
- package/dist/backend.js.map +1 -1
- package/dist/candidates.d.ts +39 -0
- package/dist/candidates.js +89 -1
- package/dist/candidates.js.map +1 -1
- package/dist/catalog.d.ts +31 -0
- package/dist/catalog.js +101 -2
- package/dist/catalog.js.map +1 -1
- package/dist/circuit-breaker.d.ts +14 -1
- package/dist/circuit-breaker.js +21 -0
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/cli.d.ts +26 -1
- package/dist/cli.js +227 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +32 -0
- package/dist/config.js +45 -0
- package/dist/config.js.map +1 -1
- package/dist/configured-limits.d.ts +85 -0
- package/dist/configured-limits.js +143 -0
- package/dist/configured-limits.js.map +1 -0
- package/dist/credential-fleet.d.ts +7 -0
- package/dist/credential-fleet.js.map +1 -1
- package/dist/dashboard/.vite/manifest.json +2 -2
- package/dist/dashboard/assets/{index-RtLgb7Cg.js → index-B2fuSM77.js} +14 -14
- package/dist/dashboard/assets/index-B_lGhkan.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard-contract.d.ts +88 -4
- package/dist/dashboard-contract.js +75 -4
- package/dist/dashboard-contract.js.map +1 -1
- package/dist/dashboard-snapshot.d.ts +17 -2
- package/dist/dashboard-snapshot.js +423 -17
- package/dist/dashboard-snapshot.js.map +1 -1
- package/dist/emitSse.js +15 -1
- package/dist/emitSse.js.map +1 -1
- package/dist/quota-demotion.d.ts +65 -0
- package/dist/quota-demotion.js +148 -0
- package/dist/quota-demotion.js.map +1 -0
- package/dist/rate-limits.d.ts +84 -0
- package/dist/rate-limits.js +259 -0
- package/dist/rate-limits.js.map +1 -0
- package/dist/server.d.ts +3 -1
- package/dist/server.js +221 -17
- package/dist/server.js.map +1 -1
- package/dist/target-facts.d.ts +27 -4
- package/dist/target-facts.js +39 -6
- package/dist/target-facts.js.map +1 -1
- package/package.json +1 -1
- 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
|
|
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.
|
package/config.example.json
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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 (
|
|
328
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|