@oh-my-pi/omp-stats 18.0.7 → 18.0.9

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.
@@ -5,6 +5,11 @@
5
5
  * behavior series.
6
6
  */
7
7
  export declare const MODEL_COLORS: string[];
8
+ export declare function buildModelColorLookup(records: readonly {
9
+ model: string;
10
+ provider: string;
11
+ totalRequests: number;
12
+ }[]): Map<string, string>;
8
13
  export declare const CHART_THEMES: {
9
14
  readonly dark: {
10
15
  readonly legendLabel: "#a89fb3";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/omp-stats",
4
- "version": "18.0.7",
4
+ "version": "18.0.9",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -39,9 +39,9 @@
39
39
  "fmt": "biome format --write ."
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "18.0.7",
43
- "@oh-my-pi/pi-catalog": "18.0.7",
44
- "@oh-my-pi/pi-utils": "18.0.7",
42
+ "@oh-my-pi/pi-ai": "18.0.9",
43
+ "@oh-my-pi/pi-catalog": "18.0.9",
44
+ "@oh-my-pi/pi-utils": "18.0.9",
45
45
  "@tailwindcss/node": "^4.3.2",
46
46
  "chart.js": "^4.5.1",
47
47
  "lucide-react": "^1.24.0",
@@ -20,6 +20,22 @@ export const MODEL_COLORS = [
20
20
  "#ff6b7d", // rose
21
21
  ];
22
22
 
23
+ export function buildModelColorLookup(
24
+ records: readonly { model: string; provider: string; totalRequests: number }[],
25
+ ): Map<string, string> {
26
+ const rankedRecords = [...records].sort(
27
+ (a, b) =>
28
+ b.totalRequests - a.totalRequests || `${a.model}::${a.provider}`.localeCompare(`${b.model}::${b.provider}`),
29
+ );
30
+
31
+ return new Map(
32
+ rankedRecords.map((record, index) => [
33
+ `${record.model}::${record.provider}`,
34
+ MODEL_COLORS[index % MODEL_COLORS.length],
35
+ ]),
36
+ );
37
+ }
38
+
23
39
  export const CHART_THEMES = {
24
40
  dark: {
25
41
  legendLabel: "#a89fb3",
@@ -1,7 +1,7 @@
1
1
  import { useMemo, useState } from "react";
2
2
  import { Line } from "react-chartjs-2";
3
3
  import { getModelDashboardStats } from "../api";
4
- import { CHART_THEMES, MODEL_COLORS } from "../components/chart-shared";
4
+ import { buildModelColorLookup, CHART_THEMES, MODEL_COLORS } from "../components/chart-shared";
5
5
  import {
6
6
  DetailChartEmpty,
7
7
  detailChartPlugins,
@@ -40,17 +40,23 @@ export function ModelsRoute({ active, range, refreshTrigger }: ModelsRouteProps)
40
40
  pollMs: 30000,
41
41
  enabled: active,
42
42
  });
43
+ const modelColorLookup = useMemo(() => buildModelColorLookup(modelStats?.byModel ?? []), [modelStats?.byModel]);
43
44
 
44
45
  return (
45
46
  <div className="stats-route-container space-y-6">
46
47
  <AsyncBoundary loading={loading} error={error} data={modelStats}>
47
48
  {modelStats && (
48
49
  <>
49
- <ModelShareChart modelSeries={modelStats.modelSeries} timeRange={range} />
50
+ <ModelShareChart
51
+ modelSeries={modelStats.modelSeries}
52
+ timeRange={range}
53
+ colorLookup={modelColorLookup}
54
+ />
50
55
  <ModelsTable
51
56
  models={modelStats.byModel}
52
57
  performanceSeries={modelStats.modelPerformanceSeries}
53
58
  timeRange={range}
59
+ colorLookup={modelColorLookup}
54
60
  />
55
61
  </>
56
62
  )}
@@ -59,7 +65,15 @@ export function ModelsRoute({ active, range, refreshTrigger }: ModelsRouteProps)
59
65
  );
60
66
  }
61
67
 
62
- function ModelShareChart({ modelSeries, timeRange }: { modelSeries: ModelTimeSeriesPoint[]; timeRange: TimeRange }) {
68
+ function ModelShareChart({
69
+ modelSeries,
70
+ timeRange,
71
+ colorLookup,
72
+ }: {
73
+ modelSeries: ModelTimeSeriesPoint[];
74
+ timeRange: TimeRange;
75
+ colorLookup: ReadonlyMap<string, string>;
76
+ }) {
63
77
  const theme = useSystemTheme();
64
78
  const chartTheme = CHART_THEMES[theme];
65
79
  const meta = rangeMeta(timeRange);
@@ -69,19 +83,25 @@ function ModelShareChart({ modelSeries, timeRange }: { modelSeries: ModelTimeSer
69
83
  const data = useMemo(() => {
70
84
  return {
71
85
  labels: chartData.data.map(d => formatRangeTick(d.timestamp, timeRange)),
72
- datasets: chartData.series.map((seriesName, index) => ({
73
- label: seriesName,
74
- data: chartData.data.map(d => d[seriesName] ?? 0),
75
- borderColor: MODEL_COLORS[index % MODEL_COLORS.length],
76
- backgroundColor: `${MODEL_COLORS[index % MODEL_COLORS.length]}20`,
77
- fill: true,
78
- tension: 0.4,
79
- pointRadius: 0,
80
- pointHoverRadius: 4,
81
- borderWidth: 2,
82
- })),
86
+ datasets: chartData.series.map((series, index) => {
87
+ const fallbackColor = MODEL_COLORS[index % MODEL_COLORS.length];
88
+ const color = series.key ? (colorLookup.get(series.key) ?? fallbackColor) : fallbackColor;
89
+ const dataKey = series.key ?? series.label;
90
+
91
+ return {
92
+ label: series.label,
93
+ data: chartData.data.map(d => d[dataKey] ?? 0),
94
+ borderColor: color,
95
+ backgroundColor: `${color}20`,
96
+ fill: true,
97
+ tension: 0.4,
98
+ pointRadius: 0,
99
+ pointHoverRadius: 4,
100
+ borderWidth: 2,
101
+ };
102
+ }),
83
103
  };
84
- }, [chartData, timeRange]);
104
+ }, [chartData, colorLookup, timeRange]);
85
105
 
86
106
  const options = useMemo(() => {
87
107
  return {
@@ -166,7 +186,7 @@ function buildModelPreferenceSeries(
166
186
  topN = 5,
167
187
  ): {
168
188
  data: Array<Record<string, number>>;
169
- series: string[];
189
+ series: Array<{ key?: string; label: string }>;
170
190
  } {
171
191
  if (points.length === 0) return { data: [], series: [] };
172
192
 
@@ -209,22 +229,26 @@ function buildModelPreferenceSeries(
209
229
  total: 0,
210
230
  };
211
231
  bucket.total += point.requests;
212
- const seriesLabel = topKeys.has(key) ? (labelByKey.get(key) ?? point.model) : "Other";
213
- bucket[seriesLabel] = (bucket[seriesLabel] ?? 0) + point.requests;
232
+ const seriesKey = topKeys.has(key) ? key : "Other";
233
+ bucket[seriesKey] = (bucket[seriesKey] ?? 0) + point.requests;
214
234
  dataMap.set(point.timestamp, bucket);
215
235
  }
216
236
 
217
- const series = topEntries.map(entry => labelByKey.get(entry.key) ?? entry.model);
237
+ const series: Array<{ key?: string; label: string }> = topEntries.map(entry => ({
238
+ key: entry.key,
239
+ label: labelByKey.get(entry.key) ?? entry.model,
240
+ }));
218
241
  if ([...dataMap.values()].some(row => (row.Other ?? 0) > 0)) {
219
- series.push("Other");
242
+ series.push({ label: "Other" });
220
243
  }
221
244
 
222
245
  const data = [...dataMap.values()]
223
246
  .sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0))
224
247
  .map(row => {
225
248
  const total = row.total ?? 0;
226
- for (const key of series) {
227
- row[key] = total > 0 ? ((row[key] ?? 0) / total) * 100 : 0;
249
+ for (const seriesItem of series) {
250
+ const seriesKey = seriesItem.key ?? seriesItem.label;
251
+ row[seriesKey] = total > 0 ? ((row[seriesKey] ?? 0) / total) * 100 : 0;
228
252
  }
229
253
  return row;
230
254
  });
@@ -238,10 +262,12 @@ function ModelsTable({
238
262
  models,
239
263
  performanceSeries,
240
264
  timeRange,
265
+ colorLookup,
241
266
  }: {
242
267
  models: ModelStats[];
243
268
  performanceSeries: ModelPerformancePoint[];
244
269
  timeRange: TimeRange;
270
+ colorLookup: ReadonlyMap<string, string>;
245
271
  }) {
246
272
  const [expandedKey, setExpandedKey] = useState<string | null>(null);
247
273
  const meta = rangeMeta(timeRange);
@@ -280,7 +306,7 @@ function ModelsTable({
280
306
  const key = `${model.model}::${model.provider}`;
281
307
  const performance = performanceSeriesByKey.get(key);
282
308
  const trendData = performance?.data ?? [];
283
- const trendColor = MODEL_COLORS[index % MODEL_COLORS.length];
309
+ const trendColor = colorLookup.get(key) ?? MODEL_COLORS[index % MODEL_COLORS.length];
284
310
  const isExpanded = expandedKey === key;
285
311
  const errorRate = model.errorRate * 100;
286
312