@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,16 +1,18 @@
|
|
|
1
|
-
import { format } from "@linxiraos/pi-utils/dates";
|
|
2
1
|
import { type ReactNode, useMemo, useState } from "react";
|
|
3
2
|
import { getProviderDashboardStats, getProviderWindowStats } from "../api";
|
|
3
|
+
import { t } from "../i18n";
|
|
4
4
|
import { Chart, type ChartSeries, Legend, ShareBar, TimeChart, useHiddenSeries } from "../charts";
|
|
5
5
|
import { buildColorLookup, OTHER_COLOR, SERIES_COLORS } from "../data/colors";
|
|
6
6
|
import {
|
|
7
7
|
formatCompact,
|
|
8
8
|
formatCost,
|
|
9
|
+
formatErrorRate,
|
|
9
10
|
formatEstimatedCost,
|
|
10
11
|
formatInteger,
|
|
11
12
|
formatPercent,
|
|
12
13
|
formatRelativeTime,
|
|
13
14
|
formatTokensPerSecond,
|
|
15
|
+
formatDateTimeShort,
|
|
14
16
|
} from "../data/formatters";
|
|
15
17
|
import { type QueryResult, useQuery } from "../data/query";
|
|
16
18
|
import { bucketAxis, formatBucket, formatTick, rangeMeta } from "../data/range";
|
|
@@ -51,23 +53,22 @@ export interface ProvidersRouteProps {
|
|
|
51
53
|
type BurnMetric = "tokens" | "cost" | "requests";
|
|
52
54
|
|
|
53
55
|
const BURN_OPTIONS = [
|
|
54
|
-
{ value: "tokens" as const, label: "
|
|
55
|
-
{ value: "cost" as const, label: "
|
|
56
|
-
{ value: "requests" as const, label: "
|
|
56
|
+
{ value: "tokens" as const, label: t("common.tokens") },
|
|
57
|
+
{ value: "cost" as const, label: t("common.cost"), title: t("providers.cost-title") },
|
|
58
|
+
{ value: "requests" as const, label: t("common.requests") },
|
|
57
59
|
];
|
|
58
60
|
|
|
59
61
|
/** Providers stacked individually in the burn chart; the rest fold into "Other". */
|
|
60
62
|
const TOP_PROVIDERS = 6;
|
|
61
63
|
|
|
62
64
|
const TOKEN_MIX = [
|
|
63
|
-
{ key: "input", label: "
|
|
64
|
-
{ key: "cacheRead", label: "
|
|
65
|
-
{ key: "cacheWrite", label: "
|
|
66
|
-
{ key: "output", label: "
|
|
65
|
+
{ key: "input", label: t("ui.uncached-input"), color: "#5b8cff" },
|
|
66
|
+
{ key: "cacheRead", label: t("common.cache-read"), color: "var(--chart-primary)" },
|
|
67
|
+
{ key: "cacheWrite", label: t("common.cache-write"), color: "#f5b54a" },
|
|
68
|
+
{ key: "output", label: t("common.output"), color: "var(--chart-secondary)" },
|
|
67
69
|
] as const;
|
|
68
70
|
|
|
69
|
-
const SNAPSHOT_HINT =
|
|
70
|
-
"Usage snapshots accumulate whenever usage limits are fetched (TUI footer, /usage, zeta-c usage).";
|
|
71
|
+
const SNAPSHOT_HINT = t("providers.snapshot-hint");
|
|
71
72
|
|
|
72
73
|
interface WindowRef {
|
|
73
74
|
provider: string;
|
|
@@ -75,14 +76,16 @@ interface WindowRef {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
78
|
-
const stats = useQuery(["providers", range], () => getProviderDashboardStats(range), {
|
|
79
|
+
const stats = useQuery(["providers", range], ({ signal }) => getProviderDashboardStats(range, signal), {
|
|
80
|
+
enabled: active,
|
|
81
|
+
});
|
|
79
82
|
const [metric, setMetric] = useState<BurnMetric>("tokens");
|
|
80
83
|
const [hidden, toggleHidden] = useHiddenSeries();
|
|
81
84
|
const meta = rangeMeta(range);
|
|
82
85
|
|
|
83
86
|
// Subscription windows load independently: insights for every provider
|
|
84
87
|
// first, then utilization series for the one provider being charted.
|
|
85
|
-
const windows = useQuery(["provider-windows", range], () => getProviderWindowStats(range, null), {
|
|
88
|
+
const windows = useQuery(["provider-windows", range], ({ signal }) => getProviderWindowStats(range, null, signal), {
|
|
86
89
|
enabled: active,
|
|
87
90
|
});
|
|
88
91
|
const [picked, setPicked] = useState<WindowRef | null>(null);
|
|
@@ -90,7 +93,7 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
90
93
|
const selectedProvider = selected?.provider ?? null;
|
|
91
94
|
const accounts = useQuery(
|
|
92
95
|
["provider-windows", range, selectedProvider],
|
|
93
|
-
() => getProviderWindowStats(range, selectedProvider),
|
|
96
|
+
({ signal }) => getProviderWindowStats(range, selectedProvider, signal),
|
|
94
97
|
{ enabled: active && selectedProvider !== null },
|
|
95
98
|
);
|
|
96
99
|
|
|
@@ -139,13 +142,13 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
139
142
|
|
|
140
143
|
const burnSeries = view.burn[metric];
|
|
141
144
|
const burnFormat = metric === "cost" ? (v: number) => formatCost(v) : formatCompact;
|
|
142
|
-
const
|
|
145
|
+
const totals = view.totals;
|
|
143
146
|
|
|
144
147
|
return (
|
|
145
148
|
<div className="page">
|
|
146
149
|
<PageHeader
|
|
147
|
-
title="
|
|
148
|
-
description={
|
|
150
|
+
title={t("common.providers")}
|
|
151
|
+
description={t("providers.description", { window: t(meta.windowKey) })}
|
|
149
152
|
/>
|
|
150
153
|
|
|
151
154
|
<QueryView query={stats} skeleton={<ChartSkeleton height={112} />}>
|
|
@@ -153,34 +156,42 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
153
156
|
<div data-stale={stats.stale}>
|
|
154
157
|
<StatGrid min={190}>
|
|
155
158
|
<Stat
|
|
156
|
-
label="
|
|
159
|
+
label={t("common.providers")}
|
|
157
160
|
value={formatInteger(providers.length)}
|
|
158
|
-
hint={
|
|
161
|
+
hint={
|
|
162
|
+
view.topProvider
|
|
163
|
+
? t("providers.most-tokens", { provider: view.topProvider.provider })
|
|
164
|
+
: undefined
|
|
165
|
+
}
|
|
159
166
|
/>
|
|
160
167
|
<Stat
|
|
161
|
-
label="
|
|
162
|
-
value={formatInteger(
|
|
163
|
-
hint={
|
|
168
|
+
label={t("common.requests")}
|
|
169
|
+
value={formatInteger(totals.requests)}
|
|
170
|
+
hint={t("providers.failed-n", { count: formatInteger(totals.failed) })}
|
|
164
171
|
spark={view.spark.requests}
|
|
165
172
|
/>
|
|
166
173
|
<Stat
|
|
167
|
-
label="
|
|
168
|
-
title="
|
|
169
|
-
value={formatCompact(
|
|
174
|
+
label={t("common.tokens")}
|
|
175
|
+
title={t("providers.tokens-title")}
|
|
176
|
+
value={formatCompact(totals.tokens)}
|
|
170
177
|
spark={view.spark.tokens}
|
|
171
178
|
/>
|
|
172
179
|
<Stat
|
|
173
|
-
label="
|
|
174
|
-
title="
|
|
175
|
-
value={formatEstimatedCost(
|
|
176
|
-
hint={
|
|
180
|
+
label={t("providers.api-equivalent-cost")}
|
|
181
|
+
title={t("providers.api-cost-title")}
|
|
182
|
+
value={formatEstimatedCost(totals.cost, totals.unpriced)}
|
|
183
|
+
hint={
|
|
184
|
+
totals.unpriced > 0
|
|
185
|
+
? t("providers.unpriced-n", { count: formatInteger(totals.unpriced) })
|
|
186
|
+
: undefined
|
|
187
|
+
}
|
|
177
188
|
spark={view.spark.cost}
|
|
178
189
|
sparkColor="var(--chart-secondary)"
|
|
179
190
|
/>
|
|
180
191
|
<Stat
|
|
181
|
-
label="
|
|
182
|
-
value={
|
|
183
|
-
hint={
|
|
192
|
+
label={t("common.error-rate")}
|
|
193
|
+
value={formatErrorRate(totals.requests > 0 ? totals.failed / totals.requests : 0)}
|
|
194
|
+
hint={t("providers.succeeded-n", { count: formatInteger(totals.requests - totals.failed) })}
|
|
184
195
|
/>
|
|
185
196
|
</StatGrid>
|
|
186
197
|
</div>
|
|
@@ -189,11 +200,16 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
189
200
|
|
|
190
201
|
<Card
|
|
191
202
|
index={1}
|
|
192
|
-
title="
|
|
203
|
+
title={t("providers.totals")}
|
|
193
204
|
description={
|
|
194
|
-
|
|
195
|
-
?
|
|
196
|
-
|
|
205
|
+
totals.unpriced > 0
|
|
206
|
+
? t(
|
|
207
|
+
totals.unpriced === 1
|
|
208
|
+
? "providers.totals-description-unpriced-one"
|
|
209
|
+
: "providers.totals-description-unpriced-n",
|
|
210
|
+
{ count: formatInteger(totals.unpriced) },
|
|
211
|
+
)
|
|
212
|
+
: t("providers.totals-description")
|
|
197
213
|
}
|
|
198
214
|
flush
|
|
199
215
|
stale={stats.stale}
|
|
@@ -206,11 +222,21 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
206
222
|
<div className="grid grid-main-side">
|
|
207
223
|
<Card
|
|
208
224
|
index={2}
|
|
209
|
-
title="
|
|
225
|
+
title={t("providers.burn")}
|
|
210
226
|
description={
|
|
211
|
-
metric === "cost" &&
|
|
212
|
-
?
|
|
213
|
-
|
|
227
|
+
metric === "cost" && totals.unpriced > 0
|
|
228
|
+
? t("providers.burn-estimate-description", {
|
|
229
|
+
bucket: meta.bucketMs < 86_400_000 ? t("providers.bucket-hour") : t("providers.bucket-day"),
|
|
230
|
+
})
|
|
231
|
+
: t("providers.burn-description", {
|
|
232
|
+
bucket:
|
|
233
|
+
meta.bucketMs < 3_600_000
|
|
234
|
+
? t("providers.bucket-5m")
|
|
235
|
+
: meta.bucketMs < 86_400_000
|
|
236
|
+
? t("providers.bucket-hour")
|
|
237
|
+
: t("providers.bucket-day"),
|
|
238
|
+
count: TOP_PROVIDERS,
|
|
239
|
+
})
|
|
214
240
|
}
|
|
215
241
|
actions={<Segmented size="sm" options={BURN_OPTIONS} value={metric} onChange={setMetric} />}
|
|
216
242
|
stale={stats.stale}
|
|
@@ -226,7 +252,7 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
226
252
|
height={260}
|
|
227
253
|
format={burnFormat}
|
|
228
254
|
formatTooltip={metric === "cost" ? v => formatCost(v) : formatInteger}
|
|
229
|
-
emptyLabel="
|
|
255
|
+
emptyLabel={t("providers.no-provider-activity")}
|
|
230
256
|
/>
|
|
231
257
|
<Legend
|
|
232
258
|
items={burnSeries.map(s => ({
|
|
@@ -250,8 +276,8 @@ export function ProvidersRoute({ active, range }: ProvidersRouteProps) {
|
|
|
250
276
|
|
|
251
277
|
<Card
|
|
252
278
|
index={4}
|
|
253
|
-
title="
|
|
254
|
-
description="
|
|
279
|
+
title={t("providers.subscription-windows")}
|
|
280
|
+
description={t("providers.subscription-windows-description")}
|
|
255
281
|
flush
|
|
256
282
|
stale={windows.stale}
|
|
257
283
|
>
|
|
@@ -310,7 +336,7 @@ function ProviderTotalsTable({
|
|
|
310
336
|
return [
|
|
311
337
|
{
|
|
312
338
|
key: "provider",
|
|
313
|
-
header: "
|
|
339
|
+
header: t("common.provider"),
|
|
314
340
|
sort: p => p.provider,
|
|
315
341
|
render: p => (
|
|
316
342
|
<span className="row" style={{ gap: 8 }}>
|
|
@@ -321,7 +347,7 @@ function ProviderTotalsTable({
|
|
|
321
347
|
},
|
|
322
348
|
{
|
|
323
349
|
key: "requests",
|
|
324
|
-
header: "
|
|
350
|
+
header: t("common.requests"),
|
|
325
351
|
align: "right",
|
|
326
352
|
sort: p => p.totalRequests,
|
|
327
353
|
render: p => (
|
|
@@ -330,7 +356,7 @@ function ProviderTotalsTable({
|
|
|
330
356
|
},
|
|
331
357
|
{
|
|
332
358
|
key: "errors",
|
|
333
|
-
header: "
|
|
359
|
+
header: t("common.error-rate"),
|
|
334
360
|
align: "right",
|
|
335
361
|
sort: p => (p.totalRequests > 0 ? p.failedRequests / p.totalRequests : 0),
|
|
336
362
|
render: p => {
|
|
@@ -339,7 +365,7 @@ function ProviderTotalsTable({
|
|
|
339
365
|
<span className="row" style={{ gap: 8, justifyContent: "flex-end" }}>
|
|
340
366
|
<span className="num dim">{formatInteger(p.failedRequests)}</span>
|
|
341
367
|
<Badge tone={p.failedRequests > 0 ? errorRateTone(rate) : "neutral"} mono>
|
|
342
|
-
{
|
|
368
|
+
{formatErrorRate(rate)}
|
|
343
369
|
</Badge>
|
|
344
370
|
</span>
|
|
345
371
|
);
|
|
@@ -347,16 +373,16 @@ function ProviderTotalsTable({
|
|
|
347
373
|
},
|
|
348
374
|
{
|
|
349
375
|
key: "models",
|
|
350
|
-
header: "
|
|
351
|
-
title: "
|
|
376
|
+
header: t("common.models"),
|
|
377
|
+
title: t("providers.models-title"),
|
|
352
378
|
align: "right",
|
|
353
379
|
sort: p => p.models,
|
|
354
380
|
render: p => <span className="num">{formatInteger(p.models)}</span>,
|
|
355
381
|
},
|
|
356
382
|
{
|
|
357
383
|
key: "tokens",
|
|
358
|
-
header: "
|
|
359
|
-
title: "
|
|
384
|
+
header: t("common.tokens"),
|
|
385
|
+
title: t("providers.tokens-title"),
|
|
360
386
|
align: "right",
|
|
361
387
|
sort: p => p.totalTokens,
|
|
362
388
|
render: p => (
|
|
@@ -370,8 +396,8 @@ function ProviderTotalsTable({
|
|
|
370
396
|
},
|
|
371
397
|
{
|
|
372
398
|
key: "share",
|
|
373
|
-
header: "
|
|
374
|
-
title: "
|
|
399
|
+
header: t("common.share"),
|
|
400
|
+
title: t("providers.share-title"),
|
|
375
401
|
align: "right",
|
|
376
402
|
sort: p => p.totalTokens,
|
|
377
403
|
render: p => (
|
|
@@ -380,14 +406,18 @@ function ProviderTotalsTable({
|
|
|
380
406
|
},
|
|
381
407
|
{
|
|
382
408
|
key: "cost",
|
|
383
|
-
header: "
|
|
384
|
-
title: "
|
|
409
|
+
header: t("common.cost"),
|
|
410
|
+
title: t("providers.cost-title"),
|
|
385
411
|
align: "right",
|
|
386
412
|
sort: p => p.totalCost,
|
|
387
413
|
render: p => (
|
|
388
414
|
<span
|
|
389
415
|
className="num"
|
|
390
|
-
title={
|
|
416
|
+
title={
|
|
417
|
+
p.unpricedRequests > 0
|
|
418
|
+
? t("providers.unpriced-n", { count: formatInteger(p.unpricedRequests) })
|
|
419
|
+
: undefined
|
|
420
|
+
}
|
|
391
421
|
>
|
|
392
422
|
{formatEstimatedCost(p.totalCost, p.unpricedRequests)}
|
|
393
423
|
</span>
|
|
@@ -395,16 +425,16 @@ function ProviderTotalsTable({
|
|
|
395
425
|
},
|
|
396
426
|
{
|
|
397
427
|
key: "tps",
|
|
398
|
-
header: "
|
|
399
|
-
title: "
|
|
428
|
+
header: t("common.tokens-per-second"),
|
|
429
|
+
title: t("providers.tps-title"),
|
|
400
430
|
align: "right",
|
|
401
431
|
sort: p => p.avgTokensPerSecond ?? -1,
|
|
402
432
|
render: p => <span className="num">{formatTokensPerSecond(p.avgTokensPerSecond)}</span>,
|
|
403
433
|
},
|
|
404
434
|
{
|
|
405
435
|
key: "premium",
|
|
406
|
-
header: "
|
|
407
|
-
title: "
|
|
436
|
+
header: t("common.premium"),
|
|
437
|
+
title: t("providers.premium-title"),
|
|
408
438
|
align: "right",
|
|
409
439
|
sort: p => p.totalPremiumRequests,
|
|
410
440
|
render: p => (
|
|
@@ -426,7 +456,7 @@ function ProviderTotalsTable({
|
|
|
426
456
|
selectedKey={open}
|
|
427
457
|
onRowClick={p => setOpen(prev => (prev === p.provider ? null : p.provider))}
|
|
428
458
|
expanded={p => (p.provider === open ? <TokenMix provider={p} /> : null)}
|
|
429
|
-
empty={<EmptyState title="
|
|
459
|
+
empty={<EmptyState title={t("providers.no-requests-in-range")} />}
|
|
430
460
|
/>
|
|
431
461
|
);
|
|
432
462
|
}
|
|
@@ -488,13 +518,13 @@ function PeakHoursCard({
|
|
|
488
518
|
const series: ChartSeries[] = [
|
|
489
519
|
{
|
|
490
520
|
key: "tokens",
|
|
491
|
-
label: "
|
|
521
|
+
label: t("common.tokens"),
|
|
492
522
|
color: "var(--chart-primary)",
|
|
493
523
|
values: tokens.map((v, hour) => (hasData && hour === peak ? null : v)),
|
|
494
524
|
},
|
|
495
525
|
{
|
|
496
526
|
key: "peak",
|
|
497
|
-
label: "
|
|
527
|
+
label: t("providers.tokens-peak-hour"),
|
|
498
528
|
color: "var(--chart-secondary)",
|
|
499
529
|
values: tokens.map((v, hour) => (hasData && hour === peak ? v : null)),
|
|
500
530
|
},
|
|
@@ -507,20 +537,20 @@ function PeakHoursCard({
|
|
|
507
537
|
return (
|
|
508
538
|
<Card
|
|
509
539
|
index={3}
|
|
510
|
-
title="
|
|
540
|
+
title={t("providers.peak-burn-hours")}
|
|
511
541
|
description={
|
|
512
542
|
hours.hasData
|
|
513
|
-
?
|
|
514
|
-
: "
|
|
543
|
+
? t("providers.peak-hours-description", { hour: hourLabel(hours.peak) })
|
|
544
|
+
: t("providers.hours-description")
|
|
515
545
|
}
|
|
516
546
|
actions={
|
|
517
547
|
<select
|
|
518
548
|
className="input providers-select"
|
|
519
549
|
value={current}
|
|
520
550
|
onChange={e => setProvider(e.target.value)}
|
|
521
|
-
aria-label="
|
|
551
|
+
aria-label={t("common.provider")}
|
|
522
552
|
>
|
|
523
|
-
<option value={ALL_PROVIDERS}>
|
|
553
|
+
<option value={ALL_PROVIDERS}>{t("providers.all-providers")}</option>
|
|
524
554
|
{providers.map(p => (
|
|
525
555
|
<option key={p.provider} value={p.provider}>
|
|
526
556
|
{p.provider}
|
|
@@ -538,15 +568,15 @@ function PeakHoursCard({
|
|
|
538
568
|
height={260}
|
|
539
569
|
showTotal={false}
|
|
540
570
|
formatTooltip={formatInteger}
|
|
541
|
-
emptyLabel="
|
|
571
|
+
emptyLabel={t("providers.no-activity-in-range")}
|
|
542
572
|
tooltipExtra={hour => (
|
|
543
573
|
<>
|
|
544
574
|
<div className="chart-tooltip-row">
|
|
545
|
-
<span className="chart-tooltip-label">
|
|
575
|
+
<span className="chart-tooltip-label">{t("common.output")}</span>
|
|
546
576
|
<span className="chart-tooltip-value">{formatInteger(hours.output[hour])}</span>
|
|
547
577
|
</div>
|
|
548
578
|
<div className="chart-tooltip-row">
|
|
549
|
-
<span className="chart-tooltip-label">
|
|
579
|
+
<span className="chart-tooltip-label">{t("common.requests")}</span>
|
|
550
580
|
<span className="chart-tooltip-value">{formatInteger(hours.requests[hour])}</span>
|
|
551
581
|
</div>
|
|
552
582
|
</>
|
|
@@ -578,7 +608,7 @@ function WindowInsightsTable({
|
|
|
578
608
|
limit={12}
|
|
579
609
|
selectedKey={selected ? `${selected.provider}::${selected.windowKey}` : null}
|
|
580
610
|
onRowClick={i => onSelect({ provider: i.provider, windowKey: i.windowKey })}
|
|
581
|
-
empty={<EmptyState title="
|
|
611
|
+
empty={<EmptyState title={t("providers.no-snapshots-in-range")} hint={SNAPSHOT_HINT} />}
|
|
582
612
|
/>
|
|
583
613
|
);
|
|
584
614
|
}
|
|
@@ -586,53 +616,53 @@ function WindowInsightsTable({
|
|
|
586
616
|
const INSIGHT_COLUMNS: Column<ProviderWindowInsight>[] = [
|
|
587
617
|
{
|
|
588
618
|
key: "window",
|
|
589
|
-
header: "
|
|
590
|
-
sort: i => `${i.provider} ${i.
|
|
591
|
-
render: i => <LabelCell primary={i.
|
|
619
|
+
header: t("providers.window"),
|
|
620
|
+
sort: i => `${i.provider} ${i.windowKey}`,
|
|
621
|
+
render: i => <LabelCell primary={i.windowKey} secondary={<span className="mono">{i.provider}</span>} />,
|
|
592
622
|
},
|
|
593
623
|
{
|
|
594
624
|
key: "accounts",
|
|
595
|
-
header: "
|
|
596
|
-
title: "
|
|
625
|
+
header: t("providers.accounts"),
|
|
626
|
+
title: t("providers.accounts-title"),
|
|
597
627
|
align: "right",
|
|
598
628
|
sort: i => i.accounts,
|
|
599
629
|
render: i => <span className="num">{formatInteger(i.accounts)}</span>,
|
|
600
630
|
},
|
|
601
631
|
{
|
|
602
632
|
key: "cycles",
|
|
603
|
-
header: "
|
|
604
|
-
title: "
|
|
633
|
+
header: t("providers.resets"),
|
|
634
|
+
title: t("providers.resets-title"),
|
|
605
635
|
align: "right",
|
|
606
636
|
sort: i => i.cycles,
|
|
607
637
|
render: i => <span className="num muted">{formatInteger(i.cycles)}</span>,
|
|
608
638
|
},
|
|
609
639
|
{
|
|
610
640
|
key: "consumed",
|
|
611
|
-
header: "
|
|
612
|
-
title: "
|
|
641
|
+
header: t("providers.windows-burned"),
|
|
642
|
+
title: t("providers.windows-burned-title"),
|
|
613
643
|
align: "right",
|
|
614
644
|
sort: i => i.fractionConsumed,
|
|
615
645
|
render: i => <span className="num">{i.fractionConsumed.toFixed(2)}</span>,
|
|
616
646
|
},
|
|
617
647
|
{
|
|
618
648
|
key: "capacity",
|
|
619
|
-
header: "
|
|
620
|
-
title: "
|
|
649
|
+
header: t("providers.tokens-per-window"),
|
|
650
|
+
title: t("providers.tokens-per-window-title"),
|
|
621
651
|
align: "right",
|
|
622
652
|
sort: i => i.estTokensPerWindow ?? -1,
|
|
623
653
|
render: i =>
|
|
624
654
|
i.estTokensPerWindow !== null ? (
|
|
625
655
|
<span className="num">{formatCompact(i.estTokensPerWindow)}</span>
|
|
626
656
|
) : (
|
|
627
|
-
<span className="dim" title="
|
|
657
|
+
<span className="dim" title={t("providers.too-little-consumed")}>
|
|
628
658
|
–
|
|
629
659
|
</span>
|
|
630
660
|
),
|
|
631
661
|
},
|
|
632
662
|
{
|
|
633
663
|
key: "peak",
|
|
634
|
-
header: "
|
|
635
|
-
title: "
|
|
664
|
+
header: t("providers.peak-utilization"),
|
|
665
|
+
title: t("providers.peak-utilization-title"),
|
|
636
666
|
align: "right",
|
|
637
667
|
sort: i => i.peakConcurrentFraction,
|
|
638
668
|
render: i => {
|
|
@@ -649,14 +679,17 @@ const INSIGHT_COLUMNS: Column<ProviderWindowInsight>[] = [
|
|
|
649
679
|
},
|
|
650
680
|
{
|
|
651
681
|
key: "ideal",
|
|
652
|
-
header: "
|
|
653
|
-
title: "
|
|
682
|
+
header: t("providers.accounts-needed"),
|
|
683
|
+
title: t("providers.accounts-needed-title"),
|
|
654
684
|
align: "right",
|
|
655
685
|
sort: i => i.idealAccounts - i.accounts,
|
|
656
686
|
render: i =>
|
|
657
687
|
i.idealAccounts > i.accounts ? (
|
|
658
688
|
<Badge tone="warn" mono>
|
|
659
|
-
{
|
|
689
|
+
{t("providers.ideal-accounts", {
|
|
690
|
+
ideal: formatInteger(i.idealAccounts),
|
|
691
|
+
have: formatInteger(i.accounts),
|
|
692
|
+
})}
|
|
660
693
|
</Badge>
|
|
661
694
|
) : (
|
|
662
695
|
<span className="num">{formatInteger(i.idealAccounts)}</span>
|
|
@@ -664,14 +697,14 @@ const INSIGHT_COLUMNS: Column<ProviderWindowInsight>[] = [
|
|
|
664
697
|
},
|
|
665
698
|
{
|
|
666
699
|
key: "exhausted",
|
|
667
|
-
header: "
|
|
668
|
-
title: "
|
|
700
|
+
header: t("providers.exhaustions"),
|
|
701
|
+
title: t("providers.exhaustions-title"),
|
|
669
702
|
align: "right",
|
|
670
703
|
sort: i => i.exhaustedEvents,
|
|
671
704
|
render: i =>
|
|
672
705
|
i.exhaustedEvents > 0 ? (
|
|
673
706
|
<Badge tone="warn" mono>
|
|
674
|
-
{formatInteger(i.exhaustedEvents)}
|
|
707
|
+
{t("providers.exhausted-n", { count: formatInteger(i.exhaustedEvents) })}
|
|
675
708
|
</Badge>
|
|
676
709
|
) : (
|
|
677
710
|
<span className="num dim">0</span>
|
|
@@ -736,7 +769,7 @@ function WindowUtilizationCard({
|
|
|
736
769
|
() =>
|
|
737
770
|
(insights ?? [])
|
|
738
771
|
.filter(i => i.provider === selected?.provider)
|
|
739
|
-
.map(i => ({ key: i.windowKey, label: i.
|
|
772
|
+
.map(i => ({ key: i.windowKey, label: i.windowKey })),
|
|
740
773
|
[insights, selected?.provider],
|
|
741
774
|
);
|
|
742
775
|
// While another provider's series are still on screen, show none of them.
|
|
@@ -790,7 +823,7 @@ function WindowUtilizationCard({
|
|
|
790
823
|
.sort(
|
|
791
824
|
(a, b) =>
|
|
792
825
|
Number(b.color !== null) - Number(a.color !== null) ||
|
|
793
|
-
a.series.
|
|
826
|
+
a.series.windowKey.localeCompare(b.series.windowKey) ||
|
|
794
827
|
(b.latest?.fraction ?? -1) - (a.latest?.fraction ?? -1),
|
|
795
828
|
);
|
|
796
829
|
}, [providerSeries, names, chart.series, selected?.windowKey]);
|
|
@@ -800,11 +833,15 @@ function WindowUtilizationCard({
|
|
|
800
833
|
return (
|
|
801
834
|
<Card
|
|
802
835
|
index={5}
|
|
803
|
-
title="
|
|
836
|
+
title={t("providers.utilization")}
|
|
804
837
|
description={
|
|
805
838
|
chart.buckets.length > 0
|
|
806
|
-
?
|
|
807
|
-
|
|
839
|
+
? t("providers.utilization-description", {
|
|
840
|
+
window: windowLabel ?? t("providers.window"),
|
|
841
|
+
from: formatDateTimeShort(chart.first),
|
|
842
|
+
to: formatDateTimeShort(chart.last),
|
|
843
|
+
})
|
|
844
|
+
: t("providers.utilization-description-plain")
|
|
808
845
|
}
|
|
809
846
|
actions={
|
|
810
847
|
selected && (
|
|
@@ -817,7 +854,7 @@ function WindowUtilizationCard({
|
|
|
817
854
|
const first = insights?.find(i => i.provider === next);
|
|
818
855
|
if (first) onSelect({ provider: next, windowKey: first.windowKey });
|
|
819
856
|
}}
|
|
820
|
-
aria-label="
|
|
857
|
+
aria-label={t("common.provider")}
|
|
821
858
|
>
|
|
822
859
|
{providers.map(p => (
|
|
823
860
|
<option key={p} value={p}>
|
|
@@ -829,7 +866,7 @@ function WindowUtilizationCard({
|
|
|
829
866
|
className="input providers-select"
|
|
830
867
|
value={selected.windowKey}
|
|
831
868
|
onChange={e => onSelect({ provider: selected.provider, windowKey: e.target.value })}
|
|
832
|
-
aria-label="
|
|
869
|
+
aria-label={t("providers.window")}
|
|
833
870
|
>
|
|
834
871
|
{windows.map(w => (
|
|
835
872
|
<option key={w.key} value={w.key}>
|
|
@@ -845,7 +882,7 @@ function WindowUtilizationCard({
|
|
|
845
882
|
{insights === null ? (
|
|
846
883
|
<ChartSkeleton height={260} />
|
|
847
884
|
) : selected === null ? (
|
|
848
|
-
<EmptyState title="
|
|
885
|
+
<EmptyState title={t("providers.no-snapshots-in-range")} hint={SNAPSHOT_HINT} />
|
|
849
886
|
) : (
|
|
850
887
|
<QueryView query={accounts} skeleton={<ChartSkeleton height={260} />}>
|
|
851
888
|
{() =>
|
|
@@ -872,12 +909,12 @@ function WindowUtilizationCard({
|
|
|
872
909
|
format={v => formatPercent(v, 0)}
|
|
873
910
|
formatTooltip={v => formatPercent(v, 1)}
|
|
874
911
|
references={[{ value: 1, label: "100%" }]}
|
|
875
|
-
emptyLabel="
|
|
912
|
+
emptyLabel={t("providers.no-utilization-readings")}
|
|
876
913
|
tooltipExtra={i => {
|
|
877
914
|
const exhausted = chart.exhausted[i];
|
|
878
915
|
return exhausted.length > 0 ? (
|
|
879
916
|
<div className="chart-tooltip-row">
|
|
880
|
-
<span className="chart-tooltip-label tone-bad">
|
|
917
|
+
<span className="chart-tooltip-label tone-bad">{t("providers.exhausted")}</span>
|
|
881
918
|
<span className="chart-tooltip-value">{exhausted.join(", ")}</span>
|
|
882
919
|
</div>
|
|
883
920
|
) : null;
|
|
@@ -978,16 +1015,16 @@ function buildUtilization(accounts: UsageWindowSeries[], names: ReadonlyMap<stri
|
|
|
978
1015
|
}
|
|
979
1016
|
|
|
980
1017
|
function statusBadge(latest: AccountRow["latest"]): ReactNode {
|
|
981
|
-
if (!latest) return <span className="dim">
|
|
982
|
-
if (latest.exhausted) return <Badge tone="bad">
|
|
983
|
-
if (latest.fraction >= 0.8) return <Badge tone="warn">
|
|
984
|
-
return <Badge tone="ok">
|
|
1018
|
+
if (!latest) return <span className="dim">{t("providers.no-reading")}</span>;
|
|
1019
|
+
if (latest.exhausted) return <Badge tone="bad">{t("providers.exhausted")}</Badge>;
|
|
1020
|
+
if (latest.fraction >= 0.8) return <Badge tone="warn">{t("providers.high")}</Badge>;
|
|
1021
|
+
return <Badge tone="ok">{t("ui.status-ok")}</Badge>;
|
|
985
1022
|
}
|
|
986
1023
|
|
|
987
1024
|
const ACCOUNT_COLUMNS: Column<AccountRow>[] = [
|
|
988
1025
|
{
|
|
989
1026
|
key: "account",
|
|
990
|
-
header: "
|
|
1027
|
+
header: t("providers.account"),
|
|
991
1028
|
sort: r => r.name,
|
|
992
1029
|
render: r => (
|
|
993
1030
|
<span className="row" style={{ gap: 8 }}>
|
|
@@ -1000,13 +1037,13 @@ const ACCOUNT_COLUMNS: Column<AccountRow>[] = [
|
|
|
1000
1037
|
},
|
|
1001
1038
|
{
|
|
1002
1039
|
key: "window",
|
|
1003
|
-
header: "
|
|
1004
|
-
sort: r => r.series.
|
|
1005
|
-
render: r => <span className={r.color ? undefined : "muted"}>{r.series.
|
|
1040
|
+
header: t("providers.window"),
|
|
1041
|
+
sort: r => r.series.windowKey,
|
|
1042
|
+
render: r => <span className={r.color ? undefined : "muted"}>{r.series.windowKey}</span>,
|
|
1006
1043
|
},
|
|
1007
1044
|
{
|
|
1008
1045
|
key: "latest",
|
|
1009
|
-
header: "
|
|
1046
|
+
header: t("providers.latest-used"),
|
|
1010
1047
|
align: "right",
|
|
1011
1048
|
sort: r => r.latest?.fraction ?? -1,
|
|
1012
1049
|
render: r =>
|
|
@@ -1023,27 +1060,27 @@ const ACCOUNT_COLUMNS: Column<AccountRow>[] = [
|
|
|
1023
1060
|
},
|
|
1024
1061
|
{
|
|
1025
1062
|
key: "status",
|
|
1026
|
-
header: "
|
|
1063
|
+
header: t("providers.status"),
|
|
1027
1064
|
sort: r => (r.latest?.exhausted ? 2 : (r.latest?.fraction ?? 0) >= 0.8 ? 1 : 0),
|
|
1028
1065
|
render: r => statusBadge(r.latest),
|
|
1029
1066
|
},
|
|
1030
1067
|
{
|
|
1031
1068
|
key: "peak",
|
|
1032
|
-
header: "
|
|
1069
|
+
header: t("providers.peak-in-range"),
|
|
1033
1070
|
align: "right",
|
|
1034
1071
|
sort: r => r.peak ?? -1,
|
|
1035
1072
|
render: r => <span className="num">{r.peak === null ? "–" : formatPercent(r.peak, 0)}</span>,
|
|
1036
1073
|
},
|
|
1037
1074
|
{
|
|
1038
1075
|
key: "samples",
|
|
1039
|
-
header: "
|
|
1076
|
+
header: t("providers.snapshots"),
|
|
1040
1077
|
align: "right",
|
|
1041
1078
|
sort: r => r.samples,
|
|
1042
1079
|
render: r => <span className="num muted">{formatInteger(r.samples)}</span>,
|
|
1043
1080
|
},
|
|
1044
1081
|
{
|
|
1045
1082
|
key: "recorded",
|
|
1046
|
-
header: "
|
|
1083
|
+
header: t("providers.recorded"),
|
|
1047
1084
|
align: "right",
|
|
1048
1085
|
sort: r => r.latest?.timestamp ?? 0,
|
|
1049
1086
|
render: r => <span className="muted">{r.latest ? formatRelativeTime(r.latest.timestamp) : "–"}</span>,
|