@oh-my-pi/omp-stats 17.2.14 → 17.3.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/README.md +2 -1
- package/dist/client/index.css +1 -1
- package/dist/client/index.js +41 -41
- package/dist/client/styles.css +1 -4
- package/dist/types/shared-types.d.ts +6 -1
- package/package.json +4 -4
- package/src/client/routes/ModelsRoute.tsx +13 -3
- package/src/client/routes/ProjectsRoute.tsx +14 -2
- package/src/client/styles.css +1 -1
- package/src/client/ui/MetricCluster.tsx +11 -1
- package/src/db.ts +104 -8
- package/src/index.ts +2 -1
- package/src/shared-types.ts +6 -1
package/dist/client/styles.css
CHANGED
|
@@ -436,9 +436,6 @@
|
|
|
436
436
|
.whitespace-nowrap {
|
|
437
437
|
white-space: nowrap;
|
|
438
438
|
}
|
|
439
|
-
.text-\[var\(--accent-cyan\)\] {
|
|
440
|
-
color: var(--accent-cyan);
|
|
441
|
-
}
|
|
442
439
|
.text-\[var\(--text-muted\)\] {
|
|
443
440
|
color: var(--text-muted);
|
|
444
441
|
}
|
|
@@ -947,7 +944,7 @@
|
|
|
947
944
|
}
|
|
948
945
|
.stats-metric-primary-grid {
|
|
949
946
|
display: grid;
|
|
950
|
-
grid-template-columns: repeat(
|
|
947
|
+
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
951
948
|
gap: 16px;
|
|
952
949
|
}
|
|
953
950
|
@media (max-width: 1023px) {
|
|
@@ -24,8 +24,13 @@ export interface AggregatedStats {
|
|
|
24
24
|
totalCacheReadTokens: number;
|
|
25
25
|
/** Total cache write tokens */
|
|
26
26
|
totalCacheWriteTokens: number;
|
|
27
|
-
/**
|
|
27
|
+
/** Percentage of prompt input tokens served from cache (0-1). */
|
|
28
28
|
cacheRate: number;
|
|
29
|
+
/**
|
|
30
|
+
* Prompt-input cost saved relative to billing the same tokens uncached
|
|
31
|
+
* (0-1; negative when cache writes cost more than reads save).
|
|
32
|
+
*/
|
|
33
|
+
cacheSavings: number;
|
|
29
34
|
/** Total cost */
|
|
30
35
|
totalCost: number;
|
|
31
36
|
/** Total premium requests */
|
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.3.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.3.0",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "17.3.0",
|
|
44
|
+
"@oh-my-pi/pi-utils": "17.3.0",
|
|
45
45
|
"@tailwindcss/node": "^4.3.2",
|
|
46
46
|
"chart.js": "^4.5.1",
|
|
47
47
|
"lucide-react": "^1.24.0",
|
|
@@ -322,7 +322,7 @@ function ModelsTable({
|
|
|
322
322
|
<div className="grid gap-4" style={{ gridTemplateColumns: "200px 1fr" }}>
|
|
323
323
|
<div className="space-y-4 text-sm">
|
|
324
324
|
<div>
|
|
325
|
-
<div className="text-[var(--text-primary)] font-medium mb-2">
|
|
325
|
+
<div className="text-[var(--text-primary)] font-medium mb-2">Efficiency</div>
|
|
326
326
|
<div className="space-y-1 text-[var(--text-secondary)]">
|
|
327
327
|
<div className="flex items-center justify-between">
|
|
328
328
|
<span>Error rate</span>
|
|
@@ -336,8 +336,18 @@ function ModelsTable({
|
|
|
336
336
|
</div>
|
|
337
337
|
<div className="flex items-center justify-between">
|
|
338
338
|
<span>Cache rate</span>
|
|
339
|
-
<span className="
|
|
340
|
-
|
|
339
|
+
<span className="font-mono">{(model.cacheRate * 100).toFixed(1)}%</span>
|
|
340
|
+
</div>
|
|
341
|
+
<div className="flex items-center justify-between">
|
|
342
|
+
<span>Cache savings</span>
|
|
343
|
+
<span
|
|
344
|
+
className={
|
|
345
|
+
model.cacheSavings < 0
|
|
346
|
+
? "text-[var(--accent-red)]"
|
|
347
|
+
: "text-[var(--accent-green)]"
|
|
348
|
+
}
|
|
349
|
+
>
|
|
350
|
+
{(model.cacheSavings * 100).toFixed(1)}%
|
|
341
351
|
</span>
|
|
342
352
|
</div>
|
|
343
353
|
</div>
|
|
@@ -87,8 +87,16 @@ export function ProjectsRoute({ active, range, refreshTrigger }: ProjectsRoutePr
|
|
|
87
87
|
key: "cacheRate",
|
|
88
88
|
header: "Cache Rate",
|
|
89
89
|
numeric: true,
|
|
90
|
+
render: (item: FolderRowView) => <span className="font-mono">{formatPercent(item.cacheRate)}</span>,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: "cacheSavings",
|
|
94
|
+
header: "Cache Savings",
|
|
95
|
+
numeric: true,
|
|
90
96
|
render: (item: FolderRowView) => (
|
|
91
|
-
<span className="stats-text-success font-medium
|
|
97
|
+
<span className={`${item.cacheSavings < 0 ? "stats-text-danger" : "stats-text-success"} font-medium`}>
|
|
98
|
+
{formatPercent(item.cacheSavings)}
|
|
99
|
+
</span>
|
|
92
100
|
),
|
|
93
101
|
},
|
|
94
102
|
{
|
|
@@ -129,9 +137,13 @@ export function ProjectsRoute({ active, range, refreshTrigger }: ProjectsRoutePr
|
|
|
129
137
|
<div className="stats-mobile-card-value font-mono">{formatCost(item.totalCost)}</div>
|
|
130
138
|
</div>
|
|
131
139
|
<div>
|
|
132
|
-
<div className="stats-mobile-card-label">Cache</div>
|
|
140
|
+
<div className="stats-mobile-card-label">Cache Rate</div>
|
|
133
141
|
<div className="stats-mobile-card-value">{formatPercent(item.cacheRate)}</div>
|
|
134
142
|
</div>
|
|
143
|
+
<div>
|
|
144
|
+
<div className="stats-mobile-card-label">Cache Savings</div>
|
|
145
|
+
<div className="stats-mobile-card-value">{formatPercent(item.cacheSavings)}</div>
|
|
146
|
+
</div>
|
|
135
147
|
<div>
|
|
136
148
|
<div className="stats-mobile-card-label">Duration</div>
|
|
137
149
|
<div className="stats-mobile-card-value">{formatDurationMs(item.avgDuration)}</div>
|
package/src/client/styles.css
CHANGED
|
@@ -29,7 +29,17 @@ export function MetricCluster({ stats }: MetricClusterProps) {
|
|
|
29
29
|
<div className="stats-metric-label">Requests</div>
|
|
30
30
|
<div className="stats-metric-value">{formatInteger(stats.totalRequests)}</div>
|
|
31
31
|
</div>
|
|
32
|
-
<div
|
|
32
|
+
<div
|
|
33
|
+
className="stats-metric-card primary"
|
|
34
|
+
title="Prompt-input cost saved versus billing the same tokens uncached; cache writes can make this negative"
|
|
35
|
+
>
|
|
36
|
+
<div className="stats-metric-label">Cache Savings</div>
|
|
37
|
+
<div className="stats-metric-value">{formatPercent(stats.cacheSavings)}</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div
|
|
40
|
+
className="stats-metric-card primary"
|
|
41
|
+
title="Prompt input served from cache: cache reads / (uncached input + cache reads)"
|
|
42
|
+
>
|
|
33
43
|
<div className="stats-metric-label">Cache Rate</div>
|
|
34
44
|
<div className="stats-metric-value">{formatPercent(stats.cacheRate)}</div>
|
|
35
45
|
</div>
|
package/src/db.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Database } from "bun:sqlite";
|
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import type { GeneratedProvider } from "@oh-my-pi/pi-catalog/models";
|
|
5
|
-
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
|
5
|
+
import { calculateUncachedInputCost, getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
|
6
6
|
import { getConfigRootDir, getStatsDbPath } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { classifyAgentType } from "./parser";
|
|
8
8
|
import type {
|
|
@@ -33,7 +33,7 @@ import type {
|
|
|
33
33
|
|
|
34
34
|
type ModelCost = { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
35
35
|
type UsageCost = Usage["cost"];
|
|
36
|
-
type CostTokens = Pick<Usage, "input" | "output" | "cacheRead" | "cacheWrite">;
|
|
36
|
+
type CostTokens = Pick<Usage, "input" | "output" | "cacheRead" | "cacheWrite" | "orchestration">;
|
|
37
37
|
|
|
38
38
|
const ZERO_USAGE_COST: UsageCost = {
|
|
39
39
|
input: 0,
|
|
@@ -53,6 +53,42 @@ interface CostBackfillRow {
|
|
|
53
53
|
cache_write_tokens: number;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
interface NoCacheInputCostBackfillRow {
|
|
57
|
+
id: number;
|
|
58
|
+
provider: string;
|
|
59
|
+
model: string;
|
|
60
|
+
input_tokens: number;
|
|
61
|
+
cache_read_tokens: number;
|
|
62
|
+
cache_write_tokens: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface AggregatedStatsRow {
|
|
66
|
+
total_requests: number;
|
|
67
|
+
failed_requests: number | null;
|
|
68
|
+
total_input_tokens: number | null;
|
|
69
|
+
total_output_tokens: number | null;
|
|
70
|
+
total_cache_read_tokens: number | null;
|
|
71
|
+
total_cache_write_tokens: number | null;
|
|
72
|
+
total_premium_requests: number | null;
|
|
73
|
+
total_cost: number | null;
|
|
74
|
+
total_cached_prompt_cost: number | null;
|
|
75
|
+
total_no_cache_input_cost: number | null;
|
|
76
|
+
avg_duration: number | null;
|
|
77
|
+
avg_ttft: number | null;
|
|
78
|
+
avg_tokens_per_second: number | null;
|
|
79
|
+
first_timestamp: number | null;
|
|
80
|
+
last_timestamp: number | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface ModelStatsRow extends AggregatedStatsRow {
|
|
84
|
+
model: string;
|
|
85
|
+
provider: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface FolderStatsRow extends AggregatedStatsRow {
|
|
89
|
+
folder: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
56
92
|
let db: Database | null = null;
|
|
57
93
|
|
|
58
94
|
const BACKFILL_COMPLETE = "complete";
|
|
@@ -112,6 +148,7 @@ export async function initDb(): Promise<Database> {
|
|
|
112
148
|
cost_cache_read REAL NOT NULL,
|
|
113
149
|
cost_cache_write REAL NOT NULL,
|
|
114
150
|
cost_total REAL NOT NULL,
|
|
151
|
+
cost_no_cache_input REAL,
|
|
115
152
|
agent_type TEXT NOT NULL DEFAULT 'main',
|
|
116
153
|
UNIQUE(session_file, entry_id)
|
|
117
154
|
);
|
|
@@ -183,6 +220,9 @@ export async function initDb(): Promise<Database> {
|
|
|
183
220
|
if (!messageColumns.some(column => column.name === "premium_requests")) {
|
|
184
221
|
db.run("ALTER TABLE messages ADD COLUMN premium_requests REAL NOT NULL DEFAULT 0");
|
|
185
222
|
}
|
|
223
|
+
if (!messageColumns.some(column => column.name === "cost_no_cache_input")) {
|
|
224
|
+
db.run("ALTER TABLE messages ADD COLUMN cost_no_cache_input REAL");
|
|
225
|
+
}
|
|
186
226
|
db.run("UPDATE messages SET premium_requests = 0 WHERE premium_requests IS NULL");
|
|
187
227
|
// Token-usage-by-agent: each message is classified main / subagent / advisor
|
|
188
228
|
// from its transcript path. A brand-new table gets the column from CREATE
|
|
@@ -265,6 +305,7 @@ export async function initDb(): Promise<Database> {
|
|
|
265
305
|
backfillPriorityPremiumRequests(db);
|
|
266
306
|
backfillAgentType(db);
|
|
267
307
|
backfillMissingCatalogCosts(db);
|
|
308
|
+
backfillNoCacheInputCosts(db);
|
|
268
309
|
backfillForkDuplicates(db);
|
|
269
310
|
return db;
|
|
270
311
|
}
|
|
@@ -323,6 +364,18 @@ function resolveStoredCost(stats: MessageStats): UsageCost {
|
|
|
323
364
|
return calculateCatalogCost(stats.provider, stats.model, stats.usage) ?? storedCost ?? ZERO_USAGE_COST;
|
|
324
365
|
}
|
|
325
366
|
|
|
367
|
+
function calculateNoCacheInputCost(provider: string, modelId: string, tokens: CostTokens): number | null {
|
|
368
|
+
const cost = getCatalogCost(provider, modelId);
|
|
369
|
+
if (!cost) return null;
|
|
370
|
+
const promptInputTokens =
|
|
371
|
+
tokens.input +
|
|
372
|
+
tokens.cacheRead +
|
|
373
|
+
tokens.cacheWrite +
|
|
374
|
+
(tokens.orchestration?.input ?? 0) +
|
|
375
|
+
(tokens.orchestration?.cacheRead ?? 0);
|
|
376
|
+
return calculateUncachedInputCost(cost, promptInputTokens);
|
|
377
|
+
}
|
|
378
|
+
|
|
326
379
|
function backfillMissingCatalogCosts(database: Database): void {
|
|
327
380
|
const rows = database
|
|
328
381
|
.prepare(`
|
|
@@ -358,6 +411,31 @@ function backfillMissingCatalogCosts(database: Database): void {
|
|
|
358
411
|
applyBackfill();
|
|
359
412
|
}
|
|
360
413
|
|
|
414
|
+
function backfillNoCacheInputCosts(database: Database): void {
|
|
415
|
+
const rows = database
|
|
416
|
+
.prepare(`
|
|
417
|
+
SELECT id, provider, model, input_tokens, cache_read_tokens, cache_write_tokens
|
|
418
|
+
FROM messages
|
|
419
|
+
WHERE cost_no_cache_input IS NULL
|
|
420
|
+
`)
|
|
421
|
+
.all() as NoCacheInputCostBackfillRow[];
|
|
422
|
+
if (rows.length === 0) return;
|
|
423
|
+
|
|
424
|
+
const update = database.prepare("UPDATE messages SET cost_no_cache_input = ? WHERE id = ?");
|
|
425
|
+
const applyBackfill = database.transaction(() => {
|
|
426
|
+
for (const row of rows) {
|
|
427
|
+
const cost = calculateNoCacheInputCost(row.provider, row.model, {
|
|
428
|
+
input: row.input_tokens,
|
|
429
|
+
output: 0,
|
|
430
|
+
cacheRead: row.cache_read_tokens,
|
|
431
|
+
cacheWrite: row.cache_write_tokens,
|
|
432
|
+
});
|
|
433
|
+
update.run(cost ?? 0, row.id);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
applyBackfill();
|
|
437
|
+
}
|
|
438
|
+
|
|
361
439
|
/**
|
|
362
440
|
* Get the stored offset for a session file.
|
|
363
441
|
*/
|
|
@@ -406,9 +484,9 @@ export function insertMessageStats(stats: MessageStats[]): number {
|
|
|
406
484
|
session_file, entry_id, folder, model, provider, api, timestamp,
|
|
407
485
|
duration, ttft, stop_reason, error_message,
|
|
408
486
|
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_tokens, premium_requests,
|
|
409
|
-
cost_input, cost_output, cost_cache_read, cost_cache_write, cost_total, agent_type
|
|
487
|
+
cost_input, cost_output, cost_cache_read, cost_cache_write, cost_total, cost_no_cache_input, agent_type
|
|
410
488
|
)
|
|
411
|
-
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
489
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
412
490
|
WHERE NOT EXISTS (
|
|
413
491
|
SELECT 1 FROM messages
|
|
414
492
|
WHERE entry_id = ? AND timestamp = ? AND session_file <> ?
|
|
@@ -422,6 +500,7 @@ export function insertMessageStats(stats: MessageStats[]): number {
|
|
|
422
500
|
const insert = db.transaction(() => {
|
|
423
501
|
for (const s of stats) {
|
|
424
502
|
const cost = resolveStoredCost(s);
|
|
503
|
+
const noCacheInputCost = calculateNoCacheInputCost(s.provider, s.model, s.usage) ?? 0;
|
|
425
504
|
const result = stmt.run(
|
|
426
505
|
s.sessionFile,
|
|
427
506
|
s.entryId,
|
|
@@ -445,6 +524,7 @@ export function insertMessageStats(stats: MessageStats[]): number {
|
|
|
445
524
|
cost.cacheRead,
|
|
446
525
|
cost.cacheWrite,
|
|
447
526
|
cost.total,
|
|
527
|
+
noCacheInputCost,
|
|
448
528
|
s.agentType,
|
|
449
529
|
// `WHERE NOT EXISTS` binds: skip when a different session_file
|
|
450
530
|
// already holds this (entry_id, timestamp).
|
|
@@ -463,7 +543,7 @@ export function insertMessageStats(stats: MessageStats[]): number {
|
|
|
463
543
|
/**
|
|
464
544
|
* Build aggregated stats from query results.
|
|
465
545
|
*/
|
|
466
|
-
function buildAggregatedStats(rows:
|
|
546
|
+
function buildAggregatedStats(rows: AggregatedStatsRow[]): AggregatedStats {
|
|
467
547
|
if (rows.length === 0) {
|
|
468
548
|
return {
|
|
469
549
|
totalRequests: 0,
|
|
@@ -475,6 +555,7 @@ function buildAggregatedStats(rows: any[]): AggregatedStats {
|
|
|
475
555
|
totalCacheReadTokens: 0,
|
|
476
556
|
totalCacheWriteTokens: 0,
|
|
477
557
|
cacheRate: 0,
|
|
558
|
+
cacheSavings: 0,
|
|
478
559
|
totalCost: 0,
|
|
479
560
|
totalPremiumRequests: 0,
|
|
480
561
|
avgDuration: null,
|
|
@@ -492,6 +573,8 @@ function buildAggregatedStats(rows: any[]): AggregatedStats {
|
|
|
492
573
|
const totalInputTokens = row.total_input_tokens || 0;
|
|
493
574
|
const totalCacheReadTokens = row.total_cache_read_tokens || 0;
|
|
494
575
|
const totalPremiumRequests = row.total_premium_requests || 0;
|
|
576
|
+
const noCacheInputCost = row.total_no_cache_input_cost || 0;
|
|
577
|
+
const cachedPromptCost = row.total_cached_prompt_cost || 0;
|
|
495
578
|
|
|
496
579
|
return {
|
|
497
580
|
totalRequests,
|
|
@@ -506,6 +589,7 @@ function buildAggregatedStats(rows: any[]): AggregatedStats {
|
|
|
506
589
|
totalInputTokens + totalCacheReadTokens > 0
|
|
507
590
|
? totalCacheReadTokens / (totalInputTokens + totalCacheReadTokens)
|
|
508
591
|
: 0,
|
|
592
|
+
cacheSavings: noCacheInputCost > 0 ? (noCacheInputCost - cachedPromptCost) / noCacheInputCost : 0,
|
|
509
593
|
totalCost: row.total_cost || 0,
|
|
510
594
|
totalPremiumRequests,
|
|
511
595
|
avgDuration: row.avg_duration,
|
|
@@ -533,6 +617,10 @@ export function getOverallStats(cutoff?: number): AggregatedStats {
|
|
|
533
617
|
SUM(cache_write_tokens) as total_cache_write_tokens,
|
|
534
618
|
SUM(premium_requests) as total_premium_requests,
|
|
535
619
|
SUM(cost_total) as total_cost,
|
|
620
|
+
SUM(CASE WHEN cost_no_cache_input > 0
|
|
621
|
+
THEN cost_input + cost_cache_read + cost_cache_write
|
|
622
|
+
ELSE 0 END) as total_cached_prompt_cost,
|
|
623
|
+
SUM(cost_no_cache_input) as total_no_cache_input_cost,
|
|
536
624
|
AVG(duration) as avg_duration,
|
|
537
625
|
AVG(ttft) as avg_ttft,
|
|
538
626
|
AVG(CASE WHEN duration > 0 THEN output_tokens * 1000.0 / duration ELSE NULL END) as avg_tokens_per_second,
|
|
@@ -543,7 +631,7 @@ export function getOverallStats(cutoff?: number): AggregatedStats {
|
|
|
543
631
|
`);
|
|
544
632
|
|
|
545
633
|
const rows = hasCutoff ? stmt.all(cutoff) : stmt.all();
|
|
546
|
-
return buildAggregatedStats(rows as
|
|
634
|
+
return buildAggregatedStats(rows as AggregatedStatsRow[]);
|
|
547
635
|
}
|
|
548
636
|
/**
|
|
549
637
|
* Get stats grouped by model.
|
|
@@ -564,6 +652,10 @@ export function getStatsByModel(cutoff?: number): ModelStats[] {
|
|
|
564
652
|
SUM(cache_write_tokens) as total_cache_write_tokens,
|
|
565
653
|
SUM(premium_requests) as total_premium_requests,
|
|
566
654
|
SUM(cost_total) as total_cost,
|
|
655
|
+
SUM(CASE WHEN cost_no_cache_input > 0
|
|
656
|
+
THEN cost_input + cost_cache_read + cost_cache_write
|
|
657
|
+
ELSE 0 END) as total_cached_prompt_cost,
|
|
658
|
+
SUM(cost_no_cache_input) as total_no_cache_input_cost,
|
|
567
659
|
AVG(duration) as avg_duration,
|
|
568
660
|
AVG(ttft) as avg_ttft,
|
|
569
661
|
AVG(CASE WHEN duration > 0 THEN output_tokens * 1000.0 / duration ELSE NULL END) as avg_tokens_per_second,
|
|
@@ -575,7 +667,7 @@ export function getStatsByModel(cutoff?: number): ModelStats[] {
|
|
|
575
667
|
ORDER BY total_requests DESC
|
|
576
668
|
`);
|
|
577
669
|
|
|
578
|
-
const rows = (hasCutoff ? stmt.all(cutoff) : stmt.all()) as
|
|
670
|
+
const rows = (hasCutoff ? stmt.all(cutoff) : stmt.all()) as ModelStatsRow[];
|
|
579
671
|
return rows.map(row => ({
|
|
580
672
|
model: row.model,
|
|
581
673
|
provider: row.provider,
|
|
@@ -601,6 +693,10 @@ export function getStatsByFolder(cutoff?: number): FolderStats[] {
|
|
|
601
693
|
SUM(cache_write_tokens) as total_cache_write_tokens,
|
|
602
694
|
SUM(premium_requests) as total_premium_requests,
|
|
603
695
|
SUM(cost_total) as total_cost,
|
|
696
|
+
SUM(CASE WHEN cost_no_cache_input > 0
|
|
697
|
+
THEN cost_input + cost_cache_read + cost_cache_write
|
|
698
|
+
ELSE 0 END) as total_cached_prompt_cost,
|
|
699
|
+
SUM(cost_no_cache_input) as total_no_cache_input_cost,
|
|
604
700
|
AVG(duration) as avg_duration,
|
|
605
701
|
AVG(ttft) as avg_ttft,
|
|
606
702
|
AVG(CASE WHEN duration > 0 THEN output_tokens * 1000.0 / duration ELSE NULL END) as avg_tokens_per_second,
|
|
@@ -612,7 +708,7 @@ export function getStatsByFolder(cutoff?: number): FolderStats[] {
|
|
|
612
708
|
ORDER BY total_requests DESC
|
|
613
709
|
`);
|
|
614
710
|
|
|
615
|
-
const rows = (hasCutoff ? stmt.all(cutoff) : stmt.all()) as
|
|
711
|
+
const rows = (hasCutoff ? stmt.all(cutoff) : stmt.all()) as FolderStatsRow[];
|
|
616
712
|
return rows.map(row => ({
|
|
617
713
|
folder: row.folder,
|
|
618
714
|
...buildAggregatedStats([row]),
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,7 @@ async function printStats(): Promise<void> {
|
|
|
68
68
|
console.log(` Input Tokens: ${formatNumber(overall.totalInputTokens)}`);
|
|
69
69
|
console.log(` Output Tokens: ${formatNumber(overall.totalOutputTokens)}`);
|
|
70
70
|
console.log(` Cache Rate: ${formatPercent(overall.cacheRate)}`);
|
|
71
|
+
console.log(` Cache Savings: ${formatPercent(overall.cacheSavings)}`);
|
|
71
72
|
console.log(` Total Cost: ${formatCost(overall.totalCost)}`);
|
|
72
73
|
console.log(` Premium Requests: ${formatNumber(normalizePremiumRequests(overall.totalPremiumRequests ?? 0))}`);
|
|
73
74
|
console.log(` Avg Duration: ${overall.avgDuration !== null ? formatDuration(overall.avgDuration) : "-"}`);
|
|
@@ -80,7 +81,7 @@ async function printStats(): Promise<void> {
|
|
|
80
81
|
console.log("\nBy Model:");
|
|
81
82
|
for (const m of byModel.slice(0, 10)) {
|
|
82
83
|
console.log(
|
|
83
|
-
` ${m.model}: ${formatNumber(m.totalRequests)} reqs, ${formatCost(m.totalCost)}, ${formatPercent(m.cacheRate)} cache`,
|
|
84
|
+
` ${m.model}: ${formatNumber(m.totalRequests)} reqs, ${formatCost(m.totalCost)}, ${formatPercent(m.cacheRate)} cache rate, ${formatPercent(m.cacheSavings)} cache savings`,
|
|
84
85
|
);
|
|
85
86
|
}
|
|
86
87
|
}
|
package/src/shared-types.ts
CHANGED
|
@@ -25,8 +25,13 @@ export interface AggregatedStats {
|
|
|
25
25
|
totalCacheReadTokens: number;
|
|
26
26
|
/** Total cache write tokens */
|
|
27
27
|
totalCacheWriteTokens: number;
|
|
28
|
-
/**
|
|
28
|
+
/** Percentage of prompt input tokens served from cache (0-1). */
|
|
29
29
|
cacheRate: number;
|
|
30
|
+
/**
|
|
31
|
+
* Prompt-input cost saved relative to billing the same tokens uncached
|
|
32
|
+
* (0-1; negative when cache writes cost more than reads save).
|
|
33
|
+
*/
|
|
34
|
+
cacheSavings: number;
|
|
30
35
|
/** Total cost */
|
|
31
36
|
totalCost: number;
|
|
32
37
|
/** Total premium requests */
|