@linxiraos/pi-stats 1.0.9 → 1.1.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +2 -278
  2. package/LICENSE +22 -0
  3. package/README.md +4 -2
  4. package/package.json +91 -93
  5. package/src/aggregator.ts +16 -4
  6. package/src/client/app/NavRail.tsx +1 -1
  7. package/src/client/data/formatters.ts +13 -0
  8. package/src/client/data/view-models.ts +3 -0
  9. package/src/client/routes/CostsRoute.tsx +28 -10
  10. package/src/client/routes/ErrorsRoute.tsx +5 -5
  11. package/src/client/routes/ModelsRoute.tsx +3 -2
  12. package/src/client/routes/OverviewRoute.tsx +6 -6
  13. package/src/client/routes/ProjectsRoute.tsx +7 -5
  14. package/src/client/routes/ProvidersRoute.tsx +30 -5
  15. package/src/client/routes/RequestsRoute.tsx +5 -5
  16. package/src/client/routes/ToolsRoute.tsx +23 -11
  17. package/src/client/ui/MetricCluster.tsx +7 -3
  18. package/src/client/ui/RequestDrawer.tsx +3 -3
  19. package/src/db.ts +127 -35
  20. package/src/embedded-client.generated.txt +0 -1
  21. package/src/index.ts +43 -19
  22. package/src/port-conflict.ts +13 -8
  23. package/src/server.ts +26 -14
  24. package/src/shared-types.ts +14 -1
  25. package/src/usage-windows.ts +104 -28
  26. package/dist/client/index.css +0 -1
  27. package/dist/client/index.html +0 -24
  28. package/dist/client/index.js +0 -257
  29. package/dist/client/styles.css +0 -1692
  30. package/dist/types/aggregator.d.ts +0 -98
  31. package/dist/types/client/App.d.ts +0 -1
  32. package/dist/types/client/api.d.ts +0 -22
  33. package/dist/types/client/app/AppLayout.d.ts +0 -16
  34. package/dist/types/client/app/NavRail.d.ts +0 -7
  35. package/dist/types/client/app/RangeControl.d.ts +0 -7
  36. package/dist/types/client/app/SyncButton.d.ts +0 -14
  37. package/dist/types/client/app/ThemeToggle.d.ts +0 -1
  38. package/dist/types/client/app/TopBar.d.ts +0 -15
  39. package/dist/types/client/app/routes.d.ts +0 -12
  40. package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
  41. package/dist/types/client/components/chart-shared.d.ts +0 -173
  42. package/dist/types/client/components/models-table-shared.d.ts +0 -175
  43. package/dist/types/client/components/range-meta.d.ts +0 -21
  44. package/dist/types/client/data/charts.d.ts +0 -1
  45. package/dist/types/client/data/formatters.d.ts +0 -8
  46. package/dist/types/client/data/useHashRoute.d.ts +0 -8
  47. package/dist/types/client/data/useResource.d.ts +0 -13
  48. package/dist/types/client/data/view-models.d.ts +0 -74
  49. package/dist/types/client/index.d.ts +0 -2
  50. package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
  51. package/dist/types/client/routes/CostsRoute.d.ts +0 -7
  52. package/dist/types/client/routes/ErrorsRoute.d.ts +0 -8
  53. package/dist/types/client/routes/GainRoute.d.ts +0 -7
  54. package/dist/types/client/routes/ModelsRoute.d.ts +0 -7
  55. package/dist/types/client/routes/OverviewRoute.d.ts +0 -8
  56. package/dist/types/client/routes/ProjectsRoute.d.ts +0 -7
  57. package/dist/types/client/routes/ProvidersRoute.d.ts +0 -7
  58. package/dist/types/client/routes/RequestsRoute.d.ts +0 -8
  59. package/dist/types/client/routes/ToolsRoute.d.ts +0 -7
  60. package/dist/types/client/routes/index.d.ts +0 -10
  61. package/dist/types/client/types.d.ts +0 -63
  62. package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
  63. package/dist/types/client/ui/DataTable.d.ts +0 -17
  64. package/dist/types/client/ui/EmptyState.d.ts +0 -7
  65. package/dist/types/client/ui/ErrorState.d.ts +0 -6
  66. package/dist/types/client/ui/JsonBlock.d.ts +0 -7
  67. package/dist/types/client/ui/MetricCluster.d.ts +0 -5
  68. package/dist/types/client/ui/Panel.d.ts +0 -7
  69. package/dist/types/client/ui/RequestDrawer.d.ts +0 -5
  70. package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
  71. package/dist/types/client/ui/Skeleton.d.ts +0 -8
  72. package/dist/types/client/ui/StatusPill.d.ts +0 -7
  73. package/dist/types/client/ui/index.d.ts +0 -11
  74. package/dist/types/client/useSystemTheme.d.ts +0 -11
  75. package/dist/types/db.d.ts +0 -159
  76. package/dist/types/embedded-client.d.ts +0 -18
  77. package/dist/types/gain-aggregator.d.ts +0 -26
  78. package/dist/types/index.d.ts +0 -7
  79. package/dist/types/parser.d.ts +0 -53
  80. package/dist/types/port-conflict.d.ts +0 -14
  81. package/dist/types/server.d.ts +0 -12
  82. package/dist/types/shared-types.d.ts +0 -408
  83. package/dist/types/sync-worker.d.ts +0 -31
  84. package/dist/types/types.d.ts +0 -164
  85. package/dist/types/usage-windows.d.ts +0 -45
  86. package/dist/types/user-metrics.d.ts +0 -81
@@ -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">