@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.
@@ -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;
@@ -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.2",
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.2",
41
- "@oh-my-pi/pi-utils": "15.1.2",
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 { timeSeriesHours, timeSeriesBucketMs, modelSeriesDays, modelPerformanceDays, costSeriesDays, cutoff } =
342
- getTimeRangeConfig(range);
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 } = getTimeRangeConfig(range);
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
 
@@ -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 => format(new Date(d.timestamp), "MMM 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 the last 14 days</p>
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(() => buildModelPerformanceLookup(performanceSeries), [performanceSeries]);
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: "14d Trend", align: "center" },
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(points: ModelPerformancePoint[], days = 14): Map<string, ModelPerformanceSeries> {
218
- const dayMs = 24 * 60 * 60 * 1000;
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() / dayMs) * dayMs;
221
- const start = anchor - (days - 1) * dayMs;
222
- const buckets = Array.from({ length: days }, (_, index) => start + index * dayMs);
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
- export function ByModelToggle({ byModel, onChange }: { byModel: boolean; onChange: (v: boolean) => void }) {
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(days = 14, cutoff?: number | null): ModelTimeSeriesPoint[] {
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 / 86400000) * 86400000 as bucket,
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 rows = hasCutoff ? (stmt.all(seriesCutoff) as any[]) : (stmt.all() as any[]);
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(days = 14, cutoff?: number | null): ModelPerformancePoint[] {
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 / 86400000) * 86400000 as bucket,
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 rows = hasCutoff ? (stmt.all(seriesCutoff) as any[]) : (stmt.all() as any[]);
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,