@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.
- package/CHANGELOG.md +5 -279
- package/LICENSE +22 -0
- package/README.md +7 -5
- package/THIRD-PARTY-NOTICES.txt +22909 -0
- package/dist/client/index.css +1 -1
- package/dist/client/index.js +115 -115
- package/dist/types/aggregator.d.ts +6 -1
- package/dist/types/client/data/formatters.d.ts +5 -0
- package/dist/types/client/data/view-models.d.ts +1 -0
- package/dist/types/db.d.ts +3 -2
- package/dist/types/shared-types.d.ts +14 -1
- package/dist/types/usage-windows.d.ts +37 -10
- package/package.json +10 -8
- package/src/aggregator.ts +17 -5
- package/src/client/app/NavRail.tsx +1 -1
- package/src/client/data/formatters.ts +13 -0
- package/src/client/data/view-models.ts +3 -0
- package/src/client/routes/CostsRoute.tsx +28 -10
- package/src/client/routes/ErrorsRoute.tsx +5 -5
- package/src/client/routes/ModelsRoute.tsx +3 -2
- package/src/client/routes/OverviewRoute.tsx +6 -6
- package/src/client/routes/ProjectsRoute.tsx +7 -5
- package/src/client/routes/ProvidersRoute.tsx +30 -5
- package/src/client/routes/RequestsRoute.tsx +5 -5
- package/src/client/routes/ToolsRoute.tsx +23 -11
- package/src/client/ui/MetricCluster.tsx +7 -3
- package/src/client/ui/RequestDrawer.tsx +3 -3
- package/src/db.ts +127 -35
- package/src/embedded-client.generated.txt +0 -1
- package/src/index.ts +10 -9
- package/src/server.ts +3 -2
- package/src/shared-types.ts +14 -1
- package/src/usage-windows.ts +104 -28
package/src/index.ts
CHANGED
|
@@ -39,10 +39,9 @@ export type {
|
|
|
39
39
|
ToolUsageStats,
|
|
40
40
|
} from "./types";
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
function formatCost(n: number): string {
|
|
42
|
+
/** Format an API-equivalent estimate in dollars, or N/A for unpriced usage. */
|
|
43
|
+
function formatCost(n: number, unpricedRequests = 0): string {
|
|
44
|
+
if (n === 0 && unpricedRequests > 0) return "N/A";
|
|
46
45
|
if (n < 0.01) return `$${n.toFixed(4)}`;
|
|
47
46
|
if (n < 1) return `$${n.toFixed(3)}`;
|
|
48
47
|
return `$${n.toFixed(2)}`;
|
|
@@ -69,7 +68,7 @@ async function printStats(): Promise<void> {
|
|
|
69
68
|
console.log(` Output Tokens: ${formatNumber(overall.totalOutputTokens)}`);
|
|
70
69
|
console.log(` Cache Rate: ${formatPercent(overall.cacheRate)}`);
|
|
71
70
|
console.log(` Cache Savings: ${formatPercent(overall.cacheSavings)}`);
|
|
72
|
-
console.log(`
|
|
71
|
+
console.log(` API-equivalent estimate: ${formatCost(overall.totalCost, overall.unpricedRequests)}`);
|
|
73
72
|
console.log(` Premium Requests: ${formatNumber(normalizePremiumRequests(overall.totalPremiumRequests ?? 0))}`);
|
|
74
73
|
console.log(` Avg Duration: ${overall.avgDuration !== null ? formatDuration(overall.avgDuration) : "-"}`);
|
|
75
74
|
console.log(` Avg TTFT: ${overall.avgTtft !== null ? formatDuration(overall.avgTtft) : "-"}`);
|
|
@@ -78,18 +77,20 @@ async function printStats(): Promise<void> {
|
|
|
78
77
|
}
|
|
79
78
|
|
|
80
79
|
if (byModel.length > 0) {
|
|
81
|
-
console.log("\nBy Model:");
|
|
80
|
+
console.log("\nBy Model (API-equivalent estimates):");
|
|
82
81
|
for (const m of byModel.slice(0, 10)) {
|
|
83
82
|
console.log(
|
|
84
|
-
` ${m.model}: ${formatNumber(m.totalRequests)} reqs, ${formatCost(m.totalCost)}, ${formatPercent(m.cacheRate)} cache rate, ${formatPercent(m.cacheSavings)} cache savings`,
|
|
83
|
+
` ${m.model}: ${formatNumber(m.totalRequests)} reqs, ${formatCost(m.totalCost, m.unpricedRequests)}, ${formatPercent(m.cacheRate)} cache rate, ${formatPercent(m.cacheSavings)} cache savings`,
|
|
85
84
|
);
|
|
86
85
|
}
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
if (byFolder.length > 0) {
|
|
90
|
-
console.log("\nBy Folder:");
|
|
89
|
+
console.log("\nBy Folder (API-equivalent estimates):");
|
|
91
90
|
for (const f of byFolder.slice(0, 10)) {
|
|
92
|
-
console.log(
|
|
91
|
+
console.log(
|
|
92
|
+
` ${f.folder}: ${formatNumber(f.totalRequests)} reqs, ${formatCost(f.totalCost, f.unpricedRequests)}`,
|
|
93
|
+
);
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
|
package/src/server.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getBehaviorDashboardStats,
|
|
9
9
|
getCostDashboardStats,
|
|
10
10
|
getDashboardStats,
|
|
11
|
+
getFolderStats,
|
|
11
12
|
getModelDashboardStats,
|
|
12
13
|
getOverviewStats,
|
|
13
14
|
getProviderDashboardStats,
|
|
@@ -253,8 +254,8 @@ export async function handleApi(req: Request): Promise<Response> {
|
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
if (path === "/api/stats/folders") {
|
|
256
|
-
const stats = await
|
|
257
|
-
return Response.json(stats
|
|
257
|
+
const stats = await getFolderStats(range);
|
|
258
|
+
return Response.json(stats);
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
if (path === "/api/stats/timeseries") {
|
package/src/shared-types.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface AggregatedStats {
|
|
|
34
34
|
cacheSavings: number;
|
|
35
35
|
/** Total cost */
|
|
36
36
|
totalCost: number;
|
|
37
|
+
/** Requests with token usage but no public-equivalent subscription price. */
|
|
38
|
+
unpricedRequests: number;
|
|
37
39
|
/** Total premium requests */
|
|
38
40
|
totalPremiumRequests: number;
|
|
39
41
|
/** Average duration in ms */
|
|
@@ -122,6 +124,8 @@ export interface CostTimeSeriesPoint {
|
|
|
122
124
|
provider: string;
|
|
123
125
|
/** Total cost for this bucket */
|
|
124
126
|
cost: number;
|
|
127
|
+
/** Requests excluded because no public-equivalent subscription price exists. */
|
|
128
|
+
unpricedRequests: number;
|
|
125
129
|
/** Cost breakdown */
|
|
126
130
|
costInput: number;
|
|
127
131
|
costOutput: number;
|
|
@@ -302,6 +306,8 @@ export interface ToolUsageStats {
|
|
|
302
306
|
outputTokensShare: number;
|
|
303
307
|
/** Cost (USD) of invoking turns, attributed per call share. */
|
|
304
308
|
costShare: number;
|
|
309
|
+
/** Share of unpriced subscription requests attributed to this tool. */
|
|
310
|
+
unpricedRequestsShare: number;
|
|
305
311
|
/** Unix ms of the most recent call in range. */
|
|
306
312
|
lastUsed: number;
|
|
307
313
|
}
|
|
@@ -343,6 +349,8 @@ export interface ProviderAggregate {
|
|
|
343
349
|
/** Uncached input + cache reads + cache writes + output. */
|
|
344
350
|
totalTokens: number;
|
|
345
351
|
totalCost: number;
|
|
352
|
+
/** Requests excluded because no public-equivalent subscription price exists. */
|
|
353
|
+
unpricedRequests: number;
|
|
346
354
|
totalPremiumRequests: number;
|
|
347
355
|
avgTokensPerSecond: number | null;
|
|
348
356
|
}
|
|
@@ -366,6 +374,8 @@ export interface ProviderTimeSeriesPoint {
|
|
|
366
374
|
provider: string;
|
|
367
375
|
totalTokens: number;
|
|
368
376
|
cost: number;
|
|
377
|
+
/** Requests excluded because no public-equivalent subscription price exists. */
|
|
378
|
+
unpricedRequests: number;
|
|
369
379
|
requests: number;
|
|
370
380
|
}
|
|
371
381
|
|
|
@@ -386,8 +396,9 @@ export interface UsageWindowSeries {
|
|
|
386
396
|
accountKey: string;
|
|
387
397
|
/** Email/account id when known, else the stable account key. */
|
|
388
398
|
accountLabel: string;
|
|
389
|
-
/** Groups the same limit window across accounts (
|
|
399
|
+
/** Groups the same limit window across accounts (the provider limit id). */
|
|
390
400
|
windowKey: string;
|
|
401
|
+
/** Human label of the limit (distinguishes same-duration windows). */
|
|
391
402
|
windowLabel: string;
|
|
392
403
|
points: UsageWindowPoint[];
|
|
393
404
|
}
|
|
@@ -399,7 +410,9 @@ export interface UsageWindowSeries {
|
|
|
399
410
|
*/
|
|
400
411
|
export interface ProviderWindowInsight {
|
|
401
412
|
provider: string;
|
|
413
|
+
/** Groups the same limit window across accounts (the provider limit id). */
|
|
402
414
|
windowKey: string;
|
|
415
|
+
/** Human label of the limit (distinguishes same-duration windows). */
|
|
403
416
|
windowLabel: string;
|
|
404
417
|
/** Accounts with at least one snapshot for this window in range. */
|
|
405
418
|
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 "@linxiraos/pi-ai/auth-broker";
|
|
17
|
+
import type { ClientUsageClientSummary } from "@linxiraos/pi-ai/usage";
|
|
17
18
|
import { getAgentDbPath, logger } from "@linxiraos/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: [] };
|