@oh-my-pi/omp-stats 13.5.7 → 13.6.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/package.json +6 -6
- package/src/client/components/RequestDetail.tsx +10 -1
- package/src/client/components/StatsGrid.tsx +11 -2
- package/src/client/types.ts +2 -0
- package/src/db.ts +16 -2
- package/src/index.ts +5 -0
- package/src/parser.ts +6 -4
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "13.
|
|
4
|
+
"version": "13.6.0",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
"build": "bun run build.ts"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@oh-my-pi/pi-ai": "13.
|
|
37
|
-
"@oh-my-pi/pi-utils": "13.
|
|
38
|
-
"@tailwindcss/node": "4",
|
|
36
|
+
"@oh-my-pi/pi-ai": "13.6.0",
|
|
37
|
+
"@oh-my-pi/pi-utils": "13.6.0",
|
|
38
|
+
"@tailwindcss/node": "^4.2",
|
|
39
39
|
"chart.js": "^4.5",
|
|
40
40
|
"date-fns": "^4.1",
|
|
41
|
-
"lucide-react": "^0.
|
|
41
|
+
"lucide-react": "^0.576",
|
|
42
42
|
"react": "^19.2",
|
|
43
43
|
"react-chartjs-2": "^5.3",
|
|
44
44
|
"react-dom": "^19.2"
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/react": "^19.2",
|
|
49
49
|
"@types/react-dom": "^19.2",
|
|
50
50
|
"postcss": "^8.5",
|
|
51
|
-
"tailwindcss": "4"
|
|
51
|
+
"tailwindcss": "^4.2"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"bun": ">=1.3.7"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Clock, Coins, FileJson, Gauge, Hash, X, Zap } from "lucide-react";
|
|
1
|
+
import { Clock, Coins, FileJson, Gauge, Hash, Star, X, Zap } from "lucide-react";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { getRequestDetails } from "../api";
|
|
4
4
|
import type { RequestDetails } from "../types";
|
|
@@ -93,6 +93,15 @@ export function RequestDetail({ id, onClose }: RequestDetailProps) {
|
|
|
93
93
|
</div>
|
|
94
94
|
</div>
|
|
95
95
|
|
|
96
|
+
<div className="surface p-4">
|
|
97
|
+
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
|
|
98
|
+
<Star size={14} />
|
|
99
|
+
<span className="text-xs uppercase tracking-wide">Premium Reqs</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="text-xl font-semibold text-[var(--text-primary)]">
|
|
102
|
+
{(details.usage.premiumRequests ?? 0).toLocaleString()}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
96
105
|
<div className="surface p-4">
|
|
97
106
|
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-2">
|
|
98
107
|
<Hash size={14} />
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Activity, AlertCircle, BarChart3, Database, Server, Zap } from "lucide-react";
|
|
1
|
+
import { Activity, AlertCircle, BarChart3, Database, Server, Star, Zap } from "lucide-react";
|
|
2
2
|
import type { AggregatedStats } from "../types";
|
|
3
3
|
|
|
4
4
|
interface StatsGridProps {
|
|
@@ -24,6 +24,15 @@ const statConfig = [
|
|
|
24
24
|
getDetail: (s: AggregatedStats) =>
|
|
25
25
|
s.totalRequests > 0 ? `$${(s.totalCost / s.totalRequests).toFixed(4)} avg/req` : "-",
|
|
26
26
|
},
|
|
27
|
+
{
|
|
28
|
+
key: "premiumRequests",
|
|
29
|
+
title: "Premium Reqs",
|
|
30
|
+
icon: Star,
|
|
31
|
+
color: "var(--accent-amber)",
|
|
32
|
+
getValue: (s: AggregatedStats) => s.totalPremiumRequests.toLocaleString(),
|
|
33
|
+
getDetail: (s: AggregatedStats) =>
|
|
34
|
+
s.totalRequests > 0 ? `${((s.totalPremiumRequests / s.totalRequests) * 100).toFixed(1)}% of requests` : "-",
|
|
35
|
+
},
|
|
27
36
|
{
|
|
28
37
|
key: "cache",
|
|
29
38
|
title: "Cache Rate",
|
|
@@ -60,7 +69,7 @@ const statConfig = [
|
|
|
60
69
|
|
|
61
70
|
export function StatsGrid({ stats }: StatsGridProps) {
|
|
62
71
|
return (
|
|
63
|
-
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-
|
|
72
|
+
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-7 gap-4 mb-8">
|
|
64
73
|
{statConfig.map(stat => {
|
|
65
74
|
const Icon = stat.icon;
|
|
66
75
|
return (
|
package/src/client/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface Usage {
|
|
|
9
9
|
cacheRead: number;
|
|
10
10
|
cacheWrite: number;
|
|
11
11
|
totalTokens: number;
|
|
12
|
+
premiumRequests?: number;
|
|
12
13
|
cost: {
|
|
13
14
|
input: number;
|
|
14
15
|
output: number;
|
|
@@ -50,6 +51,7 @@ export interface AggregatedStats {
|
|
|
50
51
|
totalCacheWriteTokens: number;
|
|
51
52
|
cacheRate: number;
|
|
52
53
|
totalCost: number;
|
|
54
|
+
totalPremiumRequests: number;
|
|
53
55
|
avgDuration: number | null;
|
|
54
56
|
avgTtft: number | null;
|
|
55
57
|
avgTokensPerSecond: number | null;
|
package/src/db.ts
CHANGED
|
@@ -47,6 +47,7 @@ export async function initDb(): Promise<Database> {
|
|
|
47
47
|
cache_read_tokens INTEGER NOT NULL,
|
|
48
48
|
cache_write_tokens INTEGER NOT NULL,
|
|
49
49
|
total_tokens INTEGER NOT NULL,
|
|
50
|
+
premium_requests REAL NOT NULL,
|
|
50
51
|
cost_input REAL NOT NULL,
|
|
51
52
|
cost_output REAL NOT NULL,
|
|
52
53
|
cost_cache_read REAL NOT NULL,
|
|
@@ -67,6 +68,11 @@ export async function initDb(): Promise<Database> {
|
|
|
67
68
|
);
|
|
68
69
|
`);
|
|
69
70
|
|
|
71
|
+
const messageColumns = db.prepare("PRAGMA table_info(messages)").all() as { name: string }[];
|
|
72
|
+
if (!messageColumns.some(column => column.name === "premium_requests")) {
|
|
73
|
+
db.exec("ALTER TABLE messages ADD COLUMN premium_requests REAL NOT NULL DEFAULT 0");
|
|
74
|
+
}
|
|
75
|
+
db.exec("UPDATE messages SET premium_requests = 0 WHERE premium_requests IS NULL");
|
|
70
76
|
return db;
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -105,9 +111,9 @@ export function insertMessageStats(stats: MessageStats[]): number {
|
|
|
105
111
|
INSERT OR IGNORE INTO messages (
|
|
106
112
|
session_file, entry_id, folder, model, provider, api, timestamp,
|
|
107
113
|
duration, ttft, stop_reason, error_message,
|
|
108
|
-
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_tokens,
|
|
114
|
+
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_tokens, premium_requests,
|
|
109
115
|
cost_input, cost_output, cost_cache_read, cost_cache_write, cost_total
|
|
110
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
116
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
111
117
|
`);
|
|
112
118
|
|
|
113
119
|
let inserted = 0;
|
|
@@ -130,6 +136,7 @@ export function insertMessageStats(stats: MessageStats[]): number {
|
|
|
130
136
|
s.usage.cacheRead,
|
|
131
137
|
s.usage.cacheWrite,
|
|
132
138
|
s.usage.totalTokens,
|
|
139
|
+
s.usage.premiumRequests ?? 0,
|
|
133
140
|
s.usage.cost.input,
|
|
134
141
|
s.usage.cost.output,
|
|
135
142
|
s.usage.cost.cacheRead,
|
|
@@ -160,6 +167,7 @@ function buildAggregatedStats(rows: any[]): AggregatedStats {
|
|
|
160
167
|
totalCacheWriteTokens: 0,
|
|
161
168
|
cacheRate: 0,
|
|
162
169
|
totalCost: 0,
|
|
170
|
+
totalPremiumRequests: 0,
|
|
163
171
|
avgDuration: null,
|
|
164
172
|
avgTtft: null,
|
|
165
173
|
avgTokensPerSecond: null,
|
|
@@ -174,6 +182,7 @@ function buildAggregatedStats(rows: any[]): AggregatedStats {
|
|
|
174
182
|
const successfulRequests = totalRequests - failedRequests;
|
|
175
183
|
const totalInputTokens = row.total_input_tokens || 0;
|
|
176
184
|
const totalCacheReadTokens = row.total_cache_read_tokens || 0;
|
|
185
|
+
const totalPremiumRequests = row.total_premium_requests || 0;
|
|
177
186
|
|
|
178
187
|
return {
|
|
179
188
|
totalRequests,
|
|
@@ -189,6 +198,7 @@ function buildAggregatedStats(rows: any[]): AggregatedStats {
|
|
|
189
198
|
? totalCacheReadTokens / (totalInputTokens + totalCacheReadTokens)
|
|
190
199
|
: 0,
|
|
191
200
|
totalCost: row.total_cost || 0,
|
|
201
|
+
totalPremiumRequests,
|
|
192
202
|
avgDuration: row.avg_duration,
|
|
193
203
|
avgTtft: row.avg_ttft,
|
|
194
204
|
avgTokensPerSecond: row.avg_tokens_per_second,
|
|
@@ -211,6 +221,7 @@ export function getOverallStats(): AggregatedStats {
|
|
|
211
221
|
SUM(output_tokens) as total_output_tokens,
|
|
212
222
|
SUM(cache_read_tokens) as total_cache_read_tokens,
|
|
213
223
|
SUM(cache_write_tokens) as total_cache_write_tokens,
|
|
224
|
+
SUM(premium_requests) as total_premium_requests,
|
|
214
225
|
SUM(cost_total) as total_cost,
|
|
215
226
|
AVG(duration) as avg_duration,
|
|
216
227
|
AVG(ttft) as avg_ttft,
|
|
@@ -240,6 +251,7 @@ export function getStatsByModel(): ModelStats[] {
|
|
|
240
251
|
SUM(output_tokens) as total_output_tokens,
|
|
241
252
|
SUM(cache_read_tokens) as total_cache_read_tokens,
|
|
242
253
|
SUM(cache_write_tokens) as total_cache_write_tokens,
|
|
254
|
+
SUM(premium_requests) as total_premium_requests,
|
|
243
255
|
SUM(cost_total) as total_cost,
|
|
244
256
|
AVG(duration) as avg_duration,
|
|
245
257
|
AVG(ttft) as avg_ttft,
|
|
@@ -274,6 +286,7 @@ export function getStatsByFolder(): FolderStats[] {
|
|
|
274
286
|
SUM(output_tokens) as total_output_tokens,
|
|
275
287
|
SUM(cache_read_tokens) as total_cache_read_tokens,
|
|
276
288
|
SUM(cache_write_tokens) as total_cache_write_tokens,
|
|
289
|
+
SUM(premium_requests) as total_premium_requests,
|
|
277
290
|
SUM(cost_total) as total_cost,
|
|
278
291
|
AVG(duration) as avg_duration,
|
|
279
292
|
AVG(ttft) as avg_ttft,
|
|
@@ -428,6 +441,7 @@ function rowToMessageStats(row: any): MessageStats {
|
|
|
428
441
|
cacheRead: row.cache_read_tokens,
|
|
429
442
|
cacheWrite: row.cache_write_tokens,
|
|
430
443
|
totalTokens: row.total_tokens,
|
|
444
|
+
premiumRequests: row.premium_requests ?? 0,
|
|
431
445
|
cost: {
|
|
432
446
|
input: row.cost_input,
|
|
433
447
|
output: row.cost_output,
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,10 @@ function formatCost(n: number): string {
|
|
|
29
29
|
return `$${n.toFixed(2)}`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function normalizePremiumRequests(n: number): number {
|
|
33
|
+
return Math.round((n + Number.EPSILON) * 100) / 100;
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
/**
|
|
33
37
|
* Print stats summary to console.
|
|
34
38
|
*/
|
|
@@ -44,6 +48,7 @@ async function printStats(): Promise<void> {
|
|
|
44
48
|
console.log(` Total Tokens: ${formatNumber(overall.totalInputTokens + overall.totalOutputTokens)}`);
|
|
45
49
|
console.log(` Cache Rate: ${formatPercent(overall.cacheRate)}`);
|
|
46
50
|
console.log(` Total Cost: ${formatCost(overall.totalCost)}`);
|
|
51
|
+
console.log(` Premium Requests: ${formatNumber(normalizePremiumRequests(overall.totalPremiumRequests ?? 0))}`);
|
|
47
52
|
console.log(` Avg Duration: ${overall.avgDuration !== null ? formatDuration(overall.avgDuration) : "-"}`);
|
|
48
53
|
console.log(` Avg TTFT: ${overall.avgTtft !== null ? formatDuration(overall.avgTtft) : "-"}`);
|
|
49
54
|
if (overall.avgTokensPerSecond !== null) {
|
package/src/parser.ts
CHANGED
|
@@ -10,9 +10,11 @@ import type { MessageStats, SessionEntry, SessionMessageEntry } from "./types";
|
|
|
10
10
|
* The folder part uses -- as path separator.
|
|
11
11
|
*/
|
|
12
12
|
function extractFolderFromPath(sessionPath: string): string {
|
|
13
|
-
const
|
|
13
|
+
const sessionsDir = getSessionsDir();
|
|
14
|
+
const rel = path.relative(sessionsDir, sessionPath);
|
|
15
|
+
const projectDir = rel.split(path.sep)[0];
|
|
14
16
|
// Convert --work--pi-- to /work/pi
|
|
15
|
-
return
|
|
17
|
+
return projectDir.replace(/^--/, "/").replace(/--/g, "/");
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -99,8 +101,8 @@ export async function listSessionFolders(): Promise<string[]> {
|
|
|
99
101
|
*/
|
|
100
102
|
export async function listSessionFiles(folderPath: string): Promise<string[]> {
|
|
101
103
|
try {
|
|
102
|
-
const entries = await fs.readdir(folderPath, { withFileTypes: true });
|
|
103
|
-
return entries.filter(e => e.isFile() && e.name.endsWith(".jsonl")).map(e => path.join(
|
|
104
|
+
const entries = await fs.readdir(folderPath, { recursive: true, withFileTypes: true });
|
|
105
|
+
return entries.filter(e => e.isFile() && e.name.endsWith(".jsonl")).map(e => path.join(e.parentPath, e.name));
|
|
104
106
|
} catch {
|
|
105
107
|
return [];
|
|
106
108
|
}
|
package/src/types.ts
CHANGED