@linxiraos/pi-stats 1.1.26 → 1.2.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 +12 -5
- package/build.ts +4 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/index.css +1 -1
- package/dist/client/index.html +1 -0
- package/dist/client/index.js +96 -96
- package/dist/types/aggregator.d.ts +4 -1
- package/dist/types/client/api.d.ts +5 -1
- package/dist/types/client/app/nav.d.ts +5 -2
- package/dist/types/client/data/formatters.d.ts +7 -2
- package/dist/types/client/data/live.d.ts +16 -11
- package/dist/types/client/data/query-store.d.ts +39 -0
- package/dist/types/client/data/query.d.ts +10 -5
- package/dist/types/client/data/range.d.ts +4 -3
- package/dist/types/client/i18n/format.d.ts +9 -0
- package/dist/types/client/i18n/index.d.ts +18 -0
- package/dist/types/client/i18n/messages/charts.d.ts +8 -0
- package/dist/types/client/i18n/messages/common.d.ts +153 -0
- package/dist/types/client/i18n/messages/costs.d.ts +92 -0
- package/dist/types/client/i18n/messages/en.d.ts +1255 -0
- package/dist/types/client/i18n/messages/errors.d.ts +78 -0
- package/dist/types/client/i18n/messages/frustration.d.ts +191 -0
- package/dist/types/client/i18n/messages/gain.d.ts +54 -0
- package/dist/types/client/i18n/messages/models.d.ts +94 -0
- package/dist/types/client/i18n/messages/nav.d.ts +46 -0
- package/dist/types/client/i18n/messages/overview.d.ts +80 -0
- package/dist/types/client/i18n/messages/projects.d.ts +84 -0
- package/dist/types/client/i18n/messages/providers.d.ts +142 -0
- package/dist/types/client/i18n/messages/requests.d.ts +72 -0
- package/dist/types/client/i18n/messages/tools.d.ts +119 -0
- package/dist/types/client/i18n/messages/traces.d.ts +144 -0
- package/dist/types/client/i18n/messages/ui.d.ts +44 -0
- package/dist/types/client/i18n/messages/zh-CN.d.ts +13 -0
- package/dist/types/client/i18n/types.d.ts +15 -0
- package/dist/types/client/traces/Minimap.d.ts +2 -0
- package/dist/types/client/traces/TimelineCanvas.d.ts +29 -8
- package/dist/types/client/traces/TranscriptList.d.ts +3 -1
- package/dist/types/db.d.ts +14 -21
- package/dist/types/embedded-client.d.ts +5 -0
- package/dist/types/frustration.d.ts +18 -9
- package/dist/types/live.d.ts +5 -3
- package/dist/types/locale.d.ts +16 -0
- package/dist/types/parser.d.ts +0 -17
- package/dist/types/rollup.d.ts +9 -1
- package/dist/types/server.d.ts +6 -0
- package/dist/types/shared-types.d.ts +7 -0
- package/dist/types/trace.d.ts +18 -6
- package/dist/types/types.d.ts +13 -1
- package/dist/types/user-metrics.d.ts +23 -1
- package/package.json +5 -5
- package/src/aggregator.ts +31 -11
- package/src/client/api.ts +40 -5
- package/src/client/app/LiveChip.tsx +14 -13
- package/src/client/app/Shell.tsx +19 -13
- package/src/client/app/ThemeToggle.tsx +4 -3
- package/src/client/app/nav.ts +19 -16
- package/src/client/data/formatters.ts +80 -13
- package/src/client/data/live.tsx +85 -45
- package/src/client/data/query-store.ts +108 -0
- package/src/client/data/query.ts +47 -56
- package/src/client/data/range.ts +34 -14
- package/src/client/data/series.ts +7 -1
- package/src/client/favicon.ico +0 -0
- package/src/client/i18n/format.ts +28 -0
- package/src/client/i18n/index.ts +53 -0
- package/src/client/i18n/messages/charts.ts +8 -0
- package/src/client/i18n/messages/common.ts +157 -0
- package/src/client/i18n/messages/costs.ts +104 -0
- package/src/client/i18n/messages/en.ts +40 -0
- package/src/client/i18n/messages/errors.ts +80 -0
- package/src/client/i18n/messages/frustration.ts +198 -0
- package/src/client/i18n/messages/gain.ts +54 -0
- package/src/client/i18n/messages/models.ts +94 -0
- package/src/client/i18n/messages/nav.ts +46 -0
- package/src/client/i18n/messages/overview.ts +90 -0
- package/src/client/i18n/messages/projects.ts +84 -0
- package/src/client/i18n/messages/providers.ts +165 -0
- package/src/client/i18n/messages/requests.ts +73 -0
- package/src/client/i18n/messages/tools.ts +120 -0
- package/src/client/i18n/messages/traces.ts +160 -0
- package/src/client/i18n/messages/ui.ts +46 -0
- package/src/client/i18n/messages/zh-CN.ts +43 -0
- package/src/client/i18n/types.ts +19 -0
- package/src/client/index.tsx +4 -0
- package/src/client/routes/CostsRoute.tsx +61 -48
- package/src/client/routes/ErrorsRoute.tsx +71 -47
- package/src/client/routes/FrustrationRoute.tsx +131 -116
- package/src/client/routes/GainRoute.tsx +42 -30
- package/src/client/routes/ModelsRoute.tsx +90 -73
- package/src/client/routes/OverviewRoute.tsx +109 -63
- package/src/client/routes/ProjectsRoute.tsx +73 -46
- package/src/client/routes/ProvidersRoute.tsx +151 -114
- package/src/client/routes/RequestsRoute.tsx +69 -46
- package/src/client/routes/ToolsRoute.tsx +108 -94
- package/src/client/routes/TracesRoute.tsx +25 -23
- package/src/client/styles.css +5 -2
- package/src/client/traces/AggregatesPanel.tsx +19 -8
- package/src/client/traces/Minimap.tsx +65 -27
- package/src/client/traces/SpanDrawer.tsx +46 -21
- package/src/client/traces/SummaryStrip.tsx +27 -11
- package/src/client/traces/TimelineCanvas.tsx +245 -116
- package/src/client/traces/TraceView.tsx +51 -36
- package/src/client/traces/TranscriptList.tsx +20 -11
- package/src/client/traces/traces.css +26 -0
- package/src/client/ui/JsonBlock.tsx +3 -2
- package/src/client/ui/Modal.tsx +3 -2
- package/src/client/ui/RequestDrawer.tsx +33 -30
- package/src/client/ui/SearchInput.tsx +2 -1
- package/src/client/ui/States.tsx +3 -2
- package/src/client/ui/Table.tsx +7 -3
- package/src/db.ts +150 -135
- package/src/embedded-client.ts +9 -0
- package/src/frustration.ts +9 -30
- package/src/index.ts +2 -1
- package/src/live.ts +36 -11
- package/src/locale.ts +29 -0
- package/src/parser.ts +37 -72
- package/src/port-conflict.ts +1 -1
- package/src/prompts/judge/frustration.json +19 -0
- package/src/rollup.ts +41 -20
- package/src/server.ts +67 -27
- package/src/shared-types.ts +7 -0
- package/src/trace.ts +54 -40
- package/src/types.ts +13 -1
- package/src/user-metrics.ts +220 -6
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState } from "react";
|
|
2
2
|
import { getGainDashboardStats } from "../api";
|
|
3
|
+
import { t } from "../i18n";
|
|
3
4
|
import { Chart, type ChartSeries, Legend } from "../charts";
|
|
4
5
|
import { formatBytes, formatCompact, formatInteger, formatPercent } from "../data/formatters";
|
|
5
6
|
import { useQuery } from "../data/query";
|
|
@@ -39,7 +40,9 @@ interface SourceRow extends GainSourceTotals {
|
|
|
39
40
|
|
|
40
41
|
export function GainRoute({ active, range }: GainRouteProps) {
|
|
41
42
|
const [project, setProject] = useState<string | null>(null);
|
|
42
|
-
const gain = useQuery(["gain", range, project], () => getGainDashboardStats(range, project), {
|
|
43
|
+
const gain = useQuery(["gain", range, project], ({ signal }) => getGainDashboardStats(range, project, signal), {
|
|
44
|
+
enabled: active,
|
|
45
|
+
});
|
|
43
46
|
const meta = rangeMeta(range);
|
|
44
47
|
const data = gain.data;
|
|
45
48
|
|
|
@@ -69,13 +72,12 @@ export function GainRoute({ active, range }: GainRouteProps) {
|
|
|
69
72
|
const projects = data?.projects ?? [];
|
|
70
73
|
// Keep the chosen project selectable even when the current range never saw it.
|
|
71
74
|
const projectOptions = project !== null && !projects.includes(project) ? [project, ...projects] : projects;
|
|
72
|
-
const scope = project ? ` for ${project}` : "";
|
|
73
75
|
|
|
74
76
|
const chartSeries: ChartSeries[] = [
|
|
75
|
-
{ key: "daily", label: "
|
|
77
|
+
{ key: "daily", label: t("gain.saved-per-day"), color: "var(--chart-primary)", values: series.daily },
|
|
76
78
|
{
|
|
77
79
|
key: "cumulative",
|
|
78
|
-
label: "
|
|
80
|
+
label: t("gain.cumulative"),
|
|
79
81
|
color: "var(--chart-secondary)",
|
|
80
82
|
values: series.cumulative,
|
|
81
83
|
kind: "line",
|
|
@@ -86,18 +88,22 @@ export function GainRoute({ active, range }: GainRouteProps) {
|
|
|
86
88
|
return (
|
|
87
89
|
<div className="page">
|
|
88
90
|
<PageHeader
|
|
89
|
-
title="
|
|
90
|
-
description={
|
|
91
|
+
title={t("gain.title")}
|
|
92
|
+
description={
|
|
93
|
+
project
|
|
94
|
+
? t("gain.description-scoped", { project, window: t(meta.windowKey) })
|
|
95
|
+
: t("gain.description", { window: t(meta.windowKey) })
|
|
96
|
+
}
|
|
91
97
|
actions={
|
|
92
98
|
projectOptions.length > 0 && (
|
|
93
99
|
<select
|
|
94
100
|
className="input"
|
|
95
|
-
aria-label="
|
|
101
|
+
aria-label={t("common.project")}
|
|
96
102
|
value={project ?? ""}
|
|
97
103
|
onChange={e => setProject(e.target.value || null)}
|
|
98
104
|
style={{ maxWidth: 320 }}
|
|
99
105
|
>
|
|
100
|
-
<option value="">
|
|
106
|
+
<option value="">{t("gain.all-projects")}</option>
|
|
101
107
|
{projectOptions.map(p => (
|
|
102
108
|
<option key={p} value={p}>
|
|
103
109
|
{p}
|
|
@@ -115,12 +121,12 @@ export function GainRoute({ active, range }: GainRouteProps) {
|
|
|
115
121
|
empty={
|
|
116
122
|
<Card>
|
|
117
123
|
<EmptyState
|
|
118
|
-
title={
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
: "Try a longer range."
|
|
124
|
+
title={
|
|
125
|
+
project
|
|
126
|
+
? t("gain.empty-title-scoped", { project, window: t(meta.windowKey) })
|
|
127
|
+
: t("gain.empty-title", { window: t(meta.windowKey) })
|
|
123
128
|
}
|
|
129
|
+
hint={range === "all" ? t("gain.empty-hint-all") : t("gain.empty-hint-longer-range")}
|
|
124
130
|
/>
|
|
125
131
|
</Card>
|
|
126
132
|
}
|
|
@@ -130,31 +136,31 @@ export function GainRoute({ active, range }: GainRouteProps) {
|
|
|
130
136
|
<div data-stale={gain.stale}>
|
|
131
137
|
<StatGrid min={180}>
|
|
132
138
|
<Stat
|
|
133
|
-
label="
|
|
139
|
+
label={t("gain.saved-tokens")}
|
|
134
140
|
value={formatCompact(overall.savedTokens)}
|
|
135
141
|
hint={formatInteger(overall.savedTokens)}
|
|
136
142
|
spark={series.daily}
|
|
137
143
|
/>
|
|
138
|
-
<Stat label="
|
|
144
|
+
<Stat label={t("gain.saved-bytes")} value={formatBytes(overall.savedBytes)} />
|
|
139
145
|
<Stat
|
|
140
|
-
label="
|
|
141
|
-
title="
|
|
146
|
+
label={t("gain.reduction")}
|
|
147
|
+
title={t("gain.reduction-title")}
|
|
142
148
|
value={overall.reductionPercent !== null ? formatPercent(overall.reductionPercent) : "–"}
|
|
143
|
-
hint={overall.reductionPercent === null ? "
|
|
149
|
+
hint={overall.reductionPercent === null ? t("gain.reduction-unknown") : undefined}
|
|
144
150
|
/>
|
|
145
|
-
<Stat label="
|
|
151
|
+
<Stat label={t("gain.hits")} value={formatInteger(overall.hits)} />
|
|
146
152
|
<Stat
|
|
147
|
-
label="
|
|
153
|
+
label={t("gain.saved-per-hit")}
|
|
148
154
|
value={overall.hits > 0 ? formatCompact(overall.savedTokens / overall.hits) : "–"}
|
|
149
|
-
hint="tokens"
|
|
155
|
+
hint={t("common.tokens")}
|
|
150
156
|
/>
|
|
151
157
|
</StatGrid>
|
|
152
158
|
</div>
|
|
153
159
|
|
|
154
160
|
<Card
|
|
155
161
|
index={1}
|
|
156
|
-
title="
|
|
157
|
-
description="
|
|
162
|
+
title={t("gain.savings-over-time")}
|
|
163
|
+
description={t("gain.savings-over-time-description")}
|
|
158
164
|
actions={<Legend items={chartSeries.map(s => ({ key: s.key, label: s.label, color: s.color }))} />}
|
|
159
165
|
stale={gain.stale}
|
|
160
166
|
>
|
|
@@ -168,7 +174,13 @@ export function GainRoute({ active, range }: GainRouteProps) {
|
|
|
168
174
|
/>
|
|
169
175
|
</Card>
|
|
170
176
|
|
|
171
|
-
<Card
|
|
177
|
+
<Card
|
|
178
|
+
index={2}
|
|
179
|
+
title={t("gain.by-source")}
|
|
180
|
+
description={t("gain.by-source-description")}
|
|
181
|
+
flush
|
|
182
|
+
stale={gain.stale}
|
|
183
|
+
>
|
|
172
184
|
<Table rows={sourceRows} rowKey={row => row.source} columns={SOURCE_COLUMNS} />
|
|
173
185
|
</Card>
|
|
174
186
|
</>
|
|
@@ -181,13 +193,13 @@ export function GainRoute({ active, range }: GainRouteProps) {
|
|
|
181
193
|
const SOURCE_COLUMNS: Column<SourceRow>[] = [
|
|
182
194
|
{
|
|
183
195
|
key: "source",
|
|
184
|
-
header: "
|
|
196
|
+
header: t("gain.source"),
|
|
185
197
|
sort: row => row.source,
|
|
186
198
|
render: row => <span className="cell-primary">{SOURCE_LABEL[row.source]}</span>,
|
|
187
199
|
},
|
|
188
200
|
{
|
|
189
201
|
key: "tokens",
|
|
190
|
-
header: "
|
|
202
|
+
header: t("gain.saved-tokens"),
|
|
191
203
|
align: "right",
|
|
192
204
|
sort: row => row.savedTokens,
|
|
193
205
|
render: row => (
|
|
@@ -198,28 +210,28 @@ const SOURCE_COLUMNS: Column<SourceRow>[] = [
|
|
|
198
210
|
},
|
|
199
211
|
{
|
|
200
212
|
key: "share",
|
|
201
|
-
header: "
|
|
213
|
+
header: t("common.share"),
|
|
202
214
|
align: "right",
|
|
203
215
|
sort: row => row.share,
|
|
204
216
|
render: row => <span className="num muted">{formatPercent(row.share)}</span>,
|
|
205
217
|
},
|
|
206
218
|
{
|
|
207
219
|
key: "bytes",
|
|
208
|
-
header: "
|
|
220
|
+
header: t("gain.saved-bytes"),
|
|
209
221
|
align: "right",
|
|
210
222
|
sort: row => row.savedBytes,
|
|
211
223
|
render: row => <span className="num">{formatBytes(row.savedBytes)}</span>,
|
|
212
224
|
},
|
|
213
225
|
{
|
|
214
226
|
key: "hits",
|
|
215
|
-
header: "
|
|
227
|
+
header: t("gain.hits"),
|
|
216
228
|
align: "right",
|
|
217
229
|
sort: row => row.hits,
|
|
218
230
|
render: row => <span className="num">{formatInteger(row.hits)}</span>,
|
|
219
231
|
},
|
|
220
232
|
{
|
|
221
233
|
key: "reduction",
|
|
222
|
-
header: "
|
|
234
|
+
header: t("gain.reduction"),
|
|
223
235
|
align: "right",
|
|
224
236
|
sort: row => row.reductionPercent ?? -1,
|
|
225
237
|
render: row => (
|
|
@@ -2,11 +2,13 @@ import "./models.css";
|
|
|
2
2
|
import { ChevronRight } from "lucide-react";
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
4
|
import { getModelDashboardStats } from "../api";
|
|
5
|
+
import { t } from "../i18n";
|
|
5
6
|
import { type ChartSeries, Legend, Sparkline, TimeChart, useHiddenSeries } from "../charts";
|
|
6
7
|
import { buildModelColorLookup, modelKey, OTHER_COLOR } from "../data/colors";
|
|
7
8
|
import {
|
|
8
9
|
formatCompact,
|
|
9
10
|
formatDurationMs,
|
|
11
|
+
formatErrorRate,
|
|
10
12
|
formatEstimatedCost,
|
|
11
13
|
formatInteger,
|
|
12
14
|
formatPercent,
|
|
@@ -51,30 +53,34 @@ export interface ModelsRouteProps {
|
|
|
51
53
|
type ShareMode = "share" | "requests";
|
|
52
54
|
|
|
53
55
|
const SHARE_OPTIONS = [
|
|
54
|
-
{ value: "share" as const, label: "
|
|
55
|
-
{ value: "requests" as const, label: "
|
|
56
|
+
{ value: "share" as const, label: t("common.share") },
|
|
57
|
+
{ value: "requests" as const, label: t("common.requests") },
|
|
56
58
|
];
|
|
57
59
|
|
|
58
60
|
/** Models plotted individually in the share chart; the rest fold into "Other". */
|
|
59
61
|
const SHARE_LIMIT = 6;
|
|
60
62
|
|
|
61
63
|
export function ModelsRoute({ active, range }: ModelsRouteProps) {
|
|
62
|
-
const query = useQuery(["models", range], () => getModelDashboardStats(range), {
|
|
64
|
+
const query = useQuery(["models", range], ({ signal }) => getModelDashboardStats(range, signal), {
|
|
65
|
+
enabled: active,
|
|
66
|
+
});
|
|
63
67
|
const meta = rangeMeta(range);
|
|
64
68
|
const [mode, setMode] = useState<ShareMode>("share");
|
|
65
69
|
const [hidden, toggleSeries] = useHiddenSeries();
|
|
66
70
|
const [expandedKey, setExpandedKey] = useState<string | null>(null);
|
|
67
71
|
const view = useMemo(() => (query.data ? buildModelsView(query.data, range) : null), [query.data, range]);
|
|
68
72
|
const bucketWord = bucketName(meta.bucketMs);
|
|
73
|
+
// Stable columns: a fresh array would make Table re-sort on every render.
|
|
74
|
+
const columns = useMemo(
|
|
75
|
+
() => (view ? buildModelColumns(view, expandedKey, bucketWord) : []),
|
|
76
|
+
[view, expandedKey, bucketWord],
|
|
77
|
+
);
|
|
69
78
|
const isEmpty = (data: ModelDashboardStats) => data.byModel.length === 0;
|
|
70
|
-
const empty = <EmptyState title="
|
|
79
|
+
const empty = <EmptyState title={t("models.empty-title")} hint={t("models.empty-hint")} />;
|
|
71
80
|
|
|
72
81
|
return (
|
|
73
82
|
<div className="page">
|
|
74
|
-
<PageHeader
|
|
75
|
-
title="Models"
|
|
76
|
-
description={`Which models did the work in ${meta.windowLabel}, and how fast they answered.`}
|
|
77
|
-
/>
|
|
83
|
+
<PageHeader title={t("common.models")} description={t("models.subtitle", { window: t(meta.windowKey) })} />
|
|
78
84
|
|
|
79
85
|
<QueryView
|
|
80
86
|
query={query}
|
|
@@ -87,32 +93,41 @@ export function ModelsRoute({ active, range }: ModelsRouteProps) {
|
|
|
87
93
|
<div data-stale={query.stale}>
|
|
88
94
|
<StatGrid min={200}>
|
|
89
95
|
<Stat
|
|
90
|
-
label="
|
|
96
|
+
label={t("models.used")}
|
|
91
97
|
value={formatInteger(view.models.length)}
|
|
92
|
-
hint={
|
|
98
|
+
hint={
|
|
99
|
+
view.providerCount === 1
|
|
100
|
+
? t("models.across-providers-one", { count: formatInteger(view.providerCount) })
|
|
101
|
+
: t("models.across-providers", { count: formatInteger(view.providerCount) })
|
|
102
|
+
}
|
|
93
103
|
/>
|
|
94
104
|
<Stat
|
|
95
|
-
label="
|
|
105
|
+
label={t("models.most-used")}
|
|
96
106
|
title={view.top ? `${view.top.model} (${view.top.provider})` : undefined}
|
|
97
107
|
value={view.top ? view.top.model : "–"}
|
|
98
108
|
hint={
|
|
99
109
|
view.top
|
|
100
|
-
?
|
|
110
|
+
? t("models.most-used-hint", {
|
|
111
|
+
share: formatPercent(view.top.totalRequests / Math.max(1, view.totalRequests)),
|
|
112
|
+
provider: view.top.provider,
|
|
113
|
+
})
|
|
101
114
|
: undefined
|
|
102
115
|
}
|
|
103
116
|
/>
|
|
104
117
|
<Stat
|
|
105
|
-
label="
|
|
118
|
+
label={t("common.requests")}
|
|
106
119
|
value={formatInteger(view.totalRequests)}
|
|
107
|
-
hint={
|
|
120
|
+
hint={t("models.failed-n", { count: formatInteger(view.failedRequests) })}
|
|
108
121
|
spark={view.requestTotals}
|
|
109
122
|
/>
|
|
110
123
|
<Stat
|
|
111
|
-
label="
|
|
112
|
-
title="
|
|
124
|
+
label={t("models.api-cost")}
|
|
125
|
+
title={t("models.api-cost-title")}
|
|
113
126
|
value={formatEstimatedCost(view.totalCost, view.unpricedRequests)}
|
|
114
127
|
hint={
|
|
115
|
-
view.unpricedRequests > 0
|
|
128
|
+
view.unpricedRequests > 0
|
|
129
|
+
? t("models.unpriced-n", { count: formatInteger(view.unpricedRequests) })
|
|
130
|
+
: undefined
|
|
116
131
|
}
|
|
117
132
|
/>
|
|
118
133
|
</StatGrid>
|
|
@@ -123,11 +138,11 @@ export function ModelsRoute({ active, range }: ModelsRouteProps) {
|
|
|
123
138
|
|
|
124
139
|
<Card
|
|
125
140
|
index={1}
|
|
126
|
-
title="
|
|
141
|
+
title={t("models.request-share")}
|
|
127
142
|
description={
|
|
128
143
|
mode === "share"
|
|
129
|
-
?
|
|
130
|
-
:
|
|
144
|
+
? t("models.share-desc", { bucket: bucketWord })
|
|
145
|
+
: t("models.count-desc", { bucket: bucketWord })
|
|
131
146
|
}
|
|
132
147
|
actions={
|
|
133
148
|
<Segmented
|
|
@@ -135,7 +150,7 @@ export function ModelsRoute({ active, range }: ModelsRouteProps) {
|
|
|
135
150
|
options={SHARE_OPTIONS}
|
|
136
151
|
value={mode}
|
|
137
152
|
onChange={setMode}
|
|
138
|
-
aria-label="
|
|
153
|
+
aria-label={t("models.share-mode-aria")}
|
|
139
154
|
/>
|
|
140
155
|
}
|
|
141
156
|
stale={query.stale}
|
|
@@ -171,20 +186,14 @@ export function ModelsRoute({ active, range }: ModelsRouteProps) {
|
|
|
171
186
|
</QueryView>
|
|
172
187
|
</Card>
|
|
173
188
|
|
|
174
|
-
<Card
|
|
175
|
-
index={2}
|
|
176
|
-
title="All models"
|
|
177
|
-
description="Click a row for latency and throughput over time"
|
|
178
|
-
stale={query.stale}
|
|
179
|
-
flush
|
|
180
|
-
>
|
|
189
|
+
<Card index={2} title={t("models.all-models")} description={t("models.table-desc")} stale={query.stale} flush>
|
|
181
190
|
<QueryView query={query} skeleton={<TableSkeleton rows={8} />} isEmpty={isEmpty} empty={empty}>
|
|
182
191
|
{() =>
|
|
183
192
|
view && (
|
|
184
193
|
<Table
|
|
185
194
|
rows={view.models}
|
|
186
195
|
rowKey={row => modelKey(row.model, row.provider)}
|
|
187
|
-
columns={
|
|
196
|
+
columns={columns}
|
|
188
197
|
initialSort={{ key: "requests", dir: "desc" }}
|
|
189
198
|
limit={25}
|
|
190
199
|
onRowClick={row => {
|
|
@@ -322,7 +331,7 @@ function buildModelColumns(view: ModelsView, expandedKey: string | null, bucketW
|
|
|
322
331
|
},
|
|
323
332
|
{
|
|
324
333
|
key: "model",
|
|
325
|
-
header: "
|
|
334
|
+
header: t("common.model"),
|
|
326
335
|
sort: row => row.model,
|
|
327
336
|
render: row => (
|
|
328
337
|
<LabelCell
|
|
@@ -334,7 +343,7 @@ function buildModelColumns(view: ModelsView, expandedKey: string | null, bucketW
|
|
|
334
343
|
},
|
|
335
344
|
{
|
|
336
345
|
key: "requests",
|
|
337
|
-
header: "
|
|
346
|
+
header: t("common.requests"),
|
|
338
347
|
align: "right",
|
|
339
348
|
sort: row => row.totalRequests,
|
|
340
349
|
render: row => (
|
|
@@ -348,16 +357,16 @@ function buildModelColumns(view: ModelsView, expandedKey: string | null, bucketW
|
|
|
348
357
|
},
|
|
349
358
|
{
|
|
350
359
|
key: "cost",
|
|
351
|
-
header: "
|
|
352
|
-
title: "
|
|
360
|
+
header: t("common.cost"),
|
|
361
|
+
title: t("models.cost-title"),
|
|
353
362
|
align: "right",
|
|
354
363
|
sort: row => row.totalCost,
|
|
355
364
|
render: row => <span className="num">{formatEstimatedCost(row.totalCost, row.unpricedRequests)}</span>,
|
|
356
365
|
},
|
|
357
366
|
{
|
|
358
367
|
key: "tokens",
|
|
359
|
-
header: "
|
|
360
|
-
title: "
|
|
368
|
+
header: t("common.tokens"),
|
|
369
|
+
title: t("models.tokens-title"),
|
|
361
370
|
align: "right",
|
|
362
371
|
sort: row => sumConversationTokens(row),
|
|
363
372
|
render: row => {
|
|
@@ -371,48 +380,48 @@ function buildModelColumns(view: ModelsView, expandedKey: string | null, bucketW
|
|
|
371
380
|
},
|
|
372
381
|
{
|
|
373
382
|
key: "cache",
|
|
374
|
-
header: "
|
|
375
|
-
title: "
|
|
383
|
+
header: t("models.cache-rate"),
|
|
384
|
+
title: t("models.cache-title"),
|
|
376
385
|
align: "right",
|
|
377
386
|
sort: row => row.cacheRate,
|
|
378
387
|
render: row => <span className="num">{formatPercent(row.cacheRate)}</span>,
|
|
379
388
|
},
|
|
380
389
|
{
|
|
381
390
|
key: "errors",
|
|
382
|
-
header: "
|
|
391
|
+
header: t("models.errors"),
|
|
383
392
|
align: "right",
|
|
384
393
|
sort: row => row.errorRate,
|
|
385
394
|
render: row =>
|
|
386
395
|
row.failedRequests === 0 ? (
|
|
387
396
|
<span className="num dim">0%</span>
|
|
388
397
|
) : (
|
|
389
|
-
<span title={
|
|
398
|
+
<span title={t("models.failed-n", { count: formatInteger(row.failedRequests) })}>
|
|
390
399
|
<Badge tone={errorRateTone(row.errorRate)} mono>
|
|
391
|
-
{
|
|
400
|
+
{formatErrorRate(row.errorRate)}
|
|
392
401
|
</Badge>
|
|
393
402
|
</span>
|
|
394
403
|
),
|
|
395
404
|
},
|
|
396
405
|
{
|
|
397
406
|
key: "tps",
|
|
398
|
-
header: "
|
|
399
|
-
title: "
|
|
407
|
+
header: t("common.tokens-per-second"),
|
|
408
|
+
title: t("models.tps-title"),
|
|
400
409
|
align: "right",
|
|
401
410
|
sort: row => row.avgTokensPerSecond ?? -1,
|
|
402
411
|
render: row => <span className="num">{formatTokensPerSecond(row.avgTokensPerSecond)}</span>,
|
|
403
412
|
},
|
|
404
413
|
{
|
|
405
414
|
key: "ttft",
|
|
406
|
-
header: "
|
|
407
|
-
title: "
|
|
415
|
+
header: t("models.ttft"),
|
|
416
|
+
title: t("models.ttft-title"),
|
|
408
417
|
align: "right",
|
|
409
418
|
sort: row => row.avgTtft ?? -1,
|
|
410
419
|
render: row => <span className="num">{formatDurationMs(row.avgTtft)}</span>,
|
|
411
420
|
},
|
|
412
421
|
{
|
|
413
422
|
key: "trend",
|
|
414
|
-
header: "
|
|
415
|
-
title:
|
|
423
|
+
header: t("models.trend"),
|
|
424
|
+
title: t("models.trend-title", { bucket: bucketWord }),
|
|
416
425
|
width: 112,
|
|
417
426
|
render: row => {
|
|
418
427
|
const key = modelKey(row.model, row.provider);
|
|
@@ -449,14 +458,14 @@ function ModelDetail({
|
|
|
449
458
|
const series: ChartSeries[] = [
|
|
450
459
|
{
|
|
451
460
|
key: "tps",
|
|
452
|
-
label: "
|
|
461
|
+
label: t("common.tokens-per-second"),
|
|
453
462
|
color,
|
|
454
463
|
kind: points.length > 1 ? "line" : "bars",
|
|
455
464
|
values: points.map(p => p.avgTokensPerSecond),
|
|
456
465
|
},
|
|
457
466
|
{
|
|
458
467
|
key: "ttft",
|
|
459
|
-
label: "
|
|
468
|
+
label: t("models.ttft"),
|
|
460
469
|
color: OTHER_COLOR,
|
|
461
470
|
kind: points.length > 1 ? "line" : "bars",
|
|
462
471
|
axis: "right",
|
|
@@ -469,23 +478,25 @@ function ModelDetail({
|
|
|
469
478
|
<div className="models-detail">
|
|
470
479
|
<div className="models-detail-facts">
|
|
471
480
|
<div>
|
|
472
|
-
<div className="section-label">
|
|
481
|
+
<div className="section-label">{t("models.efficiency")}</div>
|
|
473
482
|
<KeyValues
|
|
474
483
|
items={[
|
|
475
484
|
{
|
|
476
485
|
key: "err",
|
|
477
|
-
label: "
|
|
486
|
+
label: t("common.error-rate"),
|
|
478
487
|
value: (
|
|
479
488
|
<span className={`tone-${errorRateTone(model.errorRate)}`}>
|
|
480
|
-
{
|
|
481
|
-
<span className="dim">
|
|
489
|
+
{formatErrorRate(model.errorRate)}{" "}
|
|
490
|
+
<span className="dim">
|
|
491
|
+
({t("models.failed-n", { count: formatInteger(model.failedRequests) })})
|
|
492
|
+
</span>
|
|
482
493
|
</span>
|
|
483
494
|
),
|
|
484
495
|
},
|
|
485
|
-
{ key: "cache", label: "
|
|
496
|
+
{ key: "cache", label: t("models.cache-rate"), value: formatPercent(model.cacheRate) },
|
|
486
497
|
{
|
|
487
498
|
key: "savings",
|
|
488
|
-
label: "
|
|
499
|
+
label: t("models.cache-savings"),
|
|
489
500
|
value: (
|
|
490
501
|
<span className={model.cacheSavings < 0 ? "tone-bad" : undefined}>
|
|
491
502
|
{formatPercent(model.cacheSavings)}
|
|
@@ -494,42 +505,48 @@ function ModelDetail({
|
|
|
494
505
|
},
|
|
495
506
|
{
|
|
496
507
|
key: "premium",
|
|
497
|
-
label: "
|
|
508
|
+
label: t("common.premium-requests"),
|
|
498
509
|
value: formatInteger(Math.round(model.totalPremiumRequests * 100) / 100),
|
|
499
510
|
},
|
|
500
511
|
]}
|
|
501
512
|
/>
|
|
502
513
|
</div>
|
|
503
514
|
<div>
|
|
504
|
-
<div className="section-label">
|
|
515
|
+
<div className="section-label">{t("models.latency")}</div>
|
|
505
516
|
<KeyValues
|
|
506
517
|
items={[
|
|
507
|
-
{ key: "dur", label: "
|
|
508
|
-
{ key: "ttft", label: "
|
|
509
|
-
{
|
|
518
|
+
{ key: "dur", label: t("models.avg-duration"), value: formatDurationMs(model.avgDuration) },
|
|
519
|
+
{ key: "ttft", label: t("models.avg-ttft"), value: formatDurationMs(model.avgTtft) },
|
|
520
|
+
{
|
|
521
|
+
key: "tps",
|
|
522
|
+
label: t("common.tokens-per-second"),
|
|
523
|
+
value: formatTokensPerSecond(model.avgTokensPerSecond),
|
|
524
|
+
},
|
|
510
525
|
]}
|
|
511
526
|
/>
|
|
512
527
|
</div>
|
|
513
528
|
<div>
|
|
514
|
-
<div className="section-label">
|
|
529
|
+
<div className="section-label">{t("common.tokens")}</div>
|
|
515
530
|
<KeyValues
|
|
516
531
|
items={[
|
|
517
|
-
{ key: "in", label: "
|
|
518
|
-
{ key: "cr", label: "
|
|
519
|
-
{ key: "cw", label: "
|
|
520
|
-
{ key: "out", label: "
|
|
532
|
+
{ key: "in", label: t("models.uncached-input"), value: formatCompact(model.totalInputTokens) },
|
|
533
|
+
{ key: "cr", label: t("common.cache-read"), value: formatCompact(model.totalCacheReadTokens) },
|
|
534
|
+
{ key: "cw", label: t("common.cache-write"), value: formatCompact(model.totalCacheWriteTokens) },
|
|
535
|
+
{ key: "out", label: t("common.output"), value: formatCompact(model.totalOutputTokens) },
|
|
521
536
|
]}
|
|
522
537
|
/>
|
|
523
538
|
</div>
|
|
524
539
|
<div className="micro dim">
|
|
525
|
-
|
|
526
|
-
|
|
540
|
+
{t("models.first-last-seen", {
|
|
541
|
+
first: formatRelativeTime(model.firstTimestamp),
|
|
542
|
+
last: formatRelativeTime(model.lastTimestamp),
|
|
543
|
+
})}
|
|
527
544
|
</div>
|
|
528
545
|
</div>
|
|
529
546
|
<div className="models-detail-chart">
|
|
530
547
|
<div className="row" style={{ justifyContent: "space-between" }}>
|
|
531
548
|
<div className="section-label" style={{ marginBottom: 0 }}>
|
|
532
|
-
|
|
549
|
+
{t("models.performance-per-bucket", { bucket: bucketWord })}
|
|
533
550
|
</div>
|
|
534
551
|
<Legend
|
|
535
552
|
items={series.map(s => ({ key: s.key, label: s.label, color: s.color }))}
|
|
@@ -538,7 +555,7 @@ function ModelDetail({
|
|
|
538
555
|
/>
|
|
539
556
|
</div>
|
|
540
557
|
{points.length === 0 ? (
|
|
541
|
-
<EmptyState title="
|
|
558
|
+
<EmptyState title={t("models.no-samples-title")} hint={t("models.no-samples-hint")} />
|
|
542
559
|
) : (
|
|
543
560
|
<TimeChart
|
|
544
561
|
buckets={points.map(p => p.timestamp)}
|
|
@@ -548,11 +565,11 @@ function ModelDetail({
|
|
|
548
565
|
stacked={false}
|
|
549
566
|
height={240}
|
|
550
567
|
format={formatCompact}
|
|
551
|
-
formatTooltip={v =>
|
|
568
|
+
formatTooltip={v => t("models.tok-per-second", { value: formatTokensPerSecond(v) })}
|
|
552
569
|
formatRight={v => `${Number(v.toFixed(2))}s`}
|
|
553
570
|
tooltipExtra={i => (
|
|
554
571
|
<div className="chart-tooltip-row chart-tooltip-total">
|
|
555
|
-
<span className="chart-tooltip-label">
|
|
572
|
+
<span className="chart-tooltip-label">{t("common.requests")}</span>
|
|
556
573
|
<span className="chart-tooltip-value">{formatInteger(points[i].requests)}</span>
|
|
557
574
|
</div>
|
|
558
575
|
)}
|
|
@@ -564,9 +581,9 @@ function ModelDetail({
|
|
|
564
581
|
}
|
|
565
582
|
|
|
566
583
|
function bucketName(bucketMs: number): string {
|
|
567
|
-
if (bucketMs < 3_600_000) return "
|
|
568
|
-
if (bucketMs < 86_400_000) return "hour";
|
|
569
|
-
return "day";
|
|
584
|
+
if (bucketMs < 3_600_000) return t("models.bucket-5m");
|
|
585
|
+
if (bucketMs < 86_400_000) return t("models.bucket-hour");
|
|
586
|
+
return t("models.bucket-day");
|
|
570
587
|
}
|
|
571
588
|
|
|
572
589
|
function sumValues(values: readonly (number | null)[]): number {
|