@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
|
@@ -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;
|
package/dist/types/db.d.ts
CHANGED
|
@@ -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
|
|
31
|
-
* `premium_requests`
|
|
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 (
|
|
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
|
|
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,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@linxiraos/pi-stats",
|
|
4
|
-
"version": "1.
|
|
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/
|
|
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
|
-
"
|
|
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 `
|
|
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,
|
|
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
|
|
530
|
-
const
|
|
531
|
-
const { usageSeries, windowInsights } = computeUsageWindowStats(
|
|
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">
|
|
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
|
-
{
|
|
58
|
-
|
|
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">
|
|
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, "
|
|
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: "
|
|
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
|
|
218
|
-
subtitle=
|
|
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
|
|
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 {
|
|
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: "
|
|
62
|
+
header: "API-equivalent estimate",
|
|
63
63
|
numeric: true,
|
|
64
|
-
render: (item: MessageStats) =>
|
|
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">
|
|
86
|
-
<div className="stats-mobile-card-value">{
|
|
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: "
|
|
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
|
-
|
|
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 {
|
|
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: "
|
|
160
|
+
header: "API-equivalent estimate",
|
|
161
161
|
numeric: true,
|
|
162
|
-
render: (item: MessageStats) =>
|
|
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">
|
|
202
|
-
<div className="stats-mobile-card-value">{
|
|
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
|
|
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 {
|
|
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: "
|
|
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">{
|
|
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">
|
|
137
|
-
<div className="stats-mobile-card-value font-mono">
|
|
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>
|