@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,1104 @@
1
+ import type { AccountRateRecord } from "./account-rate-history.js";
2
+ import type { PiCatalogSnapshot } from "./api-pricing.js";
3
+ import {
4
+ planClosedCostDigests,
5
+ selectNonOverlappingDigestRows,
6
+ type CostCoverage,
7
+ type CostDigestRow,
8
+ type CostHistoryGap,
9
+ type CostObservation,
10
+ } from "./cost-digest.js";
11
+ import {
12
+ enumerateCompletedPeriods,
13
+ getPeriodBounds,
14
+ splitAtUtcMonthBoundaries,
15
+ type PeriodBounds,
16
+ type PeriodType,
17
+ } from "./period-boundaries.js";
18
+ import type { PricingCacheResult } from "./pricing-cache.js";
19
+
20
+ export interface CostReportTokens {
21
+ readonly input: number;
22
+ readonly output: number;
23
+ readonly cacheRead: number;
24
+ readonly cacheWrite: number;
25
+ readonly cacheWrite1h: number;
26
+ }
27
+
28
+ export type CostReportApiEquivalent =
29
+ | {
30
+ readonly status: "priced";
31
+ readonly estimatedUsd: number;
32
+ readonly rateAsOfMs: readonly number[];
33
+ }
34
+ | {
35
+ readonly status: "unpriced";
36
+ readonly reasons: readonly string[];
37
+ readonly rateAsOfMs: readonly number[];
38
+ };
39
+
40
+ export interface CostReportModel {
41
+ readonly requestedModel: string;
42
+ readonly tokens: CostReportTokens;
43
+ readonly retainedCostUsd: number;
44
+ readonly apiEquivalent: CostReportApiEquivalent;
45
+ }
46
+
47
+ export interface CostReportAccount {
48
+ readonly canonicalAccountId: string;
49
+ readonly models: readonly CostReportModel[];
50
+ }
51
+
52
+ export interface CostReportProject {
53
+ readonly projectKey: string;
54
+ readonly label: string;
55
+ readonly accounts: readonly CostReportAccount[];
56
+ }
57
+
58
+ export interface CostReportPeriod {
59
+ readonly periodStartMs: number;
60
+ readonly periodEndMs: number;
61
+ readonly throughMs: number;
62
+ readonly completed: boolean;
63
+ readonly coverage: "complete" | "partial" | "unknown" | "gap";
64
+ readonly tokens: CostReportTokens;
65
+ readonly retainedCostUsd: number;
66
+ readonly apiEquivalent: CostReportApiEquivalent;
67
+ readonly projects: readonly CostReportProject[];
68
+ /**
69
+ * The known API-equivalent subtotal and unknown coverage for this period,
70
+ * exposed alongside (never instead of) {@link apiEquivalent}'s existing
71
+ * all-or-nothing verdict. Optional only so an out-of-date hand-built
72
+ * literal keeps compiling; `buildCostReport` always populates it.
73
+ */
74
+ readonly apiEquivalentCoverage?: ApiEquivalentCoverage;
75
+ }
76
+
77
+ export type SubsidizationPoint =
78
+ | {
79
+ readonly periodStartMs: number;
80
+ readonly periodEndMs: number;
81
+ readonly status: "gap";
82
+ readonly coverage: "gap";
83
+ }
84
+ | {
85
+ readonly periodStartMs: number;
86
+ readonly periodEndMs: number;
87
+ readonly status: "unpriced";
88
+ readonly coverage: "complete" | "partial" | "unknown";
89
+ }
90
+ | {
91
+ readonly periodStartMs: number;
92
+ readonly periodEndMs: number;
93
+ readonly status: "priced";
94
+ readonly coverage: "complete" | "partial" | "unknown";
95
+ readonly apiEquivalentEstimateUsd: number;
96
+ readonly apportionedSubscriptionUsd: number;
97
+ readonly ratio: number;
98
+ readonly deltaFromPrevious?: number;
99
+ };
100
+
101
+ export interface SubsidizationSeries {
102
+ readonly canonicalAccountId: string;
103
+ readonly monthlySubscriptionUsd: number;
104
+ readonly observedPeriods: number;
105
+ readonly completed: readonly SubsidizationPoint[];
106
+ readonly current?: SubsidizationPoint;
107
+ }
108
+
109
+ export interface LegacyUnattributedCost {
110
+ readonly canonicalAccountId: string;
111
+ readonly requestedModel: string;
112
+ readonly observationCount: number;
113
+ readonly tokens: CostReportTokens;
114
+ readonly retainedCostUsd: number;
115
+ }
116
+
117
+ /** The account rate effective at a given instant, distinguishing an explicit zero from no known rate at all. */
118
+ export type RecordedRate =
119
+ | { readonly status: "known"; readonly monthlyUsd: number }
120
+ | { readonly status: "unknown" };
121
+
122
+ /** The rate-history allocation over one window: see {@link allocateAccountRateCost}. */
123
+ export interface AccountRateAllocation {
124
+ readonly knownUsd: number;
125
+ readonly hasUnknownCoverage: boolean;
126
+ }
127
+
128
+ /**
129
+ * A value comparison that is never a bare, unlabeled ratio. `"suppressed"`
130
+ * omits any ratio or ratio-shaped multiple entirely (a zero denominator, an
131
+ * unknown account rate for part of the window, or legacy-ambiguous pricing).
132
+ * `"conditional-lower-bound"` still carries a ratio, but only when omitting
133
+ * unpriced responses can only ever UNDER-count -- so the true ratio is
134
+ * guaranteed to be at least this value -- and it is always labeled with its
135
+ * premise. `"definitive"` requires full, non-legacy-ambiguous coverage on
136
+ * both sides of the comparison.
137
+ */
138
+ export type ValueComparison =
139
+ | { readonly status: "suppressed"; readonly reason: string }
140
+ | {
141
+ readonly status: "conditional-lower-bound";
142
+ readonly ratio: number;
143
+ readonly premise: string;
144
+ }
145
+ | { readonly status: "definitive"; readonly ratio: number };
146
+
147
+ /**
148
+ * The account's separate values for one reported window. Never summed into
149
+ * one spend figure: the saved Pi estimate stays on `rows`/`apiEquivalent`
150
+ * untouched; `apiEquivalentCoverage.knownUsd` is the tier-aware API-equivalent
151
+ * known subtotal; `recordedRate` is the operator-selected monthly rate
152
+ * effective at the window's end; `allocatedAccountCostUsd` is the UTC-month
153
+ * rate-history allocation. `comparison` is the only place a ratio between
154
+ * them appears, and it is always labeled.
155
+ */
156
+ export interface AccountValueSummary {
157
+ readonly canonicalAccountId: string;
158
+ readonly recordedRate: RecordedRate;
159
+ readonly allocatedAccountCostUsd: number;
160
+ readonly allocationHasUnknownCoverage: boolean;
161
+ readonly apiEquivalentCoverage: ApiEquivalentCoverage;
162
+ readonly comparison: ValueComparison;
163
+ }
164
+
165
+ export interface CostReport {
166
+ readonly generatedAtMs: number;
167
+ readonly periodType: PeriodType;
168
+ readonly current: CostReportPeriod;
169
+ readonly completed: readonly CostReportPeriod[];
170
+ readonly subsidization: readonly SubsidizationSeries[];
171
+ readonly legacyUnattributed: readonly LegacyUnattributedCost[];
172
+ /**
173
+ * Per-account separate values for the current reported window, driven by
174
+ * `accountRateHistory` rather than the legacy mutable `monthlySubscriptionUsd`
175
+ * apportionment. Optional only so an out-of-date hand-built literal keeps
176
+ * compiling; `buildCostReport` always populates it. Scoped to the account,
177
+ * never to a project: a project-filtered view may disclose an entry here as
178
+ * separate, unallocated context, but it is never a project charge.
179
+ */
180
+ readonly accountValue?: readonly AccountValueSummary[];
181
+ }
182
+
183
+ /**
184
+ * Overrides the calendar-period-derived window `buildCostReport` otherwise
185
+ * computes from `periodType`: `"custom"` reports exactly `[startMs, endMs)`;
186
+ * `"all-history"` reports every retained instant, from the earliest known
187
+ * digest row or attributed raw observation through `nowMs`. See
188
+ * {@link BuildCostReportInput.range}.
189
+ */
190
+ export type ResolvedReportRange =
191
+ | { readonly kind: "custom"; readonly startMs: number; readonly endMs: number }
192
+ | { readonly kind: "all-history" };
193
+
194
+ interface BuildCostReportInput {
195
+ readonly digestRows: readonly CostDigestRow[];
196
+ readonly observations: readonly CostObservation[];
197
+ readonly gaps: readonly CostHistoryGap[];
198
+ readonly earliestRawObservationAtMs?: number;
199
+ readonly rawAccountIds?: readonly string[];
200
+ readonly groupedLegacyUnattributed?: readonly LegacyUnattributedCost[];
201
+ readonly currentDayDigestRows?: readonly CostDigestRow[];
202
+ readonly pricing: PricingCacheResult;
203
+ readonly periodType: PeriodType;
204
+ readonly nowMs: number;
205
+ readonly projectLabels: Readonly<Record<string, string>>;
206
+ readonly monthlySubscriptionUsd: Readonly<Record<string, number>>;
207
+ /**
208
+ * Effective-dated USD rate records per canonical account, already validated
209
+ * and ordered (see `normalizeAccountRateHistory`). Drives `accountValue` --
210
+ * the rate-history-based allocation that replaces the legacy mutable
211
+ * `monthlySubscriptionUsd` apportionment for that purpose. `monthlySubscriptionUsd`
212
+ * itself stays readable and still drives the existing `subsidization` field
213
+ * unchanged: this input is additive, not a migration.
214
+ */
215
+ readonly accountRateHistory?: Readonly<Record<string, readonly AccountRateRecord[]>>;
216
+ /** IANA report timezone for period boundaries; defaults to "UTC". Account-cost allocation always splits at UTC month boundaries regardless. */
217
+ readonly timeZone?: string;
218
+ readonly completedLimit?: number;
219
+ /**
220
+ * Pi's installed model catalog cost data, used to price still-open,
221
+ * caller-supplied `observations` for the current period under their own
222
+ * per-response tier before grouping. Only relevant when `currentDayDigestRows`
223
+ * is omitted, so this pure function itself must build the provisional
224
+ * current-day rows; when a caller already supplies pre-priced
225
+ * `currentDayDigestRows`, their own disclosed methods are used unchanged.
226
+ */
227
+ readonly piCatalog?: PiCatalogSnapshot;
228
+ /**
229
+ * Overrides `periodType`-derived calendar bounds for the single reported
230
+ * window: `{kind:"custom"}` reports exactly `[startMs,endMs)`; `{kind:
231
+ * "all-history"}` reports from the earliest known digest row or
232
+ * attributed raw observation through `nowMs`. `completed` stays empty in
233
+ * both modes -- a custom or all-history window has no calendar cadence to
234
+ * roll into separate prior periods -- and its row set is selected through
235
+ * `selectNonOverlappingDigestRows` so a window spanning several retained
236
+ * granularities is never double-counted. `periodType` remains required
237
+ * and is NOT read for bounds when `range` is present; a caller in this
238
+ * mode still supplies a nominal `periodType` so `CostReport.periodType`
239
+ * keeps its existing shape until a later node adds a dedicated range
240
+ * label. Omitted, this field preserves the exact prior periodType-only
241
+ * behavior.
242
+ */
243
+ readonly range?: ResolvedReportRange;
244
+ }
245
+
246
+ function emptyTokens(): CostReportTokens {
247
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cacheWrite1h: 0 };
248
+ }
249
+
250
+ function addCount(left: number, right: number): number {
251
+ const total = left + right;
252
+ if (!Number.isSafeInteger(total) || total < 0) {
253
+ throw new RangeError("Cost report token total exceeds the safe integer range.");
254
+ }
255
+ return total;
256
+ }
257
+
258
+ function addTokens(left: CostReportTokens, right: CostReportTokens): CostReportTokens {
259
+ return {
260
+ input: addCount(left.input, right.input),
261
+ output: addCount(left.output, right.output),
262
+ cacheRead: addCount(left.cacheRead, right.cacheRead),
263
+ cacheWrite: addCount(left.cacheWrite, right.cacheWrite),
264
+ cacheWrite1h: addCount(left.cacheWrite1h, right.cacheWrite1h),
265
+ };
266
+ }
267
+
268
+ function addUsd(left: number, right: number): number {
269
+ const total = left + right;
270
+ if (!Number.isFinite(total) || total < 0) {
271
+ throw new RangeError("Cost report USD total must remain finite and non-negative.");
272
+ }
273
+ return total;
274
+ }
275
+
276
+ function sortedUniqueNumbers(values: readonly number[]): readonly number[] {
277
+ return [...new Set(values)].sort((left, right) => left - right);
278
+ }
279
+
280
+ function sortedUniqueStrings(values: readonly string[]): readonly string[] {
281
+ return [...new Set(values)].sort();
282
+ }
283
+
284
+ function aggregateApi(rows: readonly CostDigestRow[]): CostReportApiEquivalent {
285
+ const rateAsOfMs = sortedUniqueNumbers(
286
+ rows.flatMap((row) => row.apiEquivalent.rateAsOfMs),
287
+ );
288
+ const unpriced = rows.filter((row) => row.apiEquivalent.status === "unpriced");
289
+ if (unpriced.length > 0) {
290
+ return {
291
+ status: "unpriced",
292
+ reasons: sortedUniqueStrings(
293
+ unpriced.map((row) =>
294
+ row.apiEquivalent.status === "unpriced"
295
+ ? row.apiEquivalent.reason
296
+ : "unpriced",
297
+ ),
298
+ ),
299
+ rateAsOfMs,
300
+ };
301
+ }
302
+ return {
303
+ status: "priced",
304
+ estimatedUsd: rows.reduce(
305
+ (total, row) =>
306
+ row.apiEquivalent.status === "priced"
307
+ ? addUsd(total, row.apiEquivalent.estimatedUsd)
308
+ : total,
309
+ 0,
310
+ ),
311
+ rateAsOfMs,
312
+ };
313
+ }
314
+
315
+ /**
316
+ * The known API-equivalent subtotal and unknown coverage for a set of rows,
317
+ * WITHOUT collapsing to an all-or-nothing "unpriced" verdict the moment one
318
+ * row lacks a rate. `legacyAmbiguousCount` counts rows priced only through
319
+ * the coarse pre-tier-pricing method (a day's tokens summed under one rate
320
+ * lookup, potentially mixing tier thresholds); those rows still contribute to
321
+ * `knownUsd`, but a comparison built from them can never be called definitive.
322
+ */
323
+ export interface ApiEquivalentCoverage {
324
+ readonly knownUsd: number;
325
+ readonly knownCount: number;
326
+ readonly unknownCount: number;
327
+ readonly unknownReasons: readonly string[];
328
+ readonly legacyAmbiguousCount: number;
329
+ }
330
+
331
+ function isLegacyAmbiguous(row: CostDigestRow): boolean {
332
+ const methods = row.apiEquivalent.methods;
333
+ return (
334
+ methods === undefined ||
335
+ methods.length === 0 ||
336
+ methods.some((method) => method.source === "legacy-day-aggregate")
337
+ );
338
+ }
339
+
340
+ function aggregateKnownCoverage(rows: readonly CostDigestRow[]): ApiEquivalentCoverage {
341
+ let knownUsd = 0;
342
+ let knownCount = 0;
343
+ let unknownCount = 0;
344
+ let legacyAmbiguousCount = 0;
345
+ const unknownReasons = new Set<string>();
346
+ for (const row of rows) {
347
+ if (row.apiEquivalent.status === "priced") {
348
+ knownUsd = addUsd(knownUsd, row.apiEquivalent.estimatedUsd);
349
+ knownCount += 1;
350
+ if (isLegacyAmbiguous(row)) legacyAmbiguousCount += 1;
351
+ } else {
352
+ unknownCount += 1;
353
+ unknownReasons.add(row.apiEquivalent.reason);
354
+ }
355
+ }
356
+ return {
357
+ knownUsd,
358
+ knownCount,
359
+ unknownCount,
360
+ unknownReasons: sortedUniqueStrings([...unknownReasons]),
361
+ legacyAmbiguousCount,
362
+ };
363
+ }
364
+
365
+ function aggregateCoverage(rows: readonly CostDigestRow[]): CostCoverage {
366
+ if (rows.some((row) => row.coverage === "partial")) return "partial";
367
+ return rows.some((row) => row.coverage === "unknown")
368
+ ? "unknown"
369
+ : "complete";
370
+ }
371
+
372
+ /**
373
+ * Downgrades a `"complete"` {@link aggregateCoverage} verdict to `"partial"`
374
+ * when the supplied rows do not, between them, span every instant of
375
+ * `[startMs, endMs)`. A zoned {@link completedPeriods} window selects its
376
+ * rows through {@link selectNonOverlappingDigestRows}, which can return a
377
+ * proper subset of the window -- a coarser rollup that straddles the zoned
378
+ * boundary is correctly excluded rather than sliced, but the remaining
379
+ * selected rows may still leave real, unrepresented time inside the window.
380
+ * `aggregateCoverage` alone only reflects each selected row's own internal
381
+ * completeness, so a single fully-complete row covering a small sliver of a
382
+ * much larger gap would otherwise read as a falsely `"complete"` period.
383
+ * An exact-periodType-match result (the existing UTC/no-timezone path) is
384
+ * unaffected: its row(s) already span exactly `[startMs, endMs)` by
385
+ * construction, so this check is always a no-op there.
386
+ */
387
+ function windowCoverage(
388
+ rows: readonly CostDigestRow[],
389
+ startMs: number,
390
+ endMs: number,
391
+ ): CostCoverage {
392
+ const coverage = aggregateCoverage(rows);
393
+ if (coverage !== "complete") return coverage;
394
+ const sorted = [...rows]
395
+ .map((row) => ({ startMs: row.periodStartMs, endMs: row.periodEndMs }))
396
+ .sort((left, right) => left.startMs - right.startMs);
397
+ let cursor = startMs;
398
+ for (const interval of sorted) {
399
+ if (interval.startMs > cursor) return "partial";
400
+ if (interval.endMs > cursor) cursor = interval.endMs;
401
+ }
402
+ return cursor >= endMs ? "complete" : "partial";
403
+ }
404
+
405
+ function aggregateRows(rows: readonly CostDigestRow[]): {
406
+ readonly tokens: CostReportTokens;
407
+ readonly retainedCostUsd: number;
408
+ readonly apiEquivalent: CostReportApiEquivalent;
409
+ } {
410
+ return {
411
+ tokens: rows.reduce(
412
+ (total, row) => addTokens(total, row.tokens),
413
+ emptyTokens(),
414
+ ),
415
+ retainedCostUsd: rows.reduce(
416
+ (total, row) => addUsd(total, row.retainedCostUsd),
417
+ 0,
418
+ ),
419
+ apiEquivalent: aggregateApi(rows),
420
+ };
421
+ }
422
+
423
+ function projectBreakdown(
424
+ rows: readonly CostDigestRow[],
425
+ projectLabels: Readonly<Record<string, string>>,
426
+ ): readonly CostReportProject[] {
427
+ const projects = new Map<string, Map<string, Map<string, CostDigestRow[]>>>();
428
+ for (const row of rows) {
429
+ const accounts = projects.get(row.projectKey) ?? new Map();
430
+ const models = accounts.get(row.canonicalAccountId) ?? new Map();
431
+ const modelRows = models.get(row.requestedModel) ?? [];
432
+ modelRows.push(row);
433
+ models.set(row.requestedModel, modelRows);
434
+ accounts.set(row.canonicalAccountId, models);
435
+ projects.set(row.projectKey, accounts);
436
+ }
437
+ return [...projects.entries()]
438
+ .sort(([left], [right]) => left.localeCompare(right))
439
+ .map(([projectKey, accounts]) => ({
440
+ projectKey,
441
+ label: projectLabels[projectKey] ?? projectKey,
442
+ accounts: [...accounts.entries()]
443
+ .sort(([left], [right]) => left.localeCompare(right))
444
+ .map(([canonicalAccountId, models]) => ({
445
+ canonicalAccountId,
446
+ models: [...models.entries()]
447
+ .sort(([left], [right]) => left.localeCompare(right))
448
+ .map(([requestedModel, modelRows]) => ({
449
+ requestedModel,
450
+ ...aggregateRows(modelRows),
451
+ })),
452
+ })),
453
+ }));
454
+ }
455
+
456
+ function periodReport(
457
+ bounds: PeriodBounds,
458
+ throughMs: number,
459
+ completed: boolean,
460
+ rows: readonly CostDigestRow[],
461
+ projectLabels: Readonly<Record<string, string>>,
462
+ /**
463
+ * Overrides the default `completed ? aggregateCoverage(rows) : "partial"`
464
+ * verdict. Only {@link completedPeriods}'s zoned selection path supplies
465
+ * this, via {@link windowCoverage}, to downgrade a falsely `"complete"`
466
+ * result when its selected rows do not fully span the window. Every other
467
+ * call site omits it and keeps the exact prior coverage computation.
468
+ */
469
+ coverageOverride?: CostCoverage,
470
+ ): CostReportPeriod {
471
+ const aggregate =
472
+ rows.length === 0
473
+ ? {
474
+ tokens: emptyTokens(),
475
+ retainedCostUsd: 0,
476
+ apiEquivalent: {
477
+ status: "unpriced" as const,
478
+ reasons: ["period-gap"],
479
+ rateAsOfMs: [],
480
+ },
481
+ }
482
+ : aggregateRows(rows);
483
+ return {
484
+ periodStartMs: bounds.startMs,
485
+ periodEndMs: bounds.endMs,
486
+ throughMs,
487
+ completed,
488
+ coverage:
489
+ rows.length === 0
490
+ ? "gap"
491
+ : (coverageOverride ?? (completed ? aggregateCoverage(rows) : "partial")),
492
+ ...aggregate,
493
+ apiEquivalentCoverage: aggregateKnownCoverage(rows),
494
+ projects: projectBreakdown(rows, projectLabels),
495
+ };
496
+ }
497
+
498
+ /**
499
+ * The still-open current day's own rows, optionally clipped to a
500
+ * `[clip.startMs, clip.endMs)` narrower than the full `[day.startMs,
501
+ * input.nowMs)` window. Undefined `clip` (the existing calendar-period call
502
+ * site in {@link currentRows}) preserves the exact prior behavior, including
503
+ * the `currentDayDigestRows` fast-path override: that pre-aggregated blob
504
+ * has no per-response timestamps left to slice by, so it is only reused when
505
+ * the resolved window is not narrower than the full day-through-now it was
506
+ * built for. A range clipped narrower than that -- see
507
+ * {@link rangeRows} -- is rebuilt from {@link BuildCostReportInput.observations}
508
+ * instead, which for the real reader is today's real per-response raw detail
509
+ * (see `createDefaultCostReportReader` in `cost-report-reader.ts`), never a
510
+ * fabricated finer grain.
511
+ */
512
+ function provisionalCurrentDayRows(
513
+ input: BuildCostReportInput,
514
+ clip?: { readonly startMs: number; readonly endMs: number },
515
+ ): readonly CostDigestRow[] {
516
+ const day = getPeriodBounds(input.nowMs, "day");
517
+ const startMs = clip === undefined ? day.startMs : Math.max(day.startMs, clip.startMs);
518
+ const endMs = clip === undefined ? input.nowMs : Math.min(input.nowMs, clip.endMs);
519
+ const isFullWindow = startMs <= day.startMs && endMs >= input.nowMs;
520
+ if (isFullWindow && input.currentDayDigestRows !== undefined) {
521
+ return input.currentDayDigestRows;
522
+ }
523
+ if (endMs <= startMs) return [];
524
+ const observations = input.observations.filter(
525
+ (observation) =>
526
+ observation.projectKey !== undefined &&
527
+ observation.observedAtMs >= startMs &&
528
+ observation.observedAtMs < endMs,
529
+ );
530
+ if (observations.length === 0) return [];
531
+ return planClosedCostDigests({
532
+ observations,
533
+ gaps: input.gaps,
534
+ existingRows: input.digestRows,
535
+ pricing: input.pricing,
536
+ ...(input.piCatalog === undefined ? {} : { piCatalog: input.piCatalog }),
537
+ nowMs: day.endMs,
538
+ closedAtMs: input.nowMs,
539
+ }).filter(
540
+ (row) => row.periodType === "day" && row.periodStartMs === day.startMs,
541
+ );
542
+ }
543
+
544
+ function currentRows(input: BuildCostReportInput): readonly CostDigestRow[] {
545
+ const current = getPeriodBounds(input.nowMs, input.periodType, input.timeZone ?? "UTC");
546
+ const today = getPeriodBounds(input.nowMs, "day");
547
+ const closedDays = input.digestRows.filter(
548
+ (row) =>
549
+ row.periodType === "day" &&
550
+ row.periodStartMs >= current.startMs &&
551
+ row.periodEndMs <= current.endMs &&
552
+ row.periodEndMs <= today.startMs,
553
+ );
554
+ return [...closedDays, ...provisionalCurrentDayRows(input)];
555
+ }
556
+
557
+ function firstReportableTimestamp(input: BuildCostReportInput): number | undefined {
558
+ const matching = input.digestRows
559
+ .filter((row) => row.periodType === input.periodType)
560
+ .map((row) => row.periodStartMs);
561
+ const attributedRaw = input.observations
562
+ .filter((observation) => observation.projectKey !== undefined)
563
+ .map((observation) => observation.observedAtMs);
564
+ const values = [
565
+ ...matching,
566
+ ...attributedRaw,
567
+ ...(input.earliestRawObservationAtMs === undefined
568
+ ? []
569
+ : [input.earliestRawObservationAtMs]),
570
+ ];
571
+ return values.length === 0 ? undefined : Math.min(...values);
572
+ }
573
+
574
+ /**
575
+ * The earliest instant any retained digest row or attributed raw observation
576
+ * exists for, across EVERY periodType -- unlike {@link firstReportableTimestamp},
577
+ * which only considers rows already rolled up at the exact requested
578
+ * `periodType`. Drives the start of an `"all-history"` {@link ResolvedReportRange}.
579
+ */
580
+ function earliestRetainedTimestamp(input: BuildCostReportInput): number | undefined {
581
+ const rowStarts = input.digestRows.map((row) => row.periodStartMs);
582
+ const attributedRaw = input.observations
583
+ .filter((observation) => observation.projectKey !== undefined)
584
+ .map((observation) => observation.observedAtMs);
585
+ const values = [
586
+ ...rowStarts,
587
+ ...attributedRaw,
588
+ ...(input.earliestRawObservationAtMs === undefined
589
+ ? []
590
+ : [input.earliestRawObservationAtMs]),
591
+ ];
592
+ return values.length === 0 ? undefined : Math.min(...values);
593
+ }
594
+
595
+ function completedPeriods(input: BuildCostReportInput): {
596
+ readonly reports: readonly CostReportPeriod[];
597
+ readonly rowsByStart: ReadonlyMap<number, readonly CostDigestRow[]>;
598
+ } {
599
+ const first = firstReportableTimestamp(input);
600
+ if (first === undefined) return { reports: [], rowsByStart: new Map() };
601
+ const timeZone = input.timeZone ?? "UTC";
602
+ /**
603
+ * A non-UTC request's zoned period boundaries almost never land exactly
604
+ * on the closer's UTC-aligned rollup bounds (day/week/month/quarter/...),
605
+ * so the exact-match lookup below would normally find nothing. `zoned`
606
+ * switches to {@link selectNonOverlappingDigestRows} instead, which picks
607
+ * the best contained, non-overlapping retained representation for the
608
+ * window -- see its own JSDoc for why a coarser row straddling the zoned
609
+ * boundary is excluded rather than sliced or prorated. `timeZone ===
610
+ * "UTC"` (including the omitted-`timeZone` default) keeps the exact prior
611
+ * behavior byte-for-byte.
612
+ */
613
+ const zoned = timeZone !== "UTC";
614
+ const allBounds = enumerateCompletedPeriods(
615
+ input.periodType,
616
+ first,
617
+ input.nowMs,
618
+ { timeZone },
619
+ );
620
+ const limit = input.completedLimit ?? 12;
621
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1_000) {
622
+ throw new RangeError("completedLimit must be a safe integer from 1 to 1000.");
623
+ }
624
+ const bounds = allBounds.slice(-limit);
625
+ const rowsByStart = new Map<number, readonly CostDigestRow[]>();
626
+ const reports = bounds.map((period) => {
627
+ const rows = zoned
628
+ ? selectNonOverlappingDigestRows(input.digestRows, period.startMs, period.endMs)
629
+ : input.digestRows.filter(
630
+ (row) =>
631
+ row.periodType === input.periodType &&
632
+ row.periodStartMs === period.startMs &&
633
+ row.periodEndMs === period.endMs,
634
+ );
635
+ rowsByStart.set(period.startMs, rows);
636
+ return periodReport(
637
+ period,
638
+ period.endMs,
639
+ true,
640
+ rows,
641
+ input.projectLabels,
642
+ zoned ? windowCoverage(rows, period.startMs, period.endMs) : undefined,
643
+ );
644
+ });
645
+ return { reports, rowsByStart };
646
+ }
647
+
648
+ function apportionedSubscriptionUsd(
649
+ monthlyUsd: number,
650
+ startMs: number,
651
+ throughMs: number,
652
+ ): number {
653
+ if (!Number.isFinite(monthlyUsd) || monthlyUsd <= 0) {
654
+ throw new RangeError("Monthly subscription cost must be finite and positive.");
655
+ }
656
+ let cursor = startMs;
657
+ let total = 0;
658
+ let iterations = 0;
659
+ while (cursor < throughMs) {
660
+ iterations += 1;
661
+ if (iterations > 24) {
662
+ throw new RangeError("Subscription apportionment exceeded 24 calendar months.");
663
+ }
664
+ const month = getPeriodBounds(cursor, "month");
665
+ const segmentEnd = Math.min(throughMs, month.endMs);
666
+ const fraction = (segmentEnd - cursor) / (month.endMs - month.startMs);
667
+ total = addUsd(total, monthlyUsd * fraction);
668
+ cursor = segmentEnd;
669
+ }
670
+ return total;
671
+ }
672
+
673
+ function subsidyPoint(
674
+ rows: readonly CostDigestRow[],
675
+ bounds: PeriodBounds,
676
+ throughMs: number,
677
+ monthlyUsd: number,
678
+ forcePartialCoverage = false,
679
+ ): SubsidizationPoint {
680
+ const accountRows = rows;
681
+ if (accountRows.length === 0) {
682
+ return {
683
+ periodStartMs: bounds.startMs,
684
+ periodEndMs: bounds.endMs,
685
+ status: "gap",
686
+ coverage: "gap",
687
+ };
688
+ }
689
+ const coverage = forcePartialCoverage
690
+ ? "partial"
691
+ : aggregateCoverage(accountRows);
692
+ const apiEquivalent = aggregateApi(accountRows);
693
+ if (apiEquivalent.status === "unpriced") {
694
+ return {
695
+ periodStartMs: bounds.startMs,
696
+ periodEndMs: bounds.endMs,
697
+ status: "unpriced",
698
+ coverage,
699
+ };
700
+ }
701
+ const subscription = apportionedSubscriptionUsd(
702
+ monthlyUsd,
703
+ bounds.startMs,
704
+ throughMs,
705
+ );
706
+ if (subscription <= 0) {
707
+ return {
708
+ periodStartMs: bounds.startMs,
709
+ periodEndMs: bounds.endMs,
710
+ status: "unpriced",
711
+ coverage,
712
+ };
713
+ }
714
+ return {
715
+ periodStartMs: bounds.startMs,
716
+ periodEndMs: bounds.endMs,
717
+ status: "priced",
718
+ coverage,
719
+ apiEquivalentEstimateUsd: apiEquivalent.estimatedUsd,
720
+ apportionedSubscriptionUsd: subscription,
721
+ ratio: apiEquivalent.estimatedUsd / subscription,
722
+ };
723
+ }
724
+
725
+ function withConsecutiveDeltas(
726
+ points: readonly SubsidizationPoint[],
727
+ ): readonly SubsidizationPoint[] {
728
+ return points.map((point, index) => {
729
+ const previous = points[index - 1];
730
+ if (point.status !== "priced" || previous?.status !== "priced") {
731
+ return point;
732
+ }
733
+ return { ...point, deltaFromPrevious: point.ratio - previous.ratio };
734
+ });
735
+ }
736
+
737
+ function buildSubsidization(
738
+ input: BuildCostReportInput,
739
+ completed: ReturnType<typeof completedPeriods>,
740
+ currentBounds: PeriodBounds,
741
+ currentPeriodRows: readonly CostDigestRow[],
742
+ /**
743
+ * Overrides for a resolved custom/all-history window, where the reported
744
+ * window's own true end (when already fully elapsed) replaces `nowMs`,
745
+ * and coverage reflects the rows themselves rather than being forced
746
+ * `"partial"` -- mirroring how {@link completedPeriods} treats a fully
747
+ * elapsed calendar period. Defaults preserve the exact prior behavior for
748
+ * the still-open current calendar period.
749
+ */
750
+ options: {
751
+ readonly throughMs?: number;
752
+ readonly forcePartialCoverage?: boolean;
753
+ } = {},
754
+ ): readonly SubsidizationSeries[] {
755
+ const throughMs = options.throughMs ?? input.nowMs;
756
+ const forcePartialCoverage = options.forcePartialCoverage ?? true;
757
+ const presentAccounts = new Set([
758
+ ...input.digestRows.map((row) => row.canonicalAccountId),
759
+ ...input.observations.map((observation) => observation.canonicalAccountId),
760
+ ...(input.rawAccountIds ?? []),
761
+ ]);
762
+ return Object.entries(input.monthlySubscriptionUsd)
763
+ .filter(([accountId]) => presentAccounts.has(accountId))
764
+ .sort(([left], [right]) => left.localeCompare(right))
765
+ .map(([canonicalAccountId, monthlySubscriptionUsd]) => {
766
+ const completedPoints = completed.reports.map((report) => {
767
+ const rows = (
768
+ completed.rowsByStart.get(report.periodStartMs) ?? []
769
+ ).filter(
770
+ (row) => row.canonicalAccountId === canonicalAccountId,
771
+ );
772
+ return subsidyPoint(
773
+ rows,
774
+ {
775
+ startMs: report.periodStartMs,
776
+ endMs: report.periodEndMs,
777
+ },
778
+ report.periodEndMs,
779
+ monthlySubscriptionUsd,
780
+ );
781
+ });
782
+ const completedWithDeltas = withConsecutiveDeltas(completedPoints);
783
+ const currentAccountRows = currentPeriodRows.filter(
784
+ (row) => row.canonicalAccountId === canonicalAccountId,
785
+ );
786
+ return {
787
+ canonicalAccountId,
788
+ monthlySubscriptionUsd,
789
+ observedPeriods: completedWithDeltas.filter(
790
+ (point) => point.status === "priced",
791
+ ).length,
792
+ completed: completedWithDeltas,
793
+ current: subsidyPoint(
794
+ currentAccountRows,
795
+ currentBounds,
796
+ throughMs,
797
+ monthlySubscriptionUsd,
798
+ forcePartialCoverage,
799
+ ),
800
+ };
801
+ });
802
+ }
803
+
804
+ /**
805
+ * Sums each known-rate segment's `monthlyUsd * coveredMs / thatUtcMonthMs`
806
+ * across `[startMs, endMs)`, splitting at every effective-rate change AND
807
+ * every UTC calendar-month boundary (via {@link splitAtUtcMonthBoundaries}),
808
+ * independent of any report display timezone. A full UTC month at one known
809
+ * rate contributes exactly that rate; an explicit `monthlyUsd: 0` contributes
810
+ * 0 while remaining "known"; any covered instant before the first record's
811
+ * `effectiveFrom` is unknown and contributes nothing to `knownUsd`, but is
812
+ * reported via `hasUnknownCoverage` rather than silently coerced to 0.
813
+ */
814
+ export function allocateAccountRateCost(
815
+ records: readonly AccountRateRecord[],
816
+ startMs: number,
817
+ endMs: number,
818
+ ): AccountRateAllocation {
819
+ if (endMs <= startMs) return { knownUsd: 0, hasUnknownCoverage: false };
820
+ const sortedRecords = [...records].sort(
821
+ (left, right) => Date.parse(left.effectiveFrom) - Date.parse(right.effectiveFrom),
822
+ );
823
+ let knownUsd = 0;
824
+ let hasUnknownCoverage = false;
825
+ for (const monthSegment of splitAtUtcMonthBoundaries(startMs, endMs)) {
826
+ // `monthSegment` is clipped to `[startMs, endMs)`, so its own span is NOT
827
+ // the allocation denominator: a request that starts or ends mid-month
828
+ // (e.g. a non-UTC report timezone's local month boundary) must still
829
+ // divide by that UTC month's TRUE full length, never the clipped
830
+ // fraction being measured -- otherwise every clipped segment would
831
+ // wrongly normalize to a full month's rate.
832
+ const fullMonth = getPeriodBounds(monthSegment.startMs, "month");
833
+ const monthMs = fullMonth.endMs - fullMonth.startMs;
834
+ const changePoints = sortedRecords
835
+ .map((record) => Date.parse(record.effectiveFrom))
836
+ .filter((ms) => ms > monthSegment.startMs && ms < monthSegment.endMs);
837
+ const boundaries = [
838
+ monthSegment.startMs,
839
+ ...sortedUniqueNumbers(changePoints),
840
+ monthSegment.endMs,
841
+ ];
842
+ for (let index = 0; index < boundaries.length - 1; index += 1) {
843
+ const segStart = boundaries[index]!;
844
+ const segEnd = boundaries[index + 1]!;
845
+ if (segEnd <= segStart) continue;
846
+ const applicable = sortedRecords
847
+ .filter((record) => Date.parse(record.effectiveFrom) <= segStart)
848
+ .pop();
849
+ if (applicable === undefined) {
850
+ hasUnknownCoverage = true;
851
+ continue;
852
+ }
853
+ const coveredMs = segEnd - segStart;
854
+ knownUsd = addUsd(knownUsd, applicable.monthlyUsd * (coveredMs / monthMs));
855
+ }
856
+ }
857
+ return { knownUsd, hasUnknownCoverage };
858
+ }
859
+
860
+ function recordedRateAsOf(
861
+ records: readonly AccountRateRecord[],
862
+ asOfMs: number,
863
+ ): RecordedRate {
864
+ const applicable = [...records]
865
+ .filter((record) => Date.parse(record.effectiveFrom) <= asOfMs)
866
+ .sort((left, right) => Date.parse(left.effectiveFrom) - Date.parse(right.effectiveFrom))
867
+ .pop();
868
+ return applicable === undefined
869
+ ? { status: "unknown" }
870
+ : { status: "known", monthlyUsd: applicable.monthlyUsd };
871
+ }
872
+
873
+ function buildValueComparison(
874
+ apiCoverage: ApiEquivalentCoverage,
875
+ allocation: AccountRateAllocation,
876
+ ): ValueComparison {
877
+ if (allocation.hasUnknownCoverage) {
878
+ return {
879
+ status: "suppressed",
880
+ reason: "account-rate-unknown-for-part-of-window",
881
+ };
882
+ }
883
+ if (allocation.knownUsd <= 0) {
884
+ return { status: "suppressed", reason: "zero-denominator" };
885
+ }
886
+ if (apiCoverage.legacyAmbiguousCount > 0) {
887
+ return { status: "suppressed", reason: "legacy-ambiguous-pricing-method" };
888
+ }
889
+ const ratio = apiCoverage.knownUsd / allocation.knownUsd;
890
+ if (apiCoverage.unknownCount > 0) {
891
+ return {
892
+ status: "conditional-lower-bound",
893
+ ratio,
894
+ premise: `excludes ${apiCoverage.unknownCount} response(s) without a known API-equivalent price; the true ratio is at least this value`,
895
+ };
896
+ }
897
+ return { status: "definitive", ratio };
898
+ }
899
+
900
+ /**
901
+ * Builds the per-account value summary for one reported window: the saved Pi
902
+ * estimate stays in `rows`/the existing `apiEquivalent` aggregate untouched;
903
+ * this exposes the recorded rate, the rate-history allocation, the known/
904
+ * unknown API-equivalent split, and a comparison that is never a bare
905
+ * unlabeled ratio. Scoped to the account itself -- independent of how many
906
+ * (or which) projects reference it, so a project-filtered view can disclose
907
+ * this as separate, unallocated context without ever calling it a project
908
+ * charge, subtracting it, splitting it, or duplicating it per response.
909
+ */
910
+ function buildAccountValue(
911
+ input: BuildCostReportInput,
912
+ bounds: { readonly startMs: number; readonly endMs: number },
913
+ rows: readonly CostDigestRow[],
914
+ ): readonly AccountValueSummary[] {
915
+ const accountRateHistory = input.accountRateHistory ?? {};
916
+ const presentAccounts = new Set([
917
+ ...Object.keys(accountRateHistory),
918
+ ...input.digestRows.map((row) => row.canonicalAccountId),
919
+ ...input.observations.map((observation) => observation.canonicalAccountId),
920
+ ...(input.rawAccountIds ?? []),
921
+ ]);
922
+ return [...presentAccounts].sort().map((canonicalAccountId) => {
923
+ const records = accountRateHistory[canonicalAccountId] ?? [];
924
+ const allocation = allocateAccountRateCost(records, bounds.startMs, bounds.endMs);
925
+ const accountRows = rows.filter(
926
+ (row) => row.canonicalAccountId === canonicalAccountId,
927
+ );
928
+ const apiEquivalentCoverage = aggregateKnownCoverage(accountRows);
929
+ return {
930
+ canonicalAccountId,
931
+ recordedRate: recordedRateAsOf(records, bounds.endMs),
932
+ allocatedAccountCostUsd: allocation.knownUsd,
933
+ allocationHasUnknownCoverage: allocation.hasUnknownCoverage,
934
+ apiEquivalentCoverage,
935
+ comparison: buildValueComparison(apiEquivalentCoverage, allocation),
936
+ };
937
+ });
938
+ }
939
+
940
+ function legacyUnattributed(
941
+ observations: readonly CostObservation[],
942
+ ): readonly LegacyUnattributedCost[] {
943
+ const groups = new Map<string, CostObservation[]>();
944
+ for (const observation of observations) {
945
+ if (observation.projectKey !== undefined) continue;
946
+ const key = JSON.stringify([
947
+ observation.canonicalAccountId,
948
+ observation.requestedModel,
949
+ ]);
950
+ const group = groups.get(key) ?? [];
951
+ group.push(observation);
952
+ groups.set(key, group);
953
+ }
954
+ return [...groups.values()]
955
+ .map((group) => {
956
+ const first = group[0];
957
+ if (first === undefined) return undefined;
958
+ return {
959
+ canonicalAccountId: first.canonicalAccountId,
960
+ requestedModel: first.requestedModel,
961
+ observationCount: group.length,
962
+ tokens: group.reduce(
963
+ (total, observation) => addTokens(total, observation.tokens),
964
+ emptyTokens(),
965
+ ),
966
+ retainedCostUsd: group.reduce(
967
+ (total, observation) => addUsd(total, observation.retainedCostUsd),
968
+ 0,
969
+ ),
970
+ };
971
+ })
972
+ .filter((entry): entry is LegacyUnattributedCost => entry !== undefined)
973
+ .sort(
974
+ (left, right) =>
975
+ left.canonicalAccountId.localeCompare(right.canonicalAccountId) ||
976
+ left.requestedModel.localeCompare(right.requestedModel),
977
+ );
978
+ }
979
+
980
+ /** The `[startMs,endMs)` a {@link ResolvedReportRange} resolves to against this input's own retained data. */
981
+ function resolveRangeBounds(
982
+ input: BuildCostReportInput,
983
+ range: ResolvedReportRange,
984
+ ): { readonly startMs: number; readonly endMs: number } {
985
+ if (range.kind === "custom") {
986
+ return { startMs: range.startMs, endMs: range.endMs };
987
+ }
988
+ const startMs = earliestRetainedTimestamp(input) ?? input.nowMs;
989
+ return { startMs, endMs: input.nowMs };
990
+ }
991
+
992
+ /**
993
+ * The row set for an arbitrary `[startMs,endMs)` window -- unlike
994
+ * {@link currentRows}, which only ever assembles rows shaped by a calendar
995
+ * `periodType`. Every CLOSED digest row is a candidate for
996
+ * {@link selectNonOverlappingDigestRows}, which picks exactly one
997
+ * representation per (project, account, model) identity so a window
998
+ * spanning several retained granularities (raw, day, month, ...) is never
999
+ * double-counted. A closed row's `periodEndMs` never exceeds today's own
1000
+ * start, so none of them can ever represent today; the still-open current
1001
+ * day's own provisional rows are appended afterward whenever `bounds`
1002
+ * overlaps today at all, explicitly clipped to `bounds` itself by
1003
+ * {@link provisionalCurrentDayRows} rather than the nominal full-calendar-day
1004
+ * shape a closed row would have -- a window ending or starting mid-day-today
1005
+ * excludes exactly the out-of-window instants instead of either silently
1006
+ * absorbing everything through `nowMs` or dropping today's data entirely.
1007
+ */
1008
+ function rangeRows(
1009
+ input: BuildCostReportInput,
1010
+ bounds: { readonly startMs: number; readonly endMs: number },
1011
+ ): readonly CostDigestRow[] {
1012
+ const closed = selectNonOverlappingDigestRows(
1013
+ input.digestRows,
1014
+ bounds.startMs,
1015
+ bounds.endMs,
1016
+ );
1017
+ const today = getPeriodBounds(input.nowMs, "day");
1018
+ // Overlap with today, not just "starts on/before today": a window whose own
1019
+ // `--from` lands mid-day (after today's midnight) still needs today's live
1020
+ // data, so containment against `today.startMs` alone would wrongly drop it.
1021
+ const spansToday = bounds.endMs > today.startMs && bounds.startMs < today.endMs;
1022
+ const provisional = spansToday
1023
+ ? provisionalCurrentDayRows(input, {
1024
+ startMs: bounds.startMs,
1025
+ endMs: bounds.endMs,
1026
+ })
1027
+ : [];
1028
+ return [...closed, ...provisional];
1029
+ }
1030
+
1031
+ /**
1032
+ * Builds a `CostReport` for a resolved custom range or all-history window
1033
+ * instead of a calendar period. See {@link BuildCostReportInput.range}. A
1034
+ * `"custom"` window is `completed` once its own `endMs` has fully elapsed;
1035
+ * `"all-history"` is never `completed` -- it always extends through `nowMs`
1036
+ * by construction, so it is reported exactly like the still-open current
1037
+ * calendar period, folding in today's provisional rows the same way.
1038
+ */
1039
+ function buildRangeReport(
1040
+ input: BuildCostReportInput,
1041
+ range: ResolvedReportRange,
1042
+ ): CostReport {
1043
+ const bounds = resolveRangeBounds(input, range);
1044
+ const rows = rangeRows(input, bounds);
1045
+ const elapsed = range.kind === "custom" && bounds.endMs <= input.nowMs;
1046
+ const throughMs = elapsed ? bounds.endMs : input.nowMs;
1047
+ return {
1048
+ generatedAtMs: input.nowMs,
1049
+ periodType: input.periodType,
1050
+ current: periodReport(bounds, throughMs, elapsed, rows, input.projectLabels),
1051
+ completed: [],
1052
+ subsidization: buildSubsidization(
1053
+ input,
1054
+ { reports: [], rowsByStart: new Map() },
1055
+ bounds,
1056
+ rows,
1057
+ { throughMs, forcePartialCoverage: !elapsed },
1058
+ ),
1059
+ legacyUnattributed:
1060
+ input.groupedLegacyUnattributed ?? legacyUnattributed(input.observations),
1061
+ accountValue: buildAccountValue(input, bounds, rows),
1062
+ };
1063
+ }
1064
+
1065
+ export function buildCostReport(input: BuildCostReportInput): CostReport {
1066
+ if (!Number.isFinite(input.nowMs) || input.nowMs < 0) {
1067
+ throw new RangeError("Cost report nowMs must be a finite timestamp.");
1068
+ }
1069
+ if (input.range !== undefined) {
1070
+ return buildRangeReport(input, input.range);
1071
+ }
1072
+ const completed = completedPeriods(input);
1073
+ const currentBounds = getPeriodBounds(
1074
+ input.nowMs,
1075
+ input.periodType,
1076
+ input.timeZone ?? "UTC",
1077
+ );
1078
+ const rows = currentRows(input);
1079
+ return {
1080
+ generatedAtMs: input.nowMs,
1081
+ periodType: input.periodType,
1082
+ current: periodReport(
1083
+ currentBounds,
1084
+ input.nowMs,
1085
+ false,
1086
+ rows,
1087
+ input.projectLabels,
1088
+ ),
1089
+ completed: completed.reports,
1090
+ subsidization: buildSubsidization(
1091
+ input,
1092
+ completed,
1093
+ currentBounds,
1094
+ rows,
1095
+ ),
1096
+ legacyUnattributed:
1097
+ input.groupedLegacyUnattributed ?? legacyUnattributed(input.observations),
1098
+ accountValue: buildAccountValue(
1099
+ input,
1100
+ { startMs: currentBounds.startMs, endMs: input.nowMs },
1101
+ rows,
1102
+ ),
1103
+ };
1104
+ }