@linxiraos/pi-stats 1.0.10 → 1.1.1

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,4 +1,4 @@
1
- import type { BehaviorDashboardStats, DashboardStats, MessageStats, ProviderDashboardStats, RequestDetails, ToolDashboardStats } from "./types.js";
1
+ import type { BehaviorDashboardStats, DashboardStats, FolderStats, MessageStats, ProviderDashboardStats, RequestDetails, ToolDashboardStats } from "./types.js";
2
2
  /**
3
3
  * Progress event emitted after each session file is fully processed.
4
4
  * `current` is the number of files completed (skipped + parsed),
@@ -71,6 +71,7 @@ export declare function getDashboardStats(range?: string | null): Promise<Dashbo
71
71
  export declare function getOverviewStats(range?: string | null): Promise<Pick<DashboardStats, "overall" | "byAgentType" | "timeSeries">>;
72
72
  export declare function getModelDashboardStats(range?: string | null): Promise<Pick<DashboardStats, "byModel" | "modelSeries" | "modelPerformanceSeries">>;
73
73
  export declare function getCostDashboardStats(range?: string | null): Promise<Pick<DashboardStats, "costSeries">>;
74
+ export declare function getFolderStats(range?: string | null): Promise<FolderStats[]>;
74
75
  export declare function getRecentRequests(limit?: number): Promise<MessageStats[]>;
75
76
  export declare function getRecentErrors(range?: string | null, limit?: number): Promise<MessageStats[]>;
76
77
  export declare function getRequestDetails(id: number): Promise<RequestDetails | null>;
@@ -88,6 +89,10 @@ export declare function getToolDashboardStats(range?: string | null): Promise<To
88
89
  * Get the providers dashboard payload: per-provider totals, peak-burn-hours
89
90
  * histogram, provider token time series, and subscription-window analytics
90
91
  * (utilization series + insights) derived from recorded usage-limit snapshots.
92
+ *
93
+ * Window token estimates use broker-held fleet token burn when a broker is
94
+ * configured — the window fractions cover every install sharing the broker's
95
+ * credentials, so dividing them into local-only tokens would undercount.
91
96
  */
92
97
  export declare function getProviderDashboardStats(range?: string | null): Promise<ProviderDashboardStats>;
93
98
  /**
@@ -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. */
@@ -359,8 +369,9 @@ export interface UsageWindowSeries {
359
369
  accountKey: string;
360
370
  /** Email/account id when known, else the stable account key. */
361
371
  accountLabel: string;
362
- /** Groups the same limit window across accounts (window label or limit id). */
372
+ /** Groups the same limit window across accounts (the provider limit id). */
363
373
  windowKey: string;
374
+ /** Human label of the limit (distinguishes same-duration windows). */
364
375
  windowLabel: string;
365
376
  points: UsageWindowPoint[];
366
377
  }
@@ -371,7 +382,9 @@ export interface UsageWindowSeries {
371
382
  */
372
383
  export interface ProviderWindowInsight {
373
384
  provider: string;
385
+ /** Groups the same limit window across accounts (the provider limit id). */
374
386
  windowKey: string;
387
+ /** Human label of the limit (distinguishes same-duration windows). */
375
388
  windowLabel: string;
376
389
  /** Accounts with at least one snapshot for this window in range. */
377
390
  accounts: number;
@@ -1,3 +1,4 @@
1
+ import type { ClientUsageClientSummary } from "@linxiraos/pi-ai/usage";
1
2
  import type { ProviderWindowInsight, UsageWindowSeries } from "./shared-types.js";
2
3
  /** Subset of a `usage_history` row consumed by the window analytics. */
3
4
  export interface UsageSnapshotRow {
@@ -20,26 +21,52 @@ export interface UsageWindowStats {
20
21
  usageSeries: UsageWindowSeries[];
21
22
  windowInsights: ProviderWindowInsight[];
22
23
  }
24
+ /** Usage snapshots plus, in broker mode, fleet-wide token burn per provider. */
25
+ export interface UsageDataSnapshot {
26
+ rows: UsageSnapshotRow[];
27
+ /**
28
+ * Total tokens (input + output + cache read/write) per provider summed
29
+ * across every install reporting to the auth broker, or `null` when no
30
+ * broker is configured or no client reports exist for the range. Matches
31
+ * the fleet-wide window fractions in `rows`, unlike local message stats
32
+ * which only see this install's burn.
33
+ */
34
+ fleetTokensByProvider: Map<string, number> | null;
35
+ }
23
36
  /**
24
37
  * Read usage-limit snapshots recorded at or after `sinceMs`, oldest first.
25
38
  * Opens the agent DB read-only; returns `[]` when the DB or table is absent.
26
39
  */
27
40
  export declare function readUsageSnapshots(sinceMs: number, dbPath?: string): UsageSnapshotRow[];
28
41
  /**
29
- * Fetch usage snapshots from wherever they actually accumulate: the auth
30
- * broker's durable history when a broker is configured (the broker performs
31
- * every upstream usage fetch in that mode, so the local `usage_history` stays
32
- * frozen), else the local agent DB. Broker errors fall back to the local read
33
- * so the dashboard degrades to stale-but-present data instead of failing.
42
+ * Fetch usage data from wherever it actually accumulates: the auth broker's
43
+ * durable history plus per-client observed-usage reports when a broker is
44
+ * configured (the broker performs every upstream usage fetch in that mode, so
45
+ * the local `usage_history` stays frozen), else the local agent DB. Broker
46
+ * errors fall back to the local read so the dashboard degrades to
47
+ * stale-but-present data instead of failing.
48
+ */
49
+ export declare function fetchUsageData(sinceMs: number): Promise<UsageDataSnapshot>;
50
+ /**
51
+ * Fold per-client provider aggregates into total tokens per provider
52
+ * (input + output + cache read/write, matching message-stat `totalTokens`).
53
+ * Returns `null` when no client reported anything, signalling "no data"
54
+ * rather than "zero burn".
34
55
  */
35
- export declare function fetchUsageSnapshots(sinceMs: number): Promise<UsageSnapshotRow[]>;
56
+ export declare function sumFleetTokens(clients: ClientUsageClientSummary[]): Map<string, number> | null;
36
57
  /**
37
58
  * Derive utilization series and per-window insights from raw snapshots.
38
59
  *
60
+ * Windows are grouped by `(provider, limitId)` — never by display label.
61
+ * Distinct limits can share a duration label (Anthropic's overall and
62
+ * model-scoped 7-day windows, Codex's base and Spark weeklies); merging them
63
+ * interleaves unrelated fractions per account and wildly inflates consumption.
64
+ *
39
65
  * `tokensByProvider` supplies each provider's token burn over the same time
40
- * range (from the local message stats); it converts consumed window fraction
41
- * into an estimated token capacity per window. Attribution note: tokens are
42
- * per provider, not per account, so the estimate treats the account fleet as
43
- * one pooled subscription — which is exactly how round-robin auth uses it.
66
+ * range (fleet-wide broker client reports when available, else local message
67
+ * stats); it converts consumed window fraction into an estimated token
68
+ * capacity per window. Attribution note: tokens are per provider, not per
69
+ * account, so the estimate treats the account fleet as one pooled
70
+ * subscription — which is exactly how round-robin auth uses it.
44
71
  */
45
72
  export declare function computeUsageWindowStats(rows: UsageSnapshotRow[], tokensByProvider: ReadonlyMap<string, number>): UsageWindowStats;
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@linxiraos/pi-stats",
4
- "version": "1.0.10",
4
+ "version": "1.1.1",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
- "homepage": "https://linxira-os.github.io",
6
+ "homepage": "https://linxira-os.github.io/zeta/",
7
7
  "author": "Can Boluk",
8
8
  "license": "MIT",
9
9
  "repository": {
@@ -12,7 +12,7 @@
12
12
  "directory": "packages/stats"
13
13
  },
14
14
  "bugs": {
15
- "url": "https://github.com/can1357/oh-my-pi/issues"
15
+ "url": "https://github.com/Linxira-OS/linxira-zeta/issues"
16
16
  },
17
17
  "keywords": [
18
18
  "ai",
@@ -25,7 +25,7 @@
25
25
  "main": "./src/index.ts",
26
26
  "types": "./dist/types/index.d.ts",
27
27
  "bin": {
28
- "omp-stats": "./src/index.ts"
28
+ "zeta-stats": "./src/index.ts"
29
29
  },
30
30
  "scripts": {
31
31
  "gen:stats": "bun scripts/generate-client-bundle.ts --generate",
@@ -39,16 +39,16 @@
39
39
  "fmt": "biome format --write ."
40
40
  },
41
41
  "dependencies": {
42
+ "@linxiraos/pi-ai": "1.1.1",
43
+ "@linxiraos/pi-catalog": "1.1.1",
44
+ "@linxiraos/pi-utils": "1.1.1",
42
45
  "@tailwindcss/node": "^4.3.2",
43
46
  "chart.js": "^4.5.1",
44
47
  "lucide-react": "^1.24.0",
45
48
  "react": "19.2.7",
46
49
  "react-chartjs-2": "^5.3.1",
47
50
  "react-dom": "19.2.7",
48
- "tailwindcss": "^4.3.2",
49
- "@linxiraos/pi-ai": "1.0.10",
50
- "@linxiraos/pi-catalog": "1.0.10",
51
- "@linxiraos/pi-utils": "1.0.10"
51
+ "tailwindcss": "^4.3.2"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/bun": "^1.3.14",
@@ -65,6 +65,8 @@
65
65
  "tailwind.config.js",
66
66
  "README.md",
67
67
  "CHANGELOG.md",
68
+ "LICENSE",
69
+ "THIRD-PARTY-NOTICES.txt",
68
70
  "dist/types",
69
71
  "dist/client"
70
72
  ],
package/src/aggregator.ts CHANGED
@@ -37,17 +37,18 @@ import { getSessionEntry, listAllSessionFiles, type ParseSessionResult, parseSes
37
37
  import type { SyncWorkerRequest, SyncWorkerResponse } from "./sync-worker";
38
38
  // Coding-agent binary/bundle workers route through the CLI entrypoint with a
39
39
  // hidden argv mode, so the compiled binary and npm bundle only need one
40
- // JavaScript entry. Standalone source `omp-stats` keeps using this package's
40
+ // JavaScript entry. Standalone source `zeta-stats` keeps using this package's
41
41
  // own sync-worker source file.
42
42
  import type {
43
43
  BehaviorDashboardStats,
44
44
  DashboardStats,
45
+ FolderStats,
45
46
  MessageStats,
46
47
  ProviderDashboardStats,
47
48
  RequestDetails,
48
49
  ToolDashboardStats,
49
50
  } from "./types";
50
- import { computeUsageWindowStats, fetchUsageSnapshots } from "./usage-windows";
51
+ import { computeUsageWindowStats, fetchUsageData } from "./usage-windows";
51
52
 
52
53
  /**
53
54
  * Apply a freshly parsed result to the database. Runs entirely on the
@@ -455,6 +456,13 @@ export async function getCostDashboardStats(range?: string | null): Promise<Pick
455
456
  costSeries: getCostTimeSeries(costSeriesDays, cutoff),
456
457
  };
457
458
  }
459
+
460
+ export async function getFolderStats(range?: string | null): Promise<FolderStats[]> {
461
+ await initDb();
462
+ const { cutoff } = getTimeRangeConfig(range);
463
+ return getStatsByFolder(cutoff ?? undefined);
464
+ }
465
+
458
466
  export async function getRecentRequests(limit?: number): Promise<MessageStats[]> {
459
467
  await initDb();
460
468
  return dbGetRecentRequests(limit);
@@ -521,14 +529,18 @@ export async function getToolDashboardStats(range?: string | null): Promise<Tool
521
529
  * Get the providers dashboard payload: per-provider totals, peak-burn-hours
522
530
  * histogram, provider token time series, and subscription-window analytics
523
531
  * (utilization series + insights) derived from recorded usage-limit snapshots.
532
+ *
533
+ * Window token estimates use broker-held fleet token burn when a broker is
534
+ * configured — the window fractions cover every install sharing the broker's
535
+ * credentials, so dividing them into local-only tokens would undercount.
524
536
  */
525
537
  export async function getProviderDashboardStats(range?: string | null): Promise<ProviderDashboardStats> {
526
538
  await initDb();
527
539
  const { modelSeriesDays, modelSeriesBucketMs, cutoff } = getTimeRangeConfig(range);
528
540
  const providers = getStatsByProvider(cutoff ?? undefined);
529
- const tokensByProvider = new Map(providers.map(p => [p.provider, p.totalTokens]));
530
- const snapshots = await fetchUsageSnapshots(cutoff ?? 0);
531
- const { usageSeries, windowInsights } = computeUsageWindowStats(snapshots, tokensByProvider);
541
+ const usage = await fetchUsageData(cutoff ?? 0);
542
+ const tokensByProvider = usage.fleetTokensByProvider ?? new Map(providers.map(p => [p.provider, p.totalTokens]));
543
+ const { usageSeries, windowInsights } = computeUsageWindowStats(usage.rows, tokensByProvider);
532
544
  return {
533
545
  providers,
534
546
  hourly: getProviderHourlyBurn(cutoff ?? undefined),
@@ -37,7 +37,7 @@ export function NavRail({ activeSection, onSectionChange, className = "" }: NavR
37
37
  </nav>
38
38
 
39
39
  <div className="stats-nav-rail-footer">
40
- <span className="stats-version-tag">OMP Stats v1.0.0</span>
40
+ <span className="stats-version-tag">Zeta Stats</span>
41
41
  </div>
42
42
  </aside>
43
43
  );
@@ -1,4 +1,5 @@
1
1
  import { formatDistanceToNow } from "@linxiraos/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>