@oh-my-pi/omp-stats 18.0.1 → 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.
@@ -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;
@@ -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 so historical
31
- * `premium_requests` fix-ups continue to work.
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.1",
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.1",
43
- "@oh-my-pi/pi-catalog": "18.0.1",
44
- "@oh-my-pi/pi-utils": "18.0.1",
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
- { label: "Total Cost", value: formatCost(summary.totalCost) },
58
- { label: "Average / Day", value: formatCost(summary.avgDailyCost) },
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">Total spent: {card.sub}</p>}
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, "Cost", {
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: "Cost",
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 Cost"
218
- subtitle="API spending over time"
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 cost data available
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 { formatCost, formatInteger, formatRelativeTime } from "../data/formatters";
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: "Cost",
62
+ header: "API-equivalent estimate",
63
63
  numeric: true,
64
- render: (item: MessageStats) => formatCost(item.usage.cost.total, 4),
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">Cost</div>
86
- <div className="stats-mobile-card-value">{formatCost(item.usage.cost.total, 4)}</div>
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: "Cost", align: "right" },
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
- ${model.totalCost.toFixed(2)}
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 { formatCost, formatDurationMs, formatInteger, formatRelativeTime } from "../data/formatters";
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: "Cost",
160
+ header: "API-equivalent estimate",
161
161
  numeric: true,
162
- render: (item: MessageStats) => formatCost(item.usage.cost.total, 4),
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">Cost</div>
202
- <div className="stats-mobile-card-value">{formatCost(item.usage.cost.total, 4)}</div>
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?.cost?.total ? `· ${formatCost(req.usage.cost.total, 4)}` : ""}
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 { formatCost, formatDurationMs, formatInteger, formatPercent } from "../data/formatters";
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: "Cost",
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">{formatCost(item.totalCost)}</div>
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">Cost</div>
137
- <div className="stats-mobile-card-value font-mono">{formatCost(item.totalCost)}</div>
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
- { key: "cost", header: "Cost", numeric: true, render: p => formatCost(p.totalCost) },
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 title="Provider Totals" subtitle="Token, request, and cost totals per provider over the active range">
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" : "Cost",
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="Stacked token/cost burn per provider over time"
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: "Cost" },
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 { formatCost, formatDurationMs, formatInteger, formatRelativeTime } from "../data/formatters";
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: "Cost",
50
+ header: "API-equivalent estimate",
51
51
  numeric: true,
52
- render: (item: MessageStats) => formatCost(item.usage.cost.total, 4),
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">Cost</div>
92
- <div className="stats-mobile-card-value">{formatCost(item.usage.cost.total, 4)}</div>
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 { formatCompact, formatCost, formatInteger, formatPercent, formatRelativeTime } from "../data/formatters";
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/cost are the invoking turns' real provider usage, split across each turn's tool calls"
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 Cost</div>
93
- <div className="stats-metric-value">{formatCost(totals.cost)}</div>
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. Cost",
303
+ header: "Attr. API-equivalent estimate",
296
304
  numeric: true,
297
- render: (item: ToolRowView) => <span className="font-mono">{formatCost(item.costShare)}</span>,
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. Cost</div>
340
- <div className="stats-mobile-card-value font-mono">{formatCost(item.costShare)}</div>
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. Cost",
437
+ header: "Attr. API-equivalent estimate",
426
438
  numeric: true,
427
439
  render: (item: ToolModelStats & { errorRate: number }) => (
428
- <span className="font-mono">{formatCost(item.costShare)}</span>
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">Total Cost</div>
23
+ <div className="stats-metric-label">API-equivalent estimate</div>
24
24
  <div className="stats-metric-value">
25
- {formatCost(stats.totalCost, stats.totalCost > 0 && stats.totalCost < 0.01 ? 4 : 2)}
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 { formatCost, formatDurationMs, formatInteger } from "../data/formatters";
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
- Cost
141
+ API-equivalent estimate
142
142
  </div>
143
- <div className="stats-drawer-metric-value">{formatCost(details.usage.cost.total, 4)}</div>
143
+ <div className="stats-drawer-metric-value">{formatMessageCost(details, 4)}</div>
144
144
  </div>
145
145
 
146
146
  <div className="stats-drawer-metric-card">