@oh-my-pi/omp-stats 16.4.6 → 16.4.8
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 +6 -0
- package/dist/client/index.js +1 -1
- package/package.json +4 -4
- package/src/parser.ts +52 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "16.4.
|
|
4
|
+
"version": "16.4.8",
|
|
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": "16.4.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "16.4.
|
|
44
|
-
"@oh-my-pi/pi-utils": "16.4.
|
|
42
|
+
"@oh-my-pi/pi-ai": "16.4.8",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "16.4.8",
|
|
44
|
+
"@oh-my-pi/pi-utils": "16.4.8",
|
|
45
45
|
"@tailwindcss/node": "^4.3.0",
|
|
46
46
|
"chart.js": "^4.5.1",
|
|
47
47
|
"date-fns": "^4.4.0",
|
package/src/parser.ts
CHANGED
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
getPriorityPremiumRequests,
|
|
7
7
|
resolveModelServiceTier,
|
|
8
8
|
type ServiceTierByFamily,
|
|
9
|
+
type ToolCall,
|
|
9
10
|
type ToolResultMessage,
|
|
11
|
+
type Usage,
|
|
10
12
|
} from "@oh-my-pi/pi-ai";
|
|
11
13
|
import { getSessionsDir, isEnoent, readLines } from "@oh-my-pi/pi-utils";
|
|
12
14
|
import type {
|
|
@@ -142,6 +144,14 @@ function extractUserStats(sessionFile: string, folder: string, entry: SessionMes
|
|
|
142
144
|
|
|
143
145
|
/**
|
|
144
146
|
* Extract stats from an assistant message entry.
|
|
147
|
+
*
|
|
148
|
+
* Session JSONL on disk is not guaranteed to match the current
|
|
149
|
+
* `AssistantMessage` shape: crash-truncated turns, sessions written by older
|
|
150
|
+
* versions, and foreign producers all flow through this parser. Every field
|
|
151
|
+
* returned here feeds a NOT NULL column in stats.db, so malformed entries are
|
|
152
|
+
* coerced (missing `stopReason`, token counts, `timestamp`) or skipped
|
|
153
|
+
* (missing `model`/`provider`/`api`/`usage`) instead of crashing the whole
|
|
154
|
+
* sync with a constraint violation.
|
|
145
155
|
*/
|
|
146
156
|
function extractStats(
|
|
147
157
|
sessionFile: string,
|
|
@@ -152,6 +162,9 @@ function extractStats(
|
|
|
152
162
|
): MessageStats | null {
|
|
153
163
|
const msg = entry.message as AssistantMessage;
|
|
154
164
|
if (msg?.role !== "assistant") return null;
|
|
165
|
+
if (typeof msg.model !== "string" || typeof msg.provider !== "string" || typeof msg.api !== "string") return null;
|
|
166
|
+
const rawUsage = msg.usage as Partial<Usage> | undefined;
|
|
167
|
+
if (!rawUsage || typeof rawUsage !== "object") return null;
|
|
155
168
|
|
|
156
169
|
// Backfill: when the session recorded `priority` as the active service tier
|
|
157
170
|
// at this point but the AI usage payload was captured before priority
|
|
@@ -159,11 +172,29 @@ function extractStats(
|
|
|
159
172
|
// "Premium Reqs" stat aggregates priority traffic on re-sync. Trust any
|
|
160
173
|
// non-zero value already in `usage.premiumRequests` (Copilot multipliers or
|
|
161
174
|
// the new AI code path) and only synthesise when the field is missing/zero.
|
|
162
|
-
const recorded =
|
|
175
|
+
const recorded = rawUsage.premiumRequests ?? 0;
|
|
163
176
|
const model = { provider: msg.provider, api: msg.api, id: msg.model };
|
|
164
177
|
const tier = resolveModelServiceTier(currentServiceTier, model);
|
|
165
178
|
const derived = recorded > 0 ? recorded : getPriorityPremiumRequests(tier, model);
|
|
166
|
-
const
|
|
179
|
+
const wellFormed =
|
|
180
|
+
typeof rawUsage.input === "number" &&
|
|
181
|
+
typeof rawUsage.output === "number" &&
|
|
182
|
+
typeof rawUsage.cacheRead === "number" &&
|
|
183
|
+
typeof rawUsage.cacheWrite === "number" &&
|
|
184
|
+
typeof rawUsage.totalTokens === "number";
|
|
185
|
+
const usage: Usage =
|
|
186
|
+
wellFormed && derived === recorded
|
|
187
|
+
? (rawUsage as Usage)
|
|
188
|
+
: {
|
|
189
|
+
...rawUsage,
|
|
190
|
+
input: rawUsage.input ?? 0,
|
|
191
|
+
output: rawUsage.output ?? 0,
|
|
192
|
+
cacheRead: rawUsage.cacheRead ?? 0,
|
|
193
|
+
cacheWrite: rawUsage.cacheWrite ?? 0,
|
|
194
|
+
totalTokens: rawUsage.totalTokens ?? 0,
|
|
195
|
+
cost: rawUsage.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
196
|
+
premiumRequests: derived,
|
|
197
|
+
};
|
|
167
198
|
|
|
168
199
|
return {
|
|
169
200
|
sessionFile,
|
|
@@ -172,16 +203,25 @@ function extractStats(
|
|
|
172
203
|
model: msg.model,
|
|
173
204
|
provider: msg.provider,
|
|
174
205
|
api: msg.api,
|
|
175
|
-
timestamp: msg.timestamp,
|
|
206
|
+
timestamp: coerceEntryTimestamp(msg.timestamp, entry),
|
|
176
207
|
duration: msg.duration ?? null,
|
|
177
208
|
ttft: msg.ttft ?? null,
|
|
178
|
-
|
|
209
|
+
// A message persisted without a terminal stop reason never completed
|
|
210
|
+
// normally: classify by whether it carried an error.
|
|
211
|
+
stopReason: msg.stopReason ?? (msg.errorMessage ? "error" : "aborted"),
|
|
179
212
|
errorMessage: msg.errorMessage ?? null,
|
|
180
213
|
usage,
|
|
181
214
|
agentType,
|
|
182
215
|
};
|
|
183
216
|
}
|
|
184
217
|
|
|
218
|
+
/** Message timestamp, falling back to the entry's ISO timestamp, then 0. */
|
|
219
|
+
function coerceEntryTimestamp(timestamp: number | undefined, entry: SessionMessageEntry): number {
|
|
220
|
+
if (typeof timestamp === "number" && Number.isFinite(timestamp)) return timestamp;
|
|
221
|
+
const ts = Date.parse(entry.timestamp);
|
|
222
|
+
return Number.isFinite(ts) ? ts : 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
185
225
|
/**
|
|
186
226
|
* Extract one {@link ToolCallStats} per `toolCall` content block of an
|
|
187
227
|
* assistant message. Returns an empty array for turns without tool calls.
|
|
@@ -194,8 +234,14 @@ function extractToolCalls(
|
|
|
194
234
|
): ToolCallStats[] {
|
|
195
235
|
const msg = entry.message as AssistantMessage;
|
|
196
236
|
if (msg?.role !== "assistant" || !Array.isArray(msg.content)) return [];
|
|
237
|
+
// `tool_calls` columns are NOT NULL: skip turns that can't be attributed
|
|
238
|
+
// (malformed persisted entries — see extractStats) and blocks missing ids.
|
|
239
|
+
if (typeof msg.model !== "string" || typeof msg.provider !== "string") return [];
|
|
197
240
|
|
|
198
|
-
const blocks = msg.content.filter(
|
|
241
|
+
const blocks = msg.content.filter(
|
|
242
|
+
(block): block is ToolCall =>
|
|
243
|
+
block.type === "toolCall" && typeof block.id === "string" && typeof block.name === "string",
|
|
244
|
+
);
|
|
199
245
|
if (blocks.length === 0) return [];
|
|
200
246
|
|
|
201
247
|
return blocks.map(block => {
|
|
@@ -213,7 +259,7 @@ function extractToolCalls(
|
|
|
213
259
|
toolName: block.name,
|
|
214
260
|
model: msg.model,
|
|
215
261
|
provider: msg.provider,
|
|
216
|
-
timestamp: msg.timestamp,
|
|
262
|
+
timestamp: coerceEntryTimestamp(msg.timestamp, entry),
|
|
217
263
|
agentType,
|
|
218
264
|
callsInTurn: blocks.length,
|
|
219
265
|
argsChars,
|