@oh-my-pi/omp-stats 14.5.3 → 14.5.5

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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/db.ts +108 -7
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/omp-stats",
4
- "version": "14.5.3",
4
+ "version": "14.5.5",
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",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-ai": "14.5.3",
41
- "@oh-my-pi/pi-utils": "14.5.3",
40
+ "@oh-my-pi/pi-ai": "14.5.5",
41
+ "@oh-my-pi/pi-utils": "14.5.5",
42
42
  "@tailwindcss/node": "^4.2.4",
43
43
  "chart.js": "^4.5.1",
44
44
  "date-fns": "^4.1.0",
package/src/db.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import * as fs from "node:fs/promises";
3
+ import { type GeneratedProvider, getBundledModel, type Usage } from "@oh-my-pi/pi-ai";
3
4
  import { getConfigRootDir, getStatsDbPath } from "@oh-my-pi/pi-utils";
4
5
  import type {
5
6
  AggregatedStats,
@@ -12,7 +13,19 @@ import type {
12
13
  TimeSeriesPoint,
13
14
  } from "./types";
14
15
 
15
- const DB_PATH = getStatsDbPath();
16
+ type ModelCost = { input: number; output: number; cacheRead: number; cacheWrite: number };
17
+ type UsageCost = Usage["cost"];
18
+ type CostTokens = Pick<Usage, "input" | "output" | "cacheRead" | "cacheWrite">;
19
+
20
+ interface CostBackfillRow {
21
+ id: number;
22
+ provider: string;
23
+ model: string;
24
+ input_tokens: number;
25
+ output_tokens: number;
26
+ cache_read_tokens: number;
27
+ cache_write_tokens: number;
28
+ }
16
29
 
17
30
  let db: Database | null = null;
18
31
 
@@ -25,7 +38,7 @@ export async function initDb(): Promise<Database> {
25
38
  // Ensure directory exists
26
39
  await fs.mkdir(getConfigRootDir(), { recursive: true });
27
40
 
28
- db = new Database(DB_PATH);
41
+ db = new Database(getStatsDbPath());
29
42
  db.exec("PRAGMA journal_mode = WAL");
30
43
 
31
44
  // Create tables
@@ -74,9 +87,96 @@ export async function initDb(): Promise<Database> {
74
87
  db.exec("ALTER TABLE messages ADD COLUMN premium_requests REAL NOT NULL DEFAULT 0");
75
88
  }
76
89
  db.exec("UPDATE messages SET premium_requests = 0 WHERE premium_requests IS NULL");
90
+ backfillMissingCatalogCosts(db);
77
91
  return db;
78
92
  }
79
93
 
94
+ function hasBillableCost(cost: ModelCost): boolean {
95
+ return cost.input !== 0 || cost.output !== 0 || cost.cacheRead !== 0 || cost.cacheWrite !== 0;
96
+ }
97
+
98
+ function getBundledModelCost(provider: string, modelId: string): ModelCost | null {
99
+ const model = getBundledModel(provider as GeneratedProvider, modelId);
100
+ return model?.cost ?? null;
101
+ }
102
+
103
+ function getCatalogCost(provider: string, modelId: string): ModelCost | null {
104
+ const primaryCost = getBundledModelCost(provider, modelId);
105
+ if (primaryCost && hasBillableCost(primaryCost)) {
106
+ return primaryCost;
107
+ }
108
+
109
+ if (provider === "openai-codex") {
110
+ const openAICost = getBundledModelCost("openai", modelId);
111
+ if (openAICost && hasBillableCost(openAICost)) {
112
+ return openAICost;
113
+ }
114
+ }
115
+
116
+ return null;
117
+ }
118
+
119
+ function calculateCatalogCost(provider: string, modelId: string, tokens: CostTokens): UsageCost | null {
120
+ const cost = getCatalogCost(provider, modelId);
121
+ if (!cost) return null;
122
+
123
+ const input = (cost.input / 1_000_000) * tokens.input;
124
+ const output = (cost.output / 1_000_000) * tokens.output;
125
+ const cacheRead = (cost.cacheRead / 1_000_000) * tokens.cacheRead;
126
+ const cacheWrite = (cost.cacheWrite / 1_000_000) * tokens.cacheWrite;
127
+
128
+ return {
129
+ input,
130
+ output,
131
+ cacheRead,
132
+ cacheWrite,
133
+ total: input + output + cacheRead + cacheWrite,
134
+ };
135
+ }
136
+
137
+ function resolveStoredCost(stats: MessageStats): UsageCost {
138
+ if (stats.usage.cost.total !== 0) {
139
+ return stats.usage.cost;
140
+ }
141
+
142
+ return calculateCatalogCost(stats.provider, stats.model, stats.usage) ?? stats.usage.cost;
143
+ }
144
+
145
+ function backfillMissingCatalogCosts(database: Database): void {
146
+ const rows = database
147
+ .prepare(`
148
+ SELECT id, provider, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens
149
+ FROM messages
150
+ WHERE cost_total = 0 AND total_tokens > 0
151
+ `)
152
+ .all() as CostBackfillRow[];
153
+
154
+ if (rows.length === 0) return;
155
+
156
+ const update = database.prepare(`
157
+ UPDATE messages
158
+ SET cost_input = ?, cost_output = ?, cost_cache_read = ?, cost_cache_write = ?, cost_total = ?
159
+ WHERE id = ?
160
+ `);
161
+
162
+ const applyBackfill = database.transaction(() => {
163
+ for (const row of rows) {
164
+ const cost = calculateCatalogCost(row.provider, row.model, {
165
+ input: row.input_tokens,
166
+ output: row.output_tokens,
167
+ cacheRead: row.cache_read_tokens,
168
+ cacheWrite: row.cache_write_tokens,
169
+ });
170
+
171
+ if (!cost || cost.total === 0) continue;
172
+
173
+ update.run(cost.input, cost.output, cost.cacheRead, cost.cacheWrite, cost.total, row.id);
174
+ }
175
+ });
176
+
177
+ applyBackfill();
178
+ }
179
+
80
180
  /**
81
181
  * Get the stored offset for a session file.
82
182
  */
@@ -120,6 +220,7 @@ export function insertMessageStats(stats: MessageStats[]): number {
120
220
  let inserted = 0;
121
221
  const insert = db.transaction(() => {
122
222
  for (const s of stats) {
223
+ const cost = resolveStoredCost(s);
123
224
  const result = stmt.run(
124
225
  s.sessionFile,
125
226
  s.entryId,
@@ -138,11 +239,11 @@ export function insertMessageStats(stats: MessageStats[]): number {
138
239
  s.usage.cacheWrite,
139
240
  s.usage.totalTokens,
140
241
  s.usage.premiumRequests ?? 0,
141
- s.usage.cost.input,
142
- s.usage.cost.output,
143
- s.usage.cost.cacheRead,
144
- s.usage.cost.cacheWrite,
145
- s.usage.cost.total,
242
+ cost.input,
243
+ cost.output,
244
+ cost.cacheRead,
245
+ cost.cacheWrite,
246
+ cost.total,
146
247
  );
147
248
  if (result.changes > 0) inserted++;
148
249
  }