@oh-my-pi/omp-stats 18.3.5 → 18.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +13 -11
- package/build.ts +5 -49
- package/dist/client/index.css +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/index.js +105 -126
- package/dist/types/aggregator.d.ts +25 -22
- package/dist/types/client/api.d.ts +11 -7
- package/dist/types/client/app/LiveChip.d.ts +2 -0
- package/dist/types/client/app/Shell.d.ts +16 -0
- package/dist/types/client/app/ThemeToggle.d.ts +1 -0
- package/dist/types/client/app/nav.d.ts +18 -0
- package/dist/types/client/charts/BarList.d.ts +17 -0
- package/dist/types/client/charts/Chart.d.ts +49 -0
- package/dist/types/client/charts/Legend.d.ts +16 -0
- package/dist/types/client/charts/ShareBar.d.ts +11 -0
- package/dist/types/client/charts/Sparkline.d.ts +11 -0
- package/dist/types/client/charts/TimeChart.d.ts +9 -0
- package/dist/types/client/charts/index.d.ts +7 -0
- package/dist/types/client/charts/types.d.ts +25 -0
- package/dist/types/client/charts/useWidth.d.ts +3 -0
- package/dist/types/client/data/colors.d.ts +25 -0
- package/dist/types/client/data/formatters.d.ts +9 -1
- package/dist/types/client/data/live.d.ts +29 -0
- package/dist/types/client/data/query.d.ts +35 -0
- package/dist/types/client/data/range.d.ts +31 -0
- package/dist/types/client/data/series.d.ts +28 -0
- package/dist/types/client/data/useHashRoute.d.ts +18 -4
- package/dist/types/client/data/view-models.d.ts +111 -28
- package/dist/types/client/index.d.ts +0 -1
- package/dist/types/client/routes/CostsRoute.d.ts +2 -2
- package/dist/types/client/routes/ErrorsRoute.d.ts +2 -2
- package/dist/types/client/routes/FrustrationRoute.d.ts +7 -0
- package/dist/types/client/routes/GainRoute.d.ts +1 -2
- package/dist/types/client/routes/ModelsRoute.d.ts +2 -2
- package/dist/types/client/routes/OverviewRoute.d.ts +1 -2
- package/dist/types/client/routes/ProjectsRoute.d.ts +2 -2
- package/dist/types/client/routes/ProvidersRoute.d.ts +2 -2
- package/dist/types/client/routes/RequestsRoute.d.ts +1 -2
- package/dist/types/client/routes/ToolsRoute.d.ts +2 -2
- package/dist/types/client/routes/TracesRoute.d.ts +2 -2
- package/dist/types/client/routes/index.d.ts +1 -1
- package/dist/types/client/traces/AggregatesPanel.d.ts +4 -3
- package/dist/types/client/traces/SummaryStrip.d.ts +2 -2
- package/dist/types/client/traces/trace-colors.d.ts +18 -12
- package/dist/types/client/ui/Badge.d.ts +19 -0
- package/dist/types/client/ui/Card.d.ts +25 -0
- package/dist/types/client/ui/Drawer.d.ts +25 -0
- package/dist/types/client/ui/JsonBlock.d.ts +2 -2
- package/dist/types/client/ui/Modal.d.ts +23 -0
- package/dist/types/client/ui/RequestDrawer.d.ts +10 -1
- package/dist/types/client/ui/SearchInput.d.ts +8 -0
- package/dist/types/client/ui/Segmented.d.ts +15 -0
- package/dist/types/client/ui/Stat.d.ts +36 -0
- package/dist/types/client/ui/States.d.ts +41 -0
- package/dist/types/client/ui/Table.d.ts +48 -0
- package/dist/types/client/ui/index.d.ts +9 -10
- package/dist/types/db.d.ts +90 -106
- package/dist/types/frustration.d.ts +70 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/live.d.ts +30 -0
- package/dist/types/rollup.d.ts +115 -0
- package/dist/types/server.d.ts +6 -1
- package/dist/types/shared-types.d.ts +116 -56
- package/dist/types/types.d.ts +4 -0
- package/dist/types/user-metrics.d.ts +8 -0
- package/package.json +7 -13
- package/src/aggregator.ts +276 -211
- package/src/client/App.tsx +38 -80
- package/src/client/api.ts +65 -7
- package/src/client/app/LiveChip.tsx +97 -0
- package/src/client/app/Shell.tsx +181 -0
- package/src/client/app/ThemeToggle.tsx +5 -2
- package/src/client/app/nav.ts +78 -0
- package/src/client/charts/BarList.tsx +48 -0
- package/src/client/charts/Chart.tsx +388 -0
- package/src/client/charts/Legend.tsx +61 -0
- package/src/client/charts/ShareBar.tsx +26 -0
- package/src/client/charts/Sparkline.tsx +38 -0
- package/src/client/charts/TimeChart.tsx +22 -0
- package/src/client/charts/index.ts +7 -0
- package/src/client/charts/types.ts +27 -0
- package/src/client/charts/useWidth.ts +19 -0
- package/src/client/data/colors.ts +45 -0
- package/src/client/data/formatters.ts +30 -9
- package/src/client/data/live.tsx +85 -0
- package/src/client/data/query.ts +166 -0
- package/src/client/data/range.ts +82 -0
- package/src/client/data/series.ts +85 -0
- package/src/client/data/useHashRoute.ts +41 -83
- package/src/client/data/view-models.ts +347 -140
- package/src/client/index.tsx +3 -6
- package/src/client/routes/CostsRoute.tsx +394 -222
- package/src/client/routes/ErrorsRoute.tsx +401 -98
- package/src/client/routes/FrustrationRoute.tsx +924 -0
- package/src/client/routes/GainRoute.tsx +209 -206
- package/src/client/routes/ModelsRoute.tsx +540 -431
- package/src/client/routes/OverviewRoute.tsx +283 -311
- package/src/client/routes/ProjectsRoute.tsx +298 -156
- package/src/client/routes/ProvidersRoute.tsx +948 -416
- package/src/client/routes/RequestsRoute.tsx +295 -107
- package/src/client/routes/ToolsRoute.tsx +495 -398
- package/src/client/routes/TracesRoute.tsx +136 -117
- package/src/client/routes/costs.css +24 -0
- package/src/client/routes/errors.css +60 -0
- package/src/client/routes/frustration.css +123 -0
- package/src/client/routes/index.ts +1 -1
- package/src/client/routes/models.css +43 -0
- package/src/client/routes/projects.css +6 -0
- package/src/client/routes/providers.css +28 -0
- package/src/client/routes/tools.css +14 -0
- package/src/client/styles.css +1396 -1299
- package/src/client/traces/AggregatesPanel.tsx +55 -74
- package/src/client/traces/Minimap.tsx +6 -15
- package/src/client/traces/SpanDrawer.tsx +128 -194
- package/src/client/traces/SummaryStrip.tsx +21 -24
- package/src/client/traces/TimelineCanvas.tsx +27 -44
- package/src/client/traces/TraceView.tsx +175 -130
- package/src/client/traces/TranscriptList.tsx +13 -41
- package/src/client/traces/trace-colors.ts +60 -53
- package/src/client/traces/traces.css +279 -0
- package/src/client/ui/Badge.tsx +29 -0
- package/src/client/ui/Card.tsx +61 -0
- package/src/client/ui/Drawer.tsx +93 -0
- package/src/client/ui/JsonBlock.tsx +26 -44
- package/src/client/ui/Modal.tsx +106 -0
- package/src/client/ui/RequestDrawer.tsx +163 -188
- package/src/client/ui/SearchInput.tsx +25 -0
- package/src/client/ui/Segmented.tsx +61 -0
- package/src/client/ui/Stat.tsx +104 -0
- package/src/client/ui/States.tsx +81 -0
- package/src/client/ui/Table.tsx +229 -0
- package/src/client/ui/index.ts +9 -10
- package/src/client/ui/modal.css +54 -0
- package/src/client/ui/request-drawer.css +40 -0
- package/src/db.ts +379 -983
- package/src/frustration.ts +473 -0
- package/src/index.ts +22 -22
- package/src/live.ts +259 -0
- package/src/parser.ts +55 -66
- package/src/rollup.ts +951 -0
- package/src/server.ts +143 -87
- package/src/shared-types.ts +120 -56
- package/src/trace.ts +14 -7
- package/src/types.ts +4 -0
- package/src/user-metrics.ts +13 -0
- package/dist/client/styles.css +0 -1976
- package/dist/types/client/app/AppLayout.d.ts +0 -16
- package/dist/types/client/app/NavRail.d.ts +0 -7
- package/dist/types/client/app/RangeControl.d.ts +0 -7
- package/dist/types/client/app/SyncButton.d.ts +0 -14
- package/dist/types/client/app/TopBar.d.ts +0 -15
- package/dist/types/client/app/routes.d.ts +0 -12
- package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
- package/dist/types/client/components/chart-shared.d.ts +0 -178
- package/dist/types/client/components/models-table-shared.d.ts +0 -175
- package/dist/types/client/components/range-meta.d.ts +0 -21
- package/dist/types/client/data/charts.d.ts +0 -1
- package/dist/types/client/data/useResource.d.ts +0 -13
- package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
- package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
- package/dist/types/client/ui/DataTable.d.ts +0 -17
- package/dist/types/client/ui/EmptyState.d.ts +0 -7
- package/dist/types/client/ui/ErrorState.d.ts +0 -6
- package/dist/types/client/ui/MetricCluster.d.ts +0 -5
- package/dist/types/client/ui/Panel.d.ts +0 -7
- package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
- package/dist/types/client/ui/Skeleton.d.ts +0 -8
- package/dist/types/client/ui/StatusPill.d.ts +0 -7
- package/src/client/app/AppLayout.tsx +0 -93
- package/src/client/app/NavRail.tsx +0 -44
- package/src/client/app/RangeControl.tsx +0 -39
- package/src/client/app/SyncButton.tsx +0 -75
- package/src/client/app/TopBar.tsx +0 -73
- package/src/client/app/routes.ts +0 -93
- package/src/client/components/AgentTokenShare.tsx +0 -68
- package/src/client/components/chart-shared.tsx +0 -273
- package/src/client/components/models-table-shared.tsx +0 -255
- package/src/client/components/range-meta.ts +0 -73
- package/src/client/data/charts.ts +0 -14
- package/src/client/data/useResource.ts +0 -154
- package/src/client/routes/BehaviorRoute.tsx +0 -623
- package/src/client/ui/AsyncBoundary.tsx +0 -54
- package/src/client/ui/DataTable.tsx +0 -122
- package/src/client/ui/EmptyState.tsx +0 -16
- package/src/client/ui/ErrorState.tsx +0 -25
- package/src/client/ui/MetricCluster.tsx +0 -92
- package/src/client/ui/Panel.tsx +0 -24
- package/src/client/ui/SegmentedControl.tsx +0 -36
- package/src/client/ui/Skeleton.tsx +0 -17
- package/src/client/ui/StatusPill.tsx +0 -15
- package/tailwind.config.js +0 -40
package/src/aggregator.ts
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getStatsDbPath, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { getAgentDbPath, getStatsDbPath, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { withFileLock } from "@oh-my-pi/pi-utils/file-lock";
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
applySessionParseResults,
|
|
7
7
|
completeSessionSync,
|
|
8
8
|
getRecentErrors as dbGetRecentErrors,
|
|
9
9
|
getRecentRequests as dbGetRecentRequests,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
getBehaviorTimeSeries,
|
|
13
|
-
getCostTimeSeries,
|
|
14
|
-
getFileOffset,
|
|
10
|
+
type FileOffset,
|
|
11
|
+
getFileOffsets,
|
|
15
12
|
getMessageById,
|
|
16
13
|
getMessageCount,
|
|
14
|
+
initDb,
|
|
15
|
+
markSessionBackfillsComplete,
|
|
16
|
+
type ParsedSession,
|
|
17
|
+
prepareSessionSync,
|
|
18
|
+
} from "./db";
|
|
19
|
+
import {
|
|
20
|
+
getSessionEntry,
|
|
21
|
+
listAllSessionFiles,
|
|
22
|
+
matchesSessionFile,
|
|
23
|
+
type ParseSessionResult,
|
|
24
|
+
parseSessionFile,
|
|
25
|
+
type SessionParserState,
|
|
26
|
+
} from "./parser";
|
|
27
|
+
import {
|
|
28
|
+
getCostTimeSeries,
|
|
17
29
|
getModelPerformanceSeries,
|
|
18
30
|
getModelTimeSeries,
|
|
19
31
|
getOverallStats,
|
|
@@ -27,36 +39,31 @@ import {
|
|
|
27
39
|
getToolStats,
|
|
28
40
|
getToolStatsByModel,
|
|
29
41
|
getToolTimeSeries,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
prepareSessionSync,
|
|
33
|
-
} from "./db";
|
|
34
|
-
import {
|
|
35
|
-
getSessionEntry,
|
|
36
|
-
listAllSessionFiles,
|
|
37
|
-
matchesSessionFile,
|
|
38
|
-
type ParseSessionResult,
|
|
39
|
-
parseSessionFile,
|
|
40
|
-
type SessionParserState,
|
|
41
|
-
} from "./parser";
|
|
42
|
+
type RangeWindow,
|
|
43
|
+
} from "./rollup";
|
|
42
44
|
import type { SyncWorkerRequest, SyncWorkerResponse } from "./sync-worker";
|
|
43
45
|
// Coding-agent binary/bundle workers route through the CLI entrypoint with a
|
|
44
46
|
// hidden argv mode, so the compiled binary and npm bundle only need one
|
|
45
47
|
// JavaScript entry. Standalone source `omp-stats` keeps using this package's
|
|
46
48
|
// own sync-worker source file.
|
|
47
49
|
import type {
|
|
48
|
-
BehaviorDashboardStats,
|
|
49
50
|
DashboardStats,
|
|
50
51
|
FolderStats,
|
|
51
52
|
MessageStats,
|
|
52
53
|
ProviderDashboardStats,
|
|
54
|
+
ProviderWindowStats,
|
|
53
55
|
RequestDetails,
|
|
54
56
|
ToolDashboardStats,
|
|
55
57
|
} from "./types";
|
|
56
|
-
import { computeUsageWindowStats, fetchUsageData } from "./usage-windows";
|
|
58
|
+
import { computeUsageWindowStats, fetchUsageData, type UsageDataSnapshot } from "./usage-windows";
|
|
57
59
|
|
|
58
60
|
const STATS_SYNC_LOCK_RETRY_MS = 25;
|
|
59
61
|
const STATS_SYNC_LOCK_WAIT_MS = 60 * 60 * 1000;
|
|
62
|
+
// Bound queued results by both file count and derived rows; never hold a SQLite transaction across I/O.
|
|
63
|
+
const SYNC_BATCH_FILES = 512;
|
|
64
|
+
const SYNC_BATCH_ROWS = 8192;
|
|
65
|
+
const SYNC_METADATA_FILES = 128;
|
|
66
|
+
const SYNC_INLINE_READS = 8;
|
|
60
67
|
|
|
61
68
|
/**
|
|
62
69
|
* Serialize stats ingestion and archive reconciliation across processes.
|
|
@@ -77,7 +84,7 @@ export async function withStatsSyncLock<T>(dbPath: string, fn: () => Promise<T>)
|
|
|
77
84
|
* Progress event emitted after each session file is fully processed.
|
|
78
85
|
* `current` is the number of files completed (skipped + parsed),
|
|
79
86
|
* `total` is the size of the work set. `processed` is the running total
|
|
80
|
-
* of inserted rows.
|
|
87
|
+
* of inserted rows. Parsed files are reported only after their batch commits.
|
|
81
88
|
*/
|
|
82
89
|
export interface SyncProgress {
|
|
83
90
|
current: number;
|
|
@@ -92,9 +99,14 @@ export interface SyncOptions {
|
|
|
92
99
|
/**
|
|
93
100
|
* Worker pool size. Defaults to a sensible value derived from the host
|
|
94
101
|
* (capped to avoid drowning a small machine in workers). Set to `1` to
|
|
95
|
-
*
|
|
102
|
+
* parse on the calling thread without spawning workers. File I/O is pipelined.
|
|
96
103
|
*/
|
|
97
104
|
workers?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Sync only these transcripts (e.g. files a watcher saw change) instead of
|
|
107
|
+
* listing every session. Missing files are skipped.
|
|
108
|
+
*/
|
|
109
|
+
files?: readonly string[];
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
function defaultWorkerCount(): number {
|
|
@@ -222,9 +234,11 @@ export async function smokeTestSyncWorker({ timeoutMs = 5_000 }: { timeoutMs?: n
|
|
|
222
234
|
/**
|
|
223
235
|
* Sync all session files to the database.
|
|
224
236
|
*
|
|
225
|
-
* `workers: 1`
|
|
237
|
+
* `workers: 1` pipelines file reads and parses inline, committing in discovery order.
|
|
238
|
+
* Larger pools fan parsing out across workers
|
|
226
239
|
* (one in-flight job per worker) while DB writes and offset bookkeeping stay on
|
|
227
|
-
* the calling thread
|
|
240
|
+
* the calling thread. Bounded batches commit rows and cursors atomically without
|
|
241
|
+
* holding a database transaction open during file I/O.
|
|
228
242
|
* `onProgress` fires once per completed file (skipped files included so the
|
|
229
243
|
* bar walks at a steady rate).
|
|
230
244
|
*/
|
|
@@ -241,24 +255,58 @@ export async function syncAllSessions(opts?: SyncOptions): Promise<{ processed:
|
|
|
241
255
|
});
|
|
242
256
|
}
|
|
243
257
|
|
|
258
|
+
/** Recency tiers for {@link orderForIngest}, newest first. */
|
|
259
|
+
const INGEST_TIERS_MS = [24 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000];
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Order a full sync so recent activity lands first: transcripts modified in
|
|
263
|
+
* the last day, then the last week, then the rest. Within a tier files go in
|
|
264
|
+
* creation order, so a fork still follows its parent and the parent keeps
|
|
265
|
+
* ownership of the entries the fork copied (first writer wins). The only
|
|
266
|
+
* exception is a from-scratch ingest where a fork was touched more recently
|
|
267
|
+
* than a parent idle for a day: the fork then owns the copied entries, which
|
|
268
|
+
* changes per-session attribution but never totals.
|
|
269
|
+
*/
|
|
270
|
+
async function orderForIngest(files: string[]): Promise<string[]> {
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
const ranked: { file: string; tier: number; born: number; index: number }[] = [];
|
|
273
|
+
for (let start = 0; start < files.length; start += SYNC_METADATA_FILES) {
|
|
274
|
+
const batch = files.slice(start, start + SYNC_METADATA_FILES);
|
|
275
|
+
const stats = await Promise.all(batch.map(file => fs.promises.stat(file).catch(() => null)));
|
|
276
|
+
for (let i = 0; i < batch.length; i++) {
|
|
277
|
+
const stat = stats[i];
|
|
278
|
+
const age = stat ? now - stat.mtimeMs : Number.POSITIVE_INFINITY;
|
|
279
|
+
const tier = INGEST_TIERS_MS.findIndex(limit => age <= limit);
|
|
280
|
+
ranked.push({
|
|
281
|
+
file: batch[i],
|
|
282
|
+
tier: tier === -1 ? INGEST_TIERS_MS.length : tier,
|
|
283
|
+
born: stat ? stat.birthtimeMs || stat.ctimeMs : 0,
|
|
284
|
+
index: start + i,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
ranked.sort((a, b) => a.tier - b.tier || a.born - b.born || a.index - b.index);
|
|
289
|
+
return ranked.map(entry => entry.file);
|
|
290
|
+
}
|
|
291
|
+
|
|
244
292
|
async function syncAllSessionsLocked(
|
|
245
293
|
opts?: SyncOptions,
|
|
246
294
|
): Promise<{ processed: number; files: number; reconcile: boolean }> {
|
|
247
295
|
await initDb();
|
|
248
296
|
const replay = prepareSessionSync();
|
|
249
297
|
|
|
250
|
-
const files = await listAllSessionFiles();
|
|
298
|
+
const files = opts?.files ?? (await orderForIngest(await listAllSessionFiles()));
|
|
251
299
|
let totalProcessed = 0;
|
|
252
300
|
let filesProcessed = 0;
|
|
253
301
|
let completed = 0;
|
|
254
302
|
let cursor = 0;
|
|
255
303
|
let reconcile = false;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
304
|
+
let pending: ParsedSession[] = [];
|
|
305
|
+
let pendingRows = 0;
|
|
306
|
+
let failed = false;
|
|
307
|
+
let metadataStart = 0;
|
|
308
|
+
let metadataEnd = 0;
|
|
309
|
+
let metadata: Promise<{ fileStats?: fs.Stats; stored?: FileOffset }[]> = Promise.resolve([]);
|
|
262
310
|
|
|
263
311
|
const report = (sessionFile: string) => {
|
|
264
312
|
completed++;
|
|
@@ -270,53 +318,117 @@ async function syncAllSessionsLocked(
|
|
|
270
318
|
});
|
|
271
319
|
};
|
|
272
320
|
|
|
273
|
-
const
|
|
274
|
-
|
|
321
|
+
const flush = () => {
|
|
322
|
+
if (pending.length === 0) return;
|
|
323
|
+
const batch = pending;
|
|
324
|
+
const applied = applySessionParseResults(batch);
|
|
325
|
+
pending = [];
|
|
326
|
+
pendingRows = 0;
|
|
327
|
+
totalProcessed += applied.processed;
|
|
328
|
+
filesProcessed += applied.files;
|
|
329
|
+
reconcile ||= applied.reconcile;
|
|
330
|
+
// Report only durable progress: callbacks may interrupt the sync.
|
|
331
|
+
for (const { sessionFile } of batch) report(sessionFile);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const finish = () => {
|
|
335
|
+
flush();
|
|
336
|
+
// A targeted sync cannot settle reconciliation or backfills (they need
|
|
337
|
+
// every transcript); leave their markers for the next full sync.
|
|
338
|
+
if (opts?.files) return { processed: totalProcessed, files: filesProcessed, reconcile: false };
|
|
339
|
+
completeSessionSync(reconcile);
|
|
340
|
+
markSessionBackfillsComplete();
|
|
341
|
+
return { processed: totalProcessed, files: filesProcessed, reconcile };
|
|
342
|
+
};
|
|
343
|
+
if (files.length === 0) return finish();
|
|
344
|
+
|
|
345
|
+
const prepareFile = async (
|
|
346
|
+
index: number,
|
|
275
347
|
parse: (
|
|
276
348
|
sessionFile: string,
|
|
277
349
|
fromOffset: number,
|
|
278
350
|
state?: SessionParserState,
|
|
279
351
|
replay?: boolean,
|
|
280
352
|
) => Promise<ParseSessionResult>,
|
|
281
|
-
): Promise<
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
353
|
+
): Promise<ParsedSession | null> => {
|
|
354
|
+
if (index >= metadataEnd) {
|
|
355
|
+
const batch = files.slice(index, index + SYNC_METADATA_FILES);
|
|
356
|
+
const offsets = getFileOffsets(batch);
|
|
357
|
+
metadataStart = index;
|
|
358
|
+
metadataEnd = index + batch.length;
|
|
359
|
+
metadata = Promise.all(
|
|
360
|
+
batch.map(async sessionFile => {
|
|
361
|
+
try {
|
|
362
|
+
const fileStats = await fs.promises.stat(sessionFile);
|
|
363
|
+
return { fileStats, stored: offsets.get(sessionFile) };
|
|
364
|
+
} catch {
|
|
365
|
+
return {};
|
|
366
|
+
}
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
288
369
|
}
|
|
289
|
-
|
|
290
|
-
const
|
|
370
|
+
// Capture the position before awaiting: another worker can start the next metadata batch.
|
|
371
|
+
const metadataIndex = index - metadataStart;
|
|
372
|
+
const { fileStats, stored } = (await metadata)[metadataIndex];
|
|
373
|
+
if (failed || !fileStats) return null;
|
|
291
374
|
if (
|
|
292
375
|
!replay &&
|
|
293
376
|
stored?.parserState &&
|
|
294
|
-
stored.lastModified ===
|
|
377
|
+
stored.lastModified === fileStats.mtimeMs &&
|
|
295
378
|
stored.parserState.size === fileStats.size &&
|
|
296
379
|
matchesSessionFile(stored.parserState, fileStats)
|
|
297
380
|
) {
|
|
298
|
-
|
|
299
|
-
return;
|
|
381
|
+
return null;
|
|
300
382
|
}
|
|
301
383
|
|
|
302
|
-
const
|
|
384
|
+
const sessionFile = files[index];
|
|
385
|
+
const unknownIdentity = stored !== undefined && !stored.parserState;
|
|
303
386
|
const fromOffset = unknownIdentity ? 0 : (stored?.offset ?? 0);
|
|
304
387
|
const result = await parse(sessionFile, fromOffset, stored?.parserState, replay);
|
|
305
388
|
if (unknownIdentity && result.parserState) result.reset = true;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
389
|
+
return {
|
|
390
|
+
sessionFile,
|
|
391
|
+
result,
|
|
392
|
+
// During a full replay, a changed file can be a rewrite even if its old tail checkpoint matches.
|
|
393
|
+
rebuild:
|
|
394
|
+
!stored?.parserState ||
|
|
395
|
+
(replay &&
|
|
396
|
+
(result.parserState?.size !== stored.parserState.size ||
|
|
397
|
+
result.parserState?.mtimeMs !== stored.parserState.mtimeMs)),
|
|
398
|
+
replay,
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const acceptFile = (sessionFile: string, parsed: ParsedSession | null) => {
|
|
403
|
+
if (!parsed) {
|
|
404
|
+
report(sessionFile);
|
|
405
|
+
return;
|
|
312
406
|
}
|
|
313
|
-
|
|
407
|
+
pending.push(parsed);
|
|
408
|
+
const { result } = parsed;
|
|
409
|
+
pendingRows +=
|
|
410
|
+
result.stats.length +
|
|
411
|
+
result.userStats.length +
|
|
412
|
+
result.userLinks.length +
|
|
413
|
+
result.toolCalls.length +
|
|
414
|
+
result.toolResults.length;
|
|
415
|
+
if (pending.length >= SYNC_BATCH_FILES || pendingRows >= SYNC_BATCH_ROWS) flush();
|
|
314
416
|
};
|
|
315
417
|
|
|
316
418
|
const requestedWorkers = Math.max(1, Math.floor(opts?.workers ?? defaultWorkerCount()));
|
|
317
419
|
if (requestedWorkers === 1) {
|
|
318
|
-
for (
|
|
319
|
-
await
|
|
420
|
+
for (let start = 0; start < files.length; start += SYNC_INLINE_READS) {
|
|
421
|
+
const results = await Promise.allSettled(
|
|
422
|
+
Array.from({ length: Math.min(SYNC_INLINE_READS, files.length - start) }, (_, offset) =>
|
|
423
|
+
prepareFile(start + offset, parseSessionFile),
|
|
424
|
+
),
|
|
425
|
+
);
|
|
426
|
+
// Preserve fork ownership order and drain reads before a callback can release the sync lock.
|
|
427
|
+
for (let offset = 0; offset < results.length; offset++) {
|
|
428
|
+
const result = results[offset];
|
|
429
|
+
if (result.status === "rejected") throw result.reason;
|
|
430
|
+
acceptFile(files[start + offset], result.value);
|
|
431
|
+
}
|
|
320
432
|
}
|
|
321
433
|
return finish();
|
|
322
434
|
}
|
|
@@ -324,21 +436,30 @@ async function syncAllSessionsLocked(
|
|
|
324
436
|
const poolSize = Math.min(files.length, requestedWorkers);
|
|
325
437
|
|
|
326
438
|
const handles: WorkerHandle[] = [];
|
|
327
|
-
for (let i = 0; i < poolSize; i++) handles.push(spawnWorker());
|
|
328
439
|
|
|
329
440
|
async function drain(handle: WorkerHandle): Promise<void> {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
441
|
+
try {
|
|
442
|
+
while (!failed) {
|
|
443
|
+
const idx = cursor++;
|
|
444
|
+
if (idx >= files.length) return;
|
|
445
|
+
const parsed = await prepareFile(idx, (file, fromOffset, parserState, replay) =>
|
|
446
|
+
dispatch(handle, { sessionFile: file, fromOffset, parserState, replay }),
|
|
447
|
+
);
|
|
448
|
+
if (!failed) acceptFile(files[idx], parsed);
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
failed = true;
|
|
452
|
+
throw error;
|
|
337
453
|
}
|
|
338
454
|
}
|
|
339
455
|
|
|
340
456
|
try {
|
|
341
|
-
|
|
457
|
+
for (let i = 0; i < poolSize; i++) handles.push(spawnWorker());
|
|
458
|
+
// Drain in-flight work before releasing the sync lock, even after a failed batch.
|
|
459
|
+
const results = await Promise.allSettled(handles.map(drain));
|
|
460
|
+
for (const result of results) {
|
|
461
|
+
if (result.status === "rejected") throw result.reason;
|
|
462
|
+
}
|
|
342
463
|
} finally {
|
|
343
464
|
for (const handle of handles) handle.worker.terminate();
|
|
344
465
|
}
|
|
@@ -348,92 +469,31 @@ async function syncAllSessionsLocked(
|
|
|
348
469
|
|
|
349
470
|
const HOUR_MS = 60 * 60 * 1000;
|
|
350
471
|
const DAY_MS = 24 * HOUR_MS;
|
|
351
|
-
const FIVE_MIN_MS = 5 * 60 * 1000;
|
|
352
472
|
|
|
353
473
|
type TimeRange = "1h" | "24h" | "7d" | "30d" | "90d" | "all";
|
|
354
474
|
|
|
355
|
-
interface TimeRangeConfig {
|
|
356
|
-
timeSeriesHours: number;
|
|
357
|
-
timeSeriesBucketMs: number;
|
|
358
|
-
modelSeriesDays: number;
|
|
359
|
-
modelSeriesBucketMs: number;
|
|
360
|
-
modelPerformanceDays: number;
|
|
361
|
-
modelPerformanceBucketMs: number;
|
|
362
|
-
costSeriesDays: number;
|
|
363
|
-
cutoff: number | null;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
475
|
const DEFAULT_TIME_RANGE: TimeRange = "24h";
|
|
367
476
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
costSeriesDays: 1,
|
|
377
|
-
},
|
|
378
|
-
"24h": {
|
|
379
|
-
timeSeriesHours: 24,
|
|
380
|
-
timeSeriesBucketMs: HOUR_MS,
|
|
381
|
-
modelSeriesDays: 1,
|
|
382
|
-
modelSeriesBucketMs: HOUR_MS,
|
|
383
|
-
modelPerformanceDays: 1,
|
|
384
|
-
modelPerformanceBucketMs: HOUR_MS,
|
|
385
|
-
costSeriesDays: 1,
|
|
386
|
-
},
|
|
387
|
-
"7d": {
|
|
388
|
-
timeSeriesHours: 24 * 7,
|
|
389
|
-
timeSeriesBucketMs: DAY_MS,
|
|
390
|
-
modelSeriesDays: 7,
|
|
391
|
-
modelSeriesBucketMs: DAY_MS,
|
|
392
|
-
modelPerformanceDays: 7,
|
|
393
|
-
modelPerformanceBucketMs: DAY_MS,
|
|
394
|
-
costSeriesDays: 7,
|
|
395
|
-
},
|
|
396
|
-
"30d": {
|
|
397
|
-
timeSeriesHours: 24 * 30,
|
|
398
|
-
timeSeriesBucketMs: DAY_MS,
|
|
399
|
-
modelSeriesDays: 30,
|
|
400
|
-
modelSeriesBucketMs: DAY_MS,
|
|
401
|
-
modelPerformanceDays: 30,
|
|
402
|
-
modelPerformanceBucketMs: DAY_MS,
|
|
403
|
-
costSeriesDays: 30,
|
|
404
|
-
},
|
|
405
|
-
"90d": {
|
|
406
|
-
timeSeriesHours: 24 * 90,
|
|
407
|
-
timeSeriesBucketMs: DAY_MS,
|
|
408
|
-
modelSeriesDays: 90,
|
|
409
|
-
modelSeriesBucketMs: DAY_MS,
|
|
410
|
-
modelPerformanceDays: 90,
|
|
411
|
-
modelPerformanceBucketMs: DAY_MS,
|
|
412
|
-
costSeriesDays: 90,
|
|
413
|
-
},
|
|
414
|
-
all: {
|
|
415
|
-
timeSeriesHours: 24 * 3650,
|
|
416
|
-
timeSeriesBucketMs: DAY_MS,
|
|
417
|
-
modelSeriesDays: 3650,
|
|
418
|
-
modelSeriesBucketMs: DAY_MS,
|
|
419
|
-
modelPerformanceDays: 3650,
|
|
420
|
-
modelPerformanceBucketMs: DAY_MS,
|
|
421
|
-
costSeriesDays: 3650,
|
|
422
|
-
},
|
|
477
|
+
/** Span and series bucket per range; mirrored by the client's `data/range.ts`. */
|
|
478
|
+
const TIME_RANGES: Record<TimeRange, { spanMs: number | null; bucketMs: number }> = {
|
|
479
|
+
"1h": { spanMs: HOUR_MS, bucketMs: 5 * 60 * 1000 },
|
|
480
|
+
"24h": { spanMs: DAY_MS, bucketMs: HOUR_MS },
|
|
481
|
+
"7d": { spanMs: 7 * DAY_MS, bucketMs: DAY_MS },
|
|
482
|
+
"30d": { spanMs: 30 * DAY_MS, bucketMs: DAY_MS },
|
|
483
|
+
"90d": { spanMs: 90 * DAY_MS, bucketMs: DAY_MS },
|
|
484
|
+
all: { spanMs: null, bucketMs: DAY_MS },
|
|
423
485
|
};
|
|
424
486
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const config = TIME_RANGE_TO_CONFIG[normalized as TimeRange];
|
|
428
|
-
if (config) {
|
|
429
|
-
const cutoff = normalized === "all" ? null : Date.now() - Math.max(1, config.timeSeriesHours * 60 * 60 * 1000);
|
|
430
|
-
return { ...config, cutoff };
|
|
431
|
-
}
|
|
487
|
+
/** Most folders the projects payload carries (busiest first); the tail is thousands of temp dirs. */
|
|
488
|
+
const FOLDER_LIMIT = 2000;
|
|
432
489
|
|
|
433
|
-
|
|
490
|
+
/** Resolve a `?range=` value (default 24h) to its cutoff and series bucket size. */
|
|
491
|
+
export function getTimeRangeConfig(range?: string | null): RangeWindow {
|
|
492
|
+
const normalized = range?.trim().toLowerCase() ?? DEFAULT_TIME_RANGE;
|
|
493
|
+
const config = TIME_RANGES[normalized as TimeRange] ?? TIME_RANGES[DEFAULT_TIME_RANGE];
|
|
434
494
|
return {
|
|
435
|
-
|
|
436
|
-
|
|
495
|
+
cutoff: config.spanMs === null ? null : Date.now() - config.spanMs,
|
|
496
|
+
bucketMs: config.bucketMs,
|
|
437
497
|
};
|
|
438
498
|
}
|
|
439
499
|
|
|
@@ -442,26 +502,16 @@ export function getTimeRangeConfig(range?: string | null): TimeRangeConfig {
|
|
|
442
502
|
*/
|
|
443
503
|
export async function getDashboardStats(range?: string | null): Promise<DashboardStats> {
|
|
444
504
|
await initDb();
|
|
445
|
-
const
|
|
446
|
-
timeSeriesHours,
|
|
447
|
-
timeSeriesBucketMs,
|
|
448
|
-
modelSeriesDays,
|
|
449
|
-
modelSeriesBucketMs,
|
|
450
|
-
modelPerformanceDays,
|
|
451
|
-
modelPerformanceBucketMs,
|
|
452
|
-
costSeriesDays,
|
|
453
|
-
cutoff,
|
|
454
|
-
} = getTimeRangeConfig(range);
|
|
455
|
-
|
|
505
|
+
const window = getTimeRangeConfig(range);
|
|
456
506
|
return {
|
|
457
|
-
overall: getOverallStats(cutoff
|
|
458
|
-
byModel: getStatsByModel(cutoff
|
|
459
|
-
byFolder: getStatsByFolder(cutoff
|
|
460
|
-
byAgentType: getStatsByAgentType(cutoff
|
|
461
|
-
timeSeries: getTimeSeries(
|
|
462
|
-
modelSeries: getModelTimeSeries(
|
|
463
|
-
modelPerformanceSeries: getModelPerformanceSeries(
|
|
464
|
-
costSeries: getCostTimeSeries(
|
|
507
|
+
overall: getOverallStats(window.cutoff),
|
|
508
|
+
byModel: getStatsByModel(window.cutoff),
|
|
509
|
+
byFolder: getStatsByFolder(window.cutoff, FOLDER_LIMIT),
|
|
510
|
+
byAgentType: getStatsByAgentType(window.cutoff),
|
|
511
|
+
timeSeries: getTimeSeries(window),
|
|
512
|
+
modelSeries: getModelTimeSeries(window),
|
|
513
|
+
modelPerformanceSeries: getModelPerformanceSeries(window),
|
|
514
|
+
costSeries: getCostTimeSeries(window.cutoff),
|
|
465
515
|
};
|
|
466
516
|
}
|
|
467
517
|
|
|
@@ -469,12 +519,11 @@ export async function getOverviewStats(
|
|
|
469
519
|
range?: string | null,
|
|
470
520
|
): Promise<Pick<DashboardStats, "overall" | "byAgentType" | "timeSeries">> {
|
|
471
521
|
await initDb();
|
|
472
|
-
const
|
|
473
|
-
|
|
522
|
+
const window = getTimeRangeConfig(range);
|
|
474
523
|
return {
|
|
475
|
-
overall: getOverallStats(cutoff
|
|
476
|
-
byAgentType: getStatsByAgentType(cutoff
|
|
477
|
-
timeSeries: getTimeSeries(
|
|
524
|
+
overall: getOverallStats(window.cutoff),
|
|
525
|
+
byAgentType: getStatsByAgentType(window.cutoff),
|
|
526
|
+
timeSeries: getTimeSeries(window),
|
|
478
527
|
};
|
|
479
528
|
}
|
|
480
529
|
|
|
@@ -482,29 +531,22 @@ export async function getModelDashboardStats(
|
|
|
482
531
|
range?: string | null,
|
|
483
532
|
): Promise<Pick<DashboardStats, "byModel" | "modelSeries" | "modelPerformanceSeries">> {
|
|
484
533
|
await initDb();
|
|
485
|
-
const
|
|
486
|
-
getTimeRangeConfig(range);
|
|
487
|
-
|
|
534
|
+
const window = getTimeRangeConfig(range);
|
|
488
535
|
return {
|
|
489
|
-
byModel: getStatsByModel(cutoff
|
|
490
|
-
modelSeries: getModelTimeSeries(
|
|
491
|
-
modelPerformanceSeries: getModelPerformanceSeries(
|
|
536
|
+
byModel: getStatsByModel(window.cutoff),
|
|
537
|
+
modelSeries: getModelTimeSeries(window),
|
|
538
|
+
modelPerformanceSeries: getModelPerformanceSeries(window),
|
|
492
539
|
};
|
|
493
540
|
}
|
|
494
541
|
|
|
495
542
|
export async function getCostDashboardStats(range?: string | null): Promise<Pick<DashboardStats, "costSeries">> {
|
|
496
543
|
await initDb();
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
return {
|
|
500
|
-
costSeries: getCostTimeSeries(costSeriesDays, cutoff),
|
|
501
|
-
};
|
|
544
|
+
return { costSeries: getCostTimeSeries(getTimeRangeConfig(range).cutoff) };
|
|
502
545
|
}
|
|
503
546
|
|
|
504
547
|
export async function getFolderStats(range?: string | null): Promise<FolderStats[]> {
|
|
505
548
|
await initDb();
|
|
506
|
-
|
|
507
|
-
return getStatsByFolder(cutoff ?? undefined);
|
|
549
|
+
return getStatsByFolder(getTimeRangeConfig(range).cutoff, FOLDER_LIMIT);
|
|
508
550
|
}
|
|
509
551
|
|
|
510
552
|
export async function getRecentRequests(limit?: number): Promise<MessageStats[]> {
|
|
@@ -514,8 +556,7 @@ export async function getRecentRequests(limit?: number): Promise<MessageStats[]>
|
|
|
514
556
|
|
|
515
557
|
export async function getRecentErrors(range?: string | null, limit?: number): Promise<MessageStats[]> {
|
|
516
558
|
await initDb();
|
|
517
|
-
|
|
518
|
-
return dbGetRecentErrors(limit, cutoff);
|
|
559
|
+
return dbGetRecentErrors(limit, getTimeRangeConfig(range).cutoff);
|
|
519
560
|
}
|
|
520
561
|
|
|
521
562
|
export async function getRequestDetails(id: number): Promise<RequestDetails | null> {
|
|
@@ -524,16 +565,12 @@ export async function getRequestDetails(id: number): Promise<RequestDetails | nu
|
|
|
524
565
|
if (!msg) return null;
|
|
525
566
|
|
|
526
567
|
const entry = await getSessionEntry(msg.sessionFile, msg.entryId);
|
|
527
|
-
if (entry?.type !== "message") return null;
|
|
528
|
-
|
|
529
|
-
// TODO: Get parent/context messages?
|
|
530
|
-
// For now we return the single entry which contains the assistant response.
|
|
531
|
-
// The user prompt is likely the parent.
|
|
568
|
+
if (entry?.type !== "message" || !("message" in entry)) return null;
|
|
532
569
|
|
|
533
570
|
return {
|
|
534
571
|
...msg,
|
|
535
572
|
messages: [entry],
|
|
536
|
-
output:
|
|
573
|
+
output: entry.message,
|
|
537
574
|
};
|
|
538
575
|
}
|
|
539
576
|
|
|
@@ -545,51 +582,79 @@ export async function getTotalMessageCount(): Promise<number> {
|
|
|
545
582
|
return getMessageCount();
|
|
546
583
|
}
|
|
547
584
|
|
|
548
|
-
|
|
585
|
+
/**
|
|
586
|
+
* Get the tools dashboard payload: per-tool totals, per-(tool, model)
|
|
587
|
+
* breakdown, and the call time series.
|
|
588
|
+
*/
|
|
589
|
+
export async function getToolDashboardStats(range?: string | null): Promise<ToolDashboardStats> {
|
|
549
590
|
await initDb();
|
|
550
|
-
const
|
|
591
|
+
const window = getTimeRangeConfig(range);
|
|
551
592
|
return {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
593
|
+
byTool: getToolStats(window.cutoff),
|
|
594
|
+
byToolModel: getToolStatsByModel(window.cutoff),
|
|
595
|
+
series: getToolTimeSeries(window),
|
|
555
596
|
};
|
|
556
597
|
}
|
|
557
598
|
|
|
558
599
|
/**
|
|
559
|
-
* Get the
|
|
560
|
-
*
|
|
600
|
+
* Get the providers dashboard payload: per-provider totals, peak-burn-hours
|
|
601
|
+
* histogram and provider token time series. Subscription windows are served
|
|
602
|
+
* separately by {@link getProviderWindowStats}.
|
|
561
603
|
*/
|
|
562
|
-
export async function
|
|
604
|
+
export async function getProviderDashboardStats(range?: string | null): Promise<ProviderDashboardStats> {
|
|
563
605
|
await initDb();
|
|
564
|
-
const
|
|
606
|
+
const window = getTimeRangeConfig(range);
|
|
565
607
|
return {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
series:
|
|
608
|
+
providers: getStatsByProvider(window.cutoff),
|
|
609
|
+
hourly: getProviderHourlyBurn(window.cutoff),
|
|
610
|
+
series: getProviderTimeSeries(window),
|
|
569
611
|
};
|
|
570
612
|
}
|
|
571
613
|
|
|
614
|
+
const USAGE_CACHE_TTL_MS = 60_000;
|
|
615
|
+
let usageCache: { key: string; at: number; data: Promise<UsageDataSnapshot> } | null = null;
|
|
616
|
+
|
|
572
617
|
/**
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
618
|
+
* Usage snapshots since `sinceMs`, memoized for a minute per hour-aligned
|
|
619
|
+
* cutoff and source database: the broker fetch takes seconds and the
|
|
620
|
+
* dashboard asks on every range switch and live refresh.
|
|
621
|
+
*/
|
|
622
|
+
function cachedUsageData(sinceMs: number): Promise<UsageDataSnapshot> {
|
|
623
|
+
const sinceHour = Math.floor(sinceMs / HOUR_MS) * HOUR_MS;
|
|
624
|
+
const key = `${getAgentDbPath()}:${sinceHour}`;
|
|
625
|
+
const now = Date.now();
|
|
626
|
+
if (usageCache?.key === key && now - usageCache.at < USAGE_CACHE_TTL_MS) {
|
|
627
|
+
return usageCache.data;
|
|
628
|
+
}
|
|
629
|
+
const data = fetchUsageData(sinceHour);
|
|
630
|
+
usageCache = { key, at: now, data };
|
|
631
|
+
data.catch(() => {
|
|
632
|
+
if (usageCache?.data === data) usageCache = null;
|
|
633
|
+
});
|
|
634
|
+
return data;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Subscription-window analytics derived from recorded usage-limit snapshots:
|
|
639
|
+
* insights for every provider window, plus utilization series for `provider`
|
|
640
|
+
* only (all series can number in the thousands).
|
|
576
641
|
*
|
|
577
642
|
* Window token estimates use broker-held fleet token burn when a broker is
|
|
578
643
|
* configured — the window fractions cover every install sharing the broker's
|
|
579
644
|
* credentials, so dividing them into local-only tokens would undercount.
|
|
580
645
|
*/
|
|
581
|
-
export async function
|
|
646
|
+
export async function getProviderWindowStats(
|
|
647
|
+
range?: string | null,
|
|
648
|
+
provider?: string | null,
|
|
649
|
+
): Promise<ProviderWindowStats> {
|
|
582
650
|
await initDb();
|
|
583
|
-
const {
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
|
|
651
|
+
const { cutoff } = getTimeRangeConfig(range);
|
|
652
|
+
const usage = await cachedUsageData(cutoff ?? 0);
|
|
653
|
+
const tokensByProvider =
|
|
654
|
+
usage.fleetTokensByProvider ?? new Map(getStatsByProvider(cutoff).map(p => [p.provider, p.totalTokens]));
|
|
587
655
|
const { usageSeries, windowInsights } = computeUsageWindowStats(usage.rows, tokensByProvider);
|
|
588
656
|
return {
|
|
589
|
-
providers,
|
|
590
|
-
hourly: getProviderHourlyBurn(cutoff ?? undefined),
|
|
591
|
-
series: getProviderTimeSeries(modelSeriesDays, cutoff, modelSeriesBucketMs),
|
|
592
|
-
usageSeries,
|
|
593
657
|
windowInsights,
|
|
658
|
+
usageSeries: provider ? usageSeries.filter(series => series.provider === provider) : [],
|
|
594
659
|
};
|
|
595
660
|
}
|