@bitkyc08/opencodex 2.40.0 → 2.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +4 -0
  2. package/gui/dist/assets/index-BU1tE0sr.js +112 -0
  3. package/gui/dist/assets/index-DL9-iS6J.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/meta.svg +1 -0
  6. package/package.json +4 -3
  7. package/src/adapters/cursor/catalog.ts +71 -29
  8. package/src/adapters/cursor/claude-id.ts +76 -0
  9. package/src/adapters/cursor/discovery.ts +16 -3
  10. package/src/adapters/cursor/effort-map.ts +27 -12
  11. package/src/adapters/cursor/protobuf-request.ts +41 -21
  12. package/src/adapters/google.ts +39 -2
  13. package/src/adapters/identity.ts +8 -2
  14. package/src/adapters/openai-responses.ts +57 -4
  15. package/src/bridge.ts +25 -3
  16. package/src/cli/account-auth.ts +28 -3
  17. package/src/cli/account-extended.ts +7 -1
  18. package/src/cli/capabilities.ts +2 -2
  19. package/src/cli/claude.ts +11 -2
  20. package/src/cli/connect.ts +7 -1
  21. package/src/cli/observe.ts +3 -1
  22. package/src/cli/registry.ts +1 -1
  23. package/src/cli/status.ts +19 -4
  24. package/src/client/connect.ts +5 -1
  25. package/src/client/hub-client.ts +29 -5
  26. package/src/clients/config-export.ts +12 -2
  27. package/src/codex/auth-api.ts +102 -9
  28. package/src/codex/catalog/aggregation.ts +8 -0
  29. package/src/codex/catalog/effort.ts +15 -2
  30. package/src/codex/catalog/metadata.ts +119 -9
  31. package/src/codex/catalog/native-models.ts +71 -0
  32. package/src/codex/catalog/parsing.ts +5 -3
  33. package/src/codex/catalog/provider-fetch.ts +166 -28
  34. package/src/codex/catalog.ts +1 -1
  35. package/src/codex/convergence-types.ts +1 -0
  36. package/src/codex/data/upstream-models.json +169 -0
  37. package/src/codex/desired-state.ts +18 -11
  38. package/src/codex/inject.ts +96 -6
  39. package/src/codex/injected-marker.ts +30 -4
  40. package/src/codex/journal.ts +14 -0
  41. package/src/combos/failover.ts +185 -6
  42. package/src/combos/index.ts +6 -0
  43. package/src/combos/resolve.ts +43 -6
  44. package/src/config.ts +5 -1
  45. package/src/generated/compatibility-version.json +115 -83
  46. package/src/generated/model-metadata.ts +1 -1
  47. package/src/grok/sync.ts +10 -2
  48. package/src/integrations/cursor-effort-table.ts +143 -0
  49. package/src/integrations/state.ts +1 -1
  50. package/src/integrations/writer.ts +2 -2
  51. package/src/lib/app-owned-memory-stores.ts +27 -8
  52. package/src/lib/bounded-body.ts +16 -1
  53. package/src/oauth/account-quota-rank.ts +40 -1
  54. package/src/oauth/chatgpt-device.ts +187 -0
  55. package/src/oauth/chatgpt.ts +31 -4
  56. package/src/oauth/generic-account-failover.ts +2 -2
  57. package/src/oauth/index.ts +24 -3
  58. package/src/oauth/log.ts +3 -0
  59. package/src/oauth/meta-muse.ts +235 -0
  60. package/src/providers/antigravity-models.ts +71 -13
  61. package/src/providers/command-code-efforts.ts +15 -0
  62. package/src/providers/free-directory.ts +4 -1
  63. package/src/providers/muse-subscription-usage.ts +95 -0
  64. package/src/providers/quota.ts +96 -0
  65. package/src/providers/registry.ts +116 -8
  66. package/src/responses/code-mode-helper-compat.ts +4 -1
  67. package/src/responses/state.ts +5 -4
  68. package/src/server/auth-cors.ts +241 -56
  69. package/src/server/chat-completions.ts +11 -2
  70. package/src/server/chat-native.ts +30 -4
  71. package/src/server/claude-messages.ts +17 -3
  72. package/src/server/effort-row.ts +131 -0
  73. package/src/server/index.ts +82 -45
  74. package/src/server/live.ts +18 -4
  75. package/src/server/management/api-key-rotation.ts +2 -1
  76. package/src/server/management/api-key-usage.ts +97 -43
  77. package/src/server/management/context.ts +3 -0
  78. package/src/server/management/cursor-integration-routes.ts +36 -7
  79. package/src/server/management/logs-usage-routes.ts +64 -87
  80. package/src/server/management/oauth-account-routes.ts +10 -3
  81. package/src/server/management/provider-routes.ts +218 -1
  82. package/src/server/management/route-registry.ts +1 -0
  83. package/src/server/management/usage-aggregate-cache.ts +464 -0
  84. package/src/server/management/usage-summary-cache.ts +4 -0
  85. package/src/server/models-capabilities.ts +60 -5
  86. package/src/server/responses/core.ts +95 -7
  87. package/src/server/responses/empty-completion-guard.ts +4 -0
  88. package/src/types/config.ts +10 -1
  89. package/src/types/request.ts +8 -0
  90. package/src/types/tools.ts +12 -9
  91. package/src/usage/expected-prices.ts +43 -7
  92. package/src/usage/ledger-scanner.ts +448 -0
  93. package/src/usage/log.ts +1 -1
  94. package/src/usage/summary.ts +915 -655
  95. package/src/web-search/index.ts +1 -1
  96. package/gui/dist/assets/index-BHe2rl_C.js +0 -112
  97. package/gui/dist/assets/index-CJSb3HPe.css +0 -1
@@ -15,6 +15,8 @@ export const USAGE_RANGES = ["today", "7d", "30d", "all"] as const;
15
15
  export type UsageRange = typeof USAGE_RANGES[number];
16
16
  export const USAGE_SURFACES = ["all", "codex", "claude", "grok"] as const;
17
17
  export type UsageSurface = typeof USAGE_SURFACES[number];
18
+ /** Maximum number of calendar buckets returned by the all-history chart. */
19
+ export const MAX_USAGE_DAY_BUCKETS = 366;
18
20
 
19
21
  export interface UsageSummaryTotals {
20
22
  requests: number;
@@ -147,7 +149,7 @@ export interface UsageSummary {
147
149
  }
148
150
 
149
151
  /**
150
- * Echo of an applied provider/model projection.
152
+ * Echo of an applied API-key/provider/model projection.
151
153
  *
152
154
  * Present only on a filtered response so a consumer can distinguish "no rows
153
155
  * matched" from "no traffic in this window", and can tell that the totals it
@@ -236,16 +238,6 @@ export function computeEntryCost(entry: PersistedUsageEntry): EntryCostInfo {
236
238
  const DAY_MS = 86_400_000;
237
239
  export const MAX_USAGE_MODEL_BREAKDOWN_ROWS = 256;
238
240
 
239
- function retainedBreakdownRows<T>(
240
- rows: T[],
241
- aggregateOverflow: (overflow: T[]) => T,
242
- ): T[] {
243
- if (rows.length <= MAX_USAGE_MODEL_BREAKDOWN_ROWS) return rows;
244
- const keep = rows.slice(0, MAX_USAGE_MODEL_BREAKDOWN_ROWS - 1);
245
- keep.push(aggregateOverflow(rows.slice(MAX_USAGE_MODEL_BREAKDOWN_ROWS - 1)));
246
- return keep;
247
- }
248
-
249
241
  export function parseRange(input: string | null | undefined): UsageRange {
250
242
  // `1d` normalises here rather than becoming a second union member: a second
251
243
  // member would need its own cache slot, its own grid arm and its own test
@@ -287,17 +279,16 @@ export function rangeWindow(range: UsageRange, now: number): { since: number | n
287
279
 
288
280
  function localDateKey(ts: number): string {
289
281
  const d = new Date(ts);
290
- const y = d.getFullYear();
282
+ const y = String(d.getFullYear()).padStart(4, "0");
291
283
  const m = String(d.getMonth() + 1).padStart(2, "0");
292
284
  const day = String(d.getDate()).padStart(2, "0");
293
285
  return `${y}-${m}-${day}`;
294
286
  }
295
287
 
296
- function dayCountForAllRange(entries: PersistedUsageEntry[], now: number): number {
297
- if (entries.length === 0) return 1;
298
- const oldest = entries.reduce((min, e) => Math.min(min, e.timestamp), entries[0].timestamp);
288
+ function dayCountForAllRange(oldest: number | null, now: number): number {
289
+ if (oldest === null) return 1;
299
290
  const days = Math.ceil((now - oldest) / DAY_MS) + 1;
300
- return Math.max(1, days);
291
+ return Math.min(MAX_USAGE_DAY_BUCKETS, Math.max(1, days));
301
292
  }
302
293
 
303
294
  function blankTotals(): UsageSummaryTotals {
@@ -333,6 +324,7 @@ interface UsageAttribution {
333
324
  provider: string;
334
325
  model: string;
335
326
  resolvedModel?: string;
327
+ accountLogLabel?: string;
336
328
  usageStatus: UsageStatus;
337
329
  usage?: PersistedUsageEntry["usage"];
338
330
  totalTokens?: number;
@@ -365,7 +357,7 @@ function usageModelIdentity(
365
357
  }
366
358
 
367
359
  function usageModelKey(providerKey: string, model: string): string {
368
- return `${providerKey}/${model}`;
360
+ return `${providerKey}\0${model}`;
369
361
  }
370
362
 
371
363
  function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[] {
@@ -374,6 +366,7 @@ function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[] {
374
366
  requestId: entry.requestId,
375
367
  provider: entry.provider,
376
368
  ...usageModelIdentity(entry.provider, entry.model, entry.resolvedModel),
369
+ ...(entry.accountLogLabel ? { accountLogLabel: entry.accountLogLabel } : {}),
377
370
  usageStatus: entry.usageStatus,
378
371
  ...(entry.usage ? { usage: entry.usage } : {}),
379
372
  ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
@@ -383,6 +376,7 @@ function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[] {
383
376
  requestId: entry.requestId,
384
377
  provider: attempt.provider,
385
378
  ...usageModelIdentity(attempt.provider, attempt.model),
379
+ ...(attempt.accountLogLabel ? { accountLogLabel: attempt.accountLogLabel } : {}),
386
380
  usageStatus: attempt.usageStatus,
387
381
  ...(attempt.usage ? { usage: attempt.usage } : {}),
388
382
  ...(attempt.totalTokens !== undefined ? { totalTokens: attempt.totalTokens } : {}),
@@ -446,17 +440,6 @@ function projectedComboUsage(
446
440
  };
447
441
  }
448
442
 
449
- function foldAttributionStatuses(statuses: readonly UsageStatus[]): UsageStatus {
450
- if (statuses.length > 0 && statuses.every(status => status === "unsupported")) {
451
- return "unsupported";
452
- }
453
- if (statuses.some(status => status === "unreported" || status === "unsupported")) {
454
- return "unreported";
455
- }
456
- if (statuses.some(status => status === "estimated")) return "estimated";
457
- return statuses.length > 0 ? "reported" : "unreported";
458
- }
459
-
460
443
  function bumpStatus(totals: UsageSummaryTotals, status: UsageStatus): void {
461
444
  totals.requests += 1;
462
445
  if (isMeasuredStatus(status)) totals.measuredRequests += 1;
@@ -505,500 +488,728 @@ function addEstimatedCost(
505
488
  totals.estimatedCostUsd += costInfo.costTotal;
506
489
  }
507
490
 
508
- function buildDayGrid(range: UsageRange, since: number | null, now: number, entries: PersistedUsageEntry[], costMap: Map<PersistedUsageEntry, EntryCostInfo>): UsageDay[] {
509
- const window = rangeWindow(range, now);
510
- const days = range === "all" ? dayCountForAllRange(entries, now) : window.days;
511
- const grid = new Map<string, UsageDay>();
512
- // Per-day model breakdown accumulator, keyed by day then provider/model, so the 7d bar chart can
513
- // render a per-model stacked bar with a hover tooltip without a second pass over the entries.
514
- interface DayModelAccumulator extends UsageDayModel {
515
- cacheObserved?: boolean;
491
+ const REQUEST_REPORTED = 1 << 0;
492
+ const REQUEST_ESTIMATED = 1 << 1;
493
+ const REQUEST_UNREPORTED = 1 << 2;
494
+ const REQUEST_UNSUPPORTED = 1 << 3;
495
+ const REQUEST_PRICED = 1 << 4;
496
+ const REQUEST_UNPRICED = 1 << 5;
497
+ const REQUEST_STATUS_MASK = REQUEST_REPORTED | REQUEST_ESTIMATED | REQUEST_UNREPORTED | REQUEST_UNSUPPORTED;
498
+
499
+ type UsagePartitionSurface = Exclude<UsageSurface, "all"> | "other";
500
+ export type UsageAccumulatorMode = "exact" | "row-unique";
501
+
502
+ interface UsageRequestCounts {
503
+ requests: number;
504
+ measuredRequests: number;
505
+ reportedRequests: number;
506
+ estimatedRequests: number;
507
+ pricedRequests: number;
508
+ unpricedRequests: number;
509
+ }
510
+
511
+ interface UsageModelOverlap {
512
+ models: ReadonlyArray<readonly [modelKey: string, requestFacts: number]>;
513
+ count: number;
514
+ }
515
+
516
+ interface UsageModelAccumulator {
517
+ provider: string;
518
+ model: string;
519
+ resolvedModel?: string;
520
+ firstSeen: number;
521
+ attemptCount: number;
522
+ dayTotalTokens: number;
523
+ summaryTotalTokens: number;
524
+ inputTokens: number;
525
+ outputTokens: number;
526
+ cacheReadInputTokens: number;
527
+ cacheCreationInputTokens: number;
528
+ cacheObserved: boolean;
529
+ estimatedCostUsd?: number;
530
+ requestCounts: UsageRequestCounts;
531
+ requestFacts?: Map<number, number>;
532
+ }
533
+
534
+ interface UsageAccountAccumulator {
535
+ accountLogLabel: string;
536
+ ambiguous: boolean;
537
+ firstSeen: number;
538
+ requests: number;
539
+ requestIds?: Set<number>;
540
+ attemptCount: number;
541
+ measuredAttempts: number;
542
+ reportedAttempts: number;
543
+ estimatedAttempts: number;
544
+ unmeteredAttempts: number;
545
+ inputTokens: number;
546
+ outputTokens: number;
547
+ cacheReadInputTokens: number;
548
+ cacheCreationInputTokens: number;
549
+ reasoningOutputTokens: number;
550
+ totalTokens: number;
551
+ estimatedCostUsd?: number;
552
+ pricedAttempts: number;
553
+ unpricedAttempts: number;
554
+ }
555
+
556
+ interface UsagePartition {
557
+ date: string;
558
+ dayStart: number;
559
+ surface: UsagePartitionSurface;
560
+ oldestTimestamp: number | null;
561
+ totals: UsageSummaryTotals;
562
+ models: Map<string, UsageModelAccumulator>;
563
+ providers?: Map<string, UsageModelAccumulator>;
564
+ accounts: Map<string, UsageAccountAccumulator>;
565
+ modelOverlaps: Map<string, UsageModelOverlap>;
566
+ }
567
+
568
+ interface UsageDayAccumulator {
569
+ totals: UsageSummaryTotals;
570
+ models: Map<string, UsageModelAccumulator>;
571
+ modelOverlaps: UsageModelOverlap[];
572
+ }
573
+
574
+ interface NormalizedUsageFilter {
575
+ provider: string | null;
576
+ model: string | null;
577
+ apiKeyId: string | null;
578
+ }
579
+
580
+ export interface UsageSummaryAccumulator {
581
+ add(entry: PersistedUsageEntry): void;
582
+ /** Return a mutation-independent snapshot that may continue accepting rows. */
583
+ clone(): UsageSummaryAccumulator;
584
+ summarize(
585
+ range: UsageRange,
586
+ now: number,
587
+ surface?: UsageSurface,
588
+ ): UsageSummary & { filter?: UsageFilterEcho };
589
+ readonly snapshotWindow: { start: number | null; end: number | null };
590
+ /** Conservative O(1) retained-state estimate; excludes scan and summarize temporaries. */
591
+ readonly estimatedBytes: number;
592
+ }
593
+
594
+ function requestStatusFact(status: UsageStatus): number {
595
+ if (status === "reported") return REQUEST_REPORTED;
596
+ if (status === "estimated") return REQUEST_ESTIMATED;
597
+ if (status === "unsupported") return REQUEST_UNSUPPORTED;
598
+ return REQUEST_UNREPORTED;
599
+ }
600
+
601
+ function statusFromRequestFacts(facts: number): UsageStatus {
602
+ const statuses = facts & REQUEST_STATUS_MASK;
603
+ if (statuses === REQUEST_UNSUPPORTED) return "unsupported";
604
+ if ((statuses & (REQUEST_UNREPORTED | REQUEST_UNSUPPORTED)) !== 0) return "unreported";
605
+ if ((statuses & REQUEST_ESTIMATED) !== 0) return "estimated";
606
+ return (statuses & REQUEST_REPORTED) !== 0 ? "reported" : "unreported";
607
+ }
608
+
609
+ function blankRequestCounts(): UsageRequestCounts {
610
+ return {
611
+ requests: 0,
612
+ measuredRequests: 0,
613
+ reportedRequests: 0,
614
+ estimatedRequests: 0,
615
+ pricedRequests: 0,
616
+ unpricedRequests: 0,
617
+ };
618
+ }
619
+
620
+ function bumpRequestCounts(counts: UsageRequestCounts, facts: number, amount = 1): void {
621
+ counts.requests += amount;
622
+ const status = statusFromRequestFacts(facts);
623
+ if (isMeasuredStatus(status)) counts.measuredRequests += amount;
624
+ if (status === "reported") counts.reportedRequests += amount;
625
+ else if (status === "estimated") counts.estimatedRequests += amount;
626
+ if ((facts & REQUEST_PRICED) !== 0) counts.pricedRequests += amount;
627
+ if ((facts & REQUEST_UNPRICED) !== 0) counts.unpricedRequests += amount;
628
+ }
629
+
630
+ function mergeRequestCounts(target: UsageRequestCounts, source: UsageRequestCounts): void {
631
+ target.requests += source.requests;
632
+ target.measuredRequests += source.measuredRequests;
633
+ target.reportedRequests += source.reportedRequests;
634
+ target.estimatedRequests += source.estimatedRequests;
635
+ target.pricedRequests += source.pricedRequests;
636
+ target.unpricedRequests += source.unpricedRequests;
637
+ }
638
+
639
+ function mergeRequestFacts(target: Map<number, number>, source: Map<number, number>): void {
640
+ for (const [requestId, facts] of source) {
641
+ target.set(requestId, (target.get(requestId) ?? 0) | facts);
516
642
  }
517
- const dayModels = new Map<string, Map<string, DayModelAccumulator>>();
518
- const dayModelRequests = new Map<string, Set<string>>();
519
- const bumpDayModel = (dayKey: string, attribution: UsageAttribution): void => {
520
- let models = dayModels.get(dayKey);
521
- if (!models) { models = new Map(); dayModels.set(dayKey, models); }
522
- const providerKey = baseProviderLabel(attribution.provider);
523
- const mKey = usageModelKey(providerKey, attribution.model);
524
- let m = models.get(mKey);
525
- if (!m) {
526
- m = {
527
- model: attribution.model,
528
- provider: providerKey,
529
- requests: 0,
530
- attemptCount: 0,
531
- totalTokens: 0,
532
- inputTokens: 0,
533
- outputTokens: 0,
534
- cacheReadInputTokens: 0,
535
- cacheCreationInputTokens: 0,
536
- cacheHitRate: null,
537
- };
538
- models.set(mKey, m);
539
- }
540
- const requestKey = `${dayKey}\0${mKey}`;
541
- let requests = dayModelRequests.get(requestKey);
542
- if (!requests) { requests = new Set(); dayModelRequests.set(requestKey, requests); }
543
- requests.add(attribution.requestId);
544
- m.requests = requests.size;
545
- m.attemptCount += 1;
546
- if (attribution.usage) {
547
- m.inputTokens = (m.inputTokens ?? 0) + attribution.usage.inputTokens;
548
- m.outputTokens = (m.outputTokens ?? 0) + attribution.usage.outputTokens;
549
- const { read, creation, hasCacheTelemetry } = cacheTokensFromUsage(attribution.usage);
550
- if (hasCacheTelemetry) m.cacheObserved = true;
551
- if (typeof read === "number") m.cacheReadInputTokens = (m.cacheReadInputTokens ?? 0) + read;
552
- if (typeof creation === "number") m.cacheCreationInputTokens = (m.cacheCreationInputTokens ?? 0) + creation;
553
- }
554
- m.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
643
+ }
644
+
645
+ function requestCountsFor(model: UsageModelAccumulator): UsageRequestCounts {
646
+ if (!model.requestFacts) return model.requestCounts;
647
+ const counts = blankRequestCounts();
648
+ for (const facts of model.requestFacts.values()) bumpRequestCounts(counts, facts);
649
+ return counts;
650
+ }
651
+
652
+ function mergeTotals(target: UsageSummaryTotals, source: UsageSummaryTotals): void {
653
+ target.requests += source.requests;
654
+ target.attemptCount += source.attemptCount;
655
+ target.measuredRequests += source.measuredRequests;
656
+ target.reportedRequests += source.reportedRequests;
657
+ target.unreportedRequests += source.unreportedRequests;
658
+ target.unsupportedRequests += source.unsupportedRequests;
659
+ target.estimatedRequests += source.estimatedRequests;
660
+ target.inputTokens += source.inputTokens;
661
+ target.outputTokens += source.outputTokens;
662
+ target.cachedInputTokens += source.cachedInputTokens;
663
+ target.cacheReadInputTokens += source.cacheReadInputTokens;
664
+ target.cacheCreationInputTokens += source.cacheCreationInputTokens;
665
+ target.reasoningOutputTokens += source.reasoningOutputTokens;
666
+ target.totalTokens += source.totalTokens;
667
+ target.estimatedCostUsd += source.estimatedCostUsd;
668
+ target.pricedRequests += source.pricedRequests;
669
+ target.unpricedRequests += source.unpricedRequests;
670
+ target.unmeteredRequests += source.unmeteredRequests;
671
+ }
672
+
673
+ function blankModelAccumulator(
674
+ provider: string,
675
+ model: string,
676
+ resolvedModel: string | undefined,
677
+ firstSeen: number,
678
+ mode: UsageAccumulatorMode,
679
+ ): UsageModelAccumulator {
680
+ return {
681
+ provider,
682
+ model,
683
+ ...(resolvedModel ? { resolvedModel } : {}),
684
+ firstSeen,
685
+ attemptCount: 0,
686
+ dayTotalTokens: 0,
687
+ summaryTotalTokens: 0,
688
+ inputTokens: 0,
689
+ outputTokens: 0,
690
+ cacheReadInputTokens: 0,
691
+ cacheCreationInputTokens: 0,
692
+ cacheObserved: false,
693
+ requestCounts: blankRequestCounts(),
694
+ ...(mode === "exact" ? { requestFacts: new Map() } : {}),
695
+ };
696
+ }
697
+
698
+ function cloneModelAccumulator(source: UsageModelAccumulator): UsageModelAccumulator {
699
+ return {
700
+ ...source,
701
+ requestCounts: { ...source.requestCounts },
702
+ ...(source.requestFacts ? { requestFacts: new Map(source.requestFacts) } : {}),
555
703
  };
556
- const startOfToday = startOfLocalDay(now);
557
- for (let i = days - 1; i >= 0; i--) {
558
- const d = new Date(startOfToday);
559
- d.setDate(d.getDate() - i);
560
- const key = localDateKey(d.getTime());
561
- grid.set(key, { date: key, requests: 0, measuredRequests: 0, reportedRequests: 0, totalTokens: 0, estimatedCostUsd: 0, models: [] });
704
+ }
705
+
706
+ function mergeModelAccumulator(target: UsageModelAccumulator, source: UsageModelAccumulator): void {
707
+ if (source.firstSeen < target.firstSeen) {
708
+ target.firstSeen = source.firstSeen;
709
+ target.resolvedModel = source.resolvedModel;
562
710
  }
563
- for (const entry of entries) {
564
- const key = localDateKey(entry.timestamp);
565
- let day = grid.get(key);
566
- if (!day) {
567
- day = { date: key, requests: 0, measuredRequests: 0, reportedRequests: 0, totalTokens: 0, estimatedCostUsd: 0, models: [] };
568
- grid.set(key, day);
569
- }
570
- day.requests += 1;
571
- if (isMeasuredStatus(entry.usageStatus)) day.measuredRequests += 1;
572
- if (entry.usageStatus === "reported") day.reportedRequests += 1;
573
- day.totalTokens += usageDisplayTotalTokens(entry.usage, entry.totalTokens) ?? 0;
574
- for (const attribution of usageAttributions(entry)) bumpDayModel(key, attribution);
575
- const costInfo = costMap.get(entry);
576
- if (costInfo?.isPriced) {
577
- if (entry.attempts?.length && costInfo.attemptEstimates) {
578
- for (let i = 0; i < entry.attempts.length; i++) {
579
- const attempt = entry.attempts[i];
580
- const attemptEst = costInfo.attemptEstimates[i];
581
- if (attemptEst) {
582
- const aProviderKey = baseProviderLabel(attempt.provider);
583
- const aIdentity = usageModelIdentity(attempt.provider, attempt.model);
584
- const aKey = usageModelKey(aProviderKey, aIdentity.model);
585
- const m = dayModels.get(key)?.get(aKey);
586
- if (m) m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + attemptEst.cost.total;
587
- }
588
- }
589
- } else if (costInfo.estimate) {
590
- const providerKey = baseProviderLabel(entry.provider);
591
- const identity = usageModelIdentity(entry.provider, entry.model, entry.resolvedModel);
592
- const mKey = usageModelKey(providerKey, identity.model);
593
- const m = dayModels.get(key)?.get(mKey);
594
- if (m) m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + costInfo.estimate.cost.total;
595
- }
596
- day.estimatedCostUsd += costInfo.costTotal;
597
- }
711
+ target.attemptCount += source.attemptCount;
712
+ target.dayTotalTokens += source.dayTotalTokens;
713
+ target.summaryTotalTokens += source.summaryTotalTokens;
714
+ target.inputTokens += source.inputTokens;
715
+ target.outputTokens += source.outputTokens;
716
+ target.cacheReadInputTokens += source.cacheReadInputTokens;
717
+ target.cacheCreationInputTokens += source.cacheCreationInputTokens;
718
+ target.cacheObserved ||= source.cacheObserved;
719
+ if (source.estimatedCostUsd !== undefined) {
720
+ target.estimatedCostUsd = (target.estimatedCostUsd ?? 0) + source.estimatedCostUsd;
598
721
  }
599
- void since;
600
- const out = [...grid.values()].sort((a, b) => a.date.localeCompare(b.date));
601
- for (const day of out) {
602
- const models = dayModels.get(day.date);
603
- if (models) {
604
- for (const m of models.values()) {
605
- m.cacheHitRate = calculateCacheHitRate(!!m.cacheObserved, m.inputTokens ?? 0, m.cacheReadInputTokens ?? 0);
606
- }
607
- const sorted = [...models.values()].sort((a, b) => b.requests - a.requests);
608
- const retained = retainedBreakdownRows(sorted, overflow => {
609
- const requests = new Set<string>();
610
- let attemptCount = 0;
611
- let totalTokens = 0;
612
- let inputTokens = 0;
613
- let outputTokens = 0;
614
- let cacheReadInputTokens = 0;
615
- let cacheCreationInputTokens = 0;
616
- let cacheObserved = false;
617
- let estimatedCostUsd: number | undefined;
618
- for (const model of overflow) {
619
- attemptCount += model.attemptCount;
620
- totalTokens += model.totalTokens;
621
- inputTokens += model.inputTokens ?? 0;
622
- outputTokens += model.outputTokens ?? 0;
623
- cacheReadInputTokens += model.cacheReadInputTokens ?? 0;
624
- cacheCreationInputTokens += model.cacheCreationInputTokens ?? 0;
625
- if (model.cacheObserved) cacheObserved = true;
626
- if (model.estimatedCostUsd !== undefined) {
627
- estimatedCostUsd = (estimatedCostUsd ?? 0) + model.estimatedCostUsd;
628
- }
629
- const requestKey = `${day.date}\0${usageModelKey(model.provider, model.model)}`;
630
- for (const requestId of dayModelRequests.get(requestKey) ?? []) requests.add(requestId);
631
- }
632
- const cacheHitRate = calculateCacheHitRate(cacheObserved, inputTokens, cacheReadInputTokens);
633
- return {
634
- model: "other",
635
- provider: "other",
636
- requests: requests.size,
637
- attemptCount,
638
- totalTokens,
639
- inputTokens,
640
- outputTokens,
641
- cacheReadInputTokens,
642
- cacheCreationInputTokens,
643
- cacheHitRate,
644
- ...(estimatedCostUsd !== undefined ? { estimatedCostUsd } : {}),
645
- };
646
- });
647
- for (const model of retained) delete model.cacheObserved;
648
- day.models = retained;
649
- }
722
+ if (target.requestFacts && source.requestFacts) mergeRequestFacts(target.requestFacts, source.requestFacts);
723
+ else mergeRequestCounts(target.requestCounts, source.requestCounts);
724
+ }
725
+
726
+ function mergeModelMaps(
727
+ target: Map<string, UsageModelAccumulator>,
728
+ source: Map<string, UsageModelAccumulator>,
729
+ ): void {
730
+ for (const [key, model] of source) {
731
+ const current = target.get(key);
732
+ if (current) mergeModelAccumulator(current, model);
733
+ else target.set(key, cloneModelAccumulator(model));
650
734
  }
651
- return out;
652
735
  }
653
736
 
654
- function buildModels(entries: PersistedUsageEntry[], totalTokens: number, costMap: Map<PersistedUsageEntry, EntryCostInfo>): UsageModel[] {
655
- interface ModelAccumulator extends UsageModel {
656
- cacheObserved?: boolean;
737
+ function cloneAccountAccumulator(source: UsageAccountAccumulator): UsageAccountAccumulator {
738
+ return {
739
+ ...source,
740
+ ...(source.requestIds ? { requestIds: new Set(source.requestIds) } : {}),
741
+ };
742
+ }
743
+
744
+ function mergeAccountAccumulator(target: UsageAccountAccumulator, source: UsageAccountAccumulator): void {
745
+ target.firstSeen = Math.min(target.firstSeen, source.firstSeen);
746
+ if (target.requestIds && source.requestIds) {
747
+ for (const requestId of source.requestIds) target.requestIds.add(requestId);
748
+ } else {
749
+ target.requests += source.requests;
657
750
  }
658
- const byKey = new Map<string, ModelAccumulator>();
659
- const statusesByKey = new Map<string, Map<string, UsageStatus[]>>();
660
- for (const entry of entries) {
661
- for (const attribution of usageAttributions(entry)) {
662
- const providerKey = baseProviderLabel(attribution.provider);
663
- // resolvedModel is a routing detail, not a row identity.
664
- const key = usageModelKey(providerKey, attribution.model);
665
- let model = byKey.get(key);
666
- if (!model) {
667
- model = {
668
- provider: providerKey,
669
- model: attribution.model,
670
- ...(attribution.resolvedModel ? { resolvedModel: attribution.resolvedModel } : {}),
671
- requests: 0,
672
- attemptCount: 0,
673
- measuredRequests: 0,
674
- reportedRequests: 0,
675
- estimatedRequests: 0,
676
- totalTokens: 0,
677
- inputTokens: 0,
678
- outputTokens: 0,
679
- cachedInputTokens: 0,
680
- cacheReadInputTokens: 0,
681
- cacheCreationInputTokens: 0,
682
- pricedRequests: 0,
683
- unpricedRequests: 0,
684
- priceCoverageRatio: 0,
685
- shareRatio: 0,
686
- };
687
- byKey.set(key, model);
688
- }
689
- model.attemptCount += 1;
690
- let requests = statusesByKey.get(key);
691
- if (!requests) { requests = new Map(); statusesByKey.set(key, requests); }
692
- const statuses = requests.get(attribution.requestId) ?? [];
693
- statuses.push(attribution.usageStatus);
694
- requests.set(attribution.requestId, statuses);
695
- if (attribution.usage) {
696
- model.inputTokens += attribution.usage.inputTokens;
697
- model.outputTokens += attribution.usage.outputTokens;
698
- const { read, creation, hasCacheTelemetry } = cacheTokensFromUsage(attribution.usage);
699
- if (hasCacheTelemetry) model.cacheObserved = true;
700
- if (typeof read === "number") {
701
- model.cachedInputTokens = (model.cachedInputTokens ?? 0) + read;
702
- model.cacheReadInputTokens = (model.cacheReadInputTokens ?? 0) + read;
703
- }
704
- if (typeof creation === "number") {
705
- model.cacheCreationInputTokens = (model.cacheCreationInputTokens ?? 0) + creation;
706
- }
707
- model.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
708
- }
709
- }
751
+ target.attemptCount += source.attemptCount;
752
+ target.measuredAttempts += source.measuredAttempts;
753
+ target.reportedAttempts += source.reportedAttempts;
754
+ target.estimatedAttempts += source.estimatedAttempts;
755
+ target.unmeteredAttempts += source.unmeteredAttempts;
756
+ target.inputTokens += source.inputTokens;
757
+ target.outputTokens += source.outputTokens;
758
+ target.cacheReadInputTokens += source.cacheReadInputTokens;
759
+ target.cacheCreationInputTokens += source.cacheCreationInputTokens;
760
+ target.reasoningOutputTokens += source.reasoningOutputTokens;
761
+ target.totalTokens += source.totalTokens;
762
+ if (source.estimatedCostUsd !== undefined) {
763
+ target.estimatedCostUsd = (target.estimatedCostUsd ?? 0) + source.estimatedCostUsd;
710
764
  }
711
- for (const [key, model] of byKey) {
712
- const groups = statusesByKey.get(key) ?? new Map();
713
- model.requests = groups.size;
714
- for (const statuses of groups.values()) {
715
- const status = foldAttributionStatuses(statuses);
716
- if (isMeasuredStatus(status)) model.measuredRequests += 1;
717
- if (status === "reported") model.reportedRequests += 1;
718
- else if (status === "estimated") model.estimatedRequests += 1;
719
- }
765
+ target.pricedAttempts += source.pricedAttempts;
766
+ target.unpricedAttempts += source.unpricedAttempts;
767
+ }
768
+
769
+ function usagePartitionSurface(entry: PersistedUsageEntry): UsagePartitionSurface {
770
+ if (entry.surface === undefined) return "codex";
771
+ if (entry.surface === "claude" || entry.surface === "claude-desktop") return "claude";
772
+ if (entry.surface === "grok") return "grok";
773
+ return "other";
774
+ }
775
+
776
+ function usageSurfaceMatches(partition: UsagePartitionSurface, surface: UsageSurface): boolean {
777
+ return surface === "all" || partition === surface;
778
+ }
779
+
780
+ const LEGACY_AMBIGUOUS_ACCOUNT_LABEL = "legacy-ambiguous";
781
+
782
+ function legacyCodexAccountLabel(provider: string): string | null {
783
+ if (baseProviderLabel(provider) !== "openai") return null;
784
+ const suffix = provider.match(/-(main|p[a-f0-9]{6})$/)?.[1];
785
+ return suffix ?? LEGACY_AMBIGUOUS_ACCOUNT_LABEL;
786
+ }
787
+
788
+ /**
789
+ * An explicitly stamped label of EITHER family is authoritative for any provider (#2699).
790
+ * The legacy fallback stays openai-only so unrelated unlabeled providers are not guessed.
791
+ */
792
+ function accountLabelForAttribution(provider: string, explicit: unknown): string | null {
793
+ if (isCodexUsageAccountLogLabel(explicit)) return explicit;
794
+ return legacyCodexAccountLabel(provider);
795
+ }
796
+
797
+ function filterMatchesAttribution(
798
+ filter: NormalizedUsageFilter,
799
+ provider: string,
800
+ model: string,
801
+ ): boolean {
802
+ if (filter.provider !== null && baseProviderLabel(provider).toLowerCase() !== filter.provider) return false;
803
+ if (filter.model !== null && model.toLowerCase() !== filter.model) return false;
804
+ return true;
805
+ }
806
+
807
+ function projectedEntryForFilter(
808
+ entry: PersistedUsageEntry,
809
+ filter: NormalizedUsageFilter,
810
+ ): { entry: PersistedUsageEntry; comboOverlap: boolean } | null {
811
+ if (filter.apiKeyId !== null && entry.apiKeyId !== filter.apiKeyId) return null;
812
+ if (!entry.attempts?.length) {
813
+ const identity = usageModelIdentity(entry.provider, entry.model, entry.resolvedModel);
814
+ return filterMatchesAttribution(filter, entry.provider, identity.model)
815
+ ? { entry, comboOverlap: false }
816
+ : null;
720
817
  }
721
- // Accumulate per-model estimated cost & price coverage by request ID
722
- const pricedRequestsByModel = new Map<string, Set<string>>();
723
- const unpricedRequestsByModel = new Map<string, Set<string>>();
724
- for (const entry of entries) {
725
- const costInfo = costMap.get(entry);
726
- if (entry.attempts?.length) {
727
- const attemptEstimates = costInfo?.attemptEstimates;
728
- for (let i = 0; i < entry.attempts.length; i++) {
729
- const attempt = entry.attempts[i];
730
- const attemptEst = attemptEstimates?.[i];
731
- const aProviderKey = baseProviderLabel(attempt.provider);
732
- const aIdentity = usageModelIdentity(attempt.provider, attempt.model);
733
- const aKey = usageModelKey(aProviderKey, aIdentity.model);
734
- if (attemptEst) {
735
- const m = byKey.get(aKey);
736
- if (m) {
737
- m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + attemptEst.cost.total;
738
- }
739
- let s = pricedRequestsByModel.get(aKey);
740
- if (!s) { s = new Set(); pricedRequestsByModel.set(aKey, s); }
741
- s.add(entry.requestId);
742
- } else {
743
- let s = unpricedRequestsByModel.get(aKey);
744
- if (!s) { s = new Set(); unpricedRequestsByModel.set(aKey, s); }
745
- s.add(entry.requestId);
746
- }
747
- }
748
- } else {
749
- const providerKey = baseProviderLabel(entry.provider);
750
- const identity = usageModelIdentity(entry.provider, entry.model, entry.resolvedModel);
751
- const key = usageModelKey(providerKey, identity.model);
752
- const estimate = costInfo?.estimate;
753
- if (estimate) {
754
- const m = byKey.get(key);
755
- if (m) {
756
- m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + estimate.cost.total;
757
- }
758
- let s = pricedRequestsByModel.get(key);
759
- if (!s) { s = new Set(); pricedRequestsByModel.set(key, s); }
760
- s.add(entry.requestId);
761
- } else {
762
- let s = unpricedRequestsByModel.get(key);
763
- if (!s) { s = new Set(); unpricedRequestsByModel.set(key, s); }
764
- s.add(entry.requestId);
818
+ const attempts = entry.attempts.filter(attempt => {
819
+ const identity = usageModelIdentity(attempt.provider, attempt.model);
820
+ return filterMatchesAttribution(filter, attempt.provider, identity.model);
821
+ });
822
+ if (attempts.length === 0) return null;
823
+ const { usage: _parentUsage, totalTokens: _parentTotalTokens, ...withoutParentUsage } = entry;
824
+ return {
825
+ entry: { ...withoutParentUsage, attempts, ...projectedComboUsage(attempts) },
826
+ comboOverlap: entry.attempts.length > 1,
827
+ };
828
+ }
829
+
830
+ function overflowModelAccumulator(
831
+ models: UsageModelAccumulator[],
832
+ overlaps: readonly UsageModelOverlap[],
833
+ ): UsageModelAccumulator {
834
+ const mode: UsageAccumulatorMode = models[0]?.requestFacts ? "exact" : "row-unique";
835
+ const other = blankModelAccumulator("other", "other", undefined, models[0]?.firstSeen ?? 0, mode);
836
+ for (const model of models) mergeModelAccumulator(other, model);
837
+ if (mode === "row-unique" && overlaps.length > 0) {
838
+ const overflowKeys = new Set(models.map(model => usageModelKey(model.provider, model.model)));
839
+ for (const overlap of overlaps) {
840
+ const retained = overlap.models.filter(([modelKey]) => overflowKeys.has(modelKey));
841
+ if (retained.length < 2) continue;
842
+ let combinedFacts = 0;
843
+ for (const [, facts] of retained) {
844
+ bumpRequestCounts(other.requestCounts, facts, -overlap.count);
845
+ combinedFacts |= facts;
765
846
  }
847
+ bumpRequestCounts(other.requestCounts, combinedFacts, overlap.count);
766
848
  }
767
849
  }
768
- const models = [...byKey.values()];
769
- for (const [key, m] of byKey) {
770
- m.pricedRequests = pricedRequestsByModel.get(key)?.size ?? 0;
771
- m.unpricedRequests = unpricedRequestsByModel.get(key)?.size ?? 0;
772
- m.shareRatio = totalTokens === 0 ? 0 : m.totalTokens / totalTokens;
773
- m.cacheHitRate = calculateCacheHitRate(!!m.cacheObserved, m.inputTokens, m.cacheReadInputTokens ?? 0);
774
- m.priceCoverageRatio = m.requests > 0 ? m.pricedRequests / m.requests : 0;
775
- }
776
- const sorted = models.sort((a, b) => b.requests - a.requests);
777
- const retained = retainedBreakdownRows(sorted, overflow => {
778
- const statusesByRequest = new Map<string, UsageStatus[]>();
779
- const overflowPricedRequests = new Set<string>();
780
- const overflowUnpricedRequests = new Set<string>();
781
- let cacheObserved = false;
782
- const other: ModelAccumulator = {
783
- provider: "other",
784
- model: "other",
785
- requests: 0,
786
- attemptCount: 0,
787
- measuredRequests: 0,
788
- reportedRequests: 0,
789
- estimatedRequests: 0,
790
- totalTokens: 0,
791
- inputTokens: 0,
792
- outputTokens: 0,
793
- cachedInputTokens: 0,
794
- cacheReadInputTokens: 0,
795
- cacheCreationInputTokens: 0,
796
- pricedRequests: 0,
797
- unpricedRequests: 0,
798
- priceCoverageRatio: 0,
799
- shareRatio: 0,
850
+ other.provider = "other";
851
+ other.model = "other";
852
+ delete other.resolvedModel;
853
+ return other;
854
+ }
855
+
856
+ function retainedModelAccumulators(
857
+ models: UsageModelAccumulator[],
858
+ overlaps: readonly UsageModelOverlap[],
859
+ ): UsageModelAccumulator[] {
860
+ if (models.length <= MAX_USAGE_MODEL_BREAKDOWN_ROWS) return models;
861
+ return [
862
+ ...models.slice(0, MAX_USAGE_MODEL_BREAKDOWN_ROWS - 1),
863
+ overflowModelAccumulator(models.slice(MAX_USAGE_MODEL_BREAKDOWN_ROWS - 1), overlaps),
864
+ ];
865
+ }
866
+
867
+ function buildDayModels(
868
+ models: Map<string, UsageModelAccumulator>,
869
+ overlaps: readonly UsageModelOverlap[],
870
+ ): UsageDayModel[] {
871
+ const sorted = [...models.values()].sort((a, b) =>
872
+ requestCountsFor(b).requests - requestCountsFor(a).requests || a.firstSeen - b.firstSeen
873
+ );
874
+ return retainedModelAccumulators(sorted, overlaps).map(model => ({
875
+ model: model.model,
876
+ provider: model.provider,
877
+ requests: requestCountsFor(model).requests,
878
+ attemptCount: model.attemptCount,
879
+ totalTokens: model.dayTotalTokens,
880
+ inputTokens: model.inputTokens,
881
+ outputTokens: model.outputTokens,
882
+ cacheReadInputTokens: model.cacheReadInputTokens,
883
+ cacheCreationInputTokens: model.cacheCreationInputTokens,
884
+ cacheHitRate: calculateCacheHitRate(model.cacheObserved, model.inputTokens, model.cacheReadInputTokens),
885
+ ...(model.estimatedCostUsd !== undefined ? { estimatedCostUsd: model.estimatedCostUsd } : {}),
886
+ }));
887
+ }
888
+
889
+ function buildUsageModels(
890
+ models: Map<string, UsageModelAccumulator>,
891
+ totalTokens: number,
892
+ overlaps: readonly UsageModelOverlap[],
893
+ ): UsageModel[] {
894
+ const sorted = [...models.values()].sort((a, b) =>
895
+ requestCountsFor(b).requests - requestCountsFor(a).requests || a.firstSeen - b.firstSeen
896
+ );
897
+ return retainedModelAccumulators(sorted, overlaps).map(model => {
898
+ const counts = requestCountsFor(model);
899
+ const requests = counts.requests;
900
+ return {
901
+ provider: model.provider,
902
+ model: model.model,
903
+ ...(model.resolvedModel ? { resolvedModel: model.resolvedModel } : {}),
904
+ requests,
905
+ attemptCount: model.attemptCount,
906
+ measuredRequests: counts.measuredRequests,
907
+ reportedRequests: counts.reportedRequests,
908
+ estimatedRequests: counts.estimatedRequests,
909
+ totalTokens: model.summaryTotalTokens,
910
+ inputTokens: model.inputTokens,
911
+ outputTokens: model.outputTokens,
912
+ cachedInputTokens: model.cacheReadInputTokens,
913
+ cacheReadInputTokens: model.cacheReadInputTokens,
914
+ cacheCreationInputTokens: model.cacheCreationInputTokens,
915
+ cacheHitRate: calculateCacheHitRate(model.cacheObserved, model.inputTokens, model.cacheReadInputTokens),
916
+ priceCoverageRatio: requests > 0 ? counts.pricedRequests / requests : 0,
917
+ pricedRequests: counts.pricedRequests,
918
+ unpricedRequests: counts.unpricedRequests,
919
+ shareRatio: totalTokens === 0 ? 0 : model.summaryTotalTokens / totalTokens,
920
+ ...(model.estimatedCostUsd !== undefined ? { estimatedCostUsd: model.estimatedCostUsd } : {}),
800
921
  };
801
- for (const model of overflow) {
802
- other.attemptCount += model.attemptCount;
803
- other.totalTokens += model.totalTokens;
804
- other.inputTokens += model.inputTokens;
805
- other.outputTokens += model.outputTokens;
806
- if (model.cacheObserved) cacheObserved = true;
807
- other.cachedInputTokens = (other.cachedInputTokens ?? 0) + (model.cachedInputTokens ?? 0);
808
- other.cacheReadInputTokens = (other.cacheReadInputTokens ?? 0) + (model.cacheReadInputTokens ?? 0);
809
- other.cacheCreationInputTokens = (other.cacheCreationInputTokens ?? 0) + (model.cacheCreationInputTokens ?? 0);
810
- if (model.estimatedCostUsd !== undefined) {
811
- other.estimatedCostUsd = (other.estimatedCostUsd ?? 0) + model.estimatedCostUsd;
812
- }
813
- const key = usageModelKey(model.provider, model.model);
814
- for (const [requestId, statuses] of statusesByKey.get(key) ?? []) {
815
- const combined = statusesByRequest.get(requestId) ?? [];
816
- combined.push(...statuses);
817
- statusesByRequest.set(requestId, combined);
818
- }
819
- for (const reqId of pricedRequestsByModel.get(key) ?? []) overflowPricedRequests.add(reqId);
820
- for (const reqId of unpricedRequestsByModel.get(key) ?? []) overflowUnpricedRequests.add(reqId);
821
- }
822
- other.requests = statusesByRequest.size;
823
- other.pricedRequests = overflowPricedRequests.size;
824
- other.unpricedRequests = overflowUnpricedRequests.size;
825
- for (const statuses of statusesByRequest.values()) {
826
- const status = foldAttributionStatuses(statuses);
827
- if (isMeasuredStatus(status)) other.measuredRequests += 1;
828
- if (status === "reported") other.reportedRequests += 1;
829
- else if (status === "estimated") other.estimatedRequests += 1;
830
- }
831
- other.shareRatio = totalTokens === 0 ? 0 : other.totalTokens / totalTokens;
832
- other.cacheHitRate = calculateCacheHitRate(cacheObserved, other.inputTokens, other.cacheReadInputTokens ?? 0);
833
- other.priceCoverageRatio = other.requests > 0 ? other.pricedRequests / other.requests : 0;
834
- return other;
835
922
  });
836
- for (const model of retained) delete model.cacheObserved;
837
- return retained;
838
923
  }
839
924
 
840
- function buildProviders(entries: PersistedUsageEntry[], totalTokens: number, costMap: Map<PersistedUsageEntry, EntryCostInfo>): UsageProvider[] {
841
- interface ProviderAccumulator extends UsageProvider {
842
- cacheObserved?: boolean;
925
+ function buildUsageProviders(
926
+ models: Map<string, UsageModelAccumulator>,
927
+ totalTokens: number,
928
+ ): UsageProvider[] {
929
+ const providers = new Map<string, UsageModelAccumulator>();
930
+ for (const model of models.values()) {
931
+ const current = providers.get(model.provider);
932
+ if (current) mergeModelAccumulator(current, model);
933
+ else providers.set(model.provider, cloneModelAccumulator(model));
934
+ }
935
+ return [...providers.values()]
936
+ .sort((a, b) => requestCountsFor(b).requests - requestCountsFor(a).requests || a.firstSeen - b.firstSeen)
937
+ .map(provider => {
938
+ const counts = requestCountsFor(provider);
939
+ const requests = counts.requests;
940
+ return {
941
+ provider: provider.provider,
942
+ requests,
943
+ attemptCount: provider.attemptCount,
944
+ measuredRequests: counts.measuredRequests,
945
+ reportedRequests: counts.reportedRequests,
946
+ estimatedRequests: counts.estimatedRequests,
947
+ totalTokens: provider.summaryTotalTokens,
948
+ inputTokens: provider.inputTokens,
949
+ outputTokens: provider.outputTokens,
950
+ cachedInputTokens: provider.cacheReadInputTokens,
951
+ cacheReadInputTokens: provider.cacheReadInputTokens,
952
+ cacheCreationInputTokens: provider.cacheCreationInputTokens,
953
+ cacheHitRate: calculateCacheHitRate(provider.cacheObserved, provider.inputTokens, provider.cacheReadInputTokens),
954
+ priceCoverageRatio: requests > 0 ? counts.pricedRequests / requests : 0,
955
+ pricedRequests: counts.pricedRequests,
956
+ unpricedRequests: counts.unpricedRequests,
957
+ shareRatio: totalTokens === 0 ? 0 : provider.summaryTotalTokens / totalTokens,
958
+ ...(provider.estimatedCostUsd !== undefined ? { estimatedCostUsd: provider.estimatedCostUsd } : {}),
959
+ };
960
+ });
961
+ }
962
+
963
+ function buildUsageAccounts(accounts: Map<string, UsageAccountAccumulator>): UsageAccount[] {
964
+ return [...accounts.values()]
965
+ .sort((a, b) => b.totalTokens - a.totalTokens || a.firstSeen - b.firstSeen)
966
+ .map(account => ({
967
+ accountLogLabel: account.accountLogLabel,
968
+ ambiguous: account.ambiguous,
969
+ requests: account.requestIds?.size ?? account.requests,
970
+ attemptCount: account.attemptCount,
971
+ measuredAttempts: account.measuredAttempts,
972
+ reportedAttempts: account.reportedAttempts,
973
+ estimatedAttempts: account.estimatedAttempts,
974
+ unmeteredAttempts: account.unmeteredAttempts,
975
+ inputTokens: account.inputTokens,
976
+ outputTokens: account.outputTokens,
977
+ cacheReadInputTokens: account.cacheReadInputTokens,
978
+ cacheCreationInputTokens: account.cacheCreationInputTokens,
979
+ reasoningOutputTokens: account.reasoningOutputTokens,
980
+ totalTokens: account.totalTokens,
981
+ usageCoverageRatio: account.attemptCount === 0 ? 0 : account.measuredAttempts / account.attemptCount,
982
+ ...(account.estimatedCostUsd !== undefined ? { estimatedCostUsd: account.estimatedCostUsd } : {}),
983
+ pricedAttempts: account.pricedAttempts,
984
+ unpricedAttempts: account.unpricedAttempts,
985
+ priceCoverageRatio: account.measuredAttempts === 0 ? 0 : account.pricedAttempts / account.measuredAttempts,
986
+ }));
987
+ }
988
+
989
+ // Retained-size estimates intentionally favor over-counting. They are updated only when
990
+ // retained structures grow, so memory-budget checks stay O(1) even on very large ledgers.
991
+ const ESTIMATED_ACCUMULATOR_BASE_BYTES = 2_048;
992
+ const ESTIMATED_PARTITION_BYTES = 1_024;
993
+ const ESTIMATED_BREAKDOWN_BYTES = 1_024;
994
+ const ESTIMATED_EXACT_REQUEST_ID_BYTES = 1_024;
995
+ const ESTIMATED_REQUEST_FACT_BYTES = 512;
996
+ const ESTIMATED_ACCOUNT_REQUEST_BYTES = 256;
997
+ const ESTIMATED_OVERLAP_BYTES = 128;
998
+ const ESTIMATED_OVERLAP_MODEL_BYTES = 256;
999
+
1000
+ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
1001
+ private readonly partitions = new Map<string, UsagePartition>();
1002
+ private readonly requestIds: Map<string, number> | null;
1003
+ private readonly filter: NormalizedUsageFilter | null;
1004
+ private readonly mode: UsageAccumulatorMode;
1005
+ private nextRequestId = 0;
1006
+ private nextOrdinal = 0;
1007
+ private snapshotStart: number | null = null;
1008
+ private snapshotEnd: number | null = null;
1009
+ private comboOverlap = false;
1010
+ private estimatedRetainedBytes = ESTIMATED_ACCUMULATOR_BASE_BYTES;
1011
+
1012
+ constructor(options?: {
1013
+ filter?: { provider?: string | null; model?: string | null; apiKeyId?: string | null };
1014
+ mode?: UsageAccumulatorMode;
1015
+ }) {
1016
+ const provider = normalizeFilterValue(options?.filter?.provider);
1017
+ const model = normalizeFilterValue(options?.filter?.model);
1018
+ const apiKeyId = normalizeExactFilterValue(options?.filter?.apiKeyId);
1019
+ this.filter = provider === null && model === null && apiKeyId === null
1020
+ ? null
1021
+ : { provider, model, apiKeyId };
1022
+ this.mode = options?.mode ?? "exact";
1023
+ this.requestIds = this.mode === "exact" ? new Map() : null;
843
1024
  }
844
- const byKey = new Map<string, ProviderAccumulator>();
845
- const statusesByKey = new Map<string, Map<string, UsageStatus[]>>();
846
- for (const entry of entries) {
847
- for (const attribution of usageAttributions(entry)) {
848
- const providerKey = baseProviderLabel(attribution.provider);
849
- let provider = byKey.get(providerKey);
850
- if (!provider) {
851
- provider = {
852
- provider: providerKey,
853
- requests: 0,
854
- attemptCount: 0,
855
- measuredRequests: 0,
856
- reportedRequests: 0,
857
- estimatedRequests: 0,
858
- totalTokens: 0,
859
- inputTokens: 0,
860
- outputTokens: 0,
861
- cachedInputTokens: 0,
862
- cacheReadInputTokens: 0,
863
- cacheCreationInputTokens: 0,
864
- pricedRequests: 0,
865
- unpricedRequests: 0,
866
- priceCoverageRatio: 0,
867
- shareRatio: 0,
868
- };
869
- byKey.set(providerKey, provider);
1025
+
1026
+ get snapshotWindow(): { start: number | null; end: number | null } {
1027
+ return { start: this.snapshotStart, end: this.snapshotEnd };
1028
+ }
1029
+
1030
+ get estimatedBytes(): number {
1031
+ return this.estimatedRetainedBytes;
1032
+ }
1033
+
1034
+ clone(): UsageSummaryAccumulator {
1035
+ const cloned = new StreamingUsageSummaryAccumulator({
1036
+ ...(this.filter ? { filter: this.filter } : {}),
1037
+ mode: this.mode,
1038
+ });
1039
+ cloned.nextRequestId = this.nextRequestId;
1040
+ cloned.nextOrdinal = this.nextOrdinal;
1041
+ cloned.snapshotStart = this.snapshotStart;
1042
+ cloned.snapshotEnd = this.snapshotEnd;
1043
+ cloned.comboOverlap = this.comboOverlap;
1044
+ cloned.estimatedRetainedBytes = this.estimatedRetainedBytes;
1045
+ if (this.requestIds && cloned.requestIds) {
1046
+ for (const [requestId, key] of this.requestIds) cloned.requestIds.set(requestId, key);
1047
+ }
1048
+ for (const [key, partition] of this.partitions) {
1049
+ const models = new Map<string, UsageModelAccumulator>();
1050
+ for (const [modelKey, model] of partition.models) {
1051
+ models.set(modelKey, cloneModelAccumulator(model));
870
1052
  }
871
- provider.attemptCount += 1;
872
- let requests = statusesByKey.get(providerKey);
873
- if (!requests) { requests = new Map(); statusesByKey.set(providerKey, requests); }
874
- const statuses = requests.get(attribution.requestId) ?? [];
875
- statuses.push(attribution.usageStatus);
876
- requests.set(attribution.requestId, statuses);
877
- if (attribution.usage) {
878
- provider.inputTokens = (provider.inputTokens ?? 0) + attribution.usage.inputTokens;
879
- provider.outputTokens = (provider.outputTokens ?? 0) + attribution.usage.outputTokens;
880
- const { read, creation, hasCacheTelemetry } = cacheTokensFromUsage(attribution.usage);
881
- if (hasCacheTelemetry) provider.cacheObserved = true;
882
- if (typeof read === "number") {
883
- provider.cachedInputTokens = (provider.cachedInputTokens ?? 0) + read;
884
- provider.cacheReadInputTokens = (provider.cacheReadInputTokens ?? 0) + read;
885
- }
886
- if (typeof creation === "number") {
887
- provider.cacheCreationInputTokens = (provider.cacheCreationInputTokens ?? 0) + creation;
888
- }
889
- provider.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
1053
+ const providers = partition.providers
1054
+ ? new Map([...partition.providers].map(([providerKey, provider]) => [providerKey, cloneModelAccumulator(provider)]))
1055
+ : undefined;
1056
+ const accounts = new Map<string, UsageAccountAccumulator>();
1057
+ for (const [label, account] of partition.accounts) {
1058
+ accounts.set(label, cloneAccountAccumulator(account));
890
1059
  }
1060
+ cloned.partitions.set(key, {
1061
+ ...partition,
1062
+ totals: { ...partition.totals },
1063
+ models,
1064
+ ...(providers ? { providers } : {}),
1065
+ accounts,
1066
+ modelOverlaps: new Map(
1067
+ [...partition.modelOverlaps].map(([signature, overlap]) => [signature, { ...overlap }]),
1068
+ ),
1069
+ });
891
1070
  }
1071
+ return cloned;
1072
+ }
1073
+
1074
+ private requestKey(requestId: string): number {
1075
+ if (!this.requestIds) throw new Error("row-unique accumulators do not retain request ids");
1076
+ const existing = this.requestIds.get(requestId);
1077
+ if (existing !== undefined) return existing;
1078
+ const key = this.nextRequestId++;
1079
+ this.requestIds.set(requestId, key);
1080
+ this.estimatedRetainedBytes += ESTIMATED_EXACT_REQUEST_ID_BYTES + requestId.length * 2;
1081
+ return key;
892
1082
  }
893
- for (const [key, provider] of byKey) {
894
- const groups = statusesByKey.get(key) ?? new Map();
895
- provider.requests = groups.size;
896
- for (const statuses of groups.values()) {
897
- const status = foldAttributionStatuses(statuses);
898
- if (isMeasuredStatus(status)) provider.measuredRequests += 1;
899
- if (status === "reported") provider.reportedRequests += 1;
900
- else if (status === "estimated") provider.estimatedRequests += 1;
1083
+
1084
+ private partitionFor(entry: PersistedUsageEntry): UsagePartition {
1085
+ const date = localDateKey(entry.timestamp);
1086
+ const dayStart = startOfLocalDay(entry.timestamp);
1087
+ const surface = usagePartitionSurface(entry);
1088
+ const key = `${date}\0${surface}`;
1089
+ let partition = this.partitions.get(key);
1090
+ if (!partition) {
1091
+ partition = {
1092
+ date,
1093
+ dayStart,
1094
+ surface,
1095
+ oldestTimestamp: null,
1096
+ totals: blankTotals(),
1097
+ models: new Map(),
1098
+ ...(this.mode === "row-unique" ? { providers: new Map() } : {}),
1099
+ accounts: new Map(),
1100
+ modelOverlaps: new Map(),
1101
+ };
1102
+ this.partitions.set(key, partition);
1103
+ this.estimatedRetainedBytes += ESTIMATED_PARTITION_BYTES;
901
1104
  }
1105
+ if (Number.isFinite(entry.timestamp)) {
1106
+ partition.oldestTimestamp = partition.oldestTimestamp === null
1107
+ ? entry.timestamp
1108
+ : Math.min(partition.oldestTimestamp, entry.timestamp);
1109
+ }
1110
+ return partition;
902
1111
  }
903
- const pricedRequestsByProvider = new Map<string, Set<string>>();
904
- const unpricedRequestsByProvider = new Map<string, Set<string>>();
905
- for (const entry of entries) {
906
- const costInfo = costMap.get(entry);
907
- if (entry.attempts?.length) {
908
- const attemptEstimates = costInfo?.attemptEstimates;
909
- for (let i = 0; i < entry.attempts.length; i++) {
910
- const attempt = entry.attempts[i];
911
- const attemptEst = attemptEstimates?.[i];
912
- const aProviderKey = baseProviderLabel(attempt.provider);
913
- if (attemptEst) {
914
- const p = byKey.get(aProviderKey);
915
- if (p) {
916
- p.estimatedCostUsd = (p.estimatedCostUsd ?? 0) + attemptEst.cost.total;
917
- }
918
- let s = pricedRequestsByProvider.get(aProviderKey);
919
- if (!s) { s = new Set(); pricedRequestsByProvider.set(aProviderKey, s); }
920
- s.add(entry.requestId);
921
- } else {
922
- let s = unpricedRequestsByProvider.get(aProviderKey);
923
- if (!s) { s = new Set(); unpricedRequestsByProvider.set(aProviderKey, s); }
924
- s.add(entry.requestId);
925
- }
926
- }
927
- } else {
928
- const providerKey = baseProviderLabel(entry.provider);
929
- const estimate = costInfo?.estimate;
930
- if (estimate) {
931
- const p = byKey.get(providerKey);
932
- if (p) {
933
- p.estimatedCostUsd = (p.estimatedCostUsd ?? 0) + estimate.cost.total;
934
- }
935
- let s = pricedRequestsByProvider.get(providerKey);
936
- if (!s) { s = new Set(); pricedRequestsByProvider.set(providerKey, s); }
937
- s.add(entry.requestId);
938
- } else {
939
- let s = unpricedRequestsByProvider.get(providerKey);
940
- if (!s) { s = new Set(); unpricedRequestsByProvider.set(providerKey, s); }
941
- s.add(entry.requestId);
942
- }
1112
+
1113
+ private addAttributionMetrics(
1114
+ breakdown: UsageModelAccumulator,
1115
+ attribution: UsageAttribution,
1116
+ estimate: AttemptCostEstimate | CostEstimate | null,
1117
+ ): void {
1118
+ breakdown.attemptCount += 1;
1119
+ if (attribution.usage) {
1120
+ breakdown.inputTokens += attribution.usage.inputTokens;
1121
+ breakdown.outputTokens += attribution.usage.outputTokens;
1122
+ const { read, creation, hasCacheTelemetry } = cacheTokensFromUsage(attribution.usage);
1123
+ breakdown.cacheObserved ||= hasCacheTelemetry;
1124
+ if (typeof read === "number") breakdown.cacheReadInputTokens += read;
1125
+ if (typeof creation === "number") breakdown.cacheCreationInputTokens += creation;
1126
+ breakdown.summaryTotalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
943
1127
  }
1128
+ breakdown.dayTotalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
1129
+ if (estimate) breakdown.estimatedCostUsd = (breakdown.estimatedCostUsd ?? 0) + estimate.cost.total;
944
1130
  }
945
- const providers = [...byKey.values()];
946
- for (const [key, p] of byKey) {
947
- p.pricedRequests = pricedRequestsByProvider.get(key)?.size ?? 0;
948
- p.unpricedRequests = unpricedRequestsByProvider.get(key)?.size ?? 0;
949
- p.shareRatio = totalTokens === 0 ? 0 : p.totalTokens / totalTokens;
950
- p.cacheHitRate = calculateCacheHitRate(!!p.cacheObserved, p.inputTokens ?? 0, p.cacheReadInputTokens ?? 0);
951
- p.priceCoverageRatio = p.requests > 0 ? p.pricedRequests / p.requests : 0;
1131
+
1132
+ private addModelAttribution(
1133
+ partition: UsagePartition,
1134
+ attribution: UsageAttribution,
1135
+ estimate: AttemptCostEstimate | CostEstimate | null,
1136
+ ordinal: number,
1137
+ ): string {
1138
+ const provider = baseProviderLabel(attribution.provider);
1139
+ const key = usageModelKey(provider, attribution.model);
1140
+ let model = partition.models.get(key);
1141
+ if (!model) {
1142
+ model = blankModelAccumulator(provider, attribution.model, attribution.resolvedModel, ordinal, this.mode);
1143
+ partition.models.set(key, model);
1144
+ this.estimatedRetainedBytes += ESTIMATED_BREAKDOWN_BYTES + key.length * 2;
1145
+ }
1146
+ this.addAttributionMetrics(model, attribution, estimate);
1147
+ return key;
952
1148
  }
953
- const sorted = providers.sort((a, b) => b.requests - a.requests);
954
- for (const provider of sorted) delete provider.cacheObserved;
955
- return sorted;
956
- }
957
1149
 
958
- const LEGACY_AMBIGUOUS_ACCOUNT_LABEL = "legacy-ambiguous";
1150
+ private addProviderAttribution(
1151
+ partition: UsagePartition,
1152
+ attribution: UsageAttribution,
1153
+ estimate: AttemptCostEstimate | CostEstimate | null,
1154
+ ordinal: number,
1155
+ ): string {
1156
+ const providerKey = baseProviderLabel(attribution.provider);
1157
+ const providers = partition.providers;
1158
+ if (!providers) return providerKey;
1159
+ let provider = providers.get(providerKey);
1160
+ if (!provider) {
1161
+ provider = blankModelAccumulator(providerKey, "", undefined, ordinal, "row-unique");
1162
+ providers.set(providerKey, provider);
1163
+ this.estimatedRetainedBytes += ESTIMATED_BREAKDOWN_BYTES + providerKey.length * 2;
1164
+ }
1165
+ this.addAttributionMetrics(provider, attribution, estimate);
1166
+ return providerKey;
1167
+ }
959
1168
 
960
- function legacyCodexAccountLabel(provider: string): string | null {
961
- if (baseProviderLabel(provider) !== "openai") return null;
962
- const suffix = provider.match(/-(main|p[a-f0-9]{6})$/)?.[1];
963
- return suffix ?? LEGACY_AMBIGUOUS_ACCOUNT_LABEL;
964
- }
1169
+ private addBreakdownRequest(
1170
+ breakdown: UsageModelAccumulator,
1171
+ facts: number,
1172
+ requestKey: number | null,
1173
+ ): void {
1174
+ if (breakdown.requestFacts) {
1175
+ if (requestKey === null) throw new Error("exact accumulators require request identity");
1176
+ const previous = breakdown.requestFacts.get(requestKey);
1177
+ breakdown.requestFacts.set(requestKey, (previous ?? 0) | facts);
1178
+ if (previous === undefined) this.estimatedRetainedBytes += ESTIMATED_REQUEST_FACT_BYTES;
1179
+ return;
1180
+ }
1181
+ bumpRequestCounts(breakdown.requestCounts, facts);
1182
+ }
965
1183
 
966
- /**
967
- * An explicitly stamped label of EITHER family is authoritative for any provider (#2699).
968
- *
969
- * No `o`-label branch is needed here: `isCodexUsageAccountLogLabel` now accepts both families,
970
- * and adding a second predicate call would be a no-op guarded by a comment claiming otherwise.
971
- *
972
- * The legacy fallback stays openai-only on purpose. It infers an account from the PROVIDER
973
- * string, and inferring for a non-Codex row would merge unrelated accounts under one label --
974
- * so an unlabeled xai row is dropped from the account table rather than guessed at.
975
- */
976
- function accountLabelForAttribution(provider: string, explicit: unknown): string | null {
977
- if (isCodexUsageAccountLogLabel(explicit)) return explicit;
978
- return legacyCodexAccountLabel(provider);
979
- }
1184
+ private addAccountRequest(account: UsageAccountAccumulator, requestKey: number | null): void {
1185
+ if (account.requestIds) {
1186
+ if (requestKey === null) throw new Error("exact accumulators require request identity");
1187
+ const previousSize = account.requestIds.size;
1188
+ account.requestIds.add(requestKey);
1189
+ if (account.requestIds.size !== previousSize) {
1190
+ this.estimatedRetainedBytes += ESTIMATED_ACCOUNT_REQUEST_BYTES;
1191
+ }
1192
+ return;
1193
+ }
1194
+ account.requests += 1;
1195
+ }
980
1196
 
981
- function buildAccounts(entries: PersistedUsageEntry[], costMap: Map<PersistedUsageEntry, EntryCostInfo>): UsageAccount[] {
982
- const byLabel = new Map<string, UsageAccount>();
983
- const requestIds = new Map<string, Set<string>>();
984
-
985
- const add = (input: {
986
- requestId: string;
987
- provider: string;
988
- accountLogLabel?: string;
989
- usageStatus: UsageStatus;
990
- usage?: PersistedUsageEntry["usage"];
991
- totalTokens?: number;
992
- estimate: AttemptCostEstimate | CostEstimate | null;
993
- }): void => {
994
- const label = accountLabelForAttribution(input.provider, input.accountLogLabel);
995
- if (!label) return;
996
- let row = byLabel.get(label);
997
- if (!row) {
998
- row = {
1197
+ private addAccountAttribution(
1198
+ partition: UsagePartition,
1199
+ attribution: UsageAttribution,
1200
+ estimate: AttemptCostEstimate | CostEstimate | null,
1201
+ ordinal: number,
1202
+ ): string | null {
1203
+ const label = accountLabelForAttribution(attribution.provider, attribution.accountLogLabel);
1204
+ if (!label) return null;
1205
+ let account = partition.accounts.get(label);
1206
+ if (!account) {
1207
+ account = {
999
1208
  accountLogLabel: label,
1000
1209
  ambiguous: label === LEGACY_AMBIGUOUS_ACCOUNT_LABEL,
1210
+ firstSeen: ordinal,
1001
1211
  requests: 0,
1212
+ ...(this.mode === "exact" ? { requestIds: new Set() } : {}),
1002
1213
  attemptCount: 0,
1003
1214
  measuredAttempts: 0,
1004
1215
  reportedAttempts: 0,
@@ -1010,77 +1221,222 @@ function buildAccounts(entries: PersistedUsageEntry[], costMap: Map<PersistedUsa
1010
1221
  cacheCreationInputTokens: 0,
1011
1222
  reasoningOutputTokens: 0,
1012
1223
  totalTokens: 0,
1013
- usageCoverageRatio: 0,
1014
1224
  pricedAttempts: 0,
1015
1225
  unpricedAttempts: 0,
1016
- priceCoverageRatio: 0,
1017
1226
  };
1018
- byLabel.set(label, row);
1019
- requestIds.set(label, new Set());
1227
+ partition.accounts.set(label, account);
1228
+ this.estimatedRetainedBytes += ESTIMATED_BREAKDOWN_BYTES + label.length * 2;
1020
1229
  }
1021
- requestIds.get(label)!.add(input.requestId);
1022
- row.requests = requestIds.get(label)!.size;
1023
- row.attemptCount += 1;
1024
- const measured = input.usage !== undefined && isMeasuredStatus(input.usageStatus);
1025
- if (!measured) {
1026
- row.unmeteredAttempts += 1;
1027
- return;
1230
+ account.attemptCount += 1;
1231
+ if (!attribution.usage || !isMeasuredStatus(attribution.usageStatus)) {
1232
+ account.unmeteredAttempts += 1;
1233
+ return label;
1028
1234
  }
1029
-
1030
- row.measuredAttempts += 1;
1031
- if (input.usageStatus === "reported") row.reportedAttempts += 1;
1032
- else if (input.usageStatus === "estimated") row.estimatedAttempts += 1;
1033
- row.inputTokens += input.usage!.inputTokens;
1034
- row.outputTokens += input.usage!.outputTokens;
1035
- const { read, creation } = cacheTokensFromUsage(input.usage);
1036
- if (typeof read === "number") row.cacheReadInputTokens += read;
1037
- if (typeof creation === "number") row.cacheCreationInputTokens += creation;
1038
- if (typeof input.usage!.reasoningOutputTokens === "number") {
1039
- row.reasoningOutputTokens += input.usage!.reasoningOutputTokens;
1235
+ account.measuredAttempts += 1;
1236
+ if (attribution.usageStatus === "reported") account.reportedAttempts += 1;
1237
+ else if (attribution.usageStatus === "estimated") account.estimatedAttempts += 1;
1238
+ account.inputTokens += attribution.usage.inputTokens;
1239
+ account.outputTokens += attribution.usage.outputTokens;
1240
+ const { read, creation } = cacheTokensFromUsage(attribution.usage);
1241
+ if (typeof read === "number") account.cacheReadInputTokens += read;
1242
+ if (typeof creation === "number") account.cacheCreationInputTokens += creation;
1243
+ if (typeof attribution.usage.reasoningOutputTokens === "number") {
1244
+ account.reasoningOutputTokens += attribution.usage.reasoningOutputTokens;
1040
1245
  }
1041
- row.totalTokens += usageDisplayTotalTokens(input.usage, input.totalTokens) ?? 0;
1042
- if (input.estimate) {
1043
- row.pricedAttempts += 1;
1044
- row.estimatedCostUsd = (row.estimatedCostUsd ?? 0) + input.estimate.cost.total;
1246
+ account.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
1247
+ if (estimate) {
1248
+ account.pricedAttempts += 1;
1249
+ account.estimatedCostUsd = (account.estimatedCostUsd ?? 0) + estimate.cost.total;
1045
1250
  } else {
1046
- row.unpricedAttempts += 1;
1251
+ account.unpricedAttempts += 1;
1047
1252
  }
1048
- };
1253
+ return label;
1254
+ }
1255
+
1256
+ add(sourceEntry: PersistedUsageEntry): void {
1257
+ if (Number.isFinite(sourceEntry.timestamp)) {
1258
+ this.snapshotStart = this.snapshotStart === null
1259
+ ? sourceEntry.timestamp
1260
+ : Math.min(this.snapshotStart, sourceEntry.timestamp);
1261
+ this.snapshotEnd = this.snapshotEnd === null
1262
+ ? sourceEntry.timestamp
1263
+ : Math.max(this.snapshotEnd, sourceEntry.timestamp);
1264
+ }
1265
+ const projected = this.filter ? projectedEntryForFilter(sourceEntry, this.filter) : { entry: sourceEntry, comboOverlap: false };
1266
+ if (!projected) return;
1267
+ this.comboOverlap ||= projected.comboOverlap;
1268
+ const entry = projected.entry;
1269
+ const partition = this.partitionFor(entry);
1270
+ const costInfo = computeEntryCost(entry);
1271
+ bumpStatus(partition.totals, entry.usageStatus);
1272
+ partition.totals.attemptCount += entry.attempts?.length ?? 1;
1273
+ addTokens(partition.totals, entry);
1274
+ addEstimatedCost(partition.totals, entry, costInfo);
1049
1275
 
1050
- for (const entry of entries) {
1051
- const costInfo = costMap.get(entry);
1052
- if (entry.attempts?.length) {
1053
- for (let i = 0; i < entry.attempts.length; i++) {
1054
- const attempt = entry.attempts[i];
1055
- const attemptEst = costInfo?.attemptEstimates?.[i] ?? null;
1056
- add({
1057
- requestId: entry.requestId,
1058
- provider: attempt.provider,
1059
- ...(attempt.accountLogLabel ? { accountLogLabel: attempt.accountLogLabel } : {}),
1060
- usageStatus: attempt.usageStatus,
1061
- ...(attempt.usage ? { usage: attempt.usage } : {}),
1062
- ...(attempt.totalTokens !== undefined ? { totalTokens: attempt.totalTokens } : {}),
1063
- estimate: attemptEst,
1064
- });
1276
+ const requestKey = this.mode === "exact" ? this.requestKey(entry.requestId) : null;
1277
+ const attributions = usageAttributions(entry);
1278
+ const modelFacts = new Map<string, number>();
1279
+ const providerFacts = new Map<string, number>();
1280
+ const accountLabels = new Set<string>();
1281
+ for (let index = 0; index < attributions.length; index++) {
1282
+ const attribution = attributions[index]!;
1283
+ const estimate = entry.attempts?.length
1284
+ ? costInfo.attemptEstimates?.[index] ?? null
1285
+ : costInfo.estimate;
1286
+ const ordinal = this.nextOrdinal++;
1287
+ const facts = requestStatusFact(attribution.usageStatus)
1288
+ | (estimate ? REQUEST_PRICED : REQUEST_UNPRICED);
1289
+ const modelKey = this.addModelAttribution(partition, attribution, estimate, ordinal);
1290
+ modelFacts.set(modelKey, (modelFacts.get(modelKey) ?? 0) | facts);
1291
+ if (this.mode === "row-unique") {
1292
+ const providerKey = this.addProviderAttribution(partition, attribution, estimate, ordinal);
1293
+ providerFacts.set(providerKey, (providerFacts.get(providerKey) ?? 0) | facts);
1294
+ }
1295
+ const accountLabel = this.addAccountAttribution(partition, attribution, estimate, ordinal);
1296
+ if (accountLabel) accountLabels.add(accountLabel);
1297
+ }
1298
+ for (const [modelKey, facts] of modelFacts) {
1299
+ this.addBreakdownRequest(partition.models.get(modelKey)!, facts, requestKey);
1300
+ }
1301
+ if (partition.providers) {
1302
+ for (const [providerKey, facts] of providerFacts) {
1303
+ this.addBreakdownRequest(partition.providers.get(providerKey)!, facts, null);
1304
+ }
1305
+ }
1306
+ for (const label of accountLabels) {
1307
+ this.addAccountRequest(partition.accounts.get(label)!, requestKey);
1308
+ }
1309
+ if (this.mode === "row-unique" && modelFacts.size > 1) {
1310
+ const models = [...modelFacts].sort(([a], [b]) => a.localeCompare(b));
1311
+ const signature = JSON.stringify(models);
1312
+ const overlap = partition.modelOverlaps.get(signature);
1313
+ if (overlap) {
1314
+ overlap.count += 1;
1315
+ } else {
1316
+ partition.modelOverlaps.set(signature, { models, count: 1 });
1317
+ this.estimatedRetainedBytes += ESTIMATED_OVERLAP_BYTES
1318
+ + models.length * ESTIMATED_OVERLAP_MODEL_BYTES
1319
+ + signature.length * 2;
1065
1320
  }
1066
- continue;
1067
1321
  }
1068
- add({
1069
- requestId: entry.requestId,
1070
- provider: entry.provider,
1071
- ...(entry.accountLogLabel ? { accountLogLabel: entry.accountLogLabel } : {}),
1072
- usageStatus: entry.usageStatus,
1073
- ...(entry.usage ? { usage: entry.usage } : {}),
1074
- ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
1075
- estimate: costInfo?.estimate ?? null,
1076
- });
1077
1322
  }
1078
1323
 
1079
- for (const row of byLabel.values()) {
1080
- row.usageCoverageRatio = row.attemptCount === 0 ? 0 : row.measuredAttempts / row.attemptCount;
1081
- row.priceCoverageRatio = row.measuredAttempts === 0 ? 0 : row.pricedAttempts / row.measuredAttempts;
1324
+ summarize(
1325
+ range: UsageRange,
1326
+ now: number,
1327
+ surface: UsageSurface = "all",
1328
+ ): UsageSummary & { filter?: UsageFilterEcho } {
1329
+ const { since, days: fixedDays } = rangeWindow(range, now);
1330
+ const totals = blankTotals();
1331
+ const models = new Map<string, UsageModelAccumulator>();
1332
+ const providers = new Map<string, UsageModelAccumulator>();
1333
+ const accounts = new Map<string, UsageAccountAccumulator>();
1334
+ const dayAccumulators = new Map<string, UsageDayAccumulator>();
1335
+ const modelOverlaps: UsageModelOverlap[] = [];
1336
+ let oldestTimestamp: number | null = null;
1337
+
1338
+ for (const partition of this.partitions.values()) {
1339
+ if (!usageSurfaceMatches(partition.surface, surface)) continue;
1340
+ if (since !== null && partition.dayStart < since) continue;
1341
+ mergeTotals(totals, partition.totals);
1342
+ mergeModelMaps(models, partition.models);
1343
+ if (partition.providers) mergeModelMaps(providers, partition.providers);
1344
+ modelOverlaps.push(...partition.modelOverlaps.values());
1345
+ for (const [label, account] of partition.accounts) {
1346
+ const current = accounts.get(label);
1347
+ if (current) mergeAccountAccumulator(current, account);
1348
+ else accounts.set(label, cloneAccountAccumulator(account));
1349
+ }
1350
+ if (partition.oldestTimestamp !== null) {
1351
+ oldestTimestamp = oldestTimestamp === null
1352
+ ? partition.oldestTimestamp
1353
+ : Math.min(oldestTimestamp, partition.oldestTimestamp);
1354
+ }
1355
+ let day = dayAccumulators.get(partition.date);
1356
+ if (!day) {
1357
+ day = { totals: blankTotals(), models: new Map(), modelOverlaps: [] };
1358
+ dayAccumulators.set(partition.date, day);
1359
+ }
1360
+ mergeTotals(day.totals, partition.totals);
1361
+ mergeModelMaps(day.models, partition.models);
1362
+ day.modelOverlaps.push(...partition.modelOverlaps.values());
1363
+ }
1364
+ finalizeCoverage(totals);
1365
+
1366
+ const dayCount = range === "all" ? dayCountForAllRange(oldestTimestamp, now) : fixedDays;
1367
+ const startOfToday = startOfLocalDay(now);
1368
+ const firstVisibleDay = new Date(startOfToday);
1369
+ firstVisibleDay.setDate(firstVisibleDay.getDate() - dayCount + 1);
1370
+ const firstVisibleDate = localDateKey(firstVisibleDay.getTime());
1371
+ const lastVisibleDate = localDateKey(startOfToday);
1372
+ for (let offset = dayCount - 1; offset >= 0; offset--) {
1373
+ const date = new Date(startOfToday);
1374
+ date.setDate(date.getDate() - offset);
1375
+ const key = localDateKey(date.getTime());
1376
+ if (!dayAccumulators.has(key)) {
1377
+ dayAccumulators.set(key, { totals: blankTotals(), models: new Map(), modelOverlaps: [] });
1378
+ }
1379
+ }
1380
+ const days = [...dayAccumulators]
1381
+ // All-history totals, models, providers, and accounts still cover every
1382
+ // retained row. Only the chart buckets are bounded so one malformed or
1383
+ // ancient timestamp cannot synthesize an enormous JSON response.
1384
+ .filter(([date]) => range !== "all"
1385
+ || (date >= firstVisibleDate && date <= lastVisibleDate))
1386
+ .sort(([a], [b]) => a.localeCompare(b))
1387
+ .map(([date, day]): UsageDay => ({
1388
+ date,
1389
+ requests: day.totals.requests,
1390
+ measuredRequests: day.totals.measuredRequests,
1391
+ reportedRequests: day.totals.reportedRequests,
1392
+ totalTokens: day.totals.totalTokens,
1393
+ estimatedCostUsd: day.totals.estimatedCostUsd,
1394
+ models: buildDayModels(day.models, day.modelOverlaps),
1395
+ }));
1396
+
1397
+ const summary: UsageSummary = {
1398
+ range,
1399
+ surface,
1400
+ since,
1401
+ generatedAt: now,
1402
+ summary: totals,
1403
+ days,
1404
+ models: buildUsageModels(models, totals.totalTokens, modelOverlaps),
1405
+ providers: buildUsageProviders(this.mode === "row-unique" ? providers : models, totals.totalTokens),
1406
+ accounts: buildUsageAccounts(accounts),
1407
+ };
1408
+ if (!this.filter) return summary;
1409
+ const matches = (provider: string, model: string): boolean =>
1410
+ filterMatchesAttribution(this.filter!, provider, model);
1411
+ const retainedModels = summary.models.filter(row => matches(row.provider, row.model));
1412
+ const retainedProviders = new Set(retainedModels.map(row => row.provider));
1413
+ return {
1414
+ ...summary,
1415
+ days: summary.days.map(day => ({
1416
+ ...day,
1417
+ models: day.models.filter(row => matches(row.provider, row.model)),
1418
+ })),
1419
+ models: retainedModels,
1420
+ providers: summary.providers.filter(row => retainedProviders.has(row.provider)),
1421
+ accounts: this.filter.provider === null && this.filter.model === null
1422
+ ? summary.accounts
1423
+ : [],
1424
+ filter: {
1425
+ provider: this.filter.provider,
1426
+ model: this.filter.model,
1427
+ apiKeyId: this.filter.apiKeyId,
1428
+ matched: summary.summary.requests > 0,
1429
+ comboOverlap: this.comboOverlap,
1430
+ },
1431
+ };
1082
1432
  }
1083
- return [...byLabel.values()].sort((a, b) => b.totalTokens - a.totalTokens);
1433
+ }
1434
+
1435
+ export function createUsageSummaryAccumulator(options?: {
1436
+ filter?: { provider?: string | null; model?: string | null; apiKeyId?: string | null };
1437
+ mode?: UsageAccumulatorMode;
1438
+ }): UsageSummaryAccumulator {
1439
+ return new StreamingUsageSummaryAccumulator(options);
1084
1440
  }
1085
1441
 
1086
1442
  export function summarizeUsage(
@@ -1089,40 +1445,9 @@ export function summarizeUsage(
1089
1445
  now: number,
1090
1446
  surface: UsageSurface = "all",
1091
1447
  ): UsageSummary {
1092
- const { since } = rangeWindow(range, now);
1093
- const filteredEntries = entries.filter(entry => {
1094
- if (since !== null && entry.timestamp < since) return false;
1095
- if (surface === "claude") return entry.surface === "claude" || entry.surface === "claude-desktop";
1096
- if (surface === "grok") return entry.surface === "grok";
1097
- // Codex = the historical unlabelled bucket. Before the grok tag existed every
1098
- // non-Claude turn landed here, and `surface !== "claude"` also swallowed
1099
- // claude-desktop — disjoint predicates fix both.
1100
- if (surface === "codex") return entry.surface === undefined;
1101
- return true;
1102
- });
1103
- const costMap = new Map<PersistedUsageEntry, EntryCostInfo>();
1104
- for (const entry of filteredEntries) {
1105
- costMap.set(entry, computeEntryCost(entry));
1106
- }
1107
- const totals = blankTotals();
1108
- for (const entry of filteredEntries) {
1109
- bumpStatus(totals, entry.usageStatus);
1110
- totals.attemptCount += entry.attempts?.length ?? 1;
1111
- addTokens(totals, entry);
1112
- addEstimatedCost(totals, entry, costMap.get(entry)!);
1113
- }
1114
- finalizeCoverage(totals);
1115
- return {
1116
- range,
1117
- surface,
1118
- since,
1119
- generatedAt: now,
1120
- summary: totals,
1121
- days: buildDayGrid(range, since, now, filteredEntries, costMap),
1122
- models: buildModels(filteredEntries, totals.totalTokens, costMap),
1123
- providers: buildProviders(filteredEntries, totals.totalTokens, costMap),
1124
- accounts: buildAccounts(filteredEntries, costMap),
1125
- };
1448
+ const accumulator = createUsageSummaryAccumulator();
1449
+ for (const entry of entries) accumulator.add(entry);
1450
+ return accumulator.summarize(range, now, surface);
1126
1451
  }
1127
1452
 
1128
1453
  function normalizeFilterValue(input: string | null | undefined): string | null {
@@ -1138,12 +1463,9 @@ function normalizeExactFilterValue(input: string | null | undefined): string | n
1138
1463
  /**
1139
1464
  * Narrow an already-summarised window to one provider and/or model.
1140
1465
  *
1141
- * Deliberately a projection over a finished summary rather than a parameter to
1142
- * {@link summarizeUsage}. The management route caches summaries under
1143
- * `range:surface` and warms that key space as a cross-product; a filtered
1144
- * summary that reached either would be served to the next UNFILTERED caller,
1145
- * the dashboard included. Keeping the filter outside the producer makes that
1146
- * mistake unrepresentable rather than merely discouraged.
1466
+ * The compatibility wrapper feeds source rows through a filter-bound streaming
1467
+ * accumulator. The management route can use the same accumulator directly and
1468
+ * still keep filtered results outside its unfiltered `range:surface` cache.
1147
1469
  *
1148
1470
  * Totals are recomputed from the retained rows. For combo traffic a request is
1149
1471
  * counted once per participating model, so a filtered request count can exceed
@@ -1165,78 +1487,16 @@ export function projectUsageSummary<T extends UsageSummary>(
1165
1487
  const model = normalizeFilterValue(filter.model);
1166
1488
  const apiKeyId = normalizeExactFilterValue(filter.apiKeyId);
1167
1489
  if (provider === null && model === null && apiKeyId === null) return summary;
1168
-
1169
- // Re-summarise from the entries the summary was built from, rather than
1170
- // projecting over its rows.
1171
- //
1172
- // Projecting rows looked cheaper and was wrong in three ways that only show
1173
- // up together: breakdown rows past MAX_USAGE_MODEL_BREAKDOWN_ROWS are
1174
- // collapsed into a synthetic "other" row, so a provider living only in that
1175
- // tail is unfindable and reports matched:false despite real usage; a
1176
- // provider row is a whole-provider aggregate, so a model filter kept the
1177
- // provider's OTHER models in providers[] while models[] and the totals
1178
- // excluded them, contradicting itself inside one response; and a model row
1179
- // carries a single optional cost, so priced/unpriced/unmetered counts could
1180
- // only be guessed per model rather than counted per request.
1181
- //
1182
- // Key ownership is the outer slice: no provider/model attribution or bucket
1183
- // construction may observe rows belonging to another client key.
1184
- const keyFilteredEntries = apiKeyId === null
1185
- ? entries ?? []
1186
- : (entries ?? []).filter(entry => entry.apiKeyId === apiKeyId);
1187
-
1188
- // The entries are already in hand on every path that filters, so the honest
1189
- // computation is also the simple one.
1190
- const matches = (rowProvider: string, rowModel: string): boolean => {
1191
- if (provider !== null && baseProviderLabel(rowProvider).toLowerCase() !== provider) return false;
1192
- if (model !== null && rowModel.toLowerCase() !== model) return false;
1193
- return true;
1194
- };
1195
-
1196
- // Narrow to matching ATTRIBUTIONS, not matching entries.
1197
- //
1198
- // Keeping a whole combo entry because one of its attempts matched drags the
1199
- // other attempts' tokens and cost into the filtered totals: a two-attempt
1200
- // combo filtered to its cheap model reported the expensive model's spend
1201
- // too. Rewriting the entry down to its matching attempts is what makes the
1202
- // filtered numbers mean what the flag says.
1203
- let comboOverlap = false;
1204
- const filtered: PersistedUsageEntry[] = [];
1205
- for (const entry of keyFilteredEntries) {
1206
- if (!entry.attempts?.length) {
1207
- const identity = usageModelIdentity(entry.provider, entry.model, entry.resolvedModel);
1208
- if (matches(entry.provider, identity.model)) filtered.push(entry);
1209
- continue;
1210
- }
1211
- const attempts = entry.attempts.filter(a => {
1212
- const identity = usageModelIdentity(a.provider, a.model);
1213
- return matches(a.provider, identity.model);
1214
- });
1215
- if (attempts.length === 0) continue;
1216
- if (entry.attempts.length > 1) comboOverlap = true;
1217
- const { usage: _parentUsage, totalTokens: _parentTotalTokens, ...withoutParentUsage } = entry;
1218
- filtered.push({ ...withoutParentUsage, attempts, ...projectedComboUsage(attempts) });
1219
- }
1220
-
1221
- const projected = summarizeUsage(filtered, summary.range, summary.generatedAt, summary.surface);
1222
- const matched = projected.summary.requests > 0;
1223
- const models = projected.models.filter(row => matches(row.provider, row.model));
1224
- const retainedProviders = new Set(models.map(row => row.provider));
1490
+ const accumulator = createUsageSummaryAccumulator({ filter: { provider, model, apiKeyId } });
1491
+ for (const entry of entries ?? []) accumulator.add(entry);
1492
+ const projected = accumulator.summarize(summary.range, summary.generatedAt, summary.surface);
1225
1493
  return {
1226
1494
  ...summary,
1227
1495
  summary: projected.summary,
1228
- days: projected.days.map(day => ({ ...day, models: day.models.filter(row => matches(row.provider, row.model)) })),
1229
- models,
1230
- providers: projected.providers.filter(row => retainedProviders.has(row.provider)),
1231
- // Account rows are not provider-partitioned in a way this projection could
1232
- // honestly re-derive, and unfiltered account totals sitting beside filtered
1233
- // model totals would invite exactly the wrong reading — so a provider or model
1234
- // filter drops them.
1235
- //
1236
- // An apiKeyId-only filter is different: it selects whole entries, so the account
1237
- // rows projected from those entries are exactly the accounts that key used. They
1238
- // are honest under that filter and are kept.
1239
- accounts: provider === null && model === null ? projected.accounts : [],
1240
- filter: { provider, model, apiKeyId, matched, comboOverlap },
1496
+ days: projected.days,
1497
+ models: projected.models,
1498
+ providers: projected.providers,
1499
+ accounts: projected.accounts,
1500
+ filter: projected.filter,
1241
1501
  };
1242
1502
  }