@pify/usage 0.14.0 → 0.15.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pify/usage",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Token and cost reporting for pi sessions: live footer, local-history dashboard with per-project costs, opt-in provider quota",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/aggregate.ts CHANGED
@@ -15,6 +15,7 @@ export function addRecord(totals: UsageTotals, record: UsageRecord): UsageTotals
15
15
  totals.cacheWrite += record.cacheWrite;
16
16
  totals.totalTokens += record.totalTokens;
17
17
  totals.cost += record.cost;
18
+ if (record.estimated) totals.estimatedCost += record.cost;
18
19
  totals.messages += 1;
19
20
  return totals;
20
21
  }
@@ -65,6 +66,7 @@ export function windowTotals(byDay: Map<string, UsageTotals>, days: number, now:
65
66
  totals.cacheWrite += t.cacheWrite;
66
67
  totals.totalTokens += t.totalTokens;
67
68
  totals.cost += t.cost;
69
+ totals.estimatedCost += t.estimatedCost;
68
70
  totals.messages += t.messages;
69
71
  }
70
72
  return totals;
@@ -101,6 +103,10 @@ export function recordFromEntry(entry: unknown): UsageRecord | null {
101
103
  const total = finite(usage.totalTokens);
102
104
  const cost = isRecord(usage.cost) ? finite(usage.cost.total) : 0;
103
105
  if (total === 0 && cost === 0) return null;
106
+ // pi 1.5.0: cost.total is the provider's billed figure only when
107
+ // totalFromProvider is true; otherwise it is a catalog estimate (and the
108
+ // field is absent on older entries, which are all estimates).
109
+ const estimated = cost > 0 && !(isRecord(usage.cost) && usage.cost.totalFromProvider === true);
104
110
 
105
111
  const ts =
106
112
  typeof entry.timestamp === "string"
@@ -134,6 +140,7 @@ export function recordFromEntry(entry: unknown): UsageRecord | null {
134
140
  cacheWrite: finite(usage.cacheWrite),
135
141
  totalTokens: total,
136
142
  cost,
143
+ estimated,
137
144
  };
138
145
  }
139
146
 
package/src/format.ts CHANGED
@@ -7,10 +7,21 @@ export function formatTokens(n: number): string {
7
7
  return String(Math.round(n));
8
8
  }
9
9
 
10
- export function formatCost(n: number): string {
10
+ /**
11
+ * `estimated` prefixes a `≈` when the figure is (partly) a catalog estimate
12
+ * rather than a provider-billed total — see `UsageTotals.estimatedCost`. A
13
+ * zero cost is never marked: there is nothing to be approximate about.
14
+ */
15
+ export function formatCost(n: number, estimated = false): string {
11
16
  if (n === 0) return "$0";
12
- if (n < 0.01) return `<$0.01`;
13
- return `$${n.toFixed(2)}`;
17
+ const mark = estimated ? "≈" : "";
18
+ if (n < 0.01) return `${mark}<$0.01`;
19
+ return `${mark}$${n.toFixed(2)}`;
20
+ }
21
+
22
+ /** True when a bucket's cost includes a catalog estimate, so it is not fully exact. */
23
+ export function costEstimated(totals: UsageTotals): boolean {
24
+ return totals.estimatedCost > 0;
14
25
  }
15
26
 
16
27
  /** Footer text: short, live. Null clears the indicator. */
@@ -27,7 +38,7 @@ function hasChildSpend(children: ChildSpendLike | undefined): children is ChildS
27
38
  export function footerText(session: UsageTotals, children?: ChildSpendLike): string | undefined {
28
39
  const agents = hasChildSpend(children) ? ` · agents ${formatCost(children.cost)}` : "";
29
40
  if (session.messages === 0) return agents ? `📊 0 tok · $0${agents}` : undefined;
30
- return `📊 ${formatTokens(session.totalTokens)} tok · ${formatCost(session.cost)}${agents}`;
41
+ return `📊 ${formatTokens(session.totalTokens)} tok · ${formatCost(session.cost, costEstimated(session))}${agents}`;
31
42
  }
32
43
 
33
44
  const GAUGE_CELLS = 6;
@@ -53,7 +64,7 @@ export function sessionBlock(session: UsageTotals, contextPct: number | null, ch
53
64
  const lines = [
54
65
  "Session",
55
66
  ` tokens in ${formatTokens(session.input)} · out ${formatTokens(session.output)} · cache ${formatTokens(session.cacheRead)} read / ${formatTokens(session.cacheWrite)} write`,
56
- ` cost ${formatCost(session.cost)} (${session.messages} responses)`,
67
+ ` cost ${formatCost(session.cost, costEstimated(session))} (${session.messages} responses)`,
57
68
  ];
58
69
  if (hasChildSpend(children)) {
59
70
  // Child sessions of subagent/swarm/workflow run outside this branch; their
@@ -73,13 +84,17 @@ export function historyBlock(history: HistoryAggregate, now: number): string {
73
84
 
74
85
  const lines = [
75
86
  `History (${history.files} local session files)`,
76
- ` today ${formatCost(today.cost)} · ${formatTokens(today.totalTokens)} tok`,
77
- ` 7 days ${formatCost(week.cost)} · ${formatTokens(week.totalTokens)} tok`,
78
- ` 30 days ${formatCost(month.cost)} · ${formatTokens(month.totalTokens)} tok`,
87
+ ` today ${formatCost(today.cost, costEstimated(today))} · ${formatTokens(today.totalTokens)} tok`,
88
+ ` 7 days ${formatCost(week.cost, costEstimated(week))} · ${formatTokens(week.totalTokens)} tok`,
89
+ ` 30 days ${formatCost(month.cost, costEstimated(month))} · ${formatTokens(month.totalTokens)} tok`,
79
90
  ];
80
91
 
81
92
  pushBreakdown(lines, "By model (all time)", history.byModel, 6);
82
93
  pushBreakdown(lines, "By project (all time)", history.byProject, 5);
94
+ // Explain the mark only when it actually appears in this view.
95
+ if (costEstimated(history.total)) {
96
+ lines.push("", "≈ estimated from the model catalog; unmarked figures are billed by the provider");
97
+ }
83
98
  return lines.join("\n");
84
99
  }
85
100
 
@@ -96,6 +111,6 @@ function pushBreakdown(
96
111
  const width = rows.reduce((m, [name]) => Math.max(m, Math.min(name.length, 40)), 0);
97
112
  for (const [name, t] of rows) {
98
113
  const label = name.length > 40 ? `${name.slice(0, 39)}…` : name;
99
- lines.push(` ${label.padEnd(width)} ${formatCost(t.cost)} · ${formatTokens(t.totalTokens)} tok`);
114
+ lines.push(` ${label.padEnd(width)} ${formatCost(t.cost, costEstimated(t))} · ${formatTokens(t.totalTokens)} tok`);
100
115
  }
101
116
  }
package/src/types.ts CHANGED
@@ -10,11 +10,18 @@ export interface UsageTotals {
10
10
  cacheWrite: number;
11
11
  totalTokens: number;
12
12
  cost: number;
13
+ /**
14
+ * The part of `cost` that is a catalog estimate rather than a figure the
15
+ * provider billed (pi's `usage.cost.totalFromProvider === false`). When this
16
+ * is above zero the bucket's cost is not fully exact, so it is shown with a
17
+ * `≈`. Zero with a non-zero `cost` means every record was provider-reported.
18
+ */
19
+ estimatedCost: number;
13
20
  messages: number;
14
21
  }
15
22
 
16
23
  export function emptyTotals(): UsageTotals {
17
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: 0, messages: 0 };
24
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: 0, estimatedCost: 0, messages: 0 };
18
25
  }
19
26
 
20
27
  /** One usage-bearing record extracted from a session file or live event. */
@@ -31,6 +38,8 @@ export interface UsageRecord {
31
38
  cacheWrite: number;
32
39
  totalTokens: number;
33
40
  cost: number;
41
+ /** True when `cost` is a catalog estimate (pi's `usage.cost.totalFromProvider` is not true). */
42
+ estimated: boolean;
34
43
  }
35
44
 
36
45
  export interface HistoryAggregate {