@oh-my-pi/omp-stats 18.0.3 → 18.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +4 -2
- package/dist/client/index.js +84 -84
- package/dist/types/client/data/formatters.d.ts +5 -0
- package/dist/types/client/data/view-models.d.ts +1 -0
- package/dist/types/db.d.ts +3 -2
- package/dist/types/shared-types.d.ts +10 -0
- package/package.json +4 -4
- package/src/client/data/formatters.ts +13 -0
- package/src/client/data/view-models.ts +3 -0
- package/src/client/routes/CostsRoute.tsx +28 -10
- package/src/client/routes/ErrorsRoute.tsx +5 -5
- package/src/client/routes/ModelsRoute.tsx +3 -2
- package/src/client/routes/OverviewRoute.tsx +6 -6
- package/src/client/routes/ProjectsRoute.tsx +7 -5
- package/src/client/routes/ProvidersRoute.tsx +30 -5
- package/src/client/routes/RequestsRoute.tsx +5 -5
- package/src/client/routes/ToolsRoute.tsx +23 -11
- package/src/client/ui/MetricCluster.tsx +7 -3
- package/src/client/ui/RequestDrawer.tsx +3 -3
- package/src/db.ts +107 -29
- package/src/index.ts +10 -9
- package/src/shared-types.ts +10 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import type { MessageStats } from "../types.js";
|
|
1
2
|
export declare function formatInteger(value: number): string;
|
|
2
3
|
export declare function formatCompact(value: number): string;
|
|
3
4
|
export declare function formatCost(value: number, digits?: number): string;
|
|
5
|
+
/** Format an API-equivalent estimate, using N/A when all usage is unpriced. */
|
|
6
|
+
export declare function formatEstimatedCost(value: number, unpricedRequests: number, digits?: number): string;
|
|
7
|
+
/** Format one request's cost, distinguishing unpriced SuperGrok usage from free usage. */
|
|
8
|
+
export declare function formatMessageCost(message: Pick<MessageStats, "provider" | "usage">, digits?: number): string;
|
|
4
9
|
export declare function formatPercent(value: number, digits?: number): string;
|
|
5
10
|
export declare function formatDurationMs(value: number | null, digits?: number): string;
|
|
6
11
|
export declare function formatTokensPerSecond(value: number | null): string;
|
|
@@ -30,6 +30,7 @@ export interface AgentTokenShareView {
|
|
|
30
30
|
export declare function buildAgentTokenShare(stats: AgentTypeStats[]): AgentTokenShareView;
|
|
31
31
|
export interface CostSummaryView {
|
|
32
32
|
totalCost: number;
|
|
33
|
+
unpricedRequests: number;
|
|
33
34
|
avgDailyCost: number;
|
|
34
35
|
topModelName: string;
|
|
35
36
|
topModelCost: number;
|
package/dist/types/db.d.ts
CHANGED
|
@@ -27,8 +27,9 @@ export declare function setFileOffset(sessionFile: string, offset: number, lastM
|
|
|
27
27
|
* aggregate. The `WHERE NOT EXISTS` clause skips inserts whose
|
|
28
28
|
* `(entry_id, timestamp)` already exists under a different `session_file` —
|
|
29
29
|
* first-write-wins across the lineage. Same-file re-syncs still hit the
|
|
30
|
-
* `ON CONFLICT(session_file, entry_id)` upsert below
|
|
31
|
-
* `premium_requests`
|
|
30
|
+
* `ON CONFLICT(session_file, entry_id)` upsert below, which re-derives the
|
|
31
|
+
* stored cost (orchestration-aware) and keeps `premium_requests` monotonic, so
|
|
32
|
+
* a forced re-parse repairs historical `premium_requests` and cost fix-ups.
|
|
32
33
|
*/
|
|
33
34
|
export declare function insertMessageStats(stats: MessageStats[]): number;
|
|
34
35
|
/**
|
|
@@ -33,6 +33,8 @@ export interface AggregatedStats {
|
|
|
33
33
|
cacheSavings: number;
|
|
34
34
|
/** Total cost */
|
|
35
35
|
totalCost: number;
|
|
36
|
+
/** Requests with token usage but no public-equivalent subscription price. */
|
|
37
|
+
unpricedRequests: number;
|
|
36
38
|
/** Total premium requests */
|
|
37
39
|
totalPremiumRequests: number;
|
|
38
40
|
/** Average duration in ms */
|
|
@@ -115,6 +117,8 @@ export interface CostTimeSeriesPoint {
|
|
|
115
117
|
provider: string;
|
|
116
118
|
/** Total cost for this bucket */
|
|
117
119
|
cost: number;
|
|
120
|
+
/** Requests excluded because no public-equivalent subscription price exists. */
|
|
121
|
+
unpricedRequests: number;
|
|
118
122
|
/** Cost breakdown */
|
|
119
123
|
costInput: number;
|
|
120
124
|
costOutput: number;
|
|
@@ -283,6 +287,8 @@ export interface ToolUsageStats {
|
|
|
283
287
|
outputTokensShare: number;
|
|
284
288
|
/** Cost (USD) of invoking turns, attributed per call share. */
|
|
285
289
|
costShare: number;
|
|
290
|
+
/** Share of unpriced subscription requests attributed to this tool. */
|
|
291
|
+
unpricedRequestsShare: number;
|
|
286
292
|
/** Unix ms of the most recent call in range. */
|
|
287
293
|
lastUsed: number;
|
|
288
294
|
}
|
|
@@ -320,6 +326,8 @@ export interface ProviderAggregate {
|
|
|
320
326
|
/** Uncached input + cache reads + cache writes + output. */
|
|
321
327
|
totalTokens: number;
|
|
322
328
|
totalCost: number;
|
|
329
|
+
/** Requests excluded because no public-equivalent subscription price exists. */
|
|
330
|
+
unpricedRequests: number;
|
|
323
331
|
totalPremiumRequests: number;
|
|
324
332
|
avgTokensPerSecond: number | null;
|
|
325
333
|
}
|
|
@@ -341,6 +349,8 @@ export interface ProviderTimeSeriesPoint {
|
|
|
341
349
|
provider: string;
|
|
342
350
|
totalTokens: number;
|
|
343
351
|
cost: number;
|
|
352
|
+
/** Requests excluded because no public-equivalent subscription price exists. */
|
|
353
|
+
unpricedRequests: number;
|
|
344
354
|
requests: number;
|
|
345
355
|
}
|
|
346
356
|
/** One recorded usage-limit snapshot for an (account, window) series. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "18.0.
|
|
4
|
+
"version": "18.0.4",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"fmt": "biome format --write ."
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "18.0.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "18.0.
|
|
44
|
-
"@oh-my-pi/pi-utils": "18.0.
|
|
42
|
+
"@oh-my-pi/pi-ai": "18.0.4",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "18.0.4",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.0.4",
|
|
45
45
|
"@tailwindcss/node": "^4.3.2",
|
|
46
46
|
"chart.js": "^4.5.1",
|
|
47
47
|
"lucide-react": "^1.24.0",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatDistanceToNow } from "@oh-my-pi/pi-utils/dates";
|
|
2
|
+
import type { MessageStats } from "../types";
|
|
2
3
|
|
|
3
4
|
export function formatInteger(value: number): string {
|
|
4
5
|
return value.toLocaleString();
|
|
@@ -17,6 +18,18 @@ export function formatCost(value: number, digits?: number): string {
|
|
|
17
18
|
})}`;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/** Format an API-equivalent estimate, using N/A when all usage is unpriced. */
|
|
22
|
+
export function formatEstimatedCost(value: number, unpricedRequests: number, digits?: number): string {
|
|
23
|
+
return value === 0 && unpricedRequests > 0 ? "N/A" : formatCost(value, digits);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Format one request's cost, distinguishing unpriced SuperGrok usage from free usage. */
|
|
27
|
+
export function formatMessageCost(message: Pick<MessageStats, "provider" | "usage">, digits?: number): string {
|
|
28
|
+
const unpricedRequests =
|
|
29
|
+
message.provider === "xai-oauth" && message.usage.totalTokens > 0 && message.usage.cost.total === 0 ? 1 : 0;
|
|
30
|
+
return formatEstimatedCost(message.usage.cost.total, unpricedRequests, digits);
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
export function formatPercent(value: number, digits = 1): string {
|
|
21
34
|
return `${(value * 100).toFixed(digits)}%`;
|
|
22
35
|
}
|
|
@@ -74,6 +74,7 @@ export function buildAgentTokenShare(stats: AgentTypeStats[]): AgentTokenShareVi
|
|
|
74
74
|
|
|
75
75
|
export interface CostSummaryView {
|
|
76
76
|
totalCost: number;
|
|
77
|
+
unpricedRequests: number;
|
|
77
78
|
avgDailyCost: number;
|
|
78
79
|
topModelName: string;
|
|
79
80
|
topModelCost: number;
|
|
@@ -111,6 +112,7 @@ export interface FolderRowView extends FolderStats {
|
|
|
111
112
|
|
|
112
113
|
export function buildCostSummary(costSeries: CostTimeSeriesPoint[]): CostSummaryView {
|
|
113
114
|
const totalCost = costSeries.reduce((sum, p) => sum + p.cost, 0);
|
|
115
|
+
const unpricedRequests = costSeries.reduce((sum, point) => sum + point.unpricedRequests, 0);
|
|
114
116
|
const dayBuckets = new Set(costSeries.map(p => p.timestamp)).size;
|
|
115
117
|
const avgDailyCost = dayBuckets > 0 ? totalCost / dayBuckets : 0;
|
|
116
118
|
|
|
@@ -130,6 +132,7 @@ export function buildCostSummary(costSeries: CostTimeSeriesPoint[]): CostSummary
|
|
|
130
132
|
|
|
131
133
|
return {
|
|
132
134
|
totalCost,
|
|
135
|
+
unpricedRequests,
|
|
133
136
|
avgDailyCost,
|
|
134
137
|
topModelName,
|
|
135
138
|
topModelCost,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
MODEL_COLORS,
|
|
14
14
|
styleDatasets,
|
|
15
15
|
} from "../components/chart-shared";
|
|
16
|
-
import { formatCost } from "../data/formatters";
|
|
16
|
+
import { formatCost, formatEstimatedCost } from "../data/formatters";
|
|
17
17
|
import { useResource } from "../data/useResource";
|
|
18
18
|
import { buildCostSummary } from "../data/view-models";
|
|
19
19
|
import type { CostTimeSeriesPoint, TimeRange } from "../types";
|
|
@@ -54,12 +54,22 @@ function CostOverviewPanel({ costSeries }: { costSeries: CostTimeSeriesPoint[] }
|
|
|
54
54
|
const summary = useMemo(() => buildCostSummary(costSeries), [costSeries]);
|
|
55
55
|
|
|
56
56
|
const cards = [
|
|
57
|
-
{
|
|
58
|
-
|
|
57
|
+
{
|
|
58
|
+
label: "API-equivalent estimate",
|
|
59
|
+
value: formatEstimatedCost(summary.totalCost, summary.unpricedRequests),
|
|
60
|
+
sub:
|
|
61
|
+
summary.unpricedRequests > 0
|
|
62
|
+
? `Excludes ${summary.unpricedRequests.toLocaleString()} unpriced subscription request${summary.unpricedRequests === 1 ? "" : "s"}`
|
|
63
|
+
: undefined,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: "Average estimate / Day",
|
|
67
|
+
value: formatEstimatedCost(summary.avgDailyCost, summary.unpricedRequests),
|
|
68
|
+
},
|
|
59
69
|
{
|
|
60
70
|
label: "Top Model",
|
|
61
71
|
value: summary.topModelName || "—",
|
|
62
|
-
sub: summary.topModelName ? formatCost(summary.topModelCost) : undefined,
|
|
72
|
+
sub: summary.topModelName ? `API-equivalent estimate: ${formatCost(summary.topModelCost)}` : undefined,
|
|
63
73
|
},
|
|
64
74
|
];
|
|
65
75
|
|
|
@@ -71,7 +81,7 @@ function CostOverviewPanel({ costSeries }: { costSeries: CostTimeSeriesPoint[] }
|
|
|
71
81
|
<p className="text-2xl font-bold stats-text-primary truncate" title={card.value}>
|
|
72
82
|
{card.value}
|
|
73
83
|
</p>
|
|
74
|
-
{card.sub && <p className="text-xs stats-text-muted mt-1 font-medium">
|
|
84
|
+
{card.sub && <p className="text-xs stats-text-muted mt-1 font-medium">{card.sub}</p>}
|
|
75
85
|
</Panel>
|
|
76
86
|
))}
|
|
77
87
|
</div>
|
|
@@ -118,6 +128,10 @@ function CostTrendPanel({ costSeries }: { costSeries: CostTimeSeriesPoint[] }) {
|
|
|
118
128
|
const [byModel, setByModel] = useState(false);
|
|
119
129
|
const theme = useSystemTheme();
|
|
120
130
|
const chartTheme = CHART_THEMES[theme];
|
|
131
|
+
const unpricedRequests = useMemo(
|
|
132
|
+
() => costSeries.reduce((sum, point) => sum + point.unpricedRequests, 0),
|
|
133
|
+
[costSeries],
|
|
134
|
+
);
|
|
121
135
|
|
|
122
136
|
const chartData = useMemo(() => {
|
|
123
137
|
if (byModel) {
|
|
@@ -130,7 +144,7 @@ function CostTrendPanel({ costSeries }: { costSeries: CostTimeSeriesPoint[] }) {
|
|
|
130
144
|
bucketToValue: bucket => bucket.total,
|
|
131
145
|
});
|
|
132
146
|
}
|
|
133
|
-
return buildAggregateTimeSeries<CostTimeSeriesPoint, { total: number }>(costSeries, "
|
|
147
|
+
return buildAggregateTimeSeries<CostTimeSeriesPoint, { total: number }>(costSeries, "API-equivalent estimate", {
|
|
134
148
|
initBucket: () => ({ total: 0 }),
|
|
135
149
|
accumulate: (bucket, point) => {
|
|
136
150
|
bucket.total += point.cost;
|
|
@@ -143,7 +157,7 @@ function CostTrendPanel({ costSeries }: { costSeries: CostTimeSeriesPoint[] }) {
|
|
|
143
157
|
return buildSharedPlugins({
|
|
144
158
|
chartTheme,
|
|
145
159
|
showLegend: byModel,
|
|
146
|
-
defaultLabel: "
|
|
160
|
+
defaultLabel: "API-equivalent estimate",
|
|
147
161
|
formatValue: v => `$${v.toFixed(2)}`,
|
|
148
162
|
footer: items => {
|
|
149
163
|
if (!byModel || items.length < 2) return undefined;
|
|
@@ -214,14 +228,18 @@ function CostTrendPanel({ costSeries }: { costSeries: CostTimeSeriesPoint[] }) {
|
|
|
214
228
|
|
|
215
229
|
return (
|
|
216
230
|
<Panel
|
|
217
|
-
title="Daily
|
|
218
|
-
subtitle=
|
|
231
|
+
title="Daily API-equivalent estimate"
|
|
232
|
+
subtitle={
|
|
233
|
+
unpricedRequests > 0
|
|
234
|
+
? `Public API rate-card value over time; excludes ${unpricedRequests.toLocaleString()} unpriced subscription request${unpricedRequests === 1 ? "" : "s"}`
|
|
235
|
+
: "Public API rate-card value over time"
|
|
236
|
+
}
|
|
219
237
|
actions={<SegmentedControl options={toggleOptions} value={byModel} onChange={setByModel} />}
|
|
220
238
|
>
|
|
221
239
|
<div className="h-[300px]">
|
|
222
240
|
{chartData.labels.length === 0 ? (
|
|
223
241
|
<div className="h-full flex items-center justify-center text-stats-muted text-sm">
|
|
224
|
-
No
|
|
242
|
+
No API-equivalent estimate data available
|
|
225
243
|
</div>
|
|
226
244
|
) : byModel && lineData ? (
|
|
227
245
|
<Line data={lineData} options={lineOptions} />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { getRecentErrors } from "../api";
|
|
3
|
-
import {
|
|
3
|
+
import { formatInteger, formatMessageCost, formatRelativeTime } from "../data/formatters";
|
|
4
4
|
import { useResource } from "../data/useResource";
|
|
5
5
|
import type { MessageStats, TimeRange } from "../types";
|
|
6
6
|
import { AsyncBoundary, DataTable, Panel, StatusPill } from "../ui";
|
|
@@ -59,9 +59,9 @@ export function ErrorsRoute({ active, range, refreshTrigger, onRequestClick }: E
|
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
key: "cost",
|
|
62
|
-
header: "
|
|
62
|
+
header: "API-equivalent estimate",
|
|
63
63
|
numeric: true,
|
|
64
|
-
render: (item: MessageStats) =>
|
|
64
|
+
render: (item: MessageStats) => formatMessageCost(item, 4),
|
|
65
65
|
},
|
|
66
66
|
],
|
|
67
67
|
[],
|
|
@@ -82,8 +82,8 @@ export function ErrorsRoute({ active, range, refreshTrigger, onRequestClick }: E
|
|
|
82
82
|
<div className="stats-mobile-card-value">{formatRelativeTime(item.timestamp)}</div>
|
|
83
83
|
</div>
|
|
84
84
|
<div>
|
|
85
|
-
<div className="stats-mobile-card-label">
|
|
86
|
-
<div className="stats-mobile-card-value">{
|
|
85
|
+
<div className="stats-mobile-card-label">API-equivalent estimate</div>
|
|
86
|
+
<div className="stats-mobile-card-value">{formatMessageCost(item, 4)}</div>
|
|
87
87
|
</div>
|
|
88
88
|
<div>
|
|
89
89
|
<div className="stats-mobile-card-label">Tokens</div>
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
TrendEmpty,
|
|
19
19
|
} from "../components/models-table-shared";
|
|
20
20
|
import { formatRangeTick, rangeMeta } from "../components/range-meta";
|
|
21
|
+
import { formatEstimatedCost } from "../data/formatters";
|
|
21
22
|
import { useResource } from "../data/useResource";
|
|
22
23
|
import { buildModelPerformanceLookup } from "../data/view-models";
|
|
23
24
|
import type { ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeRange } from "../types";
|
|
@@ -266,7 +267,7 @@ function ModelsTable({
|
|
|
266
267
|
columns={[
|
|
267
268
|
{ label: "Model" },
|
|
268
269
|
{ label: "Requests", align: "right" },
|
|
269
|
-
{ label: "
|
|
270
|
+
{ label: "API-equivalent estimate", align: "right" },
|
|
270
271
|
{ label: "Tokens", align: "right" },
|
|
271
272
|
{ label: "Tokens/s", align: "right" },
|
|
272
273
|
{ label: "TTFT", align: "right" },
|
|
@@ -295,7 +296,7 @@ function ModelsTable({
|
|
|
295
296
|
{model.totalRequests.toLocaleString()}
|
|
296
297
|
</div>,
|
|
297
298
|
<div key="cost" className="text-right text-[var(--text-secondary)] font-mono text-sm">
|
|
298
|
-
|
|
299
|
+
{formatEstimatedCost(model.totalCost, model.unpricedRequests)}
|
|
299
300
|
</div>,
|
|
300
301
|
<div key="tokens" className="text-right text-[var(--text-secondary)] font-mono text-sm">
|
|
301
302
|
{(model.totalInputTokens + model.totalOutputTokens).toLocaleString()}
|
|
@@ -4,7 +4,7 @@ import { Line } from "react-chartjs-2";
|
|
|
4
4
|
import { getOverviewStats, getRecentRequests } from "../api";
|
|
5
5
|
import { AgentTokenShare } from "../components/AgentTokenShare";
|
|
6
6
|
import { CHART_THEMES } from "../components/chart-shared";
|
|
7
|
-
import {
|
|
7
|
+
import { formatDurationMs, formatInteger, formatMessageCost, formatRelativeTime } from "../data/formatters";
|
|
8
8
|
import { useResource } from "../data/useResource";
|
|
9
9
|
import type { MessageStats, TimeRange } from "../types";
|
|
10
10
|
import { AsyncBoundary, DataTable, MetricCluster, Panel, Skeleton, StatusPill } from "../ui";
|
|
@@ -157,9 +157,9 @@ export function OverviewRoute({ active, range, refreshTrigger, onRequestClick }:
|
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
key: "cost",
|
|
160
|
-
header: "
|
|
160
|
+
header: "API-equivalent estimate",
|
|
161
161
|
numeric: true,
|
|
162
|
-
render: (item: MessageStats) =>
|
|
162
|
+
render: (item: MessageStats) => formatMessageCost(item, 4),
|
|
163
163
|
},
|
|
164
164
|
{
|
|
165
165
|
key: "duration",
|
|
@@ -198,8 +198,8 @@ export function OverviewRoute({ active, range, refreshTrigger, onRequestClick }:
|
|
|
198
198
|
<div className="stats-mobile-card-value">{formatRelativeTime(item.timestamp)}</div>
|
|
199
199
|
</div>
|
|
200
200
|
<div>
|
|
201
|
-
<div className="stats-mobile-card-label">
|
|
202
|
-
<div className="stats-mobile-card-value">{
|
|
201
|
+
<div className="stats-mobile-card-label">API-equivalent estimate</div>
|
|
202
|
+
<div className="stats-mobile-card-value">{formatMessageCost(item, 4)}</div>
|
|
203
203
|
</div>
|
|
204
204
|
<div>
|
|
205
205
|
<div className="stats-mobile-card-label">Tokens</div>
|
|
@@ -298,7 +298,7 @@ export function OverviewRoute({ active, range, refreshTrigger, onRequestClick }:
|
|
|
298
298
|
<div>{req.provider}</div>
|
|
299
299
|
<div>
|
|
300
300
|
{req.duration ? formatDurationMs(req.duration) : ""}{" "}
|
|
301
|
-
{req.usage
|
|
301
|
+
{req.usage.totalTokens > 0 ? `· ${formatMessageCost(req, 4)}` : ""}
|
|
302
302
|
</div>
|
|
303
303
|
</div>
|
|
304
304
|
{isError && (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { getFolderStats } from "../api";
|
|
3
|
-
import {
|
|
3
|
+
import { formatDurationMs, formatEstimatedCost, formatInteger, formatPercent } from "../data/formatters";
|
|
4
4
|
import { useResource } from "../data/useResource";
|
|
5
5
|
import { buildFolderRows, type FolderRowView } from "../data/view-models";
|
|
6
6
|
import type { TimeRange } from "../types";
|
|
@@ -60,11 +60,11 @@ export function ProjectsRoute({ active, range, refreshTrigger }: ProjectsRoutePr
|
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
key: "totalCost",
|
|
63
|
-
header: "
|
|
63
|
+
header: "API-equivalent estimate",
|
|
64
64
|
numeric: true,
|
|
65
65
|
render: (item: FolderRowView) => (
|
|
66
66
|
<div className="stats-text-right">
|
|
67
|
-
<div className="font-mono">{
|
|
67
|
+
<div className="font-mono">{formatEstimatedCost(item.totalCost, item.unpricedRequests)}</div>
|
|
68
68
|
<div className="stats-progress-bar-track mt-1 ml-auto w-24 h-1">
|
|
69
69
|
<div
|
|
70
70
|
className="stats-progress-bar-fill"
|
|
@@ -133,8 +133,10 @@ export function ProjectsRoute({ active, range, refreshTrigger }: ProjectsRoutePr
|
|
|
133
133
|
<div className="stats-mobile-card-value font-mono">{formatInteger(item.totalRequests)}</div>
|
|
134
134
|
</div>
|
|
135
135
|
<div>
|
|
136
|
-
<div className="stats-mobile-card-label">
|
|
137
|
-
<div className="stats-mobile-card-value font-mono">
|
|
136
|
+
<div className="stats-mobile-card-label">API-equivalent estimate</div>
|
|
137
|
+
<div className="stats-mobile-card-value font-mono">
|
|
138
|
+
{formatEstimatedCost(item.totalCost, item.unpricedRequests)}
|
|
139
|
+
</div>
|
|
138
140
|
</div>
|
|
139
141
|
<div>
|
|
140
142
|
<div className="stats-mobile-card-label">Cache Rate</div>
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
formatCompact,
|
|
15
15
|
formatCost,
|
|
16
|
+
formatEstimatedCost,
|
|
16
17
|
formatInteger,
|
|
17
18
|
formatPercent,
|
|
18
19
|
formatRelativeTime,
|
|
@@ -69,6 +70,10 @@ export function ProvidersRoute({ active, range, refreshTrigger }: ProvidersRoute
|
|
|
69
70
|
|
|
70
71
|
function ProviderTotalsPanel({ providers }: { providers: ProviderAggregate[] }) {
|
|
71
72
|
const grandTotal = useMemo(() => providers.reduce((sum, p) => sum + p.totalTokens, 0), [providers]);
|
|
73
|
+
const unpricedRequests = useMemo(
|
|
74
|
+
() => providers.reduce((sum, provider) => sum + provider.unpricedRequests, 0),
|
|
75
|
+
[providers],
|
|
76
|
+
);
|
|
72
77
|
|
|
73
78
|
const columns: DataTableColumn<ProviderAggregate>[] = [
|
|
74
79
|
{ key: "provider", header: "Provider", render: p => <span className="font-medium">{p.provider}</span> },
|
|
@@ -98,12 +103,24 @@ function ProviderTotalsPanel({ providers }: { providers: ProviderAggregate[] })
|
|
|
98
103
|
numeric: true,
|
|
99
104
|
render: p => formatPercent(grandTotal > 0 ? p.totalTokens / grandTotal : 0),
|
|
100
105
|
},
|
|
101
|
-
{
|
|
106
|
+
{
|
|
107
|
+
key: "cost",
|
|
108
|
+
header: "API-equivalent estimate",
|
|
109
|
+
numeric: true,
|
|
110
|
+
render: provider => formatEstimatedCost(provider.totalCost, provider.unpricedRequests),
|
|
111
|
+
},
|
|
102
112
|
{ key: "tps", header: "Tok/s", numeric: true, render: p => formatTokensPerSecond(p.avgTokensPerSecond) },
|
|
103
113
|
];
|
|
104
114
|
|
|
105
115
|
return (
|
|
106
|
-
<Panel
|
|
116
|
+
<Panel
|
|
117
|
+
title="Provider Totals"
|
|
118
|
+
subtitle={
|
|
119
|
+
unpricedRequests > 0
|
|
120
|
+
? `Token, request, and API-equivalent estimates; excludes ${unpricedRequests.toLocaleString()} unpriced subscription request${unpricedRequests === 1 ? "" : "s"}`
|
|
121
|
+
: "Token, request, and API-equivalent estimates over the active range"
|
|
122
|
+
}
|
|
123
|
+
>
|
|
107
124
|
<DataTable
|
|
108
125
|
columns={columns}
|
|
109
126
|
data={providers}
|
|
@@ -122,6 +139,10 @@ function ProviderTrendPanel({ stats }: { stats: ProviderDashboardStats }) {
|
|
|
122
139
|
const [metric, setMetric] = useState<"tokens" | "cost">("tokens");
|
|
123
140
|
const theme = useSystemTheme();
|
|
124
141
|
const chartTheme = CHART_THEMES[theme];
|
|
142
|
+
const unpricedRequests = useMemo(
|
|
143
|
+
() => stats.providers.reduce((sum, provider) => sum + provider.unpricedRequests, 0),
|
|
144
|
+
[stats.providers],
|
|
145
|
+
);
|
|
125
146
|
|
|
126
147
|
// buildTopNByModelSeries keys on `model`; feed it the provider name so we
|
|
127
148
|
// get the same top-N + "Other" rollup without a parallel implementation.
|
|
@@ -148,7 +169,7 @@ function ProviderTrendPanel({ stats }: { stats: ProviderDashboardStats }) {
|
|
|
148
169
|
plugins: buildSharedPlugins({
|
|
149
170
|
chartTheme,
|
|
150
171
|
showLegend: true,
|
|
151
|
-
defaultLabel: metric === "tokens" ? "Tokens" : "
|
|
172
|
+
defaultLabel: metric === "tokens" ? "Tokens" : "API-equivalent estimate",
|
|
152
173
|
formatValue,
|
|
153
174
|
footer: items => {
|
|
154
175
|
if (items.length < 2) return undefined;
|
|
@@ -174,12 +195,16 @@ function ProviderTrendPanel({ stats }: { stats: ProviderDashboardStats }) {
|
|
|
174
195
|
return (
|
|
175
196
|
<Panel
|
|
176
197
|
title="Burn by Provider"
|
|
177
|
-
subtitle=
|
|
198
|
+
subtitle={
|
|
199
|
+
metric === "cost" && unpricedRequests > 0
|
|
200
|
+
? `API-equivalent estimates over time; excludes ${unpricedRequests.toLocaleString()} unpriced subscription request${unpricedRequests === 1 ? "" : "s"}`
|
|
201
|
+
: "Stacked token or API-equivalent estimate burn over time"
|
|
202
|
+
}
|
|
178
203
|
actions={
|
|
179
204
|
<SegmentedControl
|
|
180
205
|
options={[
|
|
181
206
|
{ value: "tokens" as const, label: "Tokens" },
|
|
182
|
-
{ value: "cost" as const, label: "
|
|
207
|
+
{ value: "cost" as const, label: "API-equivalent estimate" },
|
|
183
208
|
]}
|
|
184
209
|
value={metric}
|
|
185
210
|
onChange={setMetric}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { getRecentRequests } from "../api";
|
|
3
|
-
import {
|
|
3
|
+
import { formatDurationMs, formatInteger, formatMessageCost, formatRelativeTime } from "../data/formatters";
|
|
4
4
|
import { useResource } from "../data/useResource";
|
|
5
5
|
import type { MessageStats, TimeRange } from "../types";
|
|
6
6
|
import { AsyncBoundary, DataTable, Panel, StatusPill } from "../ui";
|
|
@@ -47,9 +47,9 @@ export function RequestsRoute({ active, refreshTrigger, onRequestClick }: Reques
|
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
key: "cost",
|
|
50
|
-
header: "
|
|
50
|
+
header: "API-equivalent estimate",
|
|
51
51
|
numeric: true,
|
|
52
|
-
render: (item: MessageStats) =>
|
|
52
|
+
render: (item: MessageStats) => formatMessageCost(item, 4),
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
key: "duration",
|
|
@@ -88,8 +88,8 @@ export function RequestsRoute({ active, refreshTrigger, onRequestClick }: Reques
|
|
|
88
88
|
<div className="stats-mobile-card-value">{formatRelativeTime(item.timestamp)}</div>
|
|
89
89
|
</div>
|
|
90
90
|
<div>
|
|
91
|
-
<div className="stats-mobile-card-label">
|
|
92
|
-
<div className="stats-mobile-card-value">{
|
|
91
|
+
<div className="stats-mobile-card-label">API-equivalent estimate</div>
|
|
92
|
+
<div className="stats-mobile-card-value">{formatMessageCost(item, 4)}</div>
|
|
93
93
|
</div>
|
|
94
94
|
<div>
|
|
95
95
|
<div className="stats-mobile-card-label">Tokens</div>
|
|
@@ -3,7 +3,13 @@ import { Line } from "react-chartjs-2";
|
|
|
3
3
|
import { getToolDashboardStats } from "../api";
|
|
4
4
|
import { CHART_THEMES, MODEL_COLORS } from "../components/chart-shared";
|
|
5
5
|
import { formatRangeTick, rangeMeta } from "../components/range-meta";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
formatCompact,
|
|
8
|
+
formatEstimatedCost,
|
|
9
|
+
formatInteger,
|
|
10
|
+
formatPercent,
|
|
11
|
+
formatRelativeTime,
|
|
12
|
+
} from "../data/formatters";
|
|
7
13
|
import { useResource } from "../data/useResource";
|
|
8
14
|
import { buildToolRows, type ToolRowView } from "../data/view-models";
|
|
9
15
|
import type { TimeRange, ToolModelStats, ToolTimeSeriesPoint, ToolUsageStats } from "../types";
|
|
@@ -53,6 +59,7 @@ function ToolsSummaryPanel({ byTool }: { byTool: ToolUsageStats[] }) {
|
|
|
53
59
|
let tokens = 0;
|
|
54
60
|
let output = 0;
|
|
55
61
|
let cost = 0;
|
|
62
|
+
let unpricedRequests = 0;
|
|
56
63
|
let resultChars = 0;
|
|
57
64
|
let argsChars = 0;
|
|
58
65
|
for (const t of byTool) {
|
|
@@ -61,16 +68,17 @@ function ToolsSummaryPanel({ byTool }: { byTool: ToolUsageStats[] }) {
|
|
|
61
68
|
tokens += t.totalTokensShare;
|
|
62
69
|
output += t.outputTokensShare;
|
|
63
70
|
cost += t.costShare;
|
|
71
|
+
unpricedRequests += t.unpricedRequestsShare;
|
|
64
72
|
resultChars += t.resultChars;
|
|
65
73
|
argsChars += t.argsChars;
|
|
66
74
|
}
|
|
67
|
-
return { calls, errors, tokens, output, cost, resultChars, argsChars, tools: byTool.length };
|
|
75
|
+
return { calls, errors, tokens, output, cost, unpricedRequests, resultChars, argsChars, tools: byTool.length };
|
|
68
76
|
}, [byTool]);
|
|
69
77
|
|
|
70
78
|
return (
|
|
71
79
|
<Panel
|
|
72
80
|
title="Tool Usage"
|
|
73
|
-
subtitle="Tokens
|
|
81
|
+
subtitle="Tokens and API-equivalent estimates are split from invoking turns across each turn's tool calls"
|
|
74
82
|
>
|
|
75
83
|
<div className="stats-metric-cluster">
|
|
76
84
|
<div className="stats-metric-primary-grid">
|
|
@@ -89,8 +97,8 @@ function ToolsSummaryPanel({ byTool }: { byTool: ToolUsageStats[] }) {
|
|
|
89
97
|
</div>
|
|
90
98
|
</div>
|
|
91
99
|
<div className="stats-metric-card primary">
|
|
92
|
-
<div className="stats-metric-label">Attributed
|
|
93
|
-
<div className="stats-metric-value">{
|
|
100
|
+
<div className="stats-metric-label">Attributed API-equivalent estimate</div>
|
|
101
|
+
<div className="stats-metric-value">{formatEstimatedCost(totals.cost, totals.unpricedRequests)}</div>
|
|
94
102
|
</div>
|
|
95
103
|
</div>
|
|
96
104
|
|
|
@@ -292,9 +300,11 @@ function ToolsTable({ byTool }: { byTool: ToolUsageStats[] }) {
|
|
|
292
300
|
},
|
|
293
301
|
{
|
|
294
302
|
key: "cost",
|
|
295
|
-
header: "Attr.
|
|
303
|
+
header: "Attr. API-equivalent estimate",
|
|
296
304
|
numeric: true,
|
|
297
|
-
render: (item: ToolRowView) =>
|
|
305
|
+
render: (item: ToolRowView) => (
|
|
306
|
+
<span className="font-mono">{formatEstimatedCost(item.costShare, item.unpricedRequestsShare)}</span>
|
|
307
|
+
),
|
|
298
308
|
},
|
|
299
309
|
{
|
|
300
310
|
key: "resultChars",
|
|
@@ -336,8 +346,10 @@ function ToolsTable({ byTool }: { byTool: ToolUsageStats[] }) {
|
|
|
336
346
|
</div>
|
|
337
347
|
</div>
|
|
338
348
|
<div>
|
|
339
|
-
<div className="stats-mobile-card-label">Attr.
|
|
340
|
-
<div className="stats-mobile-card-value font-mono">
|
|
349
|
+
<div className="stats-mobile-card-label">Attr. API-equivalent estimate</div>
|
|
350
|
+
<div className="stats-mobile-card-value font-mono">
|
|
351
|
+
{formatEstimatedCost(item.costShare, item.unpricedRequestsShare)}
|
|
352
|
+
</div>
|
|
341
353
|
</div>
|
|
342
354
|
<div>
|
|
343
355
|
<div className="stats-mobile-card-label">Result Text</div>
|
|
@@ -422,10 +434,10 @@ function ToolModelPanel({ byToolModel }: { byToolModel: ToolModelStats[] }) {
|
|
|
422
434
|
},
|
|
423
435
|
{
|
|
424
436
|
key: "cost",
|
|
425
|
-
header: "Attr.
|
|
437
|
+
header: "Attr. API-equivalent estimate",
|
|
426
438
|
numeric: true,
|
|
427
439
|
render: (item: ToolModelStats & { errorRate: number }) => (
|
|
428
|
-
<span className="font-mono">{
|
|
440
|
+
<span className="font-mono">{formatEstimatedCost(item.costShare, item.unpricedRequestsShare)}</span>
|
|
429
441
|
),
|
|
430
442
|
},
|
|
431
443
|
],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
formatCompact,
|
|
3
|
-
formatCost,
|
|
4
3
|
formatDurationMs,
|
|
4
|
+
formatEstimatedCost,
|
|
5
5
|
formatInteger,
|
|
6
6
|
formatPercent,
|
|
7
7
|
formatTokensPerSecond,
|
|
@@ -20,9 +20,13 @@ export function MetricCluster({ stats }: MetricClusterProps) {
|
|
|
20
20
|
<div className="stats-metric-cluster">
|
|
21
21
|
<div className="stats-metric-primary-grid">
|
|
22
22
|
<div className="stats-metric-card primary">
|
|
23
|
-
<div className="stats-metric-label">
|
|
23
|
+
<div className="stats-metric-label">API-equivalent estimate</div>
|
|
24
24
|
<div className="stats-metric-value">
|
|
25
|
-
{
|
|
25
|
+
{formatEstimatedCost(
|
|
26
|
+
stats.totalCost,
|
|
27
|
+
stats.unpricedRequests,
|
|
28
|
+
stats.totalCost > 0 && stats.totalCost < 0.01 ? 4 : 2,
|
|
29
|
+
)}
|
|
26
30
|
</div>
|
|
27
31
|
</div>
|
|
28
32
|
<div className="stats-metric-card primary">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Clock, Coins, Gauge, Hash, Star, X, Zap } from "lucide-react";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { getRequestDetails } from "../api";
|
|
4
|
-
import {
|
|
4
|
+
import { formatDurationMs, formatInteger, formatMessageCost } from "../data/formatters";
|
|
5
5
|
import type { RequestDetails } from "../types";
|
|
6
6
|
import { JsonBlock } from "./JsonBlock";
|
|
7
7
|
import { Skeleton } from "./Skeleton";
|
|
@@ -138,9 +138,9 @@ export function RequestDrawer({ id, onClose }: RequestDrawerProps) {
|
|
|
138
138
|
<div className="stats-drawer-metric-card">
|
|
139
139
|
<div className="stats-drawer-metric-label">
|
|
140
140
|
<Coins size={14} className="stats-drawer-metric-icon" />
|
|
141
|
-
|
|
141
|
+
API-equivalent estimate
|
|
142
142
|
</div>
|
|
143
|
-
<div className="stats-drawer-metric-value">{
|
|
143
|
+
<div className="stats-drawer-metric-value">{formatMessageCost(details, 4)}</div>
|
|
144
144
|
</div>
|
|
145
145
|
|
|
146
146
|
<div className="stats-drawer-metric-card">
|