@oh-my-pi/omp-stats 16.3.0 → 16.3.3
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 +6 -0
- package/dist/client/index.js +83 -83
- package/dist/client/styles.css +4 -1
- package/dist/types/aggregator.d.ts +6 -1
- package/dist/types/client/api.d.ts +2 -1
- package/dist/types/client/app/routes.d.ts +1 -1
- package/dist/types/client/data/view-models.d.ts +9 -1
- package/dist/types/client/routes/ToolsRoute.d.ts +7 -0
- package/dist/types/client/routes/index.d.ts +1 -0
- package/dist/types/db.d.ts +31 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/parser.d.ts +3 -1
- package/dist/types/shared-types.d.ts +48 -0
- package/dist/types/types.d.ts +41 -0
- package/package.json +4 -4
- package/src/aggregator.ts +22 -1
- package/src/client/App.tsx +3 -0
- package/src/client/api.ts +8 -0
- package/src/client/app/routes.ts +7 -1
- package/src/client/data/useHashRoute.ts +1 -0
- package/src/client/data/view-models.ts +18 -0
- package/src/client/routes/ToolsRoute.tsx +463 -0
- package/src/client/routes/index.ts +1 -0
- package/src/db.ts +252 -0
- package/src/index.ts +5 -0
- package/src/parser.ts +84 -2
- package/src/server.ts +6 -0
- package/src/shared-types.ts +52 -0
- package/src/types.ts +43 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { Line } from "react-chartjs-2";
|
|
3
|
+
import { getToolDashboardStats } from "../api";
|
|
4
|
+
import { CHART_THEMES, MODEL_COLORS } from "../components/chart-shared";
|
|
5
|
+
import { formatRangeTick, rangeMeta } from "../components/range-meta";
|
|
6
|
+
import { formatCompact, formatCost, formatInteger, formatPercent, formatRelativeTime } from "../data/formatters";
|
|
7
|
+
import { useResource } from "../data/useResource";
|
|
8
|
+
import { buildToolRows, type ToolRowView } from "../data/view-models";
|
|
9
|
+
import type { TimeRange, ToolModelStats, ToolTimeSeriesPoint, ToolUsageStats } from "../types";
|
|
10
|
+
import { AsyncBoundary, DataTable, Panel, StatusPill } from "../ui";
|
|
11
|
+
import { useSystemTheme } from "../useSystemTheme";
|
|
12
|
+
|
|
13
|
+
export interface ToolsRouteProps {
|
|
14
|
+
active: boolean;
|
|
15
|
+
range: TimeRange;
|
|
16
|
+
refreshTrigger: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ToolsRoute({ active, range, refreshTrigger }: ToolsRouteProps) {
|
|
20
|
+
const {
|
|
21
|
+
data: stats,
|
|
22
|
+
error,
|
|
23
|
+
loading,
|
|
24
|
+
} = useResource(["tools", range, refreshTrigger], signal => getToolDashboardStats(range, signal), {
|
|
25
|
+
pollMs: 30000,
|
|
26
|
+
enabled: active,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="stats-route-container space-y-6">
|
|
31
|
+
<AsyncBoundary loading={loading} error={error} data={stats} emptyText="No tool calls recorded for this range.">
|
|
32
|
+
{stats && (
|
|
33
|
+
<>
|
|
34
|
+
<ToolsSummaryPanel byTool={stats.byTool} />
|
|
35
|
+
<ToolCallsChart series={stats.series} timeRange={range} />
|
|
36
|
+
<ToolsTable byTool={stats.byTool} />
|
|
37
|
+
<ToolModelPanel byToolModel={stats.byToolModel} />
|
|
38
|
+
</>
|
|
39
|
+
)}
|
|
40
|
+
</AsyncBoundary>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Summary metrics
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
function ToolsSummaryPanel({ byTool }: { byTool: ToolUsageStats[] }) {
|
|
50
|
+
const totals = useMemo(() => {
|
|
51
|
+
let calls = 0;
|
|
52
|
+
let errors = 0;
|
|
53
|
+
let tokens = 0;
|
|
54
|
+
let output = 0;
|
|
55
|
+
let cost = 0;
|
|
56
|
+
let resultChars = 0;
|
|
57
|
+
let argsChars = 0;
|
|
58
|
+
for (const t of byTool) {
|
|
59
|
+
calls += t.calls;
|
|
60
|
+
errors += t.errors;
|
|
61
|
+
tokens += t.totalTokensShare;
|
|
62
|
+
output += t.outputTokensShare;
|
|
63
|
+
cost += t.costShare;
|
|
64
|
+
resultChars += t.resultChars;
|
|
65
|
+
argsChars += t.argsChars;
|
|
66
|
+
}
|
|
67
|
+
return { calls, errors, tokens, output, cost, resultChars, argsChars, tools: byTool.length };
|
|
68
|
+
}, [byTool]);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Panel
|
|
72
|
+
title="Tool Usage"
|
|
73
|
+
subtitle="Tokens/cost are the invoking turns' real provider usage, split across each turn's tool calls"
|
|
74
|
+
>
|
|
75
|
+
<div className="stats-metric-cluster">
|
|
76
|
+
<div className="stats-metric-primary-grid">
|
|
77
|
+
<div className="stats-metric-card primary">
|
|
78
|
+
<div className="stats-metric-label">Tool Calls</div>
|
|
79
|
+
<div className="stats-metric-value">{formatInteger(totals.calls)}</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="stats-metric-card primary">
|
|
82
|
+
<div className="stats-metric-label">Tools Used</div>
|
|
83
|
+
<div className="stats-metric-value">{formatInteger(totals.tools)}</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="stats-metric-card primary">
|
|
86
|
+
<div className="stats-metric-label">Error Rate</div>
|
|
87
|
+
<div className="stats-metric-value">
|
|
88
|
+
{formatPercent(totals.calls > 0 ? totals.errors / totals.calls : 0)}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="stats-metric-card primary">
|
|
92
|
+
<div className="stats-metric-label">Attributed Cost</div>
|
|
93
|
+
<div className="stats-metric-value">{formatCost(totals.cost)}</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div className="stats-metric-secondary-grid">
|
|
98
|
+
<div className="stats-metric-card secondary">
|
|
99
|
+
<div className="stats-metric-label">Attributed Tokens</div>
|
|
100
|
+
<div className="stats-metric-value">{formatCompact(Math.round(totals.tokens))}</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div className="stats-metric-card secondary">
|
|
103
|
+
<div className="stats-metric-label">Attributed Output</div>
|
|
104
|
+
<div className="stats-metric-value">{formatCompact(Math.round(totals.output))}</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="stats-metric-card secondary">
|
|
107
|
+
<div className="stats-metric-label">Result Text</div>
|
|
108
|
+
<div className="stats-metric-value">{formatCompact(totals.resultChars)} chars</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="stats-metric-card secondary">
|
|
111
|
+
<div className="stats-metric-label">Call Arguments</div>
|
|
112
|
+
<div className="stats-metric-value">{formatCompact(totals.argsChars)} chars</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</Panel>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Calls over time (stacked by top tools)
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
const TOP_TOOLS = 6;
|
|
125
|
+
|
|
126
|
+
function buildToolCallSeries(points: ToolTimeSeriesPoint[]): {
|
|
127
|
+
buckets: number[];
|
|
128
|
+
tools: string[];
|
|
129
|
+
data: Map<number, Record<string, number>>;
|
|
130
|
+
} {
|
|
131
|
+
const totals = new Map<string, number>();
|
|
132
|
+
for (const p of points) totals.set(p.tool, (totals.get(p.tool) ?? 0) + p.calls);
|
|
133
|
+
const ranked = [...totals.entries()].sort((a, b) => b[1] - a[1]);
|
|
134
|
+
const top = ranked.slice(0, TOP_TOOLS).map(([tool]) => tool);
|
|
135
|
+
const topSet = new Set(top);
|
|
136
|
+
const hasOther = ranked.length > top.length;
|
|
137
|
+
const tools = hasOther ? [...top, "Other"] : top;
|
|
138
|
+
|
|
139
|
+
const buckets = [...new Set(points.map(p => p.timestamp))].sort((a, b) => a - b);
|
|
140
|
+
const data = new Map<number, Record<string, number>>();
|
|
141
|
+
for (const bucket of buckets) data.set(bucket, {});
|
|
142
|
+
for (const p of points) {
|
|
143
|
+
const label = topSet.has(p.tool) ? p.tool : "Other";
|
|
144
|
+
const row = data.get(p.timestamp);
|
|
145
|
+
if (row) row[label] = (row[label] ?? 0) + p.calls;
|
|
146
|
+
}
|
|
147
|
+
return { buckets, tools, data };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function ToolCallsChart({ series, timeRange }: { series: ToolTimeSeriesPoint[]; timeRange: TimeRange }) {
|
|
151
|
+
const theme = useSystemTheme();
|
|
152
|
+
const chartTheme = CHART_THEMES[theme];
|
|
153
|
+
const meta = rangeMeta(timeRange);
|
|
154
|
+
|
|
155
|
+
const chartSeries = useMemo(() => buildToolCallSeries(series), [series]);
|
|
156
|
+
|
|
157
|
+
const data = useMemo(
|
|
158
|
+
() => ({
|
|
159
|
+
labels: chartSeries.buckets.map(ts => formatRangeTick(ts, timeRange)),
|
|
160
|
+
datasets: chartSeries.tools.map((tool, index) => ({
|
|
161
|
+
label: tool,
|
|
162
|
+
data: chartSeries.buckets.map(bucket => chartSeries.data.get(bucket)?.[tool] ?? 0),
|
|
163
|
+
borderColor: MODEL_COLORS[index % MODEL_COLORS.length],
|
|
164
|
+
backgroundColor: `${MODEL_COLORS[index % MODEL_COLORS.length]}30`,
|
|
165
|
+
fill: true,
|
|
166
|
+
tension: 0.4,
|
|
167
|
+
pointRadius: 0,
|
|
168
|
+
pointHoverRadius: 4,
|
|
169
|
+
borderWidth: 2,
|
|
170
|
+
})),
|
|
171
|
+
}),
|
|
172
|
+
[chartSeries, timeRange],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const options = useMemo(
|
|
176
|
+
() => ({
|
|
177
|
+
responsive: true,
|
|
178
|
+
maintainAspectRatio: false,
|
|
179
|
+
interaction: { mode: "index" as const, intersect: false },
|
|
180
|
+
plugins: {
|
|
181
|
+
legend: {
|
|
182
|
+
position: "top" as const,
|
|
183
|
+
align: "start" as const,
|
|
184
|
+
labels: {
|
|
185
|
+
color: chartTheme.legendLabel,
|
|
186
|
+
usePointStyle: true,
|
|
187
|
+
padding: 16,
|
|
188
|
+
font: { size: 12 },
|
|
189
|
+
boxWidth: 8,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
tooltip: {
|
|
193
|
+
backgroundColor: chartTheme.tooltipBackground,
|
|
194
|
+
titleColor: chartTheme.tooltipTitle,
|
|
195
|
+
bodyColor: chartTheme.tooltipBody,
|
|
196
|
+
borderColor: chartTheme.tooltipBorder,
|
|
197
|
+
borderWidth: 1,
|
|
198
|
+
padding: 12,
|
|
199
|
+
cornerRadius: 8,
|
|
200
|
+
callbacks: {
|
|
201
|
+
label: (context: { dataset: { label?: string }; parsed: { y: number | null } }) =>
|
|
202
|
+
`${context.dataset.label ?? ""}: ${formatInteger(context.parsed.y ?? 0)} calls`,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
scales: {
|
|
207
|
+
x: {
|
|
208
|
+
stacked: true,
|
|
209
|
+
grid: { color: chartTheme.grid, drawBorder: false },
|
|
210
|
+
ticks: { color: chartTheme.tick, font: { size: 11 } },
|
|
211
|
+
},
|
|
212
|
+
y: {
|
|
213
|
+
stacked: true,
|
|
214
|
+
grid: { color: chartTheme.grid, drawBorder: false },
|
|
215
|
+
ticks: { color: chartTheme.tick, font: { size: 11 }, precision: 0 },
|
|
216
|
+
min: 0,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
[chartTheme],
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<Panel title="Calls Over Time" subtitle={`Tool calls over ${meta.windowLabel}, stacked by tool`}>
|
|
225
|
+
<div className="h-[280px]">
|
|
226
|
+
{chartSeries.buckets.length === 0 ? (
|
|
227
|
+
<div className="h-full flex items-center justify-center text-stats-muted text-sm">No data available</div>
|
|
228
|
+
) : (
|
|
229
|
+
<Line data={data} options={options} />
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</Panel>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Per-tool table
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
function errorPillVariant(errorRate: number): "danger" | "warning" | "success" {
|
|
241
|
+
return errorRate > 0.1 ? "danger" : errorRate > 0 ? "warning" : "success";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function ToolsTable({ byTool }: { byTool: ToolUsageStats[] }) {
|
|
245
|
+
const rows = useMemo(() => buildToolRows(byTool), [byTool]);
|
|
246
|
+
|
|
247
|
+
const columns = useMemo(
|
|
248
|
+
() => [
|
|
249
|
+
{
|
|
250
|
+
key: "tool",
|
|
251
|
+
header: "Tool",
|
|
252
|
+
render: (item: ToolRowView) => (
|
|
253
|
+
<div className="stats-font-medium stats-text-primary font-mono truncate max-w-[280px]" title={item.tool}>
|
|
254
|
+
{item.tool}
|
|
255
|
+
</div>
|
|
256
|
+
),
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
key: "calls",
|
|
260
|
+
header: "Calls",
|
|
261
|
+
numeric: true,
|
|
262
|
+
render: (item: ToolRowView) => (
|
|
263
|
+
<div className="stats-text-right">
|
|
264
|
+
<div className="font-mono">{formatInteger(item.calls)}</div>
|
|
265
|
+
<div className="stats-progress-bar-track mt-1 ml-auto w-24 h-1">
|
|
266
|
+
<div
|
|
267
|
+
className="stats-progress-bar-fill"
|
|
268
|
+
data-variant="link"
|
|
269
|
+
style={{ width: `${item.callsPercentage}%` }}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
),
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
key: "errorRate",
|
|
277
|
+
header: "Error Rate",
|
|
278
|
+
numeric: true,
|
|
279
|
+
render: (item: ToolRowView) => (
|
|
280
|
+
<StatusPill variant={errorPillVariant(item.errorRate)}>{formatPercent(item.errorRate)}</StatusPill>
|
|
281
|
+
),
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
key: "tokens",
|
|
285
|
+
header: "Attr. Tokens",
|
|
286
|
+
numeric: true,
|
|
287
|
+
render: (item: ToolRowView) => (
|
|
288
|
+
<span className="font-mono" title="Invoking turns' total tokens, split across each turn's calls">
|
|
289
|
+
{formatCompact(Math.round(item.totalTokensShare))}
|
|
290
|
+
</span>
|
|
291
|
+
),
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
key: "cost",
|
|
295
|
+
header: "Attr. Cost",
|
|
296
|
+
numeric: true,
|
|
297
|
+
render: (item: ToolRowView) => <span className="font-mono">{formatCost(item.costShare)}</span>,
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
key: "resultChars",
|
|
301
|
+
header: "Result Text",
|
|
302
|
+
numeric: true,
|
|
303
|
+
render: (item: ToolRowView) => (
|
|
304
|
+
<span className="font-mono" title="Characters of tool-result text fed back into context">
|
|
305
|
+
{formatCompact(item.resultChars)}
|
|
306
|
+
</span>
|
|
307
|
+
),
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
key: "lastUsed",
|
|
311
|
+
header: "Last Used",
|
|
312
|
+
numeric: true,
|
|
313
|
+
render: (item: ToolRowView) => (
|
|
314
|
+
<span className="stats-text-secondary">{formatRelativeTime(item.lastUsed)}</span>
|
|
315
|
+
),
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
[],
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const renderMobileCard = (item: ToolRowView) => (
|
|
322
|
+
<div className="stats-mobile-card">
|
|
323
|
+
<div className="stats-mobile-card-header mb-2">
|
|
324
|
+
<div className="stats-font-semibold stats-text-primary font-mono">{item.tool}</div>
|
|
325
|
+
<StatusPill variant={errorPillVariant(item.errorRate)}>{formatPercent(item.errorRate)} Err</StatusPill>
|
|
326
|
+
</div>
|
|
327
|
+
<div className="stats-mobile-card-grid">
|
|
328
|
+
<div>
|
|
329
|
+
<div className="stats-mobile-card-label">Calls</div>
|
|
330
|
+
<div className="stats-mobile-card-value font-mono">{formatInteger(item.calls)}</div>
|
|
331
|
+
</div>
|
|
332
|
+
<div>
|
|
333
|
+
<div className="stats-mobile-card-label">Attr. Tokens</div>
|
|
334
|
+
<div className="stats-mobile-card-value font-mono">
|
|
335
|
+
{formatCompact(Math.round(item.totalTokensShare))}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
<div>
|
|
339
|
+
<div className="stats-mobile-card-label">Attr. Cost</div>
|
|
340
|
+
<div className="stats-mobile-card-value font-mono">{formatCost(item.costShare)}</div>
|
|
341
|
+
</div>
|
|
342
|
+
<div>
|
|
343
|
+
<div className="stats-mobile-card-label">Result Text</div>
|
|
344
|
+
<div className="stats-mobile-card-value font-mono">{formatCompact(item.resultChars)}</div>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<Panel title="By Tool" subtitle="Usage per tool, most called first">
|
|
352
|
+
<DataTable
|
|
353
|
+
columns={columns}
|
|
354
|
+
data={rows}
|
|
355
|
+
keyExtractor={item => item.tool}
|
|
356
|
+
renderMobileCard={renderMobileCard}
|
|
357
|
+
emptyText="No tool calls recorded for this range."
|
|
358
|
+
/>
|
|
359
|
+
</Panel>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Per-(tool, model) breakdown
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
function ToolModelPanel({ byToolModel }: { byToolModel: ToolModelStats[] }) {
|
|
368
|
+
const [tool, setTool] = useState<string | null>(null);
|
|
369
|
+
|
|
370
|
+
const tools = useMemo(() => [...new Set(byToolModel.map(row => row.tool))].sort(), [byToolModel]);
|
|
371
|
+
|
|
372
|
+
const rows = useMemo(() => {
|
|
373
|
+
const filtered = tool ? byToolModel.filter(row => row.tool === tool) : byToolModel;
|
|
374
|
+
return filtered.map(row => ({
|
|
375
|
+
...row,
|
|
376
|
+
errorRate: row.calls > 0 ? row.errors / row.calls : 0,
|
|
377
|
+
}));
|
|
378
|
+
}, [byToolModel, tool]);
|
|
379
|
+
|
|
380
|
+
const columns = useMemo(
|
|
381
|
+
() => [
|
|
382
|
+
{
|
|
383
|
+
key: "tool",
|
|
384
|
+
header: "Tool",
|
|
385
|
+
render: (item: ToolModelStats & { errorRate: number }) => (
|
|
386
|
+
<span className="stats-font-medium stats-text-primary font-mono">{item.tool}</span>
|
|
387
|
+
),
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
key: "model",
|
|
391
|
+
header: "Model",
|
|
392
|
+
render: (item: ToolModelStats & { errorRate: number }) => (
|
|
393
|
+
<div>
|
|
394
|
+
<div className="stats-text-primary">{item.model || "(unknown)"}</div>
|
|
395
|
+
<div className="stats-text-secondary text-xs">{item.provider}</div>
|
|
396
|
+
</div>
|
|
397
|
+
),
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
key: "calls",
|
|
401
|
+
header: "Calls",
|
|
402
|
+
numeric: true,
|
|
403
|
+
render: (item: ToolModelStats & { errorRate: number }) => (
|
|
404
|
+
<span className="font-mono">{formatInteger(item.calls)}</span>
|
|
405
|
+
),
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
key: "errorRate",
|
|
409
|
+
header: "Error Rate",
|
|
410
|
+
numeric: true,
|
|
411
|
+
render: (item: ToolModelStats & { errorRate: number }) => (
|
|
412
|
+
<StatusPill variant={errorPillVariant(item.errorRate)}>{formatPercent(item.errorRate)}</StatusPill>
|
|
413
|
+
),
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
key: "tokens",
|
|
417
|
+
header: "Attr. Tokens",
|
|
418
|
+
numeric: true,
|
|
419
|
+
render: (item: ToolModelStats & { errorRate: number }) => (
|
|
420
|
+
<span className="font-mono">{formatCompact(Math.round(item.totalTokensShare))}</span>
|
|
421
|
+
),
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
key: "cost",
|
|
425
|
+
header: "Attr. Cost",
|
|
426
|
+
numeric: true,
|
|
427
|
+
render: (item: ToolModelStats & { errorRate: number }) => (
|
|
428
|
+
<span className="font-mono">{formatCost(item.costShare)}</span>
|
|
429
|
+
),
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
[],
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
return (
|
|
436
|
+
<Panel title="By Model" subtitle="Which models call which tools">
|
|
437
|
+
<div className="mb-4" style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
|
438
|
+
<span className="stats-text-secondary" style={{ fontSize: "0.875rem", whiteSpace: "nowrap" }}>
|
|
439
|
+
Tool
|
|
440
|
+
</span>
|
|
441
|
+
<select
|
|
442
|
+
className="stats-select"
|
|
443
|
+
value={tool ?? ""}
|
|
444
|
+
onChange={e => setTool(e.target.value || null)}
|
|
445
|
+
style={{ maxWidth: "320px", flex: 1 }}
|
|
446
|
+
>
|
|
447
|
+
<option value="">All tools</option>
|
|
448
|
+
{tools.map(name => (
|
|
449
|
+
<option key={name} value={name}>
|
|
450
|
+
{name}
|
|
451
|
+
</option>
|
|
452
|
+
))}
|
|
453
|
+
</select>
|
|
454
|
+
</div>
|
|
455
|
+
<DataTable
|
|
456
|
+
columns={columns}
|
|
457
|
+
data={rows}
|
|
458
|
+
keyExtractor={item => `${item.tool}::${item.model}::${item.provider}`}
|
|
459
|
+
emptyText="No tool calls recorded for this range."
|
|
460
|
+
/>
|
|
461
|
+
</Panel>
|
|
462
|
+
);
|
|
463
|
+
}
|