@bitkyc08/opencodex 2.40.0 → 2.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/gui/dist/assets/index-B2YjLA-i.css +1 -0
- package/gui/dist/assets/{index-BHe2rl_C.js → index-aPup8CKb.js} +20 -20
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/meta.svg +1 -0
- package/package.json +4 -3
- package/src/adapters/cursor/catalog.ts +71 -29
- package/src/adapters/cursor/claude-id.ts +76 -0
- package/src/adapters/cursor/discovery.ts +16 -3
- package/src/adapters/cursor/effort-map.ts +27 -12
- package/src/adapters/google.ts +39 -2
- package/src/adapters/openai-responses.ts +14 -1
- package/src/cli/claude.ts +11 -2
- package/src/cli/connect.ts +7 -1
- package/src/cli/registry.ts +1 -1
- package/src/cli/status.ts +19 -4
- package/src/client/connect.ts +5 -1
- package/src/client/hub-client.ts +29 -5
- package/src/clients/config-export.ts +12 -2
- package/src/codex/catalog/aggregation.ts +8 -0
- package/src/codex/catalog/metadata.ts +5 -0
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-fetch.ts +163 -26
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/desired-state.ts +18 -11
- package/src/combos/failover.ts +185 -6
- package/src/combos/index.ts +6 -0
- package/src/combos/resolve.ts +43 -6
- package/src/config.ts +5 -1
- package/src/generated/compatibility-version.json +85 -61
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/sync.ts +10 -2
- package/src/integrations/cursor-effort-table.ts +143 -0
- package/src/integrations/state.ts +1 -1
- package/src/integrations/writer.ts +2 -2
- package/src/lib/app-owned-memory-stores.ts +27 -8
- package/src/lib/bounded-body.ts +16 -1
- package/src/oauth/generic-account-failover.ts +2 -2
- package/src/oauth/index.ts +11 -0
- package/src/oauth/meta-muse.ts +235 -0
- package/src/providers/antigravity-models.ts +71 -13
- package/src/providers/command-code-efforts.ts +15 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/registry.ts +116 -8
- package/src/responses/code-mode-helper-compat.ts +4 -1
- package/src/responses/state.ts +5 -4
- package/src/server/auth-cors.ts +241 -56
- package/src/server/chat-completions.ts +11 -2
- package/src/server/chat-native.ts +30 -4
- package/src/server/claude-messages.ts +17 -3
- package/src/server/effort-row.ts +131 -0
- package/src/server/index.ts +67 -38
- package/src/server/management/api-key-rotation.ts +2 -1
- package/src/server/management/api-key-usage.ts +97 -43
- package/src/server/management/context.ts +3 -0
- package/src/server/management/cursor-integration-routes.ts +36 -7
- package/src/server/management/logs-usage-routes.ts +64 -87
- package/src/server/management/provider-routes.ts +218 -1
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management/usage-aggregate-cache.ts +464 -0
- package/src/server/management/usage-summary-cache.ts +4 -0
- package/src/server/models-capabilities.ts +60 -5
- package/src/server/responses/core.ts +61 -7
- package/src/types/config.ts +10 -1
- package/src/types/tools.ts +12 -9
- package/src/usage/expected-prices.ts +43 -7
- package/src/usage/ledger-scanner.ts +448 -0
- package/src/usage/log.ts +1 -1
- package/src/usage/summary.ts +915 -655
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-CJSb3HPe.css +0 -1
package/src/usage/summary.ts
CHANGED
|
@@ -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(
|
|
297
|
-
if (
|
|
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}
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
|
841
|
-
|
|
842
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
provider
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
1227
|
+
partition.accounts.set(label, account);
|
|
1228
|
+
this.estimatedRetainedBytes += ESTIMATED_BREAKDOWN_BYTES + label.length * 2;
|
|
1020
1229
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
-
|
|
1031
|
-
if (
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
if (typeof
|
|
1037
|
-
if (typeof
|
|
1038
|
-
|
|
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
|
-
|
|
1042
|
-
if (
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
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
|
|
1093
|
-
const
|
|
1094
|
-
|
|
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
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
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
|
-
|
|
1170
|
-
|
|
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
|
|
1229
|
-
models,
|
|
1230
|
-
providers: projected.providers
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
}
|