@centerforagenticai/pi-multi-account 0.1.1

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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +29 -0
  3. package/README.md +999 -0
  4. package/config/models/pi-multi-account.v1.json +32 -0
  5. package/config/subscription-plans.v1.json +122 -0
  6. package/package.json +76 -0
  7. package/packages/pi-anthropic-oauth/LICENSE +21 -0
  8. package/packages/pi-anthropic-oauth/package.json +54 -0
  9. package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
  10. package/packages/pi-anthropic-oauth/src/context.ts +116 -0
  11. package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
  12. package/packages/pi-anthropic-oauth/src/index.ts +37 -0
  13. package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
  14. package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
  15. package/packages/pi-antigravity/LICENSE +21 -0
  16. package/packages/pi-antigravity/package.json +77 -0
  17. package/packages/pi-antigravity/src/auth/index.ts +14 -0
  18. package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
  19. package/packages/pi-antigravity/src/client/client.ts +561 -0
  20. package/packages/pi-antigravity/src/client/index.ts +1 -0
  21. package/packages/pi-antigravity/src/context.ts +110 -0
  22. package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  23. package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
  24. package/packages/pi-antigravity/src/image/image.ts +336 -0
  25. package/packages/pi-antigravity/src/image/index.ts +1 -0
  26. package/packages/pi-antigravity/src/index.ts +280 -0
  27. package/packages/pi-antigravity/src/models/discovery.ts +154 -0
  28. package/packages/pi-antigravity/src/models/grouping.ts +424 -0
  29. package/packages/pi-antigravity/src/models/index.ts +3 -0
  30. package/packages/pi-antigravity/src/models/models.ts +500 -0
  31. package/packages/pi-antigravity/src/stream/index.ts +1 -0
  32. package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
  33. package/packages/pi-antigravity/src/types/enums.ts +42 -0
  34. package/packages/pi-antigravity/src/types/index.ts +2 -0
  35. package/packages/pi-antigravity/src/types/types.ts +292 -0
  36. package/packages/pi-antigravity/src/usage/index.ts +1 -0
  37. package/packages/pi-antigravity/src/usage/usage.ts +416 -0
  38. package/packages/pi-antigravity/src/utils/http.ts +91 -0
  39. package/packages/pi-antigravity/src/utils/index.ts +3 -0
  40. package/packages/pi-antigravity/src/utils/security.ts +73 -0
  41. package/packages/pi-antigravity/src/utils/util.ts +132 -0
  42. package/scripts/multi-account.mjs +44 -0
  43. package/src/account-labels.ts +223 -0
  44. package/src/account-plan-assignment.ts +340 -0
  45. package/src/account-rate-history.ts +372 -0
  46. package/src/anthropic-adaptive-stream.ts +531 -0
  47. package/src/anthropic-alias-stream.ts +140 -0
  48. package/src/anthropic-context-compat.ts +80 -0
  49. package/src/api-pricing.ts +579 -0
  50. package/src/bounded-file-lines.ts +97 -0
  51. package/src/catalog-rebinding.ts +177 -0
  52. package/src/catalog-registration-probe.ts +111 -0
  53. package/src/codex-adapter.ts +345 -0
  54. package/src/codex-model-defaults.ts +785 -0
  55. package/src/command-completions.ts +404 -0
  56. package/src/commands.ts +2000 -0
  57. package/src/compaction.ts +14 -0
  58. package/src/config.ts +1317 -0
  59. package/src/continuation.ts +569 -0
  60. package/src/cooldowns.ts +110 -0
  61. package/src/cost-digest-store.ts +332 -0
  62. package/src/cost-digest.ts +1044 -0
  63. package/src/cost-history.ts +251 -0
  64. package/src/cost-period-closer.ts +160 -0
  65. package/src/cost-report-json.ts +318 -0
  66. package/src/cost-report-reader.ts +368 -0
  67. package/src/cost-report-render.ts +207 -0
  68. package/src/cost-report.ts +1104 -0
  69. package/src/coverage-attestation.ts +397 -0
  70. package/src/credential-lifecycle.ts +169 -0
  71. package/src/credential-refresh.ts +248 -0
  72. package/src/declaration-notice-marker.ts +238 -0
  73. package/src/diagnostic-store.ts +276 -0
  74. package/src/diagnostics.ts +309 -0
  75. package/src/discovery.ts +471 -0
  76. package/src/duration.ts +13 -0
  77. package/src/error-classification.ts +256 -0
  78. package/src/fuzzy.ts +15 -0
  79. package/src/group-policy.ts +81 -0
  80. package/src/history-store.ts +897 -0
  81. package/src/index.ts +5572 -0
  82. package/src/lifecycle.ts +378 -0
  83. package/src/logical-dispatch.ts +279 -0
  84. package/src/logical-model-selector.ts +254 -0
  85. package/src/logical-model-switcher.ts +430 -0
  86. package/src/logical-provider-attribution.ts +544 -0
  87. package/src/logical-provider.ts +1237 -0
  88. package/src/logical-route-indicator.ts +215 -0
  89. package/src/machine-lease.ts +445 -0
  90. package/src/model-support.ts +66 -0
  91. package/src/models-declaration.ts +1091 -0
  92. package/src/openai-adapter.ts +117 -0
  93. package/src/openrouter-budget.ts +304 -0
  94. package/src/openrouter-fallback.ts +146 -0
  95. package/src/period-boundaries.ts +376 -0
  96. package/src/pi-anthropic-oauth.d.ts +6 -0
  97. package/src/preflight.ts +253 -0
  98. package/src/pricing-cache.ts +235 -0
  99. package/src/project-identity.ts +100 -0
  100. package/src/provider-registration.ts +942 -0
  101. package/src/rate-formula.ts +163 -0
  102. package/src/recovery-engine.ts +853 -0
  103. package/src/recovery-output.ts +837 -0
  104. package/src/recovery-plan.ts +239 -0
  105. package/src/report-range.ts +203 -0
  106. package/src/route-resolver.ts +789 -0
  107. package/src/routing-config-transaction.ts +232 -0
  108. package/src/routing.ts +1163 -0
  109. package/src/runtime-state.ts +630 -0
  110. package/src/session-account-groups.ts +284 -0
  111. package/src/session-restore.ts +287 -0
  112. package/src/shared-usage.ts +1392 -0
  113. package/src/standalone-cli.ts +720 -0
  114. package/src/status-view.ts +578 -0
  115. package/src/subscription-plan-catalog.ts +346 -0
  116. package/src/tier-model-resolver.ts +46 -0
  117. package/src/upstream-anthropic.ts +315 -0
  118. package/src/upstream-antigravity.ts +327 -0
  119. package/src/usage-fetch.ts +1634 -0
  120. package/src/usage.ts +1026 -0
  121. package/src/vendor.ts +87 -0
  122. package/src/warmer.ts +231 -0
  123. package/src/watchdog.ts +219 -0
  124. package/src/window-history.ts +270 -0
@@ -0,0 +1,1044 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ estimateApiEquivalentCost,
4
+ estimatePiCatalogCost,
5
+ lookupApiRate,
6
+ lookupPiCatalogCost,
7
+ OPENROUTER_METHOD_LIMITS,
8
+ PI_CATALOG_METHOD_LIMITS,
9
+ type ApiEquivalentMethod,
10
+ type PiCatalogSnapshot,
11
+ } from "./api-pricing.js";
12
+ import type { HistoryLogRecord } from "./history-store.js";
13
+ import {
14
+ getPeriodBounds,
15
+ type PeriodBounds,
16
+ type PeriodType,
17
+ } from "./period-boundaries.js";
18
+ import { isProjectKey } from "./project-identity.js";
19
+ import type { PricingCacheResult } from "./pricing-cache.js";
20
+ export type { ApiEquivalentMethod, PiCatalogSnapshot } from "./api-pricing.js";
21
+
22
+ const DAY_MS = 86_400_000;
23
+
24
+ export type CostCoverage = "complete" | "partial" | "unknown";
25
+
26
+ export interface CostObservation {
27
+ readonly observedAtMs: number;
28
+ readonly projectKey: string | undefined;
29
+ readonly canonicalAccountId: string;
30
+ readonly requestedModel: string;
31
+ readonly status: CostCoverage;
32
+ readonly tokens: {
33
+ readonly input: number;
34
+ readonly output: number;
35
+ readonly cacheRead: number;
36
+ readonly cacheWrite: number;
37
+ readonly cacheWrite1h: number;
38
+ };
39
+ readonly retainedCostUsd: number;
40
+ }
41
+
42
+ export interface CostHistoryGap {
43
+ readonly startMs: number;
44
+ readonly endMs: number;
45
+ }
46
+
47
+ export interface CostDigestRow {
48
+ readonly schemaVersion: 1;
49
+ readonly kind: "cost-period-digest";
50
+ readonly id: string;
51
+ readonly projectKey: string;
52
+ readonly canonicalAccountId: string;
53
+ readonly requestedModel: string;
54
+ readonly periodType: PeriodType;
55
+ readonly periodStartMs: number;
56
+ readonly periodEndMs: number;
57
+ readonly closedAtMs: number;
58
+ readonly source: "raw" | "day-rollup" | "month-rollup";
59
+ readonly childRowCount: number;
60
+ readonly observationCount: number;
61
+ readonly coverage: CostCoverage;
62
+ readonly tokens: CostObservation["tokens"];
63
+ readonly retainedCostUsd: number;
64
+ readonly apiEquivalent:
65
+ | {
66
+ readonly status: "priced";
67
+ readonly estimatedUsd: number;
68
+ readonly rateAsOfMs: readonly number[];
69
+ readonly sourceModelIds: readonly string[];
70
+ readonly methods?: readonly ApiEquivalentMethod[];
71
+ }
72
+ | {
73
+ readonly status: "unpriced";
74
+ readonly reason: string;
75
+ readonly rateAsOfMs: readonly number[];
76
+ readonly methods?: readonly ApiEquivalentMethod[];
77
+ };
78
+ }
79
+
80
+ function isRecord(value: unknown): value is Record<string, unknown> {
81
+ return typeof value === "object" && value !== null && !Array.isArray(value);
82
+ }
83
+
84
+ function validTimestamp(value: unknown): value is number {
85
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
86
+ }
87
+
88
+ function tokenCount(value: unknown): value is number {
89
+ return (
90
+ typeof value === "number" &&
91
+ Number.isSafeInteger(value) &&
92
+ value >= 0
93
+ );
94
+ }
95
+
96
+ function boundedString(value: unknown, maxLength: number): value is string {
97
+ return (
98
+ typeof value === "string" &&
99
+ value.length > 0 &&
100
+ value.length <= maxLength &&
101
+ !value.includes("\0")
102
+ );
103
+ }
104
+
105
+ function parseObservation(record: HistoryLogRecord): CostObservation | undefined {
106
+ if (record.recordType !== "cost-delta" || !isRecord(record.payload)) {
107
+ return undefined;
108
+ }
109
+ const payload = record.payload;
110
+ if (
111
+ !validTimestamp(record.observedAtMs) ||
112
+ !boundedString(payload.canonicalAccountId, 128) ||
113
+ !boundedString(payload.requestedModel, 200) ||
114
+ !isRecord(payload.tokens) ||
115
+ !isRecord(payload.cost) ||
116
+ !tokenCount(payload.tokens.input) ||
117
+ !tokenCount(payload.tokens.output) ||
118
+ !tokenCount(payload.tokens.cacheRead) ||
119
+ !tokenCount(payload.tokens.cacheWrite) ||
120
+ (payload.tokens.cacheWrite1h !== undefined &&
121
+ !tokenCount(payload.tokens.cacheWrite1h)) ||
122
+ !(["complete", "partial", "unknown", "unpriced"] as const).includes(
123
+ payload.status as CostCoverage | "unpriced",
124
+ ) ||
125
+ (payload.status !== "unpriced" &&
126
+ (typeof payload.cost.total !== "number" ||
127
+ !Number.isFinite(payload.cost.total) ||
128
+ payload.cost.total < 0))
129
+ ) {
130
+ return undefined;
131
+ }
132
+ const cacheWrite1h = (payload.tokens.cacheWrite1h as number | undefined) ?? 0;
133
+ if (cacheWrite1h > payload.tokens.cacheWrite) return undefined;
134
+ return {
135
+ observedAtMs: record.observedAtMs,
136
+ projectKey:
137
+ typeof payload.projectKey === "string" &&
138
+ isProjectKey(payload.projectKey)
139
+ ? payload.projectKey
140
+ : undefined,
141
+ canonicalAccountId: payload.canonicalAccountId,
142
+ requestedModel: payload.requestedModel,
143
+ status:
144
+ payload.status === "unpriced"
145
+ ? "unknown"
146
+ : (payload.status as CostCoverage),
147
+ tokens: {
148
+ input: payload.tokens.input,
149
+ output: payload.tokens.output,
150
+ cacheRead: payload.tokens.cacheRead,
151
+ cacheWrite: payload.tokens.cacheWrite,
152
+ cacheWrite1h,
153
+ },
154
+ retainedCostUsd:
155
+ payload.status === "unpriced" ? 0 : (payload.cost.total as number),
156
+ };
157
+ }
158
+
159
+ export function extractCostHistory(records: Iterable<HistoryLogRecord>): {
160
+ readonly observations: readonly CostObservation[];
161
+ readonly gaps: readonly CostHistoryGap[];
162
+ } {
163
+ const observations: CostObservation[] = [];
164
+ const gaps: CostHistoryGap[] = [];
165
+ for (const record of records) {
166
+ if (
167
+ record.recordType === "gap" &&
168
+ validTimestamp(record.gapStartMs) &&
169
+ validTimestamp(record.gapEndMs) &&
170
+ record.gapEndMs > record.gapStartMs
171
+ ) {
172
+ gaps.push({ startMs: record.gapStartMs, endMs: record.gapEndMs });
173
+ continue;
174
+ }
175
+ const observation = parseObservation(record);
176
+ if (observation !== undefined) observations.push(observation);
177
+ }
178
+ return { observations, gaps };
179
+ }
180
+
181
+ function addReportCount(left: number, right: number): number {
182
+ const total = left + right;
183
+ if (!Number.isSafeInteger(total) || total < 0) {
184
+ throw new RangeError("Cost report token total exceeds the safe integer range.");
185
+ }
186
+ return total;
187
+ }
188
+
189
+ function addReportTokens(
190
+ left: CostObservation["tokens"],
191
+ right: CostObservation["tokens"],
192
+ ): CostObservation["tokens"] {
193
+ return {
194
+ input: addReportCount(left.input, right.input),
195
+ output: addReportCount(left.output, right.output),
196
+ cacheRead: addReportCount(left.cacheRead, right.cacheRead),
197
+ cacheWrite: addReportCount(left.cacheWrite, right.cacheWrite),
198
+ cacheWrite1h: addReportCount(left.cacheWrite1h, right.cacheWrite1h),
199
+ };
200
+ }
201
+
202
+ function addReportUsd(left: number, right: number): number {
203
+ const total = left + right;
204
+ if (!Number.isFinite(total) || total < 0) {
205
+ throw new RangeError(
206
+ "Cost report USD total must remain finite and non-negative.",
207
+ );
208
+ }
209
+ return total;
210
+ }
211
+
212
+ export interface CostHistoryReportSummary {
213
+ readonly currentDayRows: readonly CostDigestRow[];
214
+ /**
215
+ * Every attributed (project-keyed) raw observation for the still-open
216
+ * current day, in arrival order. Kept alongside the pre-aggregated
217
+ * {@link currentDayRows} so a caller needing a sub-day-precise window
218
+ * inside today (a custom range ending or starting before `nowMs`) can
219
+ * rebuild a correctly clipped provisional row instead of slicing the
220
+ * already-summed aggregate, which has no per-response timestamps left to
221
+ * slice by.
222
+ */
223
+ readonly currentDayObservations: readonly CostObservation[];
224
+ /** The same current-day gap set folded into {@link currentDayRows}'s coverage, exposed so a caller rebuilding a clipped provisional row from {@link currentDayObservations} preserves the same gap-aware coverage. */
225
+ readonly currentDayGaps: readonly CostHistoryGap[];
226
+ readonly earliestAttributedObservedAtMs: number | undefined;
227
+ readonly accountIds: readonly string[];
228
+ readonly legacyUnattributed: readonly {
229
+ readonly canonicalAccountId: string;
230
+ readonly requestedModel: string;
231
+ readonly observationCount: number;
232
+ readonly tokens: CostObservation["tokens"];
233
+ readonly retainedCostUsd: number;
234
+ }[];
235
+ }
236
+
237
+ /** Retain only current-day detail and grouped legacy totals needed by cost rendering. */
238
+ export function summarizeCostHistoryForReport(
239
+ records: Iterable<HistoryLogRecord>,
240
+ nowMs: number,
241
+ pricing: PricingCacheResult,
242
+ piCatalog?: PiCatalogSnapshot,
243
+ ): CostHistoryReportSummary {
244
+ if (!validTimestamp(nowMs)) {
245
+ throw new RangeError("Cost report clock must be a finite timestamp.");
246
+ }
247
+ const day = getPeriodBounds(nowMs, "day");
248
+ const currentDayGroups = new Map<string, RawCostGroup>();
249
+ const accountIds = new Set<string>();
250
+ const legacy = new Map<
251
+ string,
252
+ CostHistoryReportSummary["legacyUnattributed"][number]
253
+ >();
254
+ let earliestAttributedObservedAtMs: number | undefined;
255
+ let currentDayGap = false;
256
+ const currentDayObservations: CostObservation[] = [];
257
+ for (const record of records) {
258
+ if (
259
+ record.recordType === "gap" &&
260
+ validTimestamp(record.gapStartMs) &&
261
+ validTimestamp(record.gapEndMs) &&
262
+ record.gapEndMs > record.gapStartMs
263
+ ) {
264
+ currentDayGap ||=
265
+ record.gapStartMs < day.endMs && record.gapEndMs > day.startMs;
266
+ continue;
267
+ }
268
+ const observation = parseObservation(record);
269
+ if (observation === undefined) continue;
270
+ accountIds.add(observation.canonicalAccountId);
271
+ if (observation.projectKey === undefined) {
272
+ const key = JSON.stringify([
273
+ observation.canonicalAccountId,
274
+ observation.requestedModel,
275
+ ]);
276
+ const previous = legacy.get(key);
277
+ legacy.set(
278
+ key,
279
+ previous === undefined
280
+ ? {
281
+ canonicalAccountId: observation.canonicalAccountId,
282
+ requestedModel: observation.requestedModel,
283
+ observationCount: 1,
284
+ tokens: observation.tokens,
285
+ retainedCostUsd: observation.retainedCostUsd,
286
+ }
287
+ : {
288
+ ...previous,
289
+ observationCount: addReportCount(previous.observationCount, 1),
290
+ tokens: addReportTokens(previous.tokens, observation.tokens),
291
+ retainedCostUsd: addReportUsd(
292
+ previous.retainedCostUsd,
293
+ observation.retainedCostUsd,
294
+ ),
295
+ },
296
+ );
297
+ continue;
298
+ }
299
+ earliestAttributedObservedAtMs =
300
+ earliestAttributedObservedAtMs === undefined
301
+ ? observation.observedAtMs
302
+ : Math.min(earliestAttributedObservedAtMs, observation.observedAtMs);
303
+ if (
304
+ observation.observedAtMs >= day.startMs &&
305
+ observation.observedAtMs < nowMs
306
+ ) {
307
+ addRawObservation(currentDayGroups, observation, day.endMs);
308
+ currentDayObservations.push(observation);
309
+ }
310
+ }
311
+ const currentDayGaps: readonly CostHistoryGap[] = currentDayGap
312
+ ? [{ startMs: day.startMs, endMs: day.endMs }]
313
+ : [];
314
+ const currentDayRows = planClosedCostDigestGroups({
315
+ groups: currentDayGroups,
316
+ gaps: currentDayGaps,
317
+ existingRows: [],
318
+ pricing,
319
+ ...(piCatalog === undefined ? {} : { piCatalog }),
320
+ nowMs: day.endMs,
321
+ closedAtMs: nowMs,
322
+ }).filter(
323
+ (row) => row.periodType === "day" && row.periodStartMs === day.startMs,
324
+ );
325
+ return {
326
+ currentDayRows,
327
+ currentDayObservations,
328
+ currentDayGaps,
329
+ earliestAttributedObservedAtMs,
330
+ accountIds: [...accountIds],
331
+ legacyUnattributed: [...legacy.values()].sort(
332
+ (left, right) =>
333
+ left.canonicalAccountId.localeCompare(right.canonicalAccountId) ||
334
+ left.requestedModel.localeCompare(right.requestedModel),
335
+ ),
336
+ };
337
+ }
338
+
339
+ function identityKey(
340
+ projectKey: string,
341
+ canonicalAccountId: string,
342
+ requestedModel: string,
343
+ periodType: PeriodType,
344
+ periodStartMs: number,
345
+ ): string {
346
+ return JSON.stringify([
347
+ projectKey,
348
+ canonicalAccountId,
349
+ requestedModel,
350
+ periodType,
351
+ periodStartMs,
352
+ ]);
353
+ }
354
+
355
+ function digestId(identity: string): string {
356
+ return `cost-digest-${createHash("sha256").update(identity).digest("hex").slice(0, 32)}`;
357
+ }
358
+
359
+ export function costDigestId(
360
+ projectKey: string,
361
+ canonicalAccountId: string,
362
+ requestedModel: string,
363
+ periodType: PeriodType,
364
+ periodStartMs: number,
365
+ ): string {
366
+ return digestId(
367
+ identityKey(
368
+ projectKey,
369
+ canonicalAccountId,
370
+ requestedModel,
371
+ periodType,
372
+ periodStartMs,
373
+ ),
374
+ );
375
+ }
376
+
377
+ function emptyTokens(): CostObservation["tokens"] {
378
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cacheWrite1h: 0 };
379
+ }
380
+
381
+ function addTokens(
382
+ target: CostObservation["tokens"],
383
+ value: CostObservation["tokens"],
384
+ ): CostObservation["tokens"] {
385
+ return addReportTokens(target, value);
386
+ }
387
+
388
+ function priceRaw(
389
+ tokens: CostObservation["tokens"],
390
+ canonicalAccountId: string,
391
+ requestedModel: string,
392
+ pricing: PricingCacheResult,
393
+ ): CostDigestRow["apiEquivalent"] {
394
+ if (pricing.state !== "fresh") {
395
+ return {
396
+ status: "unpriced",
397
+ reason:
398
+ pricing.state === "stale" ? "pricing-stale" : "pricing-unavailable",
399
+ rateAsOfMs:
400
+ pricing.state === "stale" ? [pricing.snapshot.fetchedAtMs] : [],
401
+ };
402
+ }
403
+ const rate = lookupApiRate(
404
+ pricing.snapshot,
405
+ canonicalAccountId,
406
+ requestedModel,
407
+ );
408
+ if (rate === undefined) {
409
+ return {
410
+ status: "unpriced",
411
+ reason: "model-rate-missing",
412
+ rateAsOfMs: [pricing.snapshot.fetchedAtMs],
413
+ };
414
+ }
415
+ const estimate = estimateApiEquivalentCost(
416
+ tokens,
417
+ rate,
418
+ pricing.snapshot.fetchedAtMs,
419
+ );
420
+ return estimate.status === "priced"
421
+ ? {
422
+ status: "priced",
423
+ estimatedUsd: estimate.totalUsd,
424
+ rateAsOfMs: [estimate.rateAsOfMs],
425
+ sourceModelIds: [estimate.sourceModelId],
426
+ }
427
+ : {
428
+ status: "unpriced",
429
+ reason: estimate.reason,
430
+ rateAsOfMs: [pricing.snapshot.fetchedAtMs],
431
+ };
432
+ }
433
+
434
+ type PricedObservationResult =
435
+ | {
436
+ readonly status: "priced";
437
+ readonly totalUsd: number;
438
+ readonly rateAsOfMs: number;
439
+ readonly sourceModelId: string;
440
+ readonly method: ApiEquivalentMethod;
441
+ }
442
+ | { readonly status: "unpriced"; readonly reason: string };
443
+
444
+ /**
445
+ * Price one response under its own matched tier, before any grouping.
446
+ * Tries Pi's installed catalog first (the authoritative tier metadata
447
+ * source); OpenRouter extends coverage only for models the catalog lacks.
448
+ */
449
+ function priceObservationApiEquivalent(
450
+ tokens: CostObservation["tokens"],
451
+ canonicalAccountId: string,
452
+ requestedModel: string,
453
+ piCatalog: PiCatalogSnapshot,
454
+ pricing: PricingCacheResult,
455
+ ): PricedObservationResult {
456
+ const catalogCost = lookupPiCatalogCost(
457
+ piCatalog,
458
+ canonicalAccountId,
459
+ requestedModel,
460
+ );
461
+ if (catalogCost !== undefined) {
462
+ const estimate = estimatePiCatalogCost(
463
+ tokens,
464
+ catalogCost,
465
+ piCatalog.capturedAtMs,
466
+ );
467
+ if (estimate.status !== "priced") {
468
+ return { status: "unpriced", reason: estimate.reason };
469
+ }
470
+ return {
471
+ status: "priced",
472
+ totalUsd: estimate.totalUsd,
473
+ rateAsOfMs: estimate.rateAsOfMs,
474
+ sourceModelId: estimate.sourceModelId,
475
+ method: {
476
+ source: "pi-installed-catalog",
477
+ catalogVersion: piCatalog.catalogVersion,
478
+ tierInputTokensAbove: estimate.tierInputTokensAbove,
479
+ limits: PI_CATALOG_METHOD_LIMITS,
480
+ },
481
+ };
482
+ }
483
+ // Unsupported by the installed catalog: fall back to the cached OpenRouter
484
+ // snapshot, which only extends coverage and never overrides the catalog.
485
+ if (pricing.state !== "fresh") {
486
+ return {
487
+ status: "unpriced",
488
+ reason:
489
+ pricing.state === "stale" ? "pricing-stale" : "pricing-unavailable",
490
+ };
491
+ }
492
+ const rate = lookupApiRate(pricing.snapshot, canonicalAccountId, requestedModel);
493
+ if (rate === undefined) {
494
+ return { status: "unpriced", reason: "model-rate-missing" };
495
+ }
496
+ const estimate = estimateApiEquivalentCost(tokens, rate, pricing.snapshot.fetchedAtMs);
497
+ if (estimate.status !== "priced") {
498
+ return { status: "unpriced", reason: estimate.reason };
499
+ }
500
+ return {
501
+ status: "priced",
502
+ totalUsd: estimate.totalUsd,
503
+ rateAsOfMs: estimate.rateAsOfMs,
504
+ sourceModelId: estimate.sourceModelId,
505
+ method: {
506
+ source: "openrouter",
507
+ catalogVersion: `openrouter-snapshot-${pricing.snapshot.fetchedAtMs}`,
508
+ tierInputTokensAbove: undefined,
509
+ limits: OPENROUTER_METHOD_LIMITS,
510
+ },
511
+ };
512
+ }
513
+
514
+ function methodKey(method: ApiEquivalentMethod): string {
515
+ return JSON.stringify([
516
+ method.source,
517
+ method.catalogVersion,
518
+ method.tierInputTokensAbove ?? null,
519
+ ]);
520
+ }
521
+
522
+ function sortedMethods(
523
+ methods: ReadonlyMap<string, ApiEquivalentMethod>,
524
+ ): readonly ApiEquivalentMethod[] {
525
+ return [...methods.values()].sort(
526
+ (left, right) =>
527
+ left.source.localeCompare(right.source) ||
528
+ left.catalogVersion.localeCompare(right.catalogVersion) ||
529
+ (left.tierInputTokensAbove ?? -1) - (right.tierInputTokensAbove ?? -1),
530
+ );
531
+ }
532
+
533
+ /**
534
+ * Price every response in a raw day group under its own matched tier before
535
+ * summing into the group's total: REQ-TIER-PER-RESPONSE. A day mixing
536
+ * responses below and above a tier threshold must never be priced as if the
537
+ * whole day's tokens were one request.
538
+ */
539
+ function priceRawPerResponse(
540
+ group: RawCostGroup,
541
+ piCatalog: PiCatalogSnapshot,
542
+ pricing: PricingCacheResult,
543
+ ): CostDigestRow["apiEquivalent"] {
544
+ let estimatedUsd = 0;
545
+ let unpriced = false;
546
+ let reason: string | undefined;
547
+ const rateAsOfMs = new Set<number>();
548
+ const sourceModelIds = new Set<string>();
549
+ const methods = new Map<string, ApiEquivalentMethod>();
550
+ for (const tokens of group.observationTokens) {
551
+ const priced = priceObservationApiEquivalent(
552
+ tokens,
553
+ group.canonicalAccountId,
554
+ group.requestedModel,
555
+ piCatalog,
556
+ pricing,
557
+ );
558
+ if (priced.status === "unpriced") {
559
+ unpriced = true;
560
+ reason ??= priced.reason;
561
+ continue;
562
+ }
563
+ estimatedUsd = addReportUsd(estimatedUsd, priced.totalUsd);
564
+ rateAsOfMs.add(priced.rateAsOfMs);
565
+ sourceModelIds.add(priced.sourceModelId);
566
+ methods.set(methodKey(priced.method), priced.method);
567
+ }
568
+ if (unpriced) {
569
+ return {
570
+ status: "unpriced",
571
+ reason: reason ?? "response-unpriced",
572
+ rateAsOfMs: sortedUniqueNumbers([...rateAsOfMs]),
573
+ };
574
+ }
575
+ return {
576
+ status: "priced",
577
+ estimatedUsd,
578
+ rateAsOfMs: sortedUniqueNumbers([...rateAsOfMs]),
579
+ sourceModelIds: sortedUniqueStrings([...sourceModelIds]),
580
+ methods: sortedMethods(methods),
581
+ };
582
+ }
583
+
584
+ function sortedUniqueNumbers(values: readonly number[]): readonly number[] {
585
+ return [...new Set(values)].sort((left, right) => left - right);
586
+ }
587
+
588
+ function sortedUniqueStrings(values: readonly string[]): readonly string[] {
589
+ return [...new Set(values)].sort();
590
+ }
591
+
592
+ function rollupPrice(
593
+ children: readonly CostDigestRow[],
594
+ ): CostDigestRow["apiEquivalent"] {
595
+ const rateAsOfMs = sortedUniqueNumbers(
596
+ children.flatMap((child) => child.apiEquivalent.rateAsOfMs),
597
+ );
598
+ if (children.some((child) => child.apiEquivalent.status === "unpriced")) {
599
+ return { status: "unpriced", reason: "child-unpriced", rateAsOfMs };
600
+ }
601
+ const priced = children.filter(
602
+ (child): child is CostDigestRow & {
603
+ readonly apiEquivalent: Extract<
604
+ CostDigestRow["apiEquivalent"],
605
+ { readonly status: "priced" }
606
+ >;
607
+ } => child.apiEquivalent.status === "priced",
608
+ );
609
+ return {
610
+ status: "priced",
611
+ estimatedUsd: priced.reduce(
612
+ (total, child) => addReportUsd(total, child.apiEquivalent.estimatedUsd),
613
+ 0,
614
+ ),
615
+ rateAsOfMs,
616
+ sourceModelIds: sortedUniqueStrings(
617
+ priced.flatMap((child) => child.apiEquivalent.sourceModelIds),
618
+ ),
619
+ };
620
+ }
621
+
622
+ function rollupCoverage(
623
+ children: readonly CostDigestRow[],
624
+ expectedChildren: number,
625
+ ): CostCoverage {
626
+ if (
627
+ children.length !== expectedChildren ||
628
+ children.some((child) => child.coverage === "partial")
629
+ ) {
630
+ return "partial";
631
+ }
632
+ return children.some((child) => child.coverage === "unknown")
633
+ ? "unknown"
634
+ : "complete";
635
+ }
636
+
637
+ function expectedChildCount(
638
+ periodType: Exclude<PeriodType, "day">,
639
+ bounds: PeriodBounds,
640
+ ): number {
641
+ switch (periodType) {
642
+ case "week":
643
+ return 7;
644
+ case "month":
645
+ return Math.round((bounds.endMs - bounds.startMs) / 86_400_000);
646
+ case "quarter":
647
+ return 3;
648
+ case "half-year":
649
+ return 6;
650
+ case "year":
651
+ return 12;
652
+ }
653
+ }
654
+
655
+ function parentRows(
656
+ children: readonly CostDigestRow[],
657
+ periodType: Exclude<PeriodType, "day">,
658
+ source: "day-rollup" | "month-rollup",
659
+ nowMs: number,
660
+ closedAtMs: number,
661
+ existingIds: Set<string>,
662
+ ): CostDigestRow[] {
663
+ const groups = new Map<
664
+ string,
665
+ { readonly bounds: PeriodBounds; readonly children: CostDigestRow[] }
666
+ >();
667
+ for (const child of children) {
668
+ const bounds = getPeriodBounds(child.periodStartMs, periodType);
669
+ if (bounds.endMs > nowMs) continue;
670
+ const key = identityKey(
671
+ child.projectKey,
672
+ child.canonicalAccountId,
673
+ child.requestedModel,
674
+ periodType,
675
+ bounds.startMs,
676
+ );
677
+ const current = groups.get(key) ?? { bounds, children: [] };
678
+ current.children.push(child);
679
+ groups.set(key, current);
680
+ }
681
+
682
+ const rows: CostDigestRow[] = [];
683
+ for (const group of groups.values()) {
684
+ const first = group.children[0];
685
+ if (first === undefined) continue;
686
+ const id = costDigestId(
687
+ first.projectKey,
688
+ first.canonicalAccountId,
689
+ first.requestedModel,
690
+ periodType,
691
+ group.bounds.startMs,
692
+ );
693
+ if (existingIds.has(id)) continue;
694
+ const tokens = group.children.reduce(
695
+ (total, child) => addTokens(total, child.tokens),
696
+ emptyTokens(),
697
+ );
698
+ rows.push({
699
+ schemaVersion: 1,
700
+ kind: "cost-period-digest",
701
+ id,
702
+ projectKey: first.projectKey,
703
+ canonicalAccountId: first.canonicalAccountId,
704
+ requestedModel: first.requestedModel,
705
+ periodType,
706
+ periodStartMs: group.bounds.startMs,
707
+ periodEndMs: group.bounds.endMs,
708
+ closedAtMs,
709
+ source,
710
+ childRowCount: group.children.length,
711
+ observationCount: group.children.reduce(
712
+ (total, child) => addReportCount(total, child.observationCount),
713
+ 0,
714
+ ),
715
+ coverage: rollupCoverage(
716
+ group.children,
717
+ expectedChildCount(periodType, group.bounds),
718
+ ),
719
+ tokens,
720
+ retainedCostUsd: group.children.reduce(
721
+ (total, child) => addReportUsd(total, child.retainedCostUsd),
722
+ 0,
723
+ ),
724
+ apiEquivalent: rollupPrice(group.children),
725
+ });
726
+ existingIds.add(id);
727
+ }
728
+ return rows;
729
+ }
730
+
731
+ interface RawCostGroup {
732
+ readonly bounds: PeriodBounds;
733
+ readonly projectKey: string;
734
+ readonly canonicalAccountId: string;
735
+ readonly requestedModel: string;
736
+ tokens: CostObservation["tokens"];
737
+ retainedCostUsd: number;
738
+ observationCount: number;
739
+ coverage: CostCoverage;
740
+ /** Per-response token tuples in arrival order, priced individually before grouping. */
741
+ readonly observationTokens: CostObservation["tokens"][];
742
+ }
743
+
744
+ function addRawObservation(
745
+ groups: Map<string, RawCostGroup>,
746
+ observation: CostObservation,
747
+ nowMs: number,
748
+ ): void {
749
+ if (observation.projectKey === undefined) return;
750
+ const bounds = getPeriodBounds(observation.observedAtMs, "day");
751
+ if (bounds.endMs > nowMs) return;
752
+ const key = identityKey(
753
+ observation.projectKey,
754
+ observation.canonicalAccountId,
755
+ observation.requestedModel,
756
+ "day",
757
+ bounds.startMs,
758
+ );
759
+ const current = groups.get(key);
760
+ if (current === undefined) {
761
+ groups.set(key, {
762
+ bounds,
763
+ projectKey: observation.projectKey,
764
+ canonicalAccountId: observation.canonicalAccountId,
765
+ requestedModel: observation.requestedModel,
766
+ tokens: observation.tokens,
767
+ retainedCostUsd: observation.retainedCostUsd,
768
+ observationCount: 1,
769
+ coverage: observation.status,
770
+ observationTokens: [observation.tokens],
771
+ });
772
+ return;
773
+ }
774
+ current.tokens = addReportTokens(current.tokens, observation.tokens);
775
+ current.retainedCostUsd = addReportUsd(
776
+ current.retainedCostUsd,
777
+ observation.retainedCostUsd,
778
+ );
779
+ current.observationCount = addReportCount(current.observationCount, 1);
780
+ current.observationTokens.push(observation.tokens);
781
+ if (current.coverage !== "partial") {
782
+ current.coverage =
783
+ observation.status === "partial"
784
+ ? "partial"
785
+ : current.coverage === "unknown" || observation.status === "unknown"
786
+ ? "unknown"
787
+ : "complete";
788
+ }
789
+ }
790
+
791
+ interface PlanCostDigestGroupsInput {
792
+ readonly groups: ReadonlyMap<string, RawCostGroup>;
793
+ readonly gaps: readonly CostHistoryGap[];
794
+ readonly existingRows: readonly CostDigestRow[];
795
+ readonly pricing: PricingCacheResult;
796
+ readonly piCatalog?: PiCatalogSnapshot;
797
+ readonly nowMs: number;
798
+ readonly closedAtMs: number;
799
+ }
800
+
801
+ function planClosedCostDigestGroups(
802
+ input: PlanCostDigestGroupsInput,
803
+ ): readonly CostDigestRow[] {
804
+ const existingIds = new Set(input.existingRows.map((row) => row.id));
805
+ const newRows: CostDigestRow[] = [];
806
+ for (const group of input.groups.values()) {
807
+ const id = costDigestId(
808
+ group.projectKey,
809
+ group.canonicalAccountId,
810
+ group.requestedModel,
811
+ "day",
812
+ group.bounds.startMs,
813
+ );
814
+ if (existingIds.has(id)) continue;
815
+ const gapOverlaps = input.gaps.some(
816
+ (gap) =>
817
+ gap.startMs < group.bounds.endMs && gap.endMs > group.bounds.startMs,
818
+ );
819
+ newRows.push({
820
+ schemaVersion: 1,
821
+ kind: "cost-period-digest",
822
+ id,
823
+ projectKey: group.projectKey,
824
+ canonicalAccountId: group.canonicalAccountId,
825
+ requestedModel: group.requestedModel,
826
+ periodType: "day",
827
+ periodStartMs: group.bounds.startMs,
828
+ periodEndMs: group.bounds.endMs,
829
+ closedAtMs: input.closedAtMs,
830
+ source: "raw",
831
+ childRowCount: 0,
832
+ observationCount: group.observationCount,
833
+ coverage: gapOverlaps ? "partial" : group.coverage,
834
+ tokens: group.tokens,
835
+ retainedCostUsd: group.retainedCostUsd,
836
+ apiEquivalent:
837
+ input.piCatalog === undefined
838
+ ? priceRaw(
839
+ group.tokens,
840
+ group.canonicalAccountId,
841
+ group.requestedModel,
842
+ input.pricing,
843
+ )
844
+ : priceRawPerResponse(group, input.piCatalog, input.pricing),
845
+ });
846
+ existingIds.add(id);
847
+ }
848
+
849
+ const allRows = [...input.existingRows, ...newRows];
850
+ const days = allRows.filter((row) => row.periodType === "day");
851
+ const weeks = parentRows(
852
+ days,
853
+ "week",
854
+ "day-rollup",
855
+ input.nowMs,
856
+ input.closedAtMs,
857
+ existingIds,
858
+ );
859
+ const months = parentRows(
860
+ days,
861
+ "month",
862
+ "day-rollup",
863
+ input.nowMs,
864
+ input.closedAtMs,
865
+ existingIds,
866
+ );
867
+ newRows.push(...weeks, ...months);
868
+ const allMonths = [...input.existingRows, ...newRows].filter(
869
+ (row) => row.periodType === "month",
870
+ );
871
+ for (const periodType of ["quarter", "half-year", "year"] as const) {
872
+ newRows.push(
873
+ ...parentRows(
874
+ allMonths,
875
+ periodType,
876
+ "month-rollup",
877
+ input.nowMs,
878
+ input.closedAtMs,
879
+ existingIds,
880
+ ),
881
+ );
882
+ }
883
+ const order: Record<PeriodType, number> = {
884
+ day: 0,
885
+ week: 1,
886
+ month: 2,
887
+ quarter: 3,
888
+ "half-year": 4,
889
+ year: 5,
890
+ };
891
+ return newRows.sort(
892
+ (left, right) =>
893
+ left.periodEndMs - right.periodEndMs ||
894
+ order[left.periodType] - order[right.periodType] ||
895
+ left.id.localeCompare(right.id),
896
+ );
897
+ }
898
+
899
+ function assertDigestClocks(nowMs: number, closedAtMs: number): void {
900
+ if (!validTimestamp(nowMs) || !validTimestamp(closedAtMs)) {
901
+ throw new RangeError("Cost digest clocks must be finite timestamps.");
902
+ }
903
+ }
904
+
905
+ export function planClosedCostDigests(input: {
906
+ readonly observations: readonly CostObservation[];
907
+ readonly gaps: readonly CostHistoryGap[];
908
+ readonly existingRows: readonly CostDigestRow[];
909
+ readonly pricing: PricingCacheResult;
910
+ readonly piCatalog?: PiCatalogSnapshot;
911
+ readonly nowMs: number;
912
+ readonly closedAtMs: number;
913
+ }): readonly CostDigestRow[] {
914
+ assertDigestClocks(input.nowMs, input.closedAtMs);
915
+ const groups = new Map<string, RawCostGroup>();
916
+ for (const observation of input.observations) {
917
+ addRawObservation(groups, observation, input.nowMs);
918
+ }
919
+ return planClosedCostDigestGroups({ ...input, groups });
920
+ }
921
+
922
+ export function planClosedCostDigestsFromHistory(input: {
923
+ readonly readRecords: () => Iterable<HistoryLogRecord>;
924
+ readonly existingRows: readonly CostDigestRow[];
925
+ readonly pricing: PricingCacheResult;
926
+ readonly piCatalog?: PiCatalogSnapshot;
927
+ readonly nowMs: number;
928
+ readonly closedAtMs: number;
929
+ }): readonly CostDigestRow[] {
930
+ assertDigestClocks(input.nowMs, input.closedAtMs);
931
+ const groups = new Map<string, RawCostGroup>();
932
+ for (const record of input.readRecords()) {
933
+ const observation = parseObservation(record);
934
+ if (observation !== undefined) {
935
+ addRawObservation(groups, observation, input.nowMs);
936
+ }
937
+ }
938
+
939
+ if (groups.size > 0) {
940
+ const groupsByDay = new Map<number, RawCostGroup[]>();
941
+ for (const group of groups.values()) {
942
+ const dayGroups = groupsByDay.get(group.bounds.startMs) ?? [];
943
+ dayGroups.push(group);
944
+ groupsByDay.set(group.bounds.startMs, dayGroups);
945
+ }
946
+ const dayStarts = [...groupsByDay.keys()].sort((left, right) => left - right);
947
+ for (const record of input.readRecords()) {
948
+ if (
949
+ record.recordType !== "gap" ||
950
+ !validTimestamp(record.gapStartMs) ||
951
+ !validTimestamp(record.gapEndMs) ||
952
+ record.gapEndMs <= record.gapStartMs
953
+ ) {
954
+ continue;
955
+ }
956
+ let low = 0;
957
+ let high = dayStarts.length;
958
+ while (low < high) {
959
+ const middle = Math.floor((low + high) / 2);
960
+ const start = dayStarts[middle];
961
+ if (start === undefined || start + DAY_MS > record.gapStartMs) {
962
+ high = middle;
963
+ } else {
964
+ low = middle + 1;
965
+ }
966
+ }
967
+ for (let index = low; index < dayStarts.length; index += 1) {
968
+ const start = dayStarts[index];
969
+ if (start === undefined || start >= record.gapEndMs) break;
970
+ for (const group of groupsByDay.get(start) ?? []) {
971
+ group.coverage = "partial";
972
+ }
973
+ }
974
+ }
975
+ }
976
+ return planClosedCostDigestGroups({ ...input, groups, gaps: [] });
977
+ }
978
+
979
+ const PERIOD_COARSENESS: Readonly<Record<PeriodType, number>> = {
980
+ year: 0,
981
+ "half-year": 1,
982
+ quarter: 2,
983
+ month: 3,
984
+ week: 4,
985
+ day: 5,
986
+ };
987
+
988
+ function digestRowIdentityKey(row: CostDigestRow): string {
989
+ return JSON.stringify([row.projectKey, row.canonicalAccountId, row.requestedModel]);
990
+ }
991
+
992
+ /**
993
+ * Selects one non-overlapping retained representation per (project, account,
994
+ * model) identity across `[rangeStartMs, rangeEndMs)`, so a report spanning
995
+ * several granularities never double-counts an observation that is present
996
+ * both in a raw/day row and in a coarser rollup that already absorbed it.
997
+ *
998
+ * Only rows fully contained in the requested range are candidates -- a row
999
+ * that only partially overlaps the boundary is excluded rather than sliced,
1000
+ * since neither this function nor its callers may fabricate a sub-period
1001
+ * total a coarser rollup never recorded. Candidates are considered coarsest
1002
+ * first per identity; a candidate is accepted only when its interval does not
1003
+ * overlap an already-accepted interval for that same identity, so finer rows
1004
+ * fill exactly the gaps a coarser rollup left uncovered.
1005
+ */
1006
+ export function selectNonOverlappingDigestRows(
1007
+ rows: readonly CostDigestRow[],
1008
+ rangeStartMs: number,
1009
+ rangeEndMs: number,
1010
+ ): readonly CostDigestRow[] {
1011
+ if (rangeEndMs <= rangeStartMs) return [];
1012
+ const byIdentity = new Map<string, CostDigestRow[]>();
1013
+ for (const row of rows) {
1014
+ if (row.periodStartMs < rangeStartMs || row.periodEndMs > rangeEndMs) continue;
1015
+ const key = digestRowIdentityKey(row);
1016
+ const list = byIdentity.get(key) ?? [];
1017
+ list.push(row);
1018
+ byIdentity.set(key, list);
1019
+ }
1020
+ const selected: CostDigestRow[] = [];
1021
+ for (const candidates of byIdentity.values()) {
1022
+ const sorted = [...candidates].sort(
1023
+ (left, right) =>
1024
+ PERIOD_COARSENESS[left.periodType] - PERIOD_COARSENESS[right.periodType] ||
1025
+ left.periodStartMs - right.periodStartMs,
1026
+ );
1027
+ const covered: Array<{ readonly startMs: number; readonly endMs: number }> = [];
1028
+ for (const row of sorted) {
1029
+ const overlapsCovered = covered.some(
1030
+ (interval) =>
1031
+ row.periodStartMs < interval.endMs && row.periodEndMs > interval.startMs,
1032
+ );
1033
+ if (overlapsCovered) continue;
1034
+ covered.push({ startMs: row.periodStartMs, endMs: row.periodEndMs });
1035
+ selected.push(row);
1036
+ }
1037
+ }
1038
+ return selected.sort(
1039
+ (left, right) =>
1040
+ left.periodStartMs - right.periodStartMs ||
1041
+ PERIOD_COARSENESS[left.periodType] - PERIOD_COARSENESS[right.periodType] ||
1042
+ left.id.localeCompare(right.id),
1043
+ );
1044
+ }