@oh-my-pi/omp-stats 18.3.5 → 18.4.0
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 +16 -0
- package/README.md +13 -11
- package/build.ts +5 -49
- package/dist/client/index.css +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/index.js +105 -126
- package/dist/types/aggregator.d.ts +25 -22
- package/dist/types/client/api.d.ts +11 -7
- package/dist/types/client/app/LiveChip.d.ts +2 -0
- package/dist/types/client/app/Shell.d.ts +16 -0
- package/dist/types/client/app/ThemeToggle.d.ts +1 -0
- package/dist/types/client/app/nav.d.ts +18 -0
- package/dist/types/client/charts/BarList.d.ts +17 -0
- package/dist/types/client/charts/Chart.d.ts +49 -0
- package/dist/types/client/charts/Legend.d.ts +16 -0
- package/dist/types/client/charts/ShareBar.d.ts +11 -0
- package/dist/types/client/charts/Sparkline.d.ts +11 -0
- package/dist/types/client/charts/TimeChart.d.ts +9 -0
- package/dist/types/client/charts/index.d.ts +7 -0
- package/dist/types/client/charts/types.d.ts +25 -0
- package/dist/types/client/charts/useWidth.d.ts +3 -0
- package/dist/types/client/data/colors.d.ts +25 -0
- package/dist/types/client/data/formatters.d.ts +9 -1
- package/dist/types/client/data/live.d.ts +29 -0
- package/dist/types/client/data/query.d.ts +35 -0
- package/dist/types/client/data/range.d.ts +31 -0
- package/dist/types/client/data/series.d.ts +28 -0
- package/dist/types/client/data/useHashRoute.d.ts +18 -4
- package/dist/types/client/data/view-models.d.ts +111 -28
- package/dist/types/client/index.d.ts +0 -1
- package/dist/types/client/routes/CostsRoute.d.ts +2 -2
- package/dist/types/client/routes/ErrorsRoute.d.ts +2 -2
- package/dist/types/client/routes/FrustrationRoute.d.ts +7 -0
- package/dist/types/client/routes/GainRoute.d.ts +1 -2
- package/dist/types/client/routes/ModelsRoute.d.ts +2 -2
- package/dist/types/client/routes/OverviewRoute.d.ts +1 -2
- package/dist/types/client/routes/ProjectsRoute.d.ts +2 -2
- package/dist/types/client/routes/ProvidersRoute.d.ts +2 -2
- package/dist/types/client/routes/RequestsRoute.d.ts +1 -2
- package/dist/types/client/routes/ToolsRoute.d.ts +2 -2
- package/dist/types/client/routes/TracesRoute.d.ts +2 -2
- package/dist/types/client/routes/index.d.ts +1 -1
- package/dist/types/client/traces/AggregatesPanel.d.ts +4 -3
- package/dist/types/client/traces/SummaryStrip.d.ts +2 -2
- package/dist/types/client/traces/trace-colors.d.ts +18 -12
- package/dist/types/client/ui/Badge.d.ts +19 -0
- package/dist/types/client/ui/Card.d.ts +25 -0
- package/dist/types/client/ui/Drawer.d.ts +25 -0
- package/dist/types/client/ui/JsonBlock.d.ts +2 -2
- package/dist/types/client/ui/Modal.d.ts +23 -0
- package/dist/types/client/ui/RequestDrawer.d.ts +10 -1
- package/dist/types/client/ui/SearchInput.d.ts +8 -0
- package/dist/types/client/ui/Segmented.d.ts +15 -0
- package/dist/types/client/ui/Stat.d.ts +36 -0
- package/dist/types/client/ui/States.d.ts +41 -0
- package/dist/types/client/ui/Table.d.ts +48 -0
- package/dist/types/client/ui/index.d.ts +9 -10
- package/dist/types/db.d.ts +90 -106
- package/dist/types/frustration.d.ts +70 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/live.d.ts +30 -0
- package/dist/types/rollup.d.ts +115 -0
- package/dist/types/server.d.ts +6 -1
- package/dist/types/shared-types.d.ts +116 -56
- package/dist/types/types.d.ts +4 -0
- package/dist/types/user-metrics.d.ts +8 -0
- package/package.json +7 -13
- package/src/aggregator.ts +276 -211
- package/src/client/App.tsx +38 -80
- package/src/client/api.ts +65 -7
- package/src/client/app/LiveChip.tsx +97 -0
- package/src/client/app/Shell.tsx +181 -0
- package/src/client/app/ThemeToggle.tsx +5 -2
- package/src/client/app/nav.ts +78 -0
- package/src/client/charts/BarList.tsx +48 -0
- package/src/client/charts/Chart.tsx +388 -0
- package/src/client/charts/Legend.tsx +61 -0
- package/src/client/charts/ShareBar.tsx +26 -0
- package/src/client/charts/Sparkline.tsx +38 -0
- package/src/client/charts/TimeChart.tsx +22 -0
- package/src/client/charts/index.ts +7 -0
- package/src/client/charts/types.ts +27 -0
- package/src/client/charts/useWidth.ts +19 -0
- package/src/client/data/colors.ts +45 -0
- package/src/client/data/formatters.ts +30 -9
- package/src/client/data/live.tsx +85 -0
- package/src/client/data/query.ts +166 -0
- package/src/client/data/range.ts +82 -0
- package/src/client/data/series.ts +85 -0
- package/src/client/data/useHashRoute.ts +41 -83
- package/src/client/data/view-models.ts +347 -140
- package/src/client/index.tsx +3 -6
- package/src/client/routes/CostsRoute.tsx +394 -222
- package/src/client/routes/ErrorsRoute.tsx +401 -98
- package/src/client/routes/FrustrationRoute.tsx +924 -0
- package/src/client/routes/GainRoute.tsx +209 -206
- package/src/client/routes/ModelsRoute.tsx +540 -431
- package/src/client/routes/OverviewRoute.tsx +283 -311
- package/src/client/routes/ProjectsRoute.tsx +298 -156
- package/src/client/routes/ProvidersRoute.tsx +948 -416
- package/src/client/routes/RequestsRoute.tsx +295 -107
- package/src/client/routes/ToolsRoute.tsx +495 -398
- package/src/client/routes/TracesRoute.tsx +136 -117
- package/src/client/routes/costs.css +24 -0
- package/src/client/routes/errors.css +60 -0
- package/src/client/routes/frustration.css +123 -0
- package/src/client/routes/index.ts +1 -1
- package/src/client/routes/models.css +43 -0
- package/src/client/routes/projects.css +6 -0
- package/src/client/routes/providers.css +28 -0
- package/src/client/routes/tools.css +14 -0
- package/src/client/styles.css +1396 -1299
- package/src/client/traces/AggregatesPanel.tsx +55 -74
- package/src/client/traces/Minimap.tsx +6 -15
- package/src/client/traces/SpanDrawer.tsx +128 -194
- package/src/client/traces/SummaryStrip.tsx +21 -24
- package/src/client/traces/TimelineCanvas.tsx +27 -44
- package/src/client/traces/TraceView.tsx +175 -130
- package/src/client/traces/TranscriptList.tsx +13 -41
- package/src/client/traces/trace-colors.ts +60 -53
- package/src/client/traces/traces.css +279 -0
- package/src/client/ui/Badge.tsx +29 -0
- package/src/client/ui/Card.tsx +61 -0
- package/src/client/ui/Drawer.tsx +93 -0
- package/src/client/ui/JsonBlock.tsx +26 -44
- package/src/client/ui/Modal.tsx +106 -0
- package/src/client/ui/RequestDrawer.tsx +163 -188
- package/src/client/ui/SearchInput.tsx +25 -0
- package/src/client/ui/Segmented.tsx +61 -0
- package/src/client/ui/Stat.tsx +104 -0
- package/src/client/ui/States.tsx +81 -0
- package/src/client/ui/Table.tsx +229 -0
- package/src/client/ui/index.ts +9 -10
- package/src/client/ui/modal.css +54 -0
- package/src/client/ui/request-drawer.css +40 -0
- package/src/db.ts +379 -983
- package/src/frustration.ts +473 -0
- package/src/index.ts +22 -22
- package/src/live.ts +259 -0
- package/src/parser.ts +55 -66
- package/src/rollup.ts +951 -0
- package/src/server.ts +123 -40
- package/src/shared-types.ts +120 -56
- package/src/trace.ts +14 -7
- package/src/types.ts +4 -0
- package/src/user-metrics.ts +13 -0
- package/dist/client/styles.css +0 -1976
- package/dist/types/client/app/AppLayout.d.ts +0 -16
- package/dist/types/client/app/NavRail.d.ts +0 -7
- package/dist/types/client/app/RangeControl.d.ts +0 -7
- package/dist/types/client/app/SyncButton.d.ts +0 -14
- package/dist/types/client/app/TopBar.d.ts +0 -15
- package/dist/types/client/app/routes.d.ts +0 -12
- package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
- package/dist/types/client/components/chart-shared.d.ts +0 -178
- package/dist/types/client/components/models-table-shared.d.ts +0 -175
- package/dist/types/client/components/range-meta.d.ts +0 -21
- package/dist/types/client/data/charts.d.ts +0 -1
- package/dist/types/client/data/useResource.d.ts +0 -13
- package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
- package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
- package/dist/types/client/ui/DataTable.d.ts +0 -17
- package/dist/types/client/ui/EmptyState.d.ts +0 -7
- package/dist/types/client/ui/ErrorState.d.ts +0 -6
- package/dist/types/client/ui/MetricCluster.d.ts +0 -5
- package/dist/types/client/ui/Panel.d.ts +0 -7
- package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
- package/dist/types/client/ui/Skeleton.d.ts +0 -8
- package/dist/types/client/ui/StatusPill.d.ts +0 -7
- package/src/client/app/AppLayout.tsx +0 -93
- package/src/client/app/NavRail.tsx +0 -44
- package/src/client/app/RangeControl.tsx +0 -39
- package/src/client/app/SyncButton.tsx +0 -75
- package/src/client/app/TopBar.tsx +0 -73
- package/src/client/app/routes.ts +0 -93
- package/src/client/components/AgentTokenShare.tsx +0 -68
- package/src/client/components/chart-shared.tsx +0 -273
- package/src/client/components/models-table-shared.tsx +0 -255
- package/src/client/components/range-meta.ts +0 -73
- package/src/client/data/charts.ts +0 -14
- package/src/client/data/useResource.ts +0 -154
- package/src/client/routes/BehaviorRoute.tsx +0 -623
- package/src/client/ui/AsyncBoundary.tsx +0 -54
- package/src/client/ui/DataTable.tsx +0 -122
- package/src/client/ui/EmptyState.tsx +0 -16
- package/src/client/ui/ErrorState.tsx +0 -25
- package/src/client/ui/MetricCluster.tsx +0 -92
- package/src/client/ui/Panel.tsx +0 -24
- package/src/client/ui/SegmentedControl.tsx +0 -36
- package/src/client/ui/Skeleton.tsx +0 -17
- package/src/client/ui/StatusPill.tsx +0 -15
- package/tailwind.config.js +0 -40
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { X } from "lucide-react";
|
|
1
2
|
import { useMemo, useState } from "react";
|
|
2
|
-
import { Line } from "react-chartjs-2";
|
|
3
3
|
import { getToolDashboardStats } from "../api";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { Legend, Sparkline, TimeChart, useHiddenSeries } from "../charts";
|
|
5
|
+
import { buildColorLookup, OTHER_COLOR } from "../data/colors";
|
|
6
6
|
import {
|
|
7
7
|
formatCompact,
|
|
8
8
|
formatEstimatedCost,
|
|
@@ -10,466 +10,563 @@ import {
|
|
|
10
10
|
formatPercent,
|
|
11
11
|
formatRelativeTime,
|
|
12
12
|
} from "../data/formatters";
|
|
13
|
-
import {
|
|
13
|
+
import { useQuery } from "../data/query";
|
|
14
|
+
import { bucketAxis, rangeMeta } from "../data/range";
|
|
15
|
+
import { densify, pivotSeries } from "../data/series";
|
|
14
16
|
import { buildToolRows, type ToolRowView } from "../data/view-models";
|
|
15
|
-
import type { TimeRange, ToolModelStats, ToolTimeSeriesPoint
|
|
16
|
-
import {
|
|
17
|
-
|
|
17
|
+
import type { TimeRange, ToolDashboardStats, ToolModelStats, ToolTimeSeriesPoint } from "../types";
|
|
18
|
+
import {
|
|
19
|
+
Badge,
|
|
20
|
+
Card,
|
|
21
|
+
ChartSkeleton,
|
|
22
|
+
type Column,
|
|
23
|
+
EmptyState,
|
|
24
|
+
errorRateTone,
|
|
25
|
+
LabelCell,
|
|
26
|
+
MeterCell,
|
|
27
|
+
PageHeader,
|
|
28
|
+
QueryView,
|
|
29
|
+
Segmented,
|
|
30
|
+
Stat,
|
|
31
|
+
StatGrid,
|
|
32
|
+
Swatch,
|
|
33
|
+
Table,
|
|
34
|
+
TableSkeleton,
|
|
35
|
+
} from "../ui";
|
|
36
|
+
import "./tools.css";
|
|
18
37
|
|
|
19
38
|
export interface ToolsRouteProps {
|
|
20
39
|
active: boolean;
|
|
21
40
|
range: TimeRange;
|
|
22
|
-
refreshTrigger: number;
|
|
23
41
|
}
|
|
24
42
|
|
|
25
|
-
|
|
26
|
-
const {
|
|
27
|
-
data: stats,
|
|
28
|
-
error,
|
|
29
|
-
loading,
|
|
30
|
-
} = useResource(["tools", range, refreshTrigger], signal => getToolDashboardStats(range, signal), {
|
|
31
|
-
pollMs: 30000,
|
|
32
|
-
enabled: active,
|
|
33
|
-
});
|
|
43
|
+
type CallMetric = "calls" | "errors";
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<>
|
|
40
|
-
<ToolsSummaryPanel byTool={stats.byTool} />
|
|
41
|
-
<ToolCallsChart series={stats.series} timeRange={range} />
|
|
42
|
-
<ToolsTable byTool={stats.byTool} />
|
|
43
|
-
<ToolModelPanel byToolModel={stats.byToolModel} />
|
|
44
|
-
</>
|
|
45
|
-
)}
|
|
46
|
-
</AsyncBoundary>
|
|
47
|
-
</div>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
45
|
+
const METRIC_OPTIONS = [
|
|
46
|
+
{ value: "calls" as const, label: "Calls" },
|
|
47
|
+
{ value: "errors" as const, label: "Errors" },
|
|
48
|
+
];
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
/** Tools stacked individually in the calls chart; the rest fold into "Other". */
|
|
51
|
+
const TOP_TOOLS = 6;
|
|
52
|
+
|
|
53
|
+
const ATTRIBUTION_NOTE =
|
|
54
|
+
"Tokens and API-equivalent cost of each invoking turn, split evenly across that turn's tool calls";
|
|
55
|
+
|
|
56
|
+
const NO_CALLS = <EmptyState title="No tool calls in this range" />;
|
|
57
|
+
|
|
58
|
+
export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
59
|
+
const tools = useQuery(["tools", range], () => getToolDashboardStats(range), { enabled: active });
|
|
60
|
+
const [metric, setMetric] = useState<CallMetric>("calls");
|
|
61
|
+
const [hidden, toggleHidden] = useHiddenSeries();
|
|
62
|
+
const [pickedTool, setToolFilter] = useState<string | null>(null);
|
|
63
|
+
const meta = rangeMeta(range);
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
let cost = 0;
|
|
62
|
-
let unpricedRequests = 0;
|
|
63
|
-
let resultChars = 0;
|
|
64
|
-
let argsChars = 0;
|
|
65
|
-
for (const t of byTool) {
|
|
66
|
-
calls += t.calls;
|
|
67
|
-
errors += t.errors;
|
|
68
|
-
tokens += t.totalTokensShare;
|
|
69
|
-
output += t.outputTokensShare;
|
|
70
|
-
cost += t.costShare;
|
|
71
|
-
unpricedRequests += t.unpricedRequestsShare;
|
|
72
|
-
resultChars += t.resultChars;
|
|
73
|
-
argsChars += t.argsChars;
|
|
74
|
-
}
|
|
75
|
-
return { calls, errors, tokens, output, cost, unpricedRequests, resultChars, argsChars, tools: byTool.length };
|
|
76
|
-
}, [byTool]);
|
|
65
|
+
const view = useMemo(() => buildToolsView(tools.data, range), [tools.data, range]);
|
|
66
|
+
// A pick from another range only applies while that tool still has calls.
|
|
67
|
+
const toolFilter = pickedTool !== null && view.toolNames.includes(pickedTool) ? pickedTool : null;
|
|
68
|
+
|
|
69
|
+
const chartSeries = metric === "calls" ? view.callSeries : view.errorSeries;
|
|
70
|
+
const isEmpty = (data: ToolDashboardStats) => data.byTool.length === 0;
|
|
77
71
|
|
|
78
72
|
return (
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
73
|
+
<div className="page">
|
|
74
|
+
<PageHeader
|
|
75
|
+
title="Tools"
|
|
76
|
+
description={`Which tools omp called in ${meta.windowLabel}, how often they failed, and what they cost.`}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<QueryView query={tools} skeleton={<ChartSkeleton height={112} />}>
|
|
80
|
+
{() => {
|
|
81
|
+
const t = view.totals;
|
|
82
|
+
return (
|
|
83
|
+
<div data-stale={tools.stale} className="stack" style={{ gap: 16 }}>
|
|
84
|
+
<StatGrid min={190}>
|
|
85
|
+
<Stat
|
|
86
|
+
label="Tool calls"
|
|
87
|
+
value={formatInteger(t.calls)}
|
|
88
|
+
hint={`${formatInteger(t.errors)} failed`}
|
|
89
|
+
spark={view.totalCalls}
|
|
90
|
+
/>
|
|
91
|
+
<Stat
|
|
92
|
+
label="Distinct tools"
|
|
93
|
+
value={formatInteger(t.tools)}
|
|
94
|
+
hint={view.rows[0] ? `Most used: ${view.rows[0].tool}` : undefined}
|
|
95
|
+
/>
|
|
96
|
+
<Stat
|
|
97
|
+
label="Error rate"
|
|
98
|
+
title="Tool results that came back flagged as errors"
|
|
99
|
+
value={formatPercent(t.calls > 0 ? t.errors / t.calls : 0)}
|
|
100
|
+
hint={`${formatInteger(t.calls - t.errors)} succeeded`}
|
|
101
|
+
spark={view.totalErrors}
|
|
102
|
+
sparkColor="var(--bad)"
|
|
103
|
+
/>
|
|
104
|
+
<Stat
|
|
105
|
+
label="Attributed tokens"
|
|
106
|
+
title={ATTRIBUTION_NOTE}
|
|
107
|
+
value={formatCompact(Math.round(t.tokens))}
|
|
108
|
+
hint={`${formatCompact(Math.round(t.output))} output`}
|
|
109
|
+
/>
|
|
110
|
+
<Stat
|
|
111
|
+
label="Attributed cost"
|
|
112
|
+
title={`${ATTRIBUTION_NOTE}; API-equivalent estimate`}
|
|
113
|
+
value={formatEstimatedCost(t.cost, t.unpriced)}
|
|
114
|
+
hint={
|
|
115
|
+
t.unpriced > 0
|
|
116
|
+
? `${formatInteger(Math.round(t.unpriced))} unpriced requests`
|
|
117
|
+
: "API-equivalent"
|
|
118
|
+
}
|
|
119
|
+
/>
|
|
120
|
+
</StatGrid>
|
|
121
|
+
<StatGrid min={150}>
|
|
122
|
+
<Stat
|
|
123
|
+
size="sm"
|
|
124
|
+
label="Result text"
|
|
125
|
+
title="Characters of tool-result text fed back into context"
|
|
126
|
+
value={`${formatCompact(t.resultChars)} chars`}
|
|
127
|
+
/>
|
|
128
|
+
<Stat
|
|
129
|
+
size="sm"
|
|
130
|
+
label="Call arguments"
|
|
131
|
+
title="Characters of serialized tool-call arguments"
|
|
132
|
+
value={`${formatCompact(t.argsChars)} chars`}
|
|
133
|
+
/>
|
|
134
|
+
<Stat
|
|
135
|
+
size="sm"
|
|
136
|
+
label="Avg result per call"
|
|
137
|
+
value={`${formatCompact(t.calls > 0 ? Math.round(t.resultChars / t.calls) : 0)} chars`}
|
|
138
|
+
/>
|
|
139
|
+
<Stat
|
|
140
|
+
size="sm"
|
|
141
|
+
label="Avg arguments per call"
|
|
142
|
+
value={`${formatCompact(t.calls > 0 ? Math.round(t.argsChars / t.calls) : 0)} chars`}
|
|
143
|
+
/>
|
|
144
|
+
</StatGrid>
|
|
97
145
|
</div>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<div className="stats-metric-value">{formatEstimatedCost(totals.cost, totals.unpricedRequests)}</div>
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
146
|
+
);
|
|
147
|
+
}}
|
|
148
|
+
</QueryView>
|
|
104
149
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
150
|
+
<Card
|
|
151
|
+
index={1}
|
|
152
|
+
title={metric === "calls" ? "Calls over time" : "Errors over time"}
|
|
153
|
+
description={`Per ${meta.bucketMs < 3_600_000 ? "5 minutes" : meta.bucketMs < 86_400_000 ? "hour" : "day"}, top ${TOP_TOOLS} tools stacked`}
|
|
154
|
+
actions={<Segmented size="sm" options={METRIC_OPTIONS} value={metric} onChange={setMetric} />}
|
|
155
|
+
stale={tools.stale}
|
|
156
|
+
>
|
|
157
|
+
<QueryView query={tools} skeleton={<ChartSkeleton height={260} />} isEmpty={isEmpty} empty={NO_CALLS}>
|
|
158
|
+
{() => (
|
|
159
|
+
<div className="stack" style={{ gap: 12 }}>
|
|
160
|
+
<TimeChart
|
|
161
|
+
buckets={view.buckets}
|
|
162
|
+
bucketMs={meta.bucketMs}
|
|
163
|
+
series={chartSeries}
|
|
164
|
+
hidden={hidden}
|
|
165
|
+
height={260}
|
|
166
|
+
formatTooltip={formatInteger}
|
|
167
|
+
emptyLabel={metric === "errors" ? "No tool errors in this range" : undefined}
|
|
168
|
+
/>
|
|
169
|
+
<Legend
|
|
170
|
+
items={chartSeries.map(s => ({
|
|
171
|
+
key: s.key,
|
|
172
|
+
label: s.label,
|
|
173
|
+
color: s.color,
|
|
174
|
+
value: formatCompact(s.values.reduce<number>((sum, v) => sum + (v ?? 0), 0)),
|
|
175
|
+
}))}
|
|
176
|
+
hidden={hidden}
|
|
177
|
+
onToggle={toggleHidden}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</QueryView>
|
|
182
|
+
</Card>
|
|
183
|
+
|
|
184
|
+
<Card
|
|
185
|
+
index={2}
|
|
186
|
+
title="By tool"
|
|
187
|
+
description={`${ATTRIBUTION_NOTE}. Select a tool to break it down by model.`}
|
|
188
|
+
flush
|
|
189
|
+
stale={tools.stale}
|
|
190
|
+
>
|
|
191
|
+
<QueryView query={tools} skeleton={<TableSkeleton rows={8} />} isEmpty={isEmpty} empty={NO_CALLS}>
|
|
192
|
+
{() => (
|
|
193
|
+
<Table
|
|
194
|
+
rows={view.rows}
|
|
195
|
+
rowKey={row => row.tool}
|
|
196
|
+
columns={view.toolColumns}
|
|
197
|
+
initialSort={{ key: "calls", dir: "desc" }}
|
|
198
|
+
limit={20}
|
|
199
|
+
selectedKey={toolFilter}
|
|
200
|
+
onRowClick={row => setToolFilter(prev => (prev === row.tool ? null : row.tool))}
|
|
201
|
+
dense
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
</QueryView>
|
|
205
|
+
</Card>
|
|
206
|
+
|
|
207
|
+
<Card
|
|
208
|
+
index={3}
|
|
209
|
+
title="By tool and model"
|
|
210
|
+
description="Which models call which tools, and how often those calls fail"
|
|
211
|
+
flush
|
|
212
|
+
stale={tools.stale}
|
|
213
|
+
actions={
|
|
214
|
+
<div className="row" style={{ gap: 6 }}>
|
|
215
|
+
<select
|
|
216
|
+
className="input tools-filter"
|
|
217
|
+
value={toolFilter ?? ""}
|
|
218
|
+
onChange={e => setToolFilter(e.target.value || null)}
|
|
219
|
+
aria-label="Filter by tool"
|
|
220
|
+
>
|
|
221
|
+
<option value="">All tools</option>
|
|
222
|
+
{view.toolNames.map(name => (
|
|
223
|
+
<option key={name} value={name}>
|
|
224
|
+
{name}
|
|
225
|
+
</option>
|
|
226
|
+
))}
|
|
227
|
+
</select>
|
|
228
|
+
{toolFilter !== null && (
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
className="btn"
|
|
232
|
+
data-size="sm"
|
|
233
|
+
data-variant="ghost"
|
|
234
|
+
data-icon="true"
|
|
235
|
+
title="Clear tool filter"
|
|
236
|
+
aria-label="Clear tool filter"
|
|
237
|
+
onClick={() => setToolFilter(null)}
|
|
238
|
+
>
|
|
239
|
+
<X size={13} />
|
|
240
|
+
</button>
|
|
241
|
+
)}
|
|
121
242
|
</div>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
243
|
+
}
|
|
244
|
+
>
|
|
245
|
+
<QueryView query={tools} skeleton={<TableSkeleton rows={8} />} isEmpty={isEmpty} empty={NO_CALLS}>
|
|
246
|
+
{data => <ToolModelTable rows={data.byToolModel} tool={toolFilter} colors={view.colors} />}
|
|
247
|
+
</QueryView>
|
|
248
|
+
</Card>
|
|
249
|
+
</div>
|
|
125
250
|
);
|
|
126
251
|
}
|
|
127
252
|
|
|
128
253
|
// ---------------------------------------------------------------------------
|
|
129
|
-
//
|
|
254
|
+
// View model
|
|
130
255
|
// ---------------------------------------------------------------------------
|
|
131
256
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const top = ranked.slice(0, TOP_TOOLS).map(([tool]) => tool);
|
|
143
|
-
const topSet = new Set(top);
|
|
144
|
-
const hasOther = ranked.length > top.length;
|
|
145
|
-
const tools = hasOther ? [...top, "Other"] : top;
|
|
146
|
-
|
|
147
|
-
const buckets = [...new Set(points.map(p => p.timestamp))].sort((a, b) => a - b);
|
|
148
|
-
const data = new Map<number, Record<string, number>>();
|
|
149
|
-
for (const bucket of buckets) data.set(bucket, {});
|
|
150
|
-
for (const p of points) {
|
|
151
|
-
const label = topSet.has(p.tool) ? p.tool : "Other";
|
|
152
|
-
const row = data.get(p.timestamp);
|
|
153
|
-
if (row) row[label] = (row[label] ?? 0) + p.calls;
|
|
154
|
-
}
|
|
155
|
-
return { buckets, tools, data };
|
|
257
|
+
interface ToolTotals {
|
|
258
|
+
calls: number;
|
|
259
|
+
errors: number;
|
|
260
|
+
tools: number;
|
|
261
|
+
tokens: number;
|
|
262
|
+
output: number;
|
|
263
|
+
cost: number;
|
|
264
|
+
unpriced: number;
|
|
265
|
+
resultChars: number;
|
|
266
|
+
argsChars: number;
|
|
156
267
|
}
|
|
157
268
|
|
|
158
|
-
function
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
const
|
|
269
|
+
function buildToolsView(data: ToolDashboardStats | null, range: TimeRange) {
|
|
270
|
+
const byTool = data?.byTool ?? [];
|
|
271
|
+
const points = data?.series ?? [];
|
|
272
|
+
const rows = buildToolRows(byTool).sort((a, b) => b.calls - a.calls);
|
|
273
|
+
const colors = buildColorLookup(byTool.map(t => ({ key: t.tool, weight: t.calls })));
|
|
274
|
+
const buckets = bucketAxis(
|
|
275
|
+
range,
|
|
276
|
+
points.map(p => p.timestamp),
|
|
277
|
+
);
|
|
162
278
|
|
|
163
|
-
const
|
|
279
|
+
const totals: ToolTotals = {
|
|
280
|
+
calls: 0,
|
|
281
|
+
errors: 0,
|
|
282
|
+
tools: byTool.length,
|
|
283
|
+
tokens: 0,
|
|
284
|
+
output: 0,
|
|
285
|
+
cost: 0,
|
|
286
|
+
unpriced: 0,
|
|
287
|
+
resultChars: 0,
|
|
288
|
+
argsChars: 0,
|
|
289
|
+
};
|
|
290
|
+
for (const t of byTool) {
|
|
291
|
+
totals.calls += t.calls;
|
|
292
|
+
totals.errors += t.errors;
|
|
293
|
+
totals.tokens += t.totalTokensShare;
|
|
294
|
+
totals.output += t.outputTokensShare;
|
|
295
|
+
totals.cost += t.costShare;
|
|
296
|
+
totals.unpriced += t.unpricedRequestsShare;
|
|
297
|
+
totals.resultChars += t.resultChars;
|
|
298
|
+
totals.argsChars += t.argsChars;
|
|
299
|
+
}
|
|
164
300
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
backgroundColor: `${MODEL_COLORS[index % MODEL_COLORS.length]}30`,
|
|
173
|
-
fill: true,
|
|
174
|
-
tension: 0.4,
|
|
175
|
-
pointRadius: 0,
|
|
176
|
-
pointHoverRadius: 4,
|
|
177
|
-
borderWidth: 2,
|
|
301
|
+
// Every tool keeps its table swatch color in both metrics; zero slots become
|
|
302
|
+
// gaps so the tooltip lists only tools active in that bucket.
|
|
303
|
+
const [callSeries, errorSeries] = [(p: ToolTimeSeriesPoint) => p.calls, (p: ToolTimeSeriesPoint) => p.errors].map(
|
|
304
|
+
value =>
|
|
305
|
+
pivotSeries(points, { buckets, key: p => p.tool, value, limit: TOP_TOOLS, colors }).map(s => ({
|
|
306
|
+
...s,
|
|
307
|
+
values: s.values.map(v => v || null),
|
|
178
308
|
})),
|
|
179
|
-
}),
|
|
180
|
-
[chartSeries, timeRange],
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
const options = useMemo(
|
|
184
|
-
() => ({
|
|
185
|
-
responsive: true,
|
|
186
|
-
maintainAspectRatio: false,
|
|
187
|
-
interaction: { mode: "index" as const, intersect: false },
|
|
188
|
-
plugins: {
|
|
189
|
-
legend: {
|
|
190
|
-
position: "top" as const,
|
|
191
|
-
align: "start" as const,
|
|
192
|
-
labels: {
|
|
193
|
-
color: chartTheme.legendLabel,
|
|
194
|
-
usePointStyle: true,
|
|
195
|
-
padding: 16,
|
|
196
|
-
font: { size: 12 },
|
|
197
|
-
boxWidth: 8,
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
tooltip: {
|
|
201
|
-
backgroundColor: chartTheme.tooltipBackground,
|
|
202
|
-
titleColor: chartTheme.tooltipTitle,
|
|
203
|
-
bodyColor: chartTheme.tooltipBody,
|
|
204
|
-
borderColor: chartTheme.tooltipBorder,
|
|
205
|
-
borderWidth: 1,
|
|
206
|
-
padding: 12,
|
|
207
|
-
cornerRadius: 8,
|
|
208
|
-
callbacks: {
|
|
209
|
-
label: (context: { dataset: { label?: string }; parsed: { y: number | null } }) =>
|
|
210
|
-
`${context.dataset.label ?? ""}: ${formatInteger(context.parsed.y ?? 0)} calls`,
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
scales: {
|
|
215
|
-
x: {
|
|
216
|
-
stacked: true,
|
|
217
|
-
grid: { color: chartTheme.grid, drawBorder: false },
|
|
218
|
-
ticks: { color: chartTheme.tick, font: { size: 11 } },
|
|
219
|
-
},
|
|
220
|
-
y: {
|
|
221
|
-
stacked: true,
|
|
222
|
-
grid: { color: chartTheme.grid, drawBorder: false },
|
|
223
|
-
ticks: { color: chartTheme.tick, font: { size: 11 }, precision: 0 },
|
|
224
|
-
min: 0,
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
}),
|
|
228
|
-
[chartTheme],
|
|
229
309
|
);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
<div className="h-[280px]">
|
|
234
|
-
{chartSeries.buckets.length === 0 ? (
|
|
235
|
-
<div className="h-full flex items-center justify-center text-stats-muted text-sm">No data available</div>
|
|
236
|
-
) : (
|
|
237
|
-
<Line data={data} options={options} />
|
|
238
|
-
)}
|
|
239
|
-
</div>
|
|
240
|
-
</Panel>
|
|
310
|
+
// Unlimited pivot for the per-row sparklines.
|
|
311
|
+
const trends = new Map(
|
|
312
|
+
pivotSeries(points, { buckets, key: p => p.tool, value: p => p.calls }).map(s => [s.key, s.values as number[]]),
|
|
241
313
|
);
|
|
242
|
-
}
|
|
243
314
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
315
|
+
const maxCalls = rows[0]?.calls ?? 0;
|
|
316
|
+
const toolColumns = buildToolColumns(maxCalls, colors, trends);
|
|
247
317
|
|
|
248
|
-
|
|
249
|
-
|
|
318
|
+
return {
|
|
319
|
+
rows,
|
|
320
|
+
colors,
|
|
321
|
+
buckets,
|
|
322
|
+
totals,
|
|
323
|
+
callSeries,
|
|
324
|
+
errorSeries,
|
|
325
|
+
totalCalls: densify(points, buckets, p => p.calls),
|
|
326
|
+
totalErrors: densify(points, buckets, p => p.errors),
|
|
327
|
+
toolNames: rows.map(r => r.tool).sort((a, b) => a.localeCompare(b)),
|
|
328
|
+
toolColumns,
|
|
329
|
+
};
|
|
250
330
|
}
|
|
251
331
|
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
key: "calls",
|
|
268
|
-
header: "Calls",
|
|
269
|
-
numeric: true,
|
|
270
|
-
render: (item: ToolRowView) => (
|
|
271
|
-
<div className="stats-text-right">
|
|
272
|
-
<div className="font-mono">{formatInteger(item.calls)}</div>
|
|
273
|
-
<div className="stats-progress-bar-track mt-1 ml-auto w-24 h-1">
|
|
274
|
-
<div
|
|
275
|
-
className="stats-progress-bar-fill"
|
|
276
|
-
data-variant="link"
|
|
277
|
-
style={{ width: `${item.callsPercentage}%` }}
|
|
278
|
-
/>
|
|
279
|
-
</div>
|
|
280
|
-
</div>
|
|
281
|
-
),
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
key: "errorRate",
|
|
285
|
-
header: "Error Rate",
|
|
286
|
-
numeric: true,
|
|
287
|
-
render: (item: ToolRowView) => (
|
|
288
|
-
<StatusPill variant={errorPillVariant(item.errorRate)}>{formatPercent(item.errorRate)}</StatusPill>
|
|
289
|
-
),
|
|
290
|
-
},
|
|
291
|
-
{
|
|
292
|
-
key: "tokens",
|
|
293
|
-
header: "Attr. Tokens",
|
|
294
|
-
numeric: true,
|
|
295
|
-
render: (item: ToolRowView) => (
|
|
296
|
-
<span className="font-mono" title="Invoking turns' total tokens, split across each turn's calls">
|
|
297
|
-
{formatCompact(Math.round(item.totalTokensShare))}
|
|
298
|
-
</span>
|
|
299
|
-
),
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
key: "cost",
|
|
303
|
-
header: "Attr. API-equivalent estimate",
|
|
304
|
-
numeric: true,
|
|
305
|
-
render: (item: ToolRowView) => (
|
|
306
|
-
<span className="font-mono">{formatEstimatedCost(item.costShare, item.unpricedRequestsShare)}</span>
|
|
307
|
-
),
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
key: "resultChars",
|
|
311
|
-
header: "Result Text",
|
|
312
|
-
numeric: true,
|
|
313
|
-
render: (item: ToolRowView) => (
|
|
314
|
-
<span className="font-mono" title="Characters of tool-result text fed back into context">
|
|
315
|
-
{formatCompact(item.resultChars)}
|
|
332
|
+
function buildToolColumns(
|
|
333
|
+
maxCalls: number,
|
|
334
|
+
colors: ReadonlyMap<string, string>,
|
|
335
|
+
trends: ReadonlyMap<string, number[]>,
|
|
336
|
+
): Column<ToolRowView>[] {
|
|
337
|
+
return [
|
|
338
|
+
{
|
|
339
|
+
key: "tool",
|
|
340
|
+
header: "Tool",
|
|
341
|
+
sort: row => row.tool,
|
|
342
|
+
render: row => (
|
|
343
|
+
<span className="row" style={{ gap: 8 }}>
|
|
344
|
+
<Swatch color={colors.get(row.tool) ?? OTHER_COLOR} />
|
|
345
|
+
<span className="mono truncate tools-name" title={row.tool}>
|
|
346
|
+
{row.tool}
|
|
316
347
|
</span>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
)
|
|
348
|
+
</span>
|
|
349
|
+
),
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
key: "trend",
|
|
353
|
+
header: "Trend",
|
|
354
|
+
title: "Calls per bucket over the range",
|
|
355
|
+
render: row => {
|
|
356
|
+
const values = trends.get(row.tool);
|
|
357
|
+
return values && values.length > 1 ? (
|
|
358
|
+
<Sparkline values={values} width={80} height={20} color={colors.get(row.tool) ?? OTHER_COLOR} />
|
|
359
|
+
) : (
|
|
360
|
+
<span className="dim">–</span>
|
|
361
|
+
);
|
|
326
362
|
},
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
<
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
<
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
key: "calls",
|
|
366
|
+
header: "Calls",
|
|
367
|
+
align: "right",
|
|
368
|
+
sort: row => row.calls,
|
|
369
|
+
render: row => (
|
|
370
|
+
<MeterCell
|
|
371
|
+
value={row.calls}
|
|
372
|
+
max={maxCalls}
|
|
373
|
+
display={
|
|
374
|
+
<span title={`${formatPercent(row.callFraction)} of all tool calls`}>
|
|
375
|
+
{formatInteger(row.calls)}
|
|
376
|
+
<span className="dim tools-share">{formatPercent(row.callFraction, 0)}</span>
|
|
377
|
+
</span>
|
|
378
|
+
}
|
|
379
|
+
/>
|
|
380
|
+
),
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
key: "errorRate",
|
|
384
|
+
header: "Errors",
|
|
385
|
+
title: "Calls whose result came back flagged as an error",
|
|
386
|
+
align: "right",
|
|
387
|
+
sort: row => row.errorRate,
|
|
388
|
+
render: row => (
|
|
389
|
+
<span className="row" style={{ gap: 8, justifyContent: "flex-end" }}>
|
|
390
|
+
<span className="num dim">{formatInteger(row.errors)}</span>
|
|
391
|
+
<Badge tone={row.errors > 0 ? errorRateTone(row.errorRate) : "neutral"} mono>
|
|
392
|
+
{formatPercent(row.errorRate)}
|
|
393
|
+
</Badge>
|
|
394
|
+
</span>
|
|
395
|
+
),
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
key: "args",
|
|
399
|
+
header: "Args",
|
|
400
|
+
title: "Characters of serialized tool-call arguments",
|
|
401
|
+
align: "right",
|
|
402
|
+
sort: row => row.argsChars,
|
|
403
|
+
render: row => <span className="num">{formatCompact(row.argsChars)}</span>,
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
key: "result",
|
|
407
|
+
header: "Result",
|
|
408
|
+
title: "Characters of tool-result text fed back into context",
|
|
409
|
+
align: "right",
|
|
410
|
+
sort: row => row.resultChars,
|
|
411
|
+
render: row => <span className="num">{formatCompact(row.resultChars)}</span>,
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
key: "avgResult",
|
|
415
|
+
header: "Result / call",
|
|
416
|
+
title: "Mean tool-result characters per call",
|
|
417
|
+
align: "right",
|
|
418
|
+
sort: row => row.avgResultChars,
|
|
419
|
+
render: row => <span className="num muted">{formatCompact(Math.round(row.avgResultChars))}</span>,
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
key: "tokens",
|
|
423
|
+
header: "Attr. tokens",
|
|
424
|
+
title: ATTRIBUTION_NOTE,
|
|
425
|
+
align: "right",
|
|
426
|
+
sort: row => row.totalTokensShare,
|
|
427
|
+
render: row => (
|
|
428
|
+
<span className="num" title={`${formatPercent(row.tokenFraction)} of attributed tokens`}>
|
|
429
|
+
{formatCompact(Math.round(row.totalTokensShare))}
|
|
430
|
+
<span className="dim tools-share">{formatPercent(row.tokenFraction, 0)}</span>
|
|
431
|
+
</span>
|
|
432
|
+
),
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
key: "cost",
|
|
436
|
+
header: "Attr. cost",
|
|
437
|
+
title: `${ATTRIBUTION_NOTE}; API-equivalent estimate`,
|
|
438
|
+
align: "right",
|
|
439
|
+
sort: row => row.costShare,
|
|
440
|
+
render: row => (
|
|
441
|
+
<span className="num" title={`${formatPercent(row.costFraction)} of attributed cost`}>
|
|
442
|
+
{formatEstimatedCost(row.costShare, row.unpricedRequestsShare)}
|
|
443
|
+
<span className="dim tools-share">{formatPercent(row.costFraction, 0)}</span>
|
|
444
|
+
</span>
|
|
445
|
+
),
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
key: "lastUsed",
|
|
449
|
+
header: "Last used",
|
|
450
|
+
align: "right",
|
|
451
|
+
sort: row => row.lastUsed,
|
|
452
|
+
render: row => <span className="muted">{formatRelativeTime(row.lastUsed)}</span>,
|
|
453
|
+
},
|
|
454
|
+
];
|
|
373
455
|
}
|
|
374
456
|
|
|
375
457
|
// ---------------------------------------------------------------------------
|
|
376
|
-
//
|
|
458
|
+
// Tool × model breakdown
|
|
377
459
|
// ---------------------------------------------------------------------------
|
|
378
460
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const tools = useMemo(() => [...new Set(byToolModel.map(row => row.tool))].sort(), [byToolModel]);
|
|
383
|
-
|
|
384
|
-
const rows = useMemo(() => {
|
|
385
|
-
const filtered = tool ? byToolModel.filter(row => row.tool === tool) : byToolModel;
|
|
386
|
-
return filtered.map(row => ({
|
|
387
|
-
...row,
|
|
388
|
-
errorRate: row.calls > 0 ? row.errors / row.calls : 0,
|
|
389
|
-
}));
|
|
390
|
-
}, [byToolModel, tool]);
|
|
461
|
+
interface ToolModelRow extends ToolModelStats {
|
|
462
|
+
errorRate: number;
|
|
463
|
+
}
|
|
391
464
|
|
|
392
|
-
|
|
393
|
-
|
|
465
|
+
function ToolModelTable({
|
|
466
|
+
rows,
|
|
467
|
+
tool,
|
|
468
|
+
colors,
|
|
469
|
+
}: {
|
|
470
|
+
rows: ToolModelStats[];
|
|
471
|
+
tool: string | null;
|
|
472
|
+
colors: ReadonlyMap<string, string>;
|
|
473
|
+
}) {
|
|
474
|
+
const filtered = useMemo<ToolModelRow[]>(
|
|
475
|
+
() =>
|
|
476
|
+
(tool === null ? rows : rows.filter(r => r.tool === tool)).map(r => ({
|
|
477
|
+
...r,
|
|
478
|
+
errorRate: r.calls > 0 ? r.errors / r.calls : 0,
|
|
479
|
+
})),
|
|
480
|
+
[rows, tool],
|
|
481
|
+
);
|
|
482
|
+
const columns = useMemo<Column<ToolModelRow>[]>(() => {
|
|
483
|
+
const maxCalls = filtered.reduce((max, r) => Math.max(max, r.calls), 0);
|
|
484
|
+
return [
|
|
394
485
|
{
|
|
395
486
|
key: "tool",
|
|
396
487
|
header: "Tool",
|
|
397
|
-
|
|
398
|
-
|
|
488
|
+
sort: row => row.tool,
|
|
489
|
+
render: row => (
|
|
490
|
+
<span className="row" style={{ gap: 8 }}>
|
|
491
|
+
<Swatch color={colors.get(row.tool) ?? OTHER_COLOR} />
|
|
492
|
+
<span className="mono truncate tools-name" title={row.tool}>
|
|
493
|
+
{row.tool}
|
|
494
|
+
</span>
|
|
495
|
+
</span>
|
|
399
496
|
),
|
|
400
497
|
},
|
|
401
498
|
{
|
|
402
499
|
key: "model",
|
|
403
500
|
header: "Model",
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
<div className="stats-text-secondary text-xs">{item.provider}</div>
|
|
408
|
-
</div>
|
|
501
|
+
sort: row => row.model,
|
|
502
|
+
render: row => (
|
|
503
|
+
<LabelCell primary={<span className="mono">{row.model || "(unknown)"}</span>} secondary={row.provider} />
|
|
409
504
|
),
|
|
410
505
|
},
|
|
411
506
|
{
|
|
412
507
|
key: "calls",
|
|
413
508
|
header: "Calls",
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
),
|
|
509
|
+
align: "right",
|
|
510
|
+
sort: row => row.calls,
|
|
511
|
+
render: row => <MeterCell value={row.calls} max={maxCalls} display={formatInteger(row.calls)} />,
|
|
418
512
|
},
|
|
419
513
|
{
|
|
420
514
|
key: "errorRate",
|
|
421
|
-
header: "
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
515
|
+
header: "Errors",
|
|
516
|
+
align: "right",
|
|
517
|
+
sort: row => row.errorRate,
|
|
518
|
+
render: row => (
|
|
519
|
+
<span className="row" style={{ gap: 8, justifyContent: "flex-end" }}>
|
|
520
|
+
<span className="num dim">{formatInteger(row.errors)}</span>
|
|
521
|
+
<Badge tone={row.errors > 0 ? errorRateTone(row.errorRate) : "neutral"} mono>
|
|
522
|
+
{formatPercent(row.errorRate)}
|
|
523
|
+
</Badge>
|
|
524
|
+
</span>
|
|
425
525
|
),
|
|
426
526
|
},
|
|
527
|
+
{
|
|
528
|
+
key: "result",
|
|
529
|
+
header: "Result",
|
|
530
|
+
title: "Characters of tool-result text fed back into context",
|
|
531
|
+
align: "right",
|
|
532
|
+
sort: row => row.resultChars,
|
|
533
|
+
render: row => <span className="num">{formatCompact(row.resultChars)}</span>,
|
|
534
|
+
},
|
|
427
535
|
{
|
|
428
536
|
key: "tokens",
|
|
429
|
-
header: "Attr.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
)
|
|
537
|
+
header: "Attr. tokens",
|
|
538
|
+
title: ATTRIBUTION_NOTE,
|
|
539
|
+
align: "right",
|
|
540
|
+
sort: row => row.totalTokensShare,
|
|
541
|
+
render: row => <span className="num">{formatCompact(Math.round(row.totalTokensShare))}</span>,
|
|
434
542
|
},
|
|
435
543
|
{
|
|
436
544
|
key: "cost",
|
|
437
|
-
header: "Attr.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
)
|
|
545
|
+
header: "Attr. cost",
|
|
546
|
+
title: `${ATTRIBUTION_NOTE}; API-equivalent estimate`,
|
|
547
|
+
align: "right",
|
|
548
|
+
sort: row => row.costShare,
|
|
549
|
+
render: row => <span className="num">{formatEstimatedCost(row.costShare, row.unpricedRequestsShare)}</span>,
|
|
442
550
|
},
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
551
|
+
{
|
|
552
|
+
key: "lastUsed",
|
|
553
|
+
header: "Last used",
|
|
554
|
+
align: "right",
|
|
555
|
+
sort: row => row.lastUsed,
|
|
556
|
+
render: row => <span className="muted">{formatRelativeTime(row.lastUsed)}</span>,
|
|
557
|
+
},
|
|
558
|
+
];
|
|
559
|
+
}, [filtered, colors]);
|
|
446
560
|
|
|
447
561
|
return (
|
|
448
|
-
<
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
style={{ maxWidth: "320px", flex: 1 }}
|
|
458
|
-
>
|
|
459
|
-
<option value="">All tools</option>
|
|
460
|
-
{tools.map(name => (
|
|
461
|
-
<option key={name} value={name}>
|
|
462
|
-
{name}
|
|
463
|
-
</option>
|
|
464
|
-
))}
|
|
465
|
-
</select>
|
|
466
|
-
</div>
|
|
467
|
-
<DataTable
|
|
468
|
-
columns={columns}
|
|
469
|
-
data={rows}
|
|
470
|
-
keyExtractor={item => `${item.tool}::${item.model}::${item.provider}`}
|
|
471
|
-
emptyText="No tool calls recorded for this range."
|
|
472
|
-
/>
|
|
473
|
-
</Panel>
|
|
562
|
+
<Table
|
|
563
|
+
rows={filtered}
|
|
564
|
+
rowKey={row => `${row.tool}::${row.model}::${row.provider}`}
|
|
565
|
+
columns={columns}
|
|
566
|
+
initialSort={{ key: "calls", dir: "desc" }}
|
|
567
|
+
limit={25}
|
|
568
|
+
dense
|
|
569
|
+
empty={<EmptyState title={tool ? `No calls to ${tool} in this range` : "No tool calls in this range"} />}
|
|
570
|
+
/>
|
|
474
571
|
);
|
|
475
572
|
}
|