@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 +10 -0
- package/dist/types/aggregator.d.ts +4 -0
- package/dist/types/shared-types.d.ts +4 -1
- package/dist/types/usage-windows.d.ts +37 -10
- package/package.json +4 -4
- package/src/aggregator.ts +8 -4
- package/src/shared-types.ts +4 -1
- package/src/usage-windows.ts +104 -28
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 (
|
|
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
|
|
30
|
-
*
|
|
31
|
-
* every upstream usage fetch in that mode, so
|
|
32
|
-
* frozen), else the local agent DB. Broker
|
|
33
|
-
* so the dashboard degrades to
|
|
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
|
|
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 (
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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.
|
|
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.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "17.
|
|
44
|
-
"@oh-my-pi/pi-utils": "17.
|
|
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,
|
|
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
|
|
553
|
-
const
|
|
554
|
-
const { usageSeries, windowInsights } = computeUsageWindowStats(
|
|
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),
|
package/src/shared-types.ts
CHANGED
|
@@ -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 (
|
|
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;
|
package/src/usage-windows.ts
CHANGED
|
@@ -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
|
|
105
|
-
*
|
|
106
|
-
* every upstream usage fetch in that mode, so
|
|
107
|
-
* frozen), else the local agent DB. Broker
|
|
108
|
-
* so the dashboard degrades to
|
|
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
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 (
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
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
|
|
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 = {
|
|
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
|
|
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: [] };
|