@oh-my-pi/omp-stats 18.1.16 → 18.1.18
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/dist/client/index.js +93 -93
- package/dist/types/client/data/formatters.d.ts +7 -2
- package/dist/types/client/types.d.ts +2 -0
- package/dist/types/db.d.ts +5 -2
- package/dist/types/parser.d.ts +28 -2
- package/dist/types/shared-types.d.ts +4 -0
- package/dist/types/trace.d.ts +1 -1
- package/dist/types/types.d.ts +21 -0
- package/package.json +4 -4
- package/src/client/data/formatters.ts +15 -3
- package/src/client/routes/TracesRoute.tsx +10 -3
- package/src/client/traces/SummaryStrip.tsx +2 -2
- package/src/client/types.ts +2 -0
- package/src/db.ts +181 -46
- package/src/parser.ts +80 -18
- package/src/shared-types.ts +4 -0
- package/src/trace.ts +52 -8
- package/src/types.ts +20 -0
package/src/trace.ts
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
import * as fs from "node:fs/promises";
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
+
import { getBundledModel, type GeneratedProvider } from "@oh-my-pi/pi-catalog/models";
|
|
14
15
|
import { getSessionsDir, isEnoent } from "@oh-my-pi/pi-utils";
|
|
15
|
-
import { getSessionRollups, getToolCallCountsBySession } from "./db";
|
|
16
|
-
import { extractFolderFromPath, parseAllSessionEntries } from "./parser";
|
|
16
|
+
import { getSessionRollups, getToolCallCountsBySession, isScheduledCatalogModel } from "./db";
|
|
17
|
+
import { extractFolderFromPath, parseAllSessionEntries, resolveUsageTotal } from "./parser";
|
|
17
18
|
import type {
|
|
18
19
|
SessionEntry,
|
|
19
20
|
SessionSummary,
|
|
@@ -32,7 +33,7 @@ export class TracePathError extends Error {}
|
|
|
32
33
|
* span assembly changes so browsers don't revalidate stale cached bodies
|
|
33
34
|
* against an unchanged transcript mtime.
|
|
34
35
|
*/
|
|
35
|
-
export const TRACE_ETAG_VERSION =
|
|
36
|
+
export const TRACE_ETAG_VERSION = 3;
|
|
36
37
|
|
|
37
38
|
const MAX_TRACK_DEPTH = 6;
|
|
38
39
|
const LABEL_MAX = 80;
|
|
@@ -84,7 +85,15 @@ interface MessageView {
|
|
|
84
85
|
completedAt?: number;
|
|
85
86
|
duration?: number;
|
|
86
87
|
ttft?: number;
|
|
87
|
-
usage?: {
|
|
88
|
+
usage?: {
|
|
89
|
+
input?: unknown;
|
|
90
|
+
output?: unknown;
|
|
91
|
+
cacheRead?: unknown;
|
|
92
|
+
cacheWrite?: unknown;
|
|
93
|
+
totalTokens?: unknown;
|
|
94
|
+
orchestration?: { input?: unknown; output?: unknown; cacheRead?: unknown };
|
|
95
|
+
cost?: { total?: unknown };
|
|
96
|
+
};
|
|
88
97
|
model?: string;
|
|
89
98
|
provider?: string;
|
|
90
99
|
stopReason?: string;
|
|
@@ -115,13 +124,14 @@ interface TaskResultFact {
|
|
|
115
124
|
resultEntryId: string;
|
|
116
125
|
toolSpanEnd: number;
|
|
117
126
|
}
|
|
118
|
-
|
|
119
127
|
/** Everything one transcript contributes before cross-track assembly. */
|
|
120
128
|
interface TrackScan {
|
|
121
129
|
track: TraceTrack;
|
|
122
130
|
taskResults: TaskResultFact[];
|
|
123
131
|
requests: number;
|
|
124
132
|
toolCalls: number;
|
|
133
|
+
/** Model requests whose zero cost is unknown spend (mirrors db's unpriced predicate). */
|
|
134
|
+
unpricedRequests: number;
|
|
125
135
|
cwd: string | null;
|
|
126
136
|
title: string | null;
|
|
127
137
|
/** Session-header envelope timestamp (ms), for spanless traces. */
|
|
@@ -284,6 +294,7 @@ function scanTranscript(
|
|
|
284
294
|
let initModel: string | null = null;
|
|
285
295
|
let firstAssistantModel: string | null = null;
|
|
286
296
|
let requests = 0;
|
|
297
|
+
let unpricedRequests = 0;
|
|
287
298
|
let lastChainTs = 0;
|
|
288
299
|
|
|
289
300
|
// Title slot and session header may sit outside the id chain — scan all entries.
|
|
@@ -358,6 +369,32 @@ function scanTranscript(
|
|
|
358
369
|
|
|
359
370
|
if (msg.role === "assistant") {
|
|
360
371
|
requests++;
|
|
372
|
+
// Mirror the db's unpriced predicate so the trace headline agrees
|
|
373
|
+
// with Costs/Recent Requests: a zero that is unknown spend, not free.
|
|
374
|
+
// Counted before the span-timing `continue` below so a fully
|
|
375
|
+
// dateless entry still surfaces as N/A rather than vanishing.
|
|
376
|
+
// Derive a missing total from the buckets via the shared ingest
|
|
377
|
+
// helper, so a legacy entry with tokens but no total still
|
|
378
|
+
// classifies as unpriced.
|
|
379
|
+
const totalTokens = resolveUsageTotal(msg.usage);
|
|
380
|
+
const costObj = msg.usage?.cost;
|
|
381
|
+
const costTotal =
|
|
382
|
+
typeof costObj?.total === "number" && Number.isFinite(costObj.total) ? costObj.total : undefined;
|
|
383
|
+
if (totalTokens > 0 && (costTotal === undefined || costTotal === 0)) {
|
|
384
|
+
const provider = typeof msg.provider === "string" ? msg.provider : "";
|
|
385
|
+
const model = typeof msg.model === "string" ? msg.model : "";
|
|
386
|
+
if (provider === "xai-oauth" && model) {
|
|
387
|
+
const ref = getBundledModel("xai" as GeneratedProvider, model)?.cost;
|
|
388
|
+
if (!(ref && (ref.input !== 0 || ref.output !== 0 || ref.cacheRead !== 0 || ref.cacheWrite !== 0))) {
|
|
389
|
+
unpricedRequests++;
|
|
390
|
+
}
|
|
391
|
+
} else if (costObj == null && provider && model) {
|
|
392
|
+
const hasTimestamp =
|
|
393
|
+
(typeof msg.timestamp === "number" && Number.isFinite(msg.timestamp) && msg.timestamp > 0) ||
|
|
394
|
+
envelopeTs !== undefined;
|
|
395
|
+
if (!hasTimestamp && isScheduledCatalogModel(provider, model)) unpricedRequests++;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
361
398
|
let start = typeof msg.timestamp === "number" && Number.isFinite(msg.timestamp) ? msg.timestamp : undefined;
|
|
362
399
|
let end =
|
|
363
400
|
typeof msg.completedAt === "number" && Number.isFinite(msg.completedAt)
|
|
@@ -386,12 +423,12 @@ function scanTranscript(
|
|
|
386
423
|
if (detail) span.detail = detail;
|
|
387
424
|
if (entry.id) span.entryId = entry.id;
|
|
388
425
|
if (typeof msg.model === "string") span.model = msg.model;
|
|
389
|
-
if (
|
|
390
|
-
|
|
426
|
+
if (totalTokens > 0) span.tokens = totalTokens;
|
|
427
|
+
|
|
428
|
+
if (costTotal !== undefined) span.cost = costTotal;
|
|
391
429
|
if (typeof msg.ttft === "number") span.ttft = msg.ttft;
|
|
392
430
|
if (msg.stopReason === "error" || msg.errorMessage) span.isError = true;
|
|
393
431
|
spans.push(span);
|
|
394
|
-
|
|
395
432
|
if (Array.isArray(msg.content)) {
|
|
396
433
|
for (const block of msg.content) {
|
|
397
434
|
if (!block || typeof block !== "object" || !("type" in block) || block.type !== "toolCall") continue;
|
|
@@ -572,6 +609,7 @@ function scanTranscript(
|
|
|
572
609
|
taskResults,
|
|
573
610
|
requests,
|
|
574
611
|
toolCalls: pendingTools.length,
|
|
612
|
+
unpricedRequests,
|
|
575
613
|
cwd,
|
|
576
614
|
title: slotTitle ?? headerTitle,
|
|
577
615
|
headerTs,
|
|
@@ -722,11 +760,13 @@ export async function buildSessionTrace(fileParam: string): Promise<SessionTrace
|
|
|
722
760
|
let requests = 0;
|
|
723
761
|
let totalTokens = 0;
|
|
724
762
|
let costTotal = 0;
|
|
763
|
+
let unpricedRequests = 0;
|
|
725
764
|
const toolStats = new Map<string, TraceToolStat>();
|
|
726
765
|
|
|
727
766
|
for (const scan of scans) {
|
|
728
767
|
requests += scan.requests;
|
|
729
768
|
toolCalls += scan.toolCalls;
|
|
769
|
+
unpricedRequests += scan.unpricedRequests;
|
|
730
770
|
for (const span of scan.track.spans) {
|
|
731
771
|
intervals.push([span.start, span.end]);
|
|
732
772
|
if (startedAt === undefined || span.start < startedAt) startedAt = span.start;
|
|
@@ -779,6 +819,7 @@ export async function buildSessionTrace(fileParam: string): Promise<SessionTrace
|
|
|
779
819
|
subagents: tracks.length - 1,
|
|
780
820
|
totalTokens,
|
|
781
821
|
costTotal,
|
|
822
|
+
unpricedRequests,
|
|
782
823
|
toolStats: [...toolStats.values()].sort((a, b) => b.totalMs - a.totalMs),
|
|
783
824
|
};
|
|
784
825
|
|
|
@@ -936,6 +977,7 @@ export async function listSessionSummaries(limit = 100, q?: string): Promise<Ses
|
|
|
936
977
|
subagents: 0,
|
|
937
978
|
totalTokens: 0,
|
|
938
979
|
costTotal: 0,
|
|
980
|
+
unpricedRequests: 0,
|
|
939
981
|
models: [],
|
|
940
982
|
modelSet: new Set(),
|
|
941
983
|
};
|
|
@@ -948,6 +990,7 @@ export async function listSessionSummaries(limit = 100, q?: string): Promise<Ses
|
|
|
948
990
|
if (row.endedAt > fold.endedAt) fold.endedAt = row.endedAt;
|
|
949
991
|
fold.totalTokens += row.totalTokens ?? 0;
|
|
950
992
|
fold.costTotal += row.costTotal ?? 0;
|
|
993
|
+
fold.unpricedRequests += row.unpricedRequests ?? 0;
|
|
951
994
|
if (row.models) {
|
|
952
995
|
for (const model of row.models.split(",")) {
|
|
953
996
|
if (model) fold.modelSet.add(model);
|
|
@@ -975,6 +1018,7 @@ export async function listSessionSummaries(limit = 100, q?: string): Promise<Ses
|
|
|
975
1018
|
subagents: 0,
|
|
976
1019
|
totalTokens: 0,
|
|
977
1020
|
costTotal: 0,
|
|
1021
|
+
unpricedRequests: 0,
|
|
978
1022
|
models: [],
|
|
979
1023
|
modelSet: new Set(),
|
|
980
1024
|
});
|
package/src/types.ts
CHANGED
|
@@ -35,6 +35,26 @@ export interface MessageStats {
|
|
|
35
35
|
usage: Usage;
|
|
36
36
|
/** Which agent produced this message (main agent, task subagent, advisor) */
|
|
37
37
|
agentType: AgentType;
|
|
38
|
+
/**
|
|
39
|
+
* Ingest refused to price this request: a scheduled (time-based) card with no
|
|
40
|
+
* recoverable request timestamp, so `usage.cost.total` of 0 is unknown spend
|
|
41
|
+
* rather than a free request. Always written by `rowToMessageStats`; optional
|
|
42
|
+
* only because session fixtures that stand in for ingest input are typed as
|
|
43
|
+
* `MessageStats` too.
|
|
44
|
+
*/
|
|
45
|
+
costUnpriced?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Session-recorded token usage before pricing: counters are coerced to numbers
|
|
50
|
+
* because their columns are NOT NULL, but `cost` is only present when the
|
|
51
|
+
* session entry actually recorded one. Absence is meaningful — `resolveStoredCost`
|
|
52
|
+
* estimates a request with no recorded price, whereas a recorded zero is a real
|
|
53
|
+
* charge that scheduled cards freeze. `costUnpriced` is omitted because it is a
|
|
54
|
+
* decision of the ingest path, not something the parser or a caller supplies.
|
|
55
|
+
*/
|
|
56
|
+
export interface MessageStatsInput extends Omit<MessageStats, "usage" | "costUnpriced"> {
|
|
57
|
+
usage: Omit<Usage, "cost"> & { cost?: Partial<Usage["cost"]> };
|
|
38
58
|
}
|
|
39
59
|
|
|
40
60
|
/**
|