@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/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 = 2;
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?: { totalTokens?: number; cost?: { total?: number } };
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 (typeof msg.usage?.totalTokens === "number") span.tokens = msg.usage.totalTokens;
390
- if (typeof msg.usage?.cost?.total === "number") span.cost = msg.usage.cost.total;
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
  /**