@oh-my-pi/omp-stats 17.3.8 → 17.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.4.0] - 2026-08-20
6
+
7
+ ### Changed
8
+
9
+ - Window token estimates now incorporate broker-reported fleet token burn when an auth broker is configured, accurately tracking fleet-wide usage instead of undercounting with local-only statistics.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed an issue in subscription-window insights where distinct limits sharing a duration label (such as Anthropic overall vs. model-scoped 7-day windows) were incorrectly merged, which inflated window-equivalents and skewed tokens-per-window estimates. Windows are now grouped by provider limit ID.
14
+
5
15
  ## [17.3.6] - 2026-08-17
6
16
 
7
17
  ### Fixed
@@ -96,6 +96,10 @@ export declare function getToolDashboardStats(range?: string | null): Promise<To
96
96
  * Get the providers dashboard payload: per-provider totals, peak-burn-hours
97
97
  * histogram, provider token time series, and subscription-window analytics
98
98
  * (utilization series + insights) derived from recorded usage-limit snapshots.
99
+ *
100
+ * Window token estimates use broker-held fleet token burn when a broker is
101
+ * configured — the window fractions cover every install sharing the broker's
102
+ * credentials, so dividing them into local-only tokens would undercount.
99
103
  */
100
104
  export declare function getProviderDashboardStats(range?: string | null): Promise<ProviderDashboardStats>;
101
105
  export {};
@@ -359,8 +359,9 @@ export interface UsageWindowSeries {
359
359
  accountKey: string;
360
360
  /** Email/account id when known, else the stable account key. */
361
361
  accountLabel: string;
362
- /** Groups the same limit window across accounts (window label or limit id). */
362
+ /** Groups the same limit window across accounts (the provider limit id). */
363
363
  windowKey: string;
364
+ /** Human label of the limit (distinguishes same-duration windows). */
364
365
  windowLabel: string;
365
366
  points: UsageWindowPoint[];
366
367
  }
@@ -371,7 +372,9 @@ export interface UsageWindowSeries {
371
372
  */
372
373
  export interface ProviderWindowInsight {
373
374
  provider: string;
375
+ /** Groups the same limit window across accounts (the provider limit id). */
374
376
  windowKey: string;
377
+ /** Human label of the limit (distinguishes same-duration windows). */
375
378
  windowLabel: string;
376
379
  /** Accounts with at least one snapshot for this window in range. */
377
380
  accounts: number;
@@ -1,3 +1,4 @@
1
+ import type { ClientUsageClientSummary } from "@oh-my-pi/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,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/omp-stats",
4
- "version": "17.3.8",
4
+ "version": "17.4.0",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -39,9 +39,9 @@
39
39
  "fmt": "biome format --write ."
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "17.3.8",
43
- "@oh-my-pi/pi-catalog": "17.3.8",
44
- "@oh-my-pi/pi-utils": "17.3.8",
42
+ "@oh-my-pi/pi-ai": "17.4.0",
43
+ "@oh-my-pi/pi-catalog": "17.4.0",
44
+ "@oh-my-pi/pi-utils": "17.4.0",
45
45
  "@tailwindcss/node": "^4.3.2",
46
46
  "chart.js": "^4.5.1",
47
47
  "lucide-react": "^1.24.0",
package/src/aggregator.ts CHANGED
@@ -48,7 +48,7 @@ import type {
48
48
  RequestDetails,
49
49
  ToolDashboardStats,
50
50
  } from "./types";
51
- import { computeUsageWindowStats, fetchUsageSnapshots } from "./usage-windows";
51
+ import { computeUsageWindowStats, fetchUsageData } from "./usage-windows";
52
52
 
53
53
  const STATS_SYNC_LOCK_RETRY_MS = 25;
54
54
  const STATS_SYNC_LOCK_WAIT_MS = 60 * 60 * 1000;
@@ -544,14 +544,18 @@ export async function getToolDashboardStats(range?: string | null): Promise<Tool
544
544
  * Get the providers dashboard payload: per-provider totals, peak-burn-hours
545
545
  * histogram, provider token time series, and subscription-window analytics
546
546
  * (utilization series + insights) derived from recorded usage-limit snapshots.
547
+ *
548
+ * Window token estimates use broker-held fleet token burn when a broker is
549
+ * configured — the window fractions cover every install sharing the broker's
550
+ * credentials, so dividing them into local-only tokens would undercount.
547
551
  */
548
552
  export async function getProviderDashboardStats(range?: string | null): Promise<ProviderDashboardStats> {
549
553
  await initDb();
550
554
  const { modelSeriesDays, modelSeriesBucketMs, cutoff } = getTimeRangeConfig(range);
551
555
  const providers = getStatsByProvider(cutoff ?? undefined);
552
- const tokensByProvider = new Map(providers.map(p => [p.provider, p.totalTokens]));
553
- const snapshots = await fetchUsageSnapshots(cutoff ?? 0);
554
- const { usageSeries, windowInsights } = computeUsageWindowStats(snapshots, tokensByProvider);
556
+ const usage = await fetchUsageData(cutoff ?? 0);
557
+ const tokensByProvider = usage.fleetTokensByProvider ?? new Map(providers.map(p => [p.provider, p.totalTokens]));
558
+ const { usageSeries, windowInsights } = computeUsageWindowStats(usage.rows, tokensByProvider);
555
559
  return {
556
560
  providers,
557
561
  hourly: getProviderHourlyBurn(cutoff ?? undefined),
@@ -386,8 +386,9 @@ export interface UsageWindowSeries {
386
386
  accountKey: string;
387
387
  /** Email/account id when known, else the stable account key. */
388
388
  accountLabel: string;
389
- /** Groups the same limit window across accounts (window label or limit id). */
389
+ /** Groups the same limit window across accounts (the provider limit id). */
390
390
  windowKey: string;
391
+ /** Human label of the limit (distinguishes same-duration windows). */
391
392
  windowLabel: string;
392
393
  points: UsageWindowPoint[];
393
394
  }
@@ -399,7 +400,9 @@ export interface UsageWindowSeries {
399
400
  */
400
401
  export interface ProviderWindowInsight {
401
402
  provider: string;
403
+ /** Groups the same limit window across accounts (the provider limit id). */
402
404
  windowKey: string;
405
+ /** Human label of the limit (distinguishes same-duration windows). */
403
406
  windowLabel: string;
404
407
  /** Accounts with at least one snapshot for this window in range. */
405
408
  accounts: number;
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { Database } from "bun:sqlite";
16
16
  import { AuthBrokerClient, resolveAuthBrokerConfig } from "@oh-my-pi/pi-ai/auth-broker";
17
+ import type { ClientUsageClientSummary } from "@oh-my-pi/pi-ai/usage";
17
18
  import { getAgentDbPath, logger } from "@oh-my-pi/pi-utils";
18
19
  import type { ProviderWindowInsight, UsageWindowPoint, UsageWindowSeries } from "./shared-types";
19
20
 
@@ -40,6 +41,19 @@ export interface UsageWindowStats {
40
41
  windowInsights: ProviderWindowInsight[];
41
42
  }
42
43
 
44
+ /** Usage snapshots plus, in broker mode, fleet-wide token burn per provider. */
45
+ export interface UsageDataSnapshot {
46
+ rows: UsageSnapshotRow[];
47
+ /**
48
+ * Total tokens (input + output + cache read/write) per provider summed
49
+ * across every install reporting to the auth broker, or `null` when no
50
+ * broker is configured or no client reports exist for the range. Matches
51
+ * the fleet-wide window fractions in `rows`, unlike local message stats
52
+ * which only see this install's burn.
53
+ */
54
+ fleetTokensByProvider: Map<string, number> | null;
55
+ }
56
+
43
57
  /** A used-fraction drop smaller than this is jitter, not a window reset. */
44
58
  const RESET_DROP_THRESHOLD = 0.05;
45
59
  /** Minimum window-equivalents consumed before extrapolating tokens/window. */
@@ -101,35 +115,74 @@ export function readUsageSnapshots(sinceMs: number, dbPath = getAgentDbPath()):
101
115
  }
102
116
 
103
117
  /**
104
- * Fetch usage snapshots from wherever they actually accumulate: the auth
105
- * broker's durable history when a broker is configured (the broker performs
106
- * every upstream usage fetch in that mode, so the local `usage_history` stays
107
- * frozen), else the local agent DB. Broker errors fall back to the local read
108
- * so the dashboard degrades to stale-but-present data instead of failing.
118
+ * Fetch usage data from wherever it actually accumulates: the auth broker's
119
+ * durable history plus per-client observed-usage reports when a broker is
120
+ * configured (the broker performs every upstream usage fetch in that mode, so
121
+ * the local `usage_history` stays frozen), else the local agent DB. Broker
122
+ * errors fall back to the local read so the dashboard degrades to
123
+ * stale-but-present data instead of failing.
109
124
  */
110
- export async function fetchUsageSnapshots(sinceMs: number): Promise<UsageSnapshotRow[]> {
125
+ export async function fetchUsageData(sinceMs: number): Promise<UsageDataSnapshot> {
111
126
  try {
112
127
  const brokerConfig = await resolveAuthBrokerConfig();
113
128
  if (brokerConfig) {
114
129
  const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
115
- const response = await client.fetchUsageHistory({ sinceMs });
116
- return response.entries.map(entry => ({
117
- recordedAt: entry.recordedAt,
118
- provider: entry.provider,
119
- accountKey: entry.accountKey,
120
- email: entry.email ?? null,
121
- accountId: entry.accountId ?? null,
122
- limitId: entry.limitId,
123
- label: entry.label,
124
- windowLabel: entry.windowLabel ?? null,
125
- usedFraction: entry.usedFraction ?? null,
126
- status: entry.status ?? null,
127
- }));
130
+ const [response, fleetTokensByProvider] = await Promise.all([
131
+ client.fetchUsageHistory({ sinceMs }),
132
+ fetchFleetTokens(client, sinceMs),
133
+ ]);
134
+ return {
135
+ rows: response.entries.map(entry => ({
136
+ recordedAt: entry.recordedAt,
137
+ provider: entry.provider,
138
+ accountKey: entry.accountKey,
139
+ email: entry.email ?? null,
140
+ accountId: entry.accountId ?? null,
141
+ limitId: entry.limitId,
142
+ label: entry.label,
143
+ windowLabel: entry.windowLabel ?? null,
144
+ usedFraction: entry.usedFraction ?? null,
145
+ status: entry.status ?? null,
146
+ })),
147
+ fleetTokensByProvider,
148
+ };
128
149
  }
129
150
  } catch (err) {
130
151
  logger.debug("broker usage history unavailable, falling back to local", { error: String(err) });
131
152
  }
132
- return readUsageSnapshots(sinceMs);
153
+ return { rows: readUsageSnapshots(sinceMs), fleetTokensByProvider: null };
154
+ }
155
+
156
+ /**
157
+ * Sum broker-recorded client token burn per provider since `sinceMs`.
158
+ * Returns `null` on fetch failure or when no client has reported usage, so
159
+ * callers fall back to local message stats instead of zeroing estimates.
160
+ */
161
+ async function fetchFleetTokens(client: AuthBrokerClient, sinceMs: number): Promise<Map<string, number> | null> {
162
+ try {
163
+ const summary = await client.fetchClientUsageSummary({ sinceMs });
164
+ return sumFleetTokens(summary.clients);
165
+ } catch (err) {
166
+ logger.debug("broker client usage summary unavailable", { error: String(err) });
167
+ return null;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Fold per-client provider aggregates into total tokens per provider
173
+ * (input + output + cache read/write, matching message-stat `totalTokens`).
174
+ * Returns `null` when no client reported anything, signalling "no data"
175
+ * rather than "zero burn".
176
+ */
177
+ export function sumFleetTokens(clients: ClientUsageClientSummary[]): Map<string, number> | null {
178
+ const tokens = new Map<string, number>();
179
+ for (const client of clients) {
180
+ for (const p of client.providers) {
181
+ const total = p.inputTokens + p.outputTokens + p.cacheReadTokens + p.cacheWriteTokens;
182
+ tokens.set(p.provider, (tokens.get(p.provider) ?? 0) + total);
183
+ }
184
+ }
185
+ return tokens.size > 0 ? tokens : null;
133
186
  }
134
187
 
135
188
  /** True when a snapshot reports an exhausted window, by status or by fraction. */
@@ -175,14 +228,33 @@ interface WindowGroup {
175
228
  accounts: Map<string, AccountSeries>;
176
229
  }
177
230
 
231
+ /**
232
+ * Display label for one limit window: the limit label, plus the window label
233
+ * when it adds information ("Usage (Google) · Daily"). The limit label alone
234
+ * distinguishes same-duration windows ("Claude 7 Day" vs "Claude 7 Day
235
+ * (Fable)"); the window-label suffix distinguishes same-named limits with
236
+ * different durations (Antigravity's daily vs weekly "Usage (Google)").
237
+ */
238
+ function windowDisplayLabel(row: UsageSnapshotRow): string {
239
+ const { label, windowLabel } = row;
240
+ if (!windowLabel || label.toLowerCase().includes(windowLabel.toLowerCase())) return label;
241
+ return `${label} · ${windowLabel}`;
242
+ }
243
+
178
244
  /**
179
245
  * Derive utilization series and per-window insights from raw snapshots.
180
246
  *
247
+ * Windows are grouped by `(provider, limitId)` — never by display label.
248
+ * Distinct limits can share a duration label (Anthropic's overall and
249
+ * model-scoped 7-day windows, Codex's base and Spark weeklies); merging them
250
+ * interleaves unrelated fractions per account and wildly inflates consumption.
251
+ *
181
252
  * `tokensByProvider` supplies each provider's token burn over the same time
182
- * range (from the local message stats); it converts consumed window fraction
183
- * into an estimated token capacity per window. Attribution note: tokens are
184
- * per provider, not per account, so the estimate treats the account fleet as
185
- * one pooled subscription which is exactly how round-robin auth uses it.
253
+ * range (fleet-wide broker client reports when available, else local message
254
+ * stats); it converts consumed window fraction into an estimated token
255
+ * capacity per window. Attribution note: tokens are per provider, not per
256
+ * account, so the estimate treats the account fleet as one pooled
257
+ * subscription — which is exactly how round-robin auth uses it.
186
258
  */
187
259
  export function computeUsageWindowStats(
188
260
  rows: UsageSnapshotRow[],
@@ -190,15 +262,19 @@ export function computeUsageWindowStats(
190
262
  ): UsageWindowStats {
191
263
  const groups = new Map<string, WindowGroup>();
192
264
  for (const row of rows) {
193
- const windowKey = row.windowLabel ?? row.limitId;
194
- const groupKey = `${row.provider}\u0000${windowKey}`;
265
+ const groupKey = `${row.provider}\u0000${row.limitId}`;
195
266
  let group = groups.get(groupKey);
196
267
  if (!group) {
197
- group = { provider: row.provider, windowKey, windowLabel: row.windowLabel ?? row.label, accounts: new Map() };
268
+ group = {
269
+ provider: row.provider,
270
+ windowKey: row.limitId,
271
+ windowLabel: windowDisplayLabel(row),
272
+ accounts: new Map(),
273
+ };
198
274
  groups.set(groupKey, group);
199
275
  }
200
276
  // Labels can change across snapshots (provider renames); latest wins.
201
- group.windowLabel = row.windowLabel ?? row.label;
277
+ group.windowLabel = windowDisplayLabel(row);
202
278
  let account = group.accounts.get(row.accountKey);
203
279
  if (!account) {
204
280
  account = { accountKey: row.accountKey, accountLabel: row.email ?? row.accountId ?? row.accountKey, rows: [] };