@oh-my-pi/omp-stats 15.0.0 → 15.0.2
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/package.json +6 -6
- package/src/client/components/BehaviorChart.tsx +85 -270
- package/src/client/components/BehaviorModelsTable.tsx +151 -274
- package/src/client/components/CostChart.tsx +85 -246
- package/src/client/components/ModelsTable.tsx +130 -246
- package/src/client/components/RequestDetail.tsx +0 -2
- package/src/client/components/chart-shared.tsx +320 -0
- package/src/client/components/models-table-shared.tsx +275 -0
- package/src/client/types.ts +21 -121
- package/src/db.ts +41 -1
- package/src/parser.ts +44 -5
- package/src/shared-types.ts +204 -0
- package/src/types.ts +16 -201
|
@@ -9,46 +9,31 @@ import {
|
|
|
9
9
|
Tooltip,
|
|
10
10
|
} from "chart.js";
|
|
11
11
|
import { format } from "date-fns";
|
|
12
|
-
import { ChevronDown, ChevronUp } from "lucide-react";
|
|
13
12
|
import { useMemo, useState } from "react";
|
|
14
13
|
import { Line } from "react-chartjs-2";
|
|
15
14
|
import type { ModelPerformancePoint, ModelStats } from "../types";
|
|
16
15
|
import { useSystemTheme } from "../useSystemTheme";
|
|
16
|
+
import {
|
|
17
|
+
DetailChartEmpty,
|
|
18
|
+
detailChartPlugins,
|
|
19
|
+
detailChartScalesDualAxis,
|
|
20
|
+
ExpandableModelRow,
|
|
21
|
+
lineSeriesStyle,
|
|
22
|
+
MiniSparkline,
|
|
23
|
+
MODEL_COLORS,
|
|
24
|
+
ModelNameCell,
|
|
25
|
+
ModelTableBody,
|
|
26
|
+
ModelTableHeader,
|
|
27
|
+
ModelTableShell,
|
|
28
|
+
TABLE_CHART_THEMES,
|
|
29
|
+
type TableChartTheme,
|
|
30
|
+
TrendEmpty,
|
|
31
|
+
} from "./models-table-shared";
|
|
17
32
|
|
|
18
33
|
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
|
19
34
|
|
|
20
|
-
const
|
|
21
|
-
"#a78bfa", // violet
|
|
22
|
-
"#22d3ee", // cyan
|
|
23
|
-
"#ec4899", // pink
|
|
24
|
-
"#4ade80", // green
|
|
25
|
-
"#fbbf24", // amber
|
|
26
|
-
"#f87171", // red
|
|
27
|
-
"#60a5fa", // blue
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
const CHART_THEMES = {
|
|
31
|
-
dark: {
|
|
32
|
-
legendLabel: "#cbd5e1",
|
|
33
|
-
tooltipBackground: "#16161e",
|
|
34
|
-
tooltipTitle: "#f8fafc",
|
|
35
|
-
tooltipBody: "#94a3b8",
|
|
36
|
-
tooltipBorder: "rgba(255, 255, 255, 0.1)",
|
|
37
|
-
grid: "rgba(255, 255, 255, 0.06)",
|
|
38
|
-
tick: "#94a3b8",
|
|
39
|
-
},
|
|
40
|
-
light: {
|
|
41
|
-
legendLabel: "#334155",
|
|
42
|
-
tooltipBackground: "#ffffff",
|
|
43
|
-
tooltipTitle: "#0f172a",
|
|
44
|
-
tooltipBody: "#334155",
|
|
45
|
-
tooltipBorder: "rgba(15, 23, 42, 0.18)",
|
|
46
|
-
grid: "rgba(15, 23, 42, 0.08)",
|
|
47
|
-
tick: "#475569",
|
|
48
|
-
},
|
|
49
|
-
} as const;
|
|
35
|
+
const GRID_TEMPLATE = "2fr 0.9fr 0.9fr 1fr 0.8fr 0.8fr 140px 40px";
|
|
50
36
|
|
|
51
|
-
type ChartTheme = (typeof CHART_THEMES)[keyof typeof CHART_THEMES];
|
|
52
37
|
interface ModelsTableProps {
|
|
53
38
|
models: ModelStats[];
|
|
54
39
|
performanceSeries: ModelPerformancePoint[];
|
|
@@ -69,184 +54,129 @@ export function ModelsTable({ models, performanceSeries }: ModelsTableProps) {
|
|
|
69
54
|
|
|
70
55
|
const performanceSeriesByKey = useMemo(() => buildModelPerformanceLookup(performanceSeries), [performanceSeries]);
|
|
71
56
|
const theme = useSystemTheme();
|
|
72
|
-
const chartTheme =
|
|
57
|
+
const chartTheme = TABLE_CHART_THEMES[theme];
|
|
73
58
|
const sortedModels = [...models].sort(
|
|
74
59
|
(a, b) => b.totalInputTokens + b.totalOutputTokens - (a.totalInputTokens + a.totalOutputTokens),
|
|
75
60
|
);
|
|
76
61
|
|
|
77
62
|
return (
|
|
78
|
-
<
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
63
|
+
<ModelTableShell title="Model Statistics">
|
|
64
|
+
<ModelTableHeader
|
|
65
|
+
gridTemplate={GRID_TEMPLATE}
|
|
66
|
+
columns={[
|
|
67
|
+
{ label: "Model" },
|
|
68
|
+
{ label: "Requests", align: "right" },
|
|
69
|
+
{ label: "Cost", align: "right" },
|
|
70
|
+
{ label: "Tokens", align: "right" },
|
|
71
|
+
{ label: "Tokens/s", align: "right" },
|
|
72
|
+
{ label: "TTFT", align: "right" },
|
|
73
|
+
{ label: "14d Trend", align: "center" },
|
|
74
|
+
]}
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
<ModelTableBody>
|
|
78
|
+
{sortedModels.map((model, index) => {
|
|
79
|
+
const key = `${model.model}::${model.provider}`;
|
|
80
|
+
const performance = performanceSeriesByKey.get(key);
|
|
81
|
+
const trendData = performance?.data ?? [];
|
|
82
|
+
const trendColor = MODEL_COLORS[index % MODEL_COLORS.length];
|
|
83
|
+
const isExpanded = expandedKey === key;
|
|
84
|
+
const errorRate = model.errorRate * 100;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<ExpandableModelRow
|
|
88
|
+
key={key}
|
|
89
|
+
gridTemplate={GRID_TEMPLATE}
|
|
90
|
+
isExpanded={isExpanded}
|
|
91
|
+
onToggle={() => setExpandedKey(isExpanded ? null : key)}
|
|
92
|
+
cells={[
|
|
93
|
+
<ModelNameCell key="name" model={model.model} provider={model.provider} />,
|
|
94
|
+
<div key="requests" className="text-right text-[var(--text-secondary)] font-mono text-sm">
|
|
95
|
+
{model.totalRequests.toLocaleString()}
|
|
96
|
+
</div>,
|
|
97
|
+
<div key="cost" className="text-right text-[var(--text-secondary)] font-mono text-sm">
|
|
98
|
+
${model.totalCost.toFixed(2)}
|
|
99
|
+
</div>,
|
|
100
|
+
<div key="tokens" className="text-right text-[var(--text-secondary)] font-mono text-sm">
|
|
101
|
+
{(model.totalInputTokens + model.totalOutputTokens).toLocaleString()}
|
|
102
|
+
</div>,
|
|
103
|
+
<div key="tps" className="text-right text-[var(--text-secondary)] font-mono text-sm">
|
|
104
|
+
{model.avgTokensPerSecond?.toFixed(1) ?? "-"}
|
|
105
|
+
</div>,
|
|
106
|
+
<div key="ttft" className="text-right text-[var(--text-secondary)] font-mono text-sm">
|
|
107
|
+
{model.avgTtft ? `${(model.avgTtft / 1000).toFixed(2)}s` : "-"}
|
|
108
|
+
</div>,
|
|
109
|
+
]}
|
|
110
|
+
trendCell={
|
|
111
|
+
trendData.length === 0 ? (
|
|
112
|
+
<TrendEmpty />
|
|
113
|
+
) : (
|
|
114
|
+
<MiniSparkline
|
|
115
|
+
timestamps={trendData.map(d => d.timestamp)}
|
|
116
|
+
values={trendData.map(d => d.avgTokensPerSecond ?? 0)}
|
|
117
|
+
color={trendColor}
|
|
118
|
+
/>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
expandedContent={
|
|
122
|
+
<div className="grid gap-4" style={{ gridTemplateColumns: "200px 1fr" }}>
|
|
123
|
+
<div className="space-y-4 text-sm">
|
|
118
124
|
<div>
|
|
119
|
-
<div className="
|
|
120
|
-
<div className="
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
</div>
|
|
131
|
-
<div className="text-right text-[var(--text-secondary)] font-mono text-sm">
|
|
132
|
-
{model.avgTokensPerSecond?.toFixed(1) ?? "-"}
|
|
133
|
-
</div>
|
|
134
|
-
<div className="text-right text-[var(--text-secondary)] font-mono text-sm">
|
|
135
|
-
{model.avgTtft ? `${(model.avgTtft / 1000).toFixed(2)}s` : "-"}
|
|
136
|
-
</div>
|
|
137
|
-
<div className="h-10">
|
|
138
|
-
{trendData.length === 0 ? (
|
|
139
|
-
<div className="text-[var(--text-muted)] text-center text-sm">-</div>
|
|
140
|
-
) : (
|
|
141
|
-
<TrendChart data={trendData} color={trendColor} />
|
|
142
|
-
)}
|
|
143
|
-
</div>
|
|
144
|
-
<div className="flex justify-center text-[var(--text-muted)]">
|
|
145
|
-
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
|
146
|
-
</div>
|
|
147
|
-
</div>
|
|
148
|
-
</button>
|
|
149
|
-
|
|
150
|
-
{isExpanded && (
|
|
151
|
-
<div className="px-5 py-4 bg-[var(--bg-elevated)] border-t border-[var(--border-subtle)]">
|
|
152
|
-
<div className="grid gap-4" style={{ gridTemplateColumns: "200px 1fr" }}>
|
|
153
|
-
<div className="space-y-4 text-sm">
|
|
154
|
-
<div>
|
|
155
|
-
<div className="text-[var(--text-primary)] font-medium mb-2">Quality</div>
|
|
156
|
-
<div className="space-y-1 text-[var(--text-secondary)]">
|
|
157
|
-
<div className="flex items-center justify-between">
|
|
158
|
-
<span>Error rate</span>
|
|
159
|
-
<span
|
|
160
|
-
className={
|
|
161
|
-
errorRate > 5
|
|
162
|
-
? "text-[var(--accent-red)]"
|
|
163
|
-
: "text-[var(--accent-green)]"
|
|
164
|
-
}
|
|
165
|
-
>
|
|
166
|
-
{errorRate.toFixed(1)}%
|
|
167
|
-
</span>
|
|
168
|
-
</div>
|
|
169
|
-
<div className="flex items-center justify-between">
|
|
170
|
-
<span>Cache rate</span>
|
|
171
|
-
<span className="text-[var(--accent-cyan)]">
|
|
172
|
-
{(model.cacheRate * 100).toFixed(1)}%
|
|
173
|
-
</span>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
125
|
+
<div className="text-[var(--text-primary)] font-medium mb-2">Quality</div>
|
|
126
|
+
<div className="space-y-1 text-[var(--text-secondary)]">
|
|
127
|
+
<div className="flex items-center justify-between">
|
|
128
|
+
<span>Error rate</span>
|
|
129
|
+
<span
|
|
130
|
+
className={
|
|
131
|
+
errorRate > 5 ? "text-[var(--accent-red)]" : "text-[var(--accent-green)]"
|
|
132
|
+
}
|
|
133
|
+
>
|
|
134
|
+
{errorRate.toFixed(1)}%
|
|
135
|
+
</span>
|
|
176
136
|
</div>
|
|
177
|
-
<div>
|
|
178
|
-
<
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
<span className="font-mono">
|
|
183
|
-
{model.avgDuration ? `${(model.avgDuration / 1000).toFixed(2)}s` : "-"}
|
|
184
|
-
</span>
|
|
185
|
-
</div>
|
|
186
|
-
<div className="flex items-center justify-between">
|
|
187
|
-
<span>Avg TTFT</span>
|
|
188
|
-
<span className="font-mono">
|
|
189
|
-
{model.avgTtft ? `${(model.avgTtft / 1000).toFixed(2)}s` : "-"}
|
|
190
|
-
</span>
|
|
191
|
-
</div>
|
|
192
|
-
</div>
|
|
137
|
+
<div className="flex items-center justify-between">
|
|
138
|
+
<span>Cache rate</span>
|
|
139
|
+
<span className="text-[var(--accent-cyan)]">
|
|
140
|
+
{(model.cacheRate * 100).toFixed(1)}%
|
|
141
|
+
</span>
|
|
193
142
|
</div>
|
|
194
143
|
</div>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<
|
|
202
|
-
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<div className="text-[var(--text-primary)] font-medium mb-2">Latency</div>
|
|
147
|
+
<div className="space-y-1 text-[var(--text-secondary)]">
|
|
148
|
+
<div className="flex items-center justify-between">
|
|
149
|
+
<span>Avg duration</span>
|
|
150
|
+
<span className="font-mono">
|
|
151
|
+
{model.avgDuration ? `${(model.avgDuration / 1000).toFixed(2)}s` : "-"}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
<div className="flex items-center justify-between">
|
|
155
|
+
<span>Avg TTFT</span>
|
|
156
|
+
<span className="font-mono">
|
|
157
|
+
{model.avgTtft ? `${(model.avgTtft / 1000).toFixed(2)}s` : "-"}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
203
160
|
</div>
|
|
204
161
|
</div>
|
|
205
162
|
</div>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
163
|
+
<div className="h-[200px]">
|
|
164
|
+
{trendData.length === 0 ? (
|
|
165
|
+
<DetailChartEmpty />
|
|
166
|
+
) : (
|
|
167
|
+
<PerformanceChart data={trendData} color={trendColor} chartTheme={chartTheme} />
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
})}
|
|
175
|
+
</ModelTableBody>
|
|
176
|
+
</ModelTableShell>
|
|
213
177
|
);
|
|
214
178
|
}
|
|
215
179
|
|
|
216
|
-
function TrendChart({
|
|
217
|
-
data,
|
|
218
|
-
color,
|
|
219
|
-
}: {
|
|
220
|
-
data: Array<{ timestamp: number; avgTokensPerSecond: number | null }>;
|
|
221
|
-
color: string;
|
|
222
|
-
}) {
|
|
223
|
-
const chartData = {
|
|
224
|
-
labels: data.map(d => format(new Date(d.timestamp), "MMM d")),
|
|
225
|
-
datasets: [
|
|
226
|
-
{
|
|
227
|
-
data: data.map(d => d.avgTokensPerSecond ?? 0),
|
|
228
|
-
borderColor: color,
|
|
229
|
-
backgroundColor: "transparent",
|
|
230
|
-
tension: 0.4,
|
|
231
|
-
pointRadius: 0,
|
|
232
|
-
borderWidth: 2,
|
|
233
|
-
},
|
|
234
|
-
],
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const options = {
|
|
238
|
-
responsive: true,
|
|
239
|
-
maintainAspectRatio: false,
|
|
240
|
-
plugins: { legend: { display: false }, tooltip: { enabled: false } },
|
|
241
|
-
scales: {
|
|
242
|
-
x: { display: false },
|
|
243
|
-
y: { display: false, min: 0 },
|
|
244
|
-
},
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
return <Line data={chartData} options={options} />;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
180
|
function PerformanceChart({
|
|
251
181
|
data,
|
|
252
182
|
color,
|
|
@@ -254,7 +184,7 @@ function PerformanceChart({
|
|
|
254
184
|
}: {
|
|
255
185
|
data: Array<{ timestamp: number; avgTtftSeconds: number | null; avgTokensPerSecond: number | null }>;
|
|
256
186
|
color: string;
|
|
257
|
-
chartTheme:
|
|
187
|
+
chartTheme: TableChartTheme;
|
|
258
188
|
}) {
|
|
259
189
|
const chartData = {
|
|
260
190
|
labels: data.map(d => format(new Date(d.timestamp), "MMM d")),
|
|
@@ -262,21 +192,13 @@ function PerformanceChart({
|
|
|
262
192
|
{
|
|
263
193
|
label: "TTFT",
|
|
264
194
|
data: data.map(d => d.avgTtftSeconds ?? null),
|
|
265
|
-
|
|
266
|
-
backgroundColor: "transparent",
|
|
267
|
-
tension: 0.4,
|
|
268
|
-
pointRadius: 0,
|
|
269
|
-
borderWidth: 2,
|
|
195
|
+
...lineSeriesStyle("#fbbf24"),
|
|
270
196
|
yAxisID: "y" as const,
|
|
271
197
|
},
|
|
272
198
|
{
|
|
273
199
|
label: "Tokens/s",
|
|
274
200
|
data: data.map(d => d.avgTokensPerSecond ?? null),
|
|
275
|
-
|
|
276
|
-
backgroundColor: "transparent",
|
|
277
|
-
tension: 0.4,
|
|
278
|
-
pointRadius: 0,
|
|
279
|
-
borderWidth: 2,
|
|
201
|
+
...lineSeriesStyle(color),
|
|
280
202
|
yAxisID: "y1" as const,
|
|
281
203
|
},
|
|
282
204
|
],
|
|
@@ -285,46 +207,8 @@ function PerformanceChart({
|
|
|
285
207
|
const options = {
|
|
286
208
|
responsive: true,
|
|
287
209
|
maintainAspectRatio: false,
|
|
288
|
-
plugins:
|
|
289
|
-
|
|
290
|
-
display: true,
|
|
291
|
-
position: "top" as const,
|
|
292
|
-
labels: {
|
|
293
|
-
color: chartTheme.legendLabel,
|
|
294
|
-
usePointStyle: true,
|
|
295
|
-
padding: 16,
|
|
296
|
-
font: { size: 12 },
|
|
297
|
-
},
|
|
298
|
-
},
|
|
299
|
-
tooltip: {
|
|
300
|
-
backgroundColor: chartTheme.tooltipBackground,
|
|
301
|
-
titleColor: chartTheme.tooltipTitle,
|
|
302
|
-
bodyColor: chartTheme.tooltipBody,
|
|
303
|
-
borderColor: chartTheme.tooltipBorder,
|
|
304
|
-
borderWidth: 1,
|
|
305
|
-
cornerRadius: 8,
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
scales: {
|
|
309
|
-
x: {
|
|
310
|
-
grid: { color: chartTheme.grid },
|
|
311
|
-
ticks: { color: chartTheme.tick, font: { size: 11 } },
|
|
312
|
-
},
|
|
313
|
-
y: {
|
|
314
|
-
type: "linear" as const,
|
|
315
|
-
display: true,
|
|
316
|
-
position: "left" as const,
|
|
317
|
-
grid: { color: chartTheme.grid },
|
|
318
|
-
ticks: { color: chartTheme.tick, font: { size: 11 } },
|
|
319
|
-
},
|
|
320
|
-
y1: {
|
|
321
|
-
type: "linear" as const,
|
|
322
|
-
display: true,
|
|
323
|
-
position: "right" as const,
|
|
324
|
-
grid: { drawOnChartArea: false },
|
|
325
|
-
ticks: { color: chartTheme.tick, font: { size: 11 } },
|
|
326
|
-
},
|
|
327
|
-
},
|
|
210
|
+
plugins: detailChartPlugins(chartTheme),
|
|
211
|
+
scales: detailChartScalesDualAxis(chartTheme),
|
|
328
212
|
};
|
|
329
213
|
|
|
330
214
|
return <Line data={chartData} options={options} />;
|
|
@@ -35,13 +35,11 @@ export function RequestDetail({ id, onClose }: RequestDetailProps) {
|
|
|
35
35
|
if (!details) return null;
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
|
-
// biome-ignore lint/a11y/noStaticElementInteractions: modal backdrop dismissal
|
|
39
38
|
<div
|
|
40
39
|
role="presentation"
|
|
41
40
|
className="fixed inset-0 bg-[var(--bg-overlay)] backdrop-blur-sm flex justify-end z-[100] animate-fade-in"
|
|
42
41
|
onClick={onClose}
|
|
43
42
|
>
|
|
44
|
-
{/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation for modal content */}
|
|
45
43
|
<div
|
|
46
44
|
role="dialog"
|
|
47
45
|
aria-modal="true"
|