@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.
@@ -4,8 +4,13 @@ export declare function formatCompact(value: number): string;
4
4
  export declare function formatCost(value: number, digits?: number): string;
5
5
  /** Format an API-equivalent estimate, using N/A when all usage is unpriced. */
6
6
  export declare function formatEstimatedCost(value: number, unpricedRequests: number, digits?: number): string;
7
- /** Format one request's cost, distinguishing unpriced SuperGrok usage from free usage. */
8
- export declare function formatMessageCost(message: Pick<MessageStats, "provider" | "usage">, digits?: number): string;
7
+ /**
8
+ * Format one request's cost, distinguishing unpriced usage from free usage.
9
+ * Mirrors the server's `unpricedRequestSql`: `xai-oauth` is billed through a
10
+ * subscription, and `costUnpriced` marks a row the ingest path could not price
11
+ * (a scheduled card with no recoverable request timestamp).
12
+ */
13
+ export declare function formatMessageCost(message: Pick<MessageStats, "provider" | "usage" | "costUnpriced">, digits?: number): string;
9
14
  export declare function formatPercent(value: number, digits?: number): string;
10
15
  export declare function formatDurationMs(value: number | null, digits?: number): string;
11
16
  export declare function formatTokensPerSecond(value: number | null): string;
@@ -42,6 +42,8 @@ export interface MessageStats {
42
42
  stopReason: string;
43
43
  errorMessage: string | null;
44
44
  usage: Usage;
45
+ /** Ingest found no tariff to price this request from; its zero is unknown spend. */
46
+ costUnpriced?: boolean;
45
47
  }
46
48
  export interface RequestDetails extends MessageStats {
47
49
  messages: unknown[];
@@ -1,9 +1,11 @@
1
1
  import { Database } from "bun:sqlite";
2
- import type { AgentTypeStats, AggregatedStats, BehaviorModelStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, DailyActivityPoint, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, ProviderAggregate, ProviderHourlyPoint, ProviderTimeSeriesPoint, TimeSeriesPoint, ToolCallStats, ToolModelStats, ToolResultLink, ToolTimeSeriesPoint, ToolUsageStats, UserMessageLink, UserMessageStats } from "./types.js";
2
+ import type { AgentTypeStats, AggregatedStats, BehaviorModelStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, DailyActivityPoint, FolderStats, MessageStats, MessageStatsInput, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, ProviderAggregate, ProviderHourlyPoint, ProviderTimeSeriesPoint, TimeSeriesPoint, ToolCallStats, ToolModelStats, ToolResultLink, ToolTimeSeriesPoint, ToolUsageStats, UserMessageLink, UserMessageStats } from "./types.js";
3
3
  /**
4
4
  * Initialize the database and create tables.
5
5
  */
6
6
  export declare function initDb(): Promise<Database>;
7
+ /** Whether the catalog prices this model on a time-based (scheduled) card. */
8
+ export declare function isScheduledCatalogModel(provider: string, modelId: string): boolean;
7
9
  /**
8
10
  * Get the stored offset for a session file.
9
11
  */
@@ -31,7 +33,7 @@ export declare function setFileOffset(sessionFile: string, offset: number, lastM
31
33
  * stored cost (orchestration-aware) and keeps `premium_requests` monotonic, so
32
34
  * a forced re-parse repairs historical `premium_requests` and cost fix-ups.
33
35
  */
34
- export declare function insertMessageStats(stats: MessageStats[]): number;
36
+ export declare function insertMessageStats(stats: MessageStatsInput[]): number;
35
37
  /**
36
38
  * Get overall aggregated stats.
37
39
  */
@@ -98,6 +100,7 @@ export interface SessionRollupRow {
98
100
  endedAt: number;
99
101
  totalTokens: number;
100
102
  costTotal: number;
103
+ unpricedRequests: number;
101
104
  /** Comma-joined DISTINCT models. */
102
105
  models: string;
103
106
  }
@@ -1,4 +1,4 @@
1
- import type { AgentType, MessageStats, SessionEntry, ToolCallStats, ToolResultLink, UserMessageLink, UserMessageStats } from "./types.js";
1
+ import type { AgentType, MessageStatsInput, SessionEntry, ToolCallStats, ToolResultLink, UserMessageLink, UserMessageStats } from "./types.js";
2
2
  /**
3
3
  * Classify which agent produced a transcript from its path within the sessions
4
4
  * directory. Layout: `<sessionsDir>/<project>/<file>.jsonl` is the `main`
@@ -16,6 +16,32 @@ export declare function classifyAgentType(sessionPath: string): AgentType;
16
16
  * The folder part uses -- as path separator.
17
17
  */
18
18
  export declare function extractFolderFromPath(sessionPath: string): string;
19
+ /**
20
+ * Token-bucket view for total derivation. Persisted session payloads are
21
+ * outside-controlled (old versions, foreign producers), so every counter is
22
+ * `unknown` and validated at read time.
23
+ */
24
+ export interface UsageBucketView {
25
+ totalTokens?: unknown;
26
+ input?: unknown;
27
+ output?: unknown;
28
+ cacheRead?: unknown;
29
+ cacheWrite?: unknown;
30
+ orchestration?: {
31
+ input?: unknown;
32
+ output?: unknown;
33
+ cacheRead?: unknown;
34
+ } | null;
35
+ }
36
+ /**
37
+ * Total tokens for one usage payload, per the documented contract: the
38
+ * conversation buckets plus provider-reported orchestration tokens. A present
39
+ * finite provider total stays authoritative; a missing or malformed one
40
+ * (absent, string, NaN) is derived from the buckets, which would otherwise
41
+ * persist a zero total next to real token counts. Shared by ingest and the
42
+ * trace builder so stored and displayed totals cannot disagree.
43
+ */
44
+ export declare function resolveUsageTotal(usage: UsageBucketView | null | undefined): number;
19
45
  /** Parse every well-formed entry in a transcript buffer (malformed lines skipped). */
20
46
  export declare function parseAllSessionEntries(bytes: Uint8Array): SessionEntry[];
21
47
  /**
@@ -35,7 +61,7 @@ export declare function parseAllSessionEntries(bytes: Uint8Array): SessionEntry[
35
61
  * entries, preserving offset-based memory behavior for large sessions.
36
62
  */
37
63
  export interface ParseSessionResult {
38
- stats: MessageStats[];
64
+ stats: MessageStatsInput[];
39
65
  userStats: UserMessageStats[];
40
66
  userLinks: UserMessageLink[];
41
67
  toolCalls: ToolCallStats[];
@@ -454,6 +454,8 @@ export interface SessionSummary {
454
454
  subagents: number;
455
455
  totalTokens: number;
456
456
  costTotal: number;
457
+ /** Requests whose zero cost is unknown spend (scheduled card, no timestamp). */
458
+ unpricedRequests: number;
457
459
  models: string[];
458
460
  }
459
461
  export type TraceSpanKind = "turn" | "model" | "tool" | "subagent" | "background";
@@ -533,6 +535,8 @@ export interface TraceSummary {
533
535
  subagents: number;
534
536
  totalTokens: number;
535
537
  costTotal: number;
538
+ /** Model requests whose zero cost is unknown spend, not free usage. */
539
+ unpricedRequests: number;
536
540
  /** Sorted totalMs desc. */
537
541
  toolStats: TraceToolStat[];
538
542
  }
@@ -17,7 +17,7 @@ export declare class TracePathError extends Error {
17
17
  * span assembly changes so browsers don't revalidate stale cached bodies
18
18
  * against an unchanged transcript mtime.
19
19
  */
20
- export declare const TRACE_ETAG_VERSION = 2;
20
+ export declare const TRACE_ETAG_VERSION = 3;
21
21
  /**
22
22
  * Assemble the full span tree for one root session transcript.
23
23
  * Throws {@link TracePathError} for paths outside the sessions root; ENOENT
@@ -33,6 +33,27 @@ export interface MessageStats {
33
33
  usage: Usage;
34
34
  /** Which agent produced this message (main agent, task subagent, advisor) */
35
35
  agentType: AgentType;
36
+ /**
37
+ * Ingest refused to price this request: a scheduled (time-based) card with no
38
+ * recoverable request timestamp, so `usage.cost.total` of 0 is unknown spend
39
+ * rather than a free request. Always written by `rowToMessageStats`; optional
40
+ * only because session fixtures that stand in for ingest input are typed as
41
+ * `MessageStats` too.
42
+ */
43
+ costUnpriced?: boolean;
44
+ }
45
+ /**
46
+ * Session-recorded token usage before pricing: counters are coerced to numbers
47
+ * because their columns are NOT NULL, but `cost` is only present when the
48
+ * session entry actually recorded one. Absence is meaningful — `resolveStoredCost`
49
+ * estimates a request with no recorded price, whereas a recorded zero is a real
50
+ * charge that scheduled cards freeze. `costUnpriced` is omitted because it is a
51
+ * decision of the ingest path, not something the parser or a caller supplies.
52
+ */
53
+ export interface MessageStatsInput extends Omit<MessageStats, "usage" | "costUnpriced"> {
54
+ usage: Omit<Usage, "cost"> & {
55
+ cost?: Partial<Usage["cost"]>;
56
+ };
36
57
  }
37
58
  /**
38
59
  * Full details of a request, including content.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/omp-stats",
4
- "version": "18.1.16",
4
+ "version": "18.1.18",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -39,9 +39,9 @@
39
39
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "18.1.16",
43
- "@oh-my-pi/pi-catalog": "18.1.16",
44
- "@oh-my-pi/pi-utils": "18.1.16",
42
+ "@oh-my-pi/pi-ai": "18.1.18",
43
+ "@oh-my-pi/pi-catalog": "18.1.18",
44
+ "@oh-my-pi/pi-utils": "18.1.18",
45
45
  "@tailwindcss/node": "^4.3.2",
46
46
  "chart.js": "^4.5.1",
47
47
  "lucide-react": "^1.24.0",
@@ -23,10 +23,22 @@ export function formatEstimatedCost(value: number, unpricedRequests: number, dig
23
23
  return value === 0 && unpricedRequests > 0 ? "N/A" : formatCost(value, digits);
24
24
  }
25
25
 
26
- /** Format one request's cost, distinguishing unpriced SuperGrok usage from free usage. */
27
- export function formatMessageCost(message: Pick<MessageStats, "provider" | "usage">, digits?: number): string {
26
+ /**
27
+ * Format one request's cost, distinguishing unpriced usage from free usage.
28
+ * Mirrors the server's `unpricedRequestSql`: `xai-oauth` is billed through a
29
+ * subscription, and `costUnpriced` marks a row the ingest path could not price
30
+ * (a scheduled card with no recoverable request timestamp).
31
+ */
32
+ export function formatMessageCost(
33
+ message: Pick<MessageStats, "provider" | "usage" | "costUnpriced">,
34
+ digits?: number,
35
+ ): string {
28
36
  const unpricedRequests =
29
- message.provider === "xai-oauth" && message.usage.totalTokens > 0 && message.usage.cost.total === 0 ? 1 : 0;
37
+ message.usage.totalTokens > 0 &&
38
+ message.usage.cost.total === 0 &&
39
+ (message.provider === "xai-oauth" || message.costUnpriced === true)
40
+ ? 1
41
+ : 0;
30
42
  return formatEstimatedCost(message.usage.cost.total, unpricedRequests, digits);
31
43
  }
32
44
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { useMemo, useState } from "react";
7
7
  import { getSessions } from "../api";
8
- import { formatCompact, formatCost, formatDurationMs, formatRelativeTime } from "../data/formatters";
8
+ import { formatCompact, formatDurationMs, formatEstimatedCost, formatRelativeTime } from "../data/formatters";
9
9
  import { useResource } from "../data/useResource";
10
10
  import { TraceView } from "../traces/TraceView";
11
11
  import type { SessionSummary } from "../types";
@@ -110,7 +110,12 @@ export function TracesRoute({ active, session, onOpenSession, refreshTrigger }:
110
110
  numeric: true,
111
111
  render: (item: SessionSummary) => formatCompact(item.totalTokens),
112
112
  },
113
- { key: "cost", header: "Cost", numeric: true, render: (item: SessionSummary) => formatCost(item.costTotal) },
113
+ {
114
+ key: "cost",
115
+ header: "Cost",
116
+ numeric: true,
117
+ render: (item: SessionSummary) => formatEstimatedCost(item.costTotal, item.unpricedRequests),
118
+ },
114
119
  { key: "models", header: "Models", render: (item: SessionSummary) => <ModelChips models={item.models} /> },
115
120
  ],
116
121
  [],
@@ -138,7 +143,9 @@ export function TracesRoute({ active, session, onOpenSession, refreshTrigger }:
138
143
  </div>
139
144
  <div>
140
145
  <div className="stats-mobile-card-label">Cost</div>
141
- <div className="stats-mobile-card-value">{formatCost(item.costTotal)}</div>
146
+ <div className="stats-mobile-card-value">
147
+ {formatEstimatedCost(item.costTotal, item.unpricedRequests)}
148
+ </div>
142
149
  </div>
143
150
  </div>
144
151
  </div>
@@ -3,7 +3,7 @@
3
3
  * tool calls, agents, tokens, cost.
4
4
  */
5
5
 
6
- import { formatCompact, formatCost, formatDurationMs, formatInteger } from "../data/formatters";
6
+ import { formatCompact, formatDurationMs, formatEstimatedCost, formatInteger } from "../data/formatters";
7
7
  import type { TraceSummary } from "../types";
8
8
 
9
9
  export interface SummaryStripProps {
@@ -21,7 +21,7 @@ export function SummaryStrip({ summary }: SummaryStripProps) {
21
21
  { label: "Tool Calls", value: formatInteger(summary.toolCalls) },
22
22
  { label: "Agents", value: formatInteger(summary.subagents) },
23
23
  { label: "Tokens", value: formatCompact(summary.totalTokens) },
24
- { label: "Cost", value: formatCost(summary.costTotal) },
24
+ { label: "Cost", value: formatEstimatedCost(summary.costTotal, summary.unpricedRequests) },
25
25
  ];
26
26
 
27
27
  return (
@@ -54,6 +54,8 @@ export interface MessageStats {
54
54
  stopReason: string;
55
55
  errorMessage: string | null;
56
56
  usage: Usage;
57
+ /** Ingest found no tariff to price this request from; its zero is unknown spend. */
58
+ costUnpriced?: boolean;
57
59
  }
58
60
 
59
61
  export interface RequestDetails extends MessageStats {