@oh-my-pi/omp-stats 15.1.2 → 15.1.4
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/dist/client/index.js +73 -73
- package/dist/types/client/components/ChartsContainer.d.ts +3 -2
- package/dist/types/client/components/ModelsTable.d.ts +3 -2
- package/dist/types/client/components/chart-shared.d.ts +0 -5
- package/dist/types/client/components/range-meta.d.ts +21 -0
- package/dist/types/db.d.ts +2 -2
- package/package.json +3 -3
- package/src/aggregator.ts +30 -7
- package/src/client/App.tsx +2 -1
- package/src/client/components/ChartsContainer.tsx +7 -5
- package/src/client/components/ModelsTable.tsx +20 -9
- package/src/client/components/chart-shared.tsx +1 -1
- package/src/client/components/range-meta.ts +72 -0
- package/src/db.ts +23 -6
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { ModelTimeSeriesPoint } from "../types";
|
|
1
|
+
import type { ModelTimeSeriesPoint, TimeRange } from "../types";
|
|
2
2
|
interface ChartsContainerProps {
|
|
3
3
|
modelSeries: ModelTimeSeriesPoint[];
|
|
4
|
+
timeRange: TimeRange;
|
|
4
5
|
}
|
|
5
|
-
export declare function ChartsContainer({ modelSeries }: ChartsContainerProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export declare function ChartsContainer({ modelSeries, timeRange }: ChartsContainerProps): import("react/jsx-runtime").JSX.Element;
|
|
6
7
|
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { ModelPerformancePoint, ModelStats } from "../types";
|
|
1
|
+
import type { ModelPerformancePoint, ModelStats, TimeRange } from "../types";
|
|
2
2
|
interface ModelsTableProps {
|
|
3
3
|
models: ModelStats[];
|
|
4
4
|
performanceSeries: ModelPerformancePoint[];
|
|
5
|
+
timeRange: TimeRange;
|
|
5
6
|
}
|
|
6
|
-
export declare function ModelsTable({ models, performanceSeries }: ModelsTableProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function ModelsTable({ models, performanceSeries, timeRange }: ModelsTableProps): import("react/jsx-runtime").JSX.Element;
|
|
7
8
|
export {};
|
|
@@ -169,11 +169,6 @@ export declare function buildTopNByModelSeries<T extends ModelKeyedPoint, B>(poi
|
|
|
169
169
|
accumulate: (bucket: B, point: T) => void;
|
|
170
170
|
bucketToValue: (bucket: B) => number;
|
|
171
171
|
}): ChartSeries;
|
|
172
|
-
/** All Models / By Model segmented toggle — identical UI in every time chart. */
|
|
173
|
-
export declare function ByModelToggle({ byModel, onChange }: {
|
|
174
|
-
byModel: boolean;
|
|
175
|
-
onChange: (v: boolean) => void;
|
|
176
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
177
172
|
/**
|
|
178
173
|
* Outer surface card used by both time charts. `controls` slot covers
|
|
179
174
|
* chart-specific tabs (e.g. behavior metric picker); the by-model toggle and
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display metadata for a `TimeRange` — keeps chart labels, sparkline bucket
|
|
3
|
+
* counts, and x-axis date formatting in sync with the server-side bucketing
|
|
4
|
+
* defined in `aggregator.ts`.
|
|
5
|
+
*/
|
|
6
|
+
import type { TimeRange } from "../types";
|
|
7
|
+
export interface RangeMeta {
|
|
8
|
+
/** Human label used in chart subtitles ("the last 24 hours"). */
|
|
9
|
+
windowLabel: string;
|
|
10
|
+
/** Short prefix used in compact column headers ("24h Trend"). */
|
|
11
|
+
trendLabel: string;
|
|
12
|
+
/** Bucket size matching the server query for this range. */
|
|
13
|
+
bucketMs: number;
|
|
14
|
+
/** Number of buckets the server is expected to return for this range. */
|
|
15
|
+
bucketCount: number;
|
|
16
|
+
/** date-fns format string for x-axis labels and tooltip headings. */
|
|
17
|
+
tickFormat: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function rangeMeta(range: TimeRange): RangeMeta;
|
|
20
|
+
/** Format a bucket timestamp using the active range's tick format. */
|
|
21
|
+
export declare function formatRangeTick(timestamp: number, range: TimeRange): string;
|
package/dist/types/db.d.ts
CHANGED
|
@@ -41,11 +41,11 @@ export declare function getTimeSeries(hours?: number, cutoff?: number | null, bu
|
|
|
41
41
|
/**
|
|
42
42
|
* Get daily model usage time series data for the last N days.
|
|
43
43
|
*/
|
|
44
|
-
export declare function getModelTimeSeries(days?: number, cutoff?: number | null): ModelTimeSeriesPoint[];
|
|
44
|
+
export declare function getModelTimeSeries(days?: number, cutoff?: number | null, bucketMs?: number): ModelTimeSeriesPoint[];
|
|
45
45
|
/**
|
|
46
46
|
* Get daily model performance time series data for the last N days.
|
|
47
47
|
*/
|
|
48
|
-
export declare function getModelPerformanceSeries(days?: number, cutoff?: number | null): ModelPerformancePoint[];
|
|
48
|
+
export declare function getModelPerformanceSeries(days?: number, cutoff?: number | null, bucketMs?: number): ModelPerformancePoint[];
|
|
49
49
|
/**
|
|
50
50
|
* Get total message count.
|
|
51
51
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "15.1.
|
|
4
|
+
"version": "15.1.4",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-ai": "15.1.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.1.
|
|
40
|
+
"@oh-my-pi/pi-ai": "15.1.4",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.1.4",
|
|
42
42
|
"@tailwindcss/node": "^4.2.4",
|
|
43
43
|
"chart.js": "^4.5.1",
|
|
44
44
|
"date-fns": "^4.1.0",
|
package/src/aggregator.ts
CHANGED
|
@@ -266,7 +266,9 @@ interface TimeRangeConfig {
|
|
|
266
266
|
timeSeriesHours: number;
|
|
267
267
|
timeSeriesBucketMs: number;
|
|
268
268
|
modelSeriesDays: number;
|
|
269
|
+
modelSeriesBucketMs: number;
|
|
269
270
|
modelPerformanceDays: number;
|
|
271
|
+
modelPerformanceBucketMs: number;
|
|
270
272
|
costSeriesDays: number;
|
|
271
273
|
cutoff: number | null;
|
|
272
274
|
}
|
|
@@ -278,42 +280,54 @@ const TIME_RANGE_TO_CONFIG: Record<TimeRange, Omit<TimeRangeConfig, "cutoff">> =
|
|
|
278
280
|
timeSeriesHours: 1,
|
|
279
281
|
timeSeriesBucketMs: HOUR_MS,
|
|
280
282
|
modelSeriesDays: 1,
|
|
283
|
+
modelSeriesBucketMs: HOUR_MS,
|
|
281
284
|
modelPerformanceDays: 1,
|
|
285
|
+
modelPerformanceBucketMs: HOUR_MS,
|
|
282
286
|
costSeriesDays: 1,
|
|
283
287
|
},
|
|
284
288
|
"24h": {
|
|
285
289
|
timeSeriesHours: 24,
|
|
286
290
|
timeSeriesBucketMs: HOUR_MS,
|
|
287
291
|
modelSeriesDays: 1,
|
|
292
|
+
modelSeriesBucketMs: HOUR_MS,
|
|
288
293
|
modelPerformanceDays: 1,
|
|
294
|
+
modelPerformanceBucketMs: HOUR_MS,
|
|
289
295
|
costSeriesDays: 1,
|
|
290
296
|
},
|
|
291
297
|
"7d": {
|
|
292
298
|
timeSeriesHours: 24 * 7,
|
|
293
299
|
timeSeriesBucketMs: DAY_MS,
|
|
294
300
|
modelSeriesDays: 7,
|
|
301
|
+
modelSeriesBucketMs: DAY_MS,
|
|
295
302
|
modelPerformanceDays: 7,
|
|
303
|
+
modelPerformanceBucketMs: DAY_MS,
|
|
296
304
|
costSeriesDays: 7,
|
|
297
305
|
},
|
|
298
306
|
"30d": {
|
|
299
307
|
timeSeriesHours: 24 * 30,
|
|
300
308
|
timeSeriesBucketMs: DAY_MS,
|
|
301
309
|
modelSeriesDays: 30,
|
|
310
|
+
modelSeriesBucketMs: DAY_MS,
|
|
302
311
|
modelPerformanceDays: 30,
|
|
312
|
+
modelPerformanceBucketMs: DAY_MS,
|
|
303
313
|
costSeriesDays: 30,
|
|
304
314
|
},
|
|
305
315
|
"90d": {
|
|
306
316
|
timeSeriesHours: 24 * 90,
|
|
307
317
|
timeSeriesBucketMs: DAY_MS,
|
|
308
318
|
modelSeriesDays: 90,
|
|
319
|
+
modelSeriesBucketMs: DAY_MS,
|
|
309
320
|
modelPerformanceDays: 90,
|
|
321
|
+
modelPerformanceBucketMs: DAY_MS,
|
|
310
322
|
costSeriesDays: 90,
|
|
311
323
|
},
|
|
312
324
|
all: {
|
|
313
325
|
timeSeriesHours: 24 * 3650,
|
|
314
326
|
timeSeriesBucketMs: DAY_MS,
|
|
315
327
|
modelSeriesDays: 3650,
|
|
328
|
+
modelSeriesBucketMs: DAY_MS,
|
|
316
329
|
modelPerformanceDays: 3650,
|
|
330
|
+
modelPerformanceBucketMs: DAY_MS,
|
|
317
331
|
costSeriesDays: 3650,
|
|
318
332
|
},
|
|
319
333
|
};
|
|
@@ -338,16 +352,24 @@ function getTimeRangeConfig(range?: string | null): TimeRangeConfig {
|
|
|
338
352
|
*/
|
|
339
353
|
export async function getDashboardStats(range?: string | null): Promise<DashboardStats> {
|
|
340
354
|
await initDb();
|
|
341
|
-
const {
|
|
342
|
-
|
|
355
|
+
const {
|
|
356
|
+
timeSeriesHours,
|
|
357
|
+
timeSeriesBucketMs,
|
|
358
|
+
modelSeriesDays,
|
|
359
|
+
modelSeriesBucketMs,
|
|
360
|
+
modelPerformanceDays,
|
|
361
|
+
modelPerformanceBucketMs,
|
|
362
|
+
costSeriesDays,
|
|
363
|
+
cutoff,
|
|
364
|
+
} = getTimeRangeConfig(range);
|
|
343
365
|
|
|
344
366
|
return {
|
|
345
367
|
overall: getOverallStats(cutoff ?? undefined),
|
|
346
368
|
byModel: getStatsByModel(cutoff ?? undefined),
|
|
347
369
|
byFolder: getStatsByFolder(cutoff ?? undefined),
|
|
348
370
|
timeSeries: getTimeSeries(timeSeriesHours, cutoff, timeSeriesBucketMs),
|
|
349
|
-
modelSeries: getModelTimeSeries(modelSeriesDays, cutoff),
|
|
350
|
-
modelPerformanceSeries: getModelPerformanceSeries(modelPerformanceDays, cutoff),
|
|
371
|
+
modelSeries: getModelTimeSeries(modelSeriesDays, cutoff, modelSeriesBucketMs),
|
|
372
|
+
modelPerformanceSeries: getModelPerformanceSeries(modelPerformanceDays, cutoff, modelPerformanceBucketMs),
|
|
351
373
|
costSeries: getCostTimeSeries(costSeriesDays, cutoff),
|
|
352
374
|
};
|
|
353
375
|
}
|
|
@@ -366,12 +388,13 @@ export async function getModelDashboardStats(
|
|
|
366
388
|
range?: string | null,
|
|
367
389
|
): Promise<Pick<DashboardStats, "byModel" | "modelSeries" | "modelPerformanceSeries">> {
|
|
368
390
|
await initDb();
|
|
369
|
-
const { modelSeriesDays, modelPerformanceDays, cutoff } =
|
|
391
|
+
const { modelSeriesDays, modelSeriesBucketMs, modelPerformanceDays, modelPerformanceBucketMs, cutoff } =
|
|
392
|
+
getTimeRangeConfig(range);
|
|
370
393
|
|
|
371
394
|
return {
|
|
372
395
|
byModel: getStatsByModel(cutoff ?? undefined),
|
|
373
|
-
modelSeries: getModelTimeSeries(modelSeriesDays, cutoff),
|
|
374
|
-
modelPerformanceSeries: getModelPerformanceSeries(modelPerformanceDays, cutoff),
|
|
396
|
+
modelSeries: getModelTimeSeries(modelSeriesDays, cutoff, modelSeriesBucketMs),
|
|
397
|
+
modelPerformanceSeries: getModelPerformanceSeries(modelPerformanceDays, cutoff, modelPerformanceBucketMs),
|
|
375
398
|
};
|
|
376
399
|
}
|
|
377
400
|
|
package/src/client/App.tsx
CHANGED
|
@@ -155,10 +155,11 @@ export default function App() {
|
|
|
155
155
|
<div className="space-y-6 animate-fade-in">
|
|
156
156
|
{modelStats ? (
|
|
157
157
|
<>
|
|
158
|
-
<ChartsContainer modelSeries={modelStats.modelSeries} />
|
|
158
|
+
<ChartsContainer modelSeries={modelStats.modelSeries} timeRange={timeRange} />
|
|
159
159
|
<ModelsTable
|
|
160
160
|
models={modelStats.byModel}
|
|
161
161
|
performanceSeries={modelStats.modelPerformanceSeries}
|
|
162
|
+
timeRange={timeRange}
|
|
162
163
|
/>
|
|
163
164
|
</>
|
|
164
165
|
) : (
|
|
@@ -9,11 +9,11 @@ import {
|
|
|
9
9
|
Title,
|
|
10
10
|
Tooltip,
|
|
11
11
|
} from "chart.js";
|
|
12
|
-
import { format } from "date-fns";
|
|
13
12
|
import { useMemo } from "react";
|
|
14
13
|
import { Line } from "react-chartjs-2";
|
|
15
|
-
import type { ModelTimeSeriesPoint } from "../types";
|
|
14
|
+
import type { ModelTimeSeriesPoint, TimeRange } from "../types";
|
|
16
15
|
import { useSystemTheme } from "../useSystemTheme";
|
|
16
|
+
import { formatRangeTick, rangeMeta } from "./range-meta";
|
|
17
17
|
|
|
18
18
|
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler);
|
|
19
19
|
|
|
@@ -49,14 +49,16 @@ const CHART_THEMES = {
|
|
|
49
49
|
} as const;
|
|
50
50
|
interface ChartsContainerProps {
|
|
51
51
|
modelSeries: ModelTimeSeriesPoint[];
|
|
52
|
+
timeRange: TimeRange;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
export function ChartsContainer({ modelSeries }: ChartsContainerProps) {
|
|
55
|
+
export function ChartsContainer({ modelSeries, timeRange }: ChartsContainerProps) {
|
|
55
56
|
const chartData = useMemo(() => buildModelPreferenceSeries(modelSeries), [modelSeries]);
|
|
56
57
|
const theme = useSystemTheme();
|
|
57
58
|
const chartTheme = CHART_THEMES[theme];
|
|
59
|
+
const meta = rangeMeta(timeRange);
|
|
58
60
|
const data = {
|
|
59
|
-
labels: chartData.data.map(d =>
|
|
61
|
+
labels: chartData.data.map(d => formatRangeTick(d.timestamp, timeRange)),
|
|
60
62
|
datasets: chartData.series.map((seriesName, index) => ({
|
|
61
63
|
label: seriesName,
|
|
62
64
|
data: chartData.data.map(d => d[seriesName] ?? 0),
|
|
@@ -137,7 +139,7 @@ export function ChartsContainer({ modelSeries }: ChartsContainerProps) {
|
|
|
137
139
|
<div className="surface overflow-hidden">
|
|
138
140
|
<div className="px-5 py-4 border-b border-[var(--border-subtle)]">
|
|
139
141
|
<h3 className="text-sm font-semibold text-[var(--text-primary)]">Model Preference</h3>
|
|
140
|
-
<p className="text-xs text-[var(--text-muted)] mt-1">Share of requests over
|
|
142
|
+
<p className="text-xs text-[var(--text-muted)] mt-1">Share of requests over {meta.windowLabel}</p>
|
|
141
143
|
</div>
|
|
142
144
|
<div className="p-5 min-h-[320px]">
|
|
143
145
|
{chartData.data.length === 0 ? (
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { format } from "date-fns";
|
|
12
12
|
import { useMemo, useState } from "react";
|
|
13
13
|
import { Line } from "react-chartjs-2";
|
|
14
|
-
import type { ModelPerformancePoint, ModelStats } from "../types";
|
|
14
|
+
import type { ModelPerformancePoint, ModelStats, TimeRange } from "../types";
|
|
15
15
|
import { useSystemTheme } from "../useSystemTheme";
|
|
16
16
|
import {
|
|
17
17
|
DetailChartEmpty,
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type TableChartTheme,
|
|
30
30
|
TrendEmpty,
|
|
31
31
|
} from "./models-table-shared";
|
|
32
|
+
import { rangeMeta } from "./range-meta";
|
|
32
33
|
|
|
33
34
|
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
|
34
35
|
|
|
@@ -37,6 +38,7 @@ const GRID_TEMPLATE = "2fr 0.9fr 0.9fr 1fr 0.8fr 0.8fr 140px 40px";
|
|
|
37
38
|
interface ModelsTableProps {
|
|
38
39
|
models: ModelStats[];
|
|
39
40
|
performanceSeries: ModelPerformancePoint[];
|
|
41
|
+
timeRange: TimeRange;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
type ModelPerformanceSeries = {
|
|
@@ -49,10 +51,14 @@ type ModelPerformanceSeries = {
|
|
|
49
51
|
}>;
|
|
50
52
|
};
|
|
51
53
|
|
|
52
|
-
export function ModelsTable({ models, performanceSeries }: ModelsTableProps) {
|
|
54
|
+
export function ModelsTable({ models, performanceSeries, timeRange }: ModelsTableProps) {
|
|
53
55
|
const [expandedKey, setExpandedKey] = useState<string | null>(null);
|
|
56
|
+
const meta = rangeMeta(timeRange);
|
|
54
57
|
|
|
55
|
-
const performanceSeriesByKey = useMemo(
|
|
58
|
+
const performanceSeriesByKey = useMemo(
|
|
59
|
+
() => buildModelPerformanceLookup(performanceSeries, meta.bucketCount, meta.bucketMs),
|
|
60
|
+
[performanceSeries, meta.bucketCount, meta.bucketMs],
|
|
61
|
+
);
|
|
56
62
|
const theme = useSystemTheme();
|
|
57
63
|
const chartTheme = TABLE_CHART_THEMES[theme];
|
|
58
64
|
const sortedModels = [...models].sort(
|
|
@@ -70,7 +76,7 @@ export function ModelsTable({ models, performanceSeries }: ModelsTableProps) {
|
|
|
70
76
|
{ label: "Tokens", align: "right" },
|
|
71
77
|
{ label: "Tokens/s", align: "right" },
|
|
72
78
|
{ label: "TTFT", align: "right" },
|
|
73
|
-
{ label:
|
|
79
|
+
{ label: meta.trendLabel, align: "center" },
|
|
74
80
|
]}
|
|
75
81
|
/>
|
|
76
82
|
|
|
@@ -214,12 +220,17 @@ function PerformanceChart({
|
|
|
214
220
|
return <Line data={chartData} options={options} />;
|
|
215
221
|
}
|
|
216
222
|
|
|
217
|
-
function buildModelPerformanceLookup(
|
|
218
|
-
|
|
223
|
+
function buildModelPerformanceLookup(
|
|
224
|
+
points: ModelPerformancePoint[],
|
|
225
|
+
bucketCount: number,
|
|
226
|
+
bucketMs: number,
|
|
227
|
+
): Map<string, ModelPerformanceSeries> {
|
|
219
228
|
const maxTimestamp = points.reduce((max, point) => Math.max(max, point.timestamp), 0);
|
|
220
|
-
const anchor = maxTimestamp > 0 ? maxTimestamp : Math.floor(Date.now() /
|
|
221
|
-
const
|
|
222
|
-
const
|
|
229
|
+
const anchor = maxTimestamp > 0 ? maxTimestamp : Math.floor(Date.now() / bucketMs) * bucketMs;
|
|
230
|
+
const uniqueTimestamps = new Set(points.map(p => p.timestamp));
|
|
231
|
+
const effectiveCount = bucketCount > 0 ? bucketCount : Math.max(1, uniqueTimestamps.size);
|
|
232
|
+
const start = anchor - (effectiveCount - 1) * bucketMs;
|
|
233
|
+
const buckets = Array.from({ length: effectiveCount }, (_, index) => start + index * bucketMs);
|
|
223
234
|
const bucketIndex = new Map(buckets.map((timestamp, index) => [timestamp, index]));
|
|
224
235
|
const seriesByKey = new Map<string, ModelPerformanceSeries>();
|
|
225
236
|
|
|
@@ -253,7 +253,7 @@ export function buildTopNByModelSeries<T extends ModelKeyedPoint, B>(
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
/** All Models / By Model segmented toggle — identical UI in every time chart. */
|
|
256
|
-
|
|
256
|
+
function ByModelToggle({ byModel, onChange }: { byModel: boolean; onChange: (v: boolean) => void }) {
|
|
257
257
|
return (
|
|
258
258
|
<div className="flex bg-[var(--bg-surface)] rounded-[var(--radius-sm)] p-0.5 border border-[var(--border-subtle)]">
|
|
259
259
|
<button
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display metadata for a `TimeRange` — keeps chart labels, sparkline bucket
|
|
3
|
+
* counts, and x-axis date formatting in sync with the server-side bucketing
|
|
4
|
+
* defined in `aggregator.ts`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { format } from "date-fns";
|
|
8
|
+
import type { TimeRange } from "../types";
|
|
9
|
+
|
|
10
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
11
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
12
|
+
|
|
13
|
+
export interface RangeMeta {
|
|
14
|
+
/** Human label used in chart subtitles ("the last 24 hours"). */
|
|
15
|
+
windowLabel: string;
|
|
16
|
+
/** Short prefix used in compact column headers ("24h Trend"). */
|
|
17
|
+
trendLabel: string;
|
|
18
|
+
/** Bucket size matching the server query for this range. */
|
|
19
|
+
bucketMs: number;
|
|
20
|
+
/** Number of buckets the server is expected to return for this range. */
|
|
21
|
+
bucketCount: number;
|
|
22
|
+
/** date-fns format string for x-axis labels and tooltip headings. */
|
|
23
|
+
tickFormat: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const RANGE_META: Record<TimeRange, RangeMeta> = {
|
|
27
|
+
"1h": {
|
|
28
|
+
windowLabel: "the last hour",
|
|
29
|
+
trendLabel: "1h Trend",
|
|
30
|
+
bucketMs: HOUR_MS,
|
|
31
|
+
bucketCount: 1,
|
|
32
|
+
tickFormat: "HH:mm",
|
|
33
|
+
},
|
|
34
|
+
"24h": {
|
|
35
|
+
windowLabel: "the last 24 hours",
|
|
36
|
+
trendLabel: "24h Trend",
|
|
37
|
+
bucketMs: HOUR_MS,
|
|
38
|
+
bucketCount: 24,
|
|
39
|
+
tickFormat: "HH:mm",
|
|
40
|
+
},
|
|
41
|
+
"7d": {
|
|
42
|
+
windowLabel: "the last 7 days",
|
|
43
|
+
trendLabel: "7d Trend",
|
|
44
|
+
bucketMs: DAY_MS,
|
|
45
|
+
bucketCount: 7,
|
|
46
|
+
tickFormat: "MMM d",
|
|
47
|
+
},
|
|
48
|
+
"30d": {
|
|
49
|
+
windowLabel: "the last 30 days",
|
|
50
|
+
trendLabel: "30d Trend",
|
|
51
|
+
bucketMs: DAY_MS,
|
|
52
|
+
bucketCount: 30,
|
|
53
|
+
tickFormat: "MMM d",
|
|
54
|
+
},
|
|
55
|
+
"90d": {
|
|
56
|
+
windowLabel: "the last 90 days",
|
|
57
|
+
trendLabel: "90d Trend",
|
|
58
|
+
bucketMs: DAY_MS,
|
|
59
|
+
bucketCount: 90,
|
|
60
|
+
tickFormat: "MMM d",
|
|
61
|
+
},
|
|
62
|
+
all: { windowLabel: "all time", trendLabel: "Trend", bucketMs: DAY_MS, bucketCount: 0, tickFormat: "MMM d" },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function rangeMeta(range: TimeRange): RangeMeta {
|
|
66
|
+
return RANGE_META[range];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Format a bucket timestamp using the active range's tick format. */
|
|
70
|
+
export function formatRangeTick(timestamp: number, range: TimeRange): string {
|
|
71
|
+
return format(new Date(timestamp), RANGE_META[range].tickFormat);
|
|
72
|
+
}
|
package/src/db.ts
CHANGED
|
@@ -554,7 +554,11 @@ export function getTimeSeries(hours = 24, cutoff?: number | null, bucketMs = 60
|
|
|
554
554
|
/**
|
|
555
555
|
* Get daily model usage time series data for the last N days.
|
|
556
556
|
*/
|
|
557
|
-
export function getModelTimeSeries(
|
|
557
|
+
export function getModelTimeSeries(
|
|
558
|
+
days = 14,
|
|
559
|
+
cutoff?: number | null,
|
|
560
|
+
bucketMs = 24 * 60 * 60 * 1000,
|
|
561
|
+
): ModelTimeSeriesPoint[] {
|
|
558
562
|
if (!db) return [];
|
|
559
563
|
|
|
560
564
|
const hasCutoff = cutoff !== null;
|
|
@@ -562,7 +566,7 @@ export function getModelTimeSeries(days = 14, cutoff?: number | null): ModelTime
|
|
|
562
566
|
|
|
563
567
|
const stmt = db.prepare(`
|
|
564
568
|
SELECT
|
|
565
|
-
(timestamp /
|
|
569
|
+
(timestamp / ?) * ? as bucket,
|
|
566
570
|
model,
|
|
567
571
|
provider,
|
|
568
572
|
COUNT(*) as requests
|
|
@@ -572,7 +576,8 @@ export function getModelTimeSeries(days = 14, cutoff?: number | null): ModelTime
|
|
|
572
576
|
ORDER BY bucket ASC
|
|
573
577
|
`);
|
|
574
578
|
|
|
575
|
-
const
|
|
579
|
+
const rowsRaw = hasCutoff ? stmt.all(bucketMs, bucketMs, seriesCutoff) : stmt.all(bucketMs, bucketMs);
|
|
580
|
+
const rows = rowsRaw as Array<{ bucket: number; model: string; provider: string; requests: number }>;
|
|
576
581
|
return rows.map(row => ({
|
|
577
582
|
timestamp: row.bucket,
|
|
578
583
|
model: row.model,
|
|
@@ -584,7 +589,11 @@ export function getModelTimeSeries(days = 14, cutoff?: number | null): ModelTime
|
|
|
584
589
|
/**
|
|
585
590
|
* Get daily model performance time series data for the last N days.
|
|
586
591
|
*/
|
|
587
|
-
export function getModelPerformanceSeries(
|
|
592
|
+
export function getModelPerformanceSeries(
|
|
593
|
+
days = 14,
|
|
594
|
+
cutoff?: number | null,
|
|
595
|
+
bucketMs = 24 * 60 * 60 * 1000,
|
|
596
|
+
): ModelPerformancePoint[] {
|
|
588
597
|
if (!db) return [];
|
|
589
598
|
|
|
590
599
|
const hasCutoff = cutoff !== null;
|
|
@@ -592,7 +601,7 @@ export function getModelPerformanceSeries(days = 14, cutoff?: number | null): Mo
|
|
|
592
601
|
|
|
593
602
|
const stmt = db.prepare(`
|
|
594
603
|
SELECT
|
|
595
|
-
(timestamp /
|
|
604
|
+
(timestamp / ?) * ? as bucket,
|
|
596
605
|
model,
|
|
597
606
|
provider,
|
|
598
607
|
COUNT(*) as requests,
|
|
@@ -604,7 +613,15 @@ export function getModelPerformanceSeries(days = 14, cutoff?: number | null): Mo
|
|
|
604
613
|
ORDER BY bucket ASC
|
|
605
614
|
`);
|
|
606
615
|
|
|
607
|
-
const
|
|
616
|
+
const rowsRaw = hasCutoff ? stmt.all(bucketMs, bucketMs, seriesCutoff) : stmt.all(bucketMs, bucketMs);
|
|
617
|
+
const rows = rowsRaw as Array<{
|
|
618
|
+
bucket: number;
|
|
619
|
+
model: string;
|
|
620
|
+
provider: string;
|
|
621
|
+
requests: number;
|
|
622
|
+
avg_ttft: number | null;
|
|
623
|
+
avg_tokens_per_second: number | null;
|
|
624
|
+
}>;
|
|
608
625
|
return rows.map(row => ({
|
|
609
626
|
timestamp: row.bucket,
|
|
610
627
|
model: row.model,
|