portable-agent-layer 0.96.1 → 0.98.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.
@@ -0,0 +1,134 @@
1
+ /**
2
+ * What each month cost against what the same calls would have cost at API
3
+ * prices, read off the model_call events. A call is priced exactly as
4
+ * pal cli usage prices it, so the two never disagree about the same data.
5
+ */
6
+
7
+ import type { LoggedEvent } from "../../hooks/lib/events";
8
+ import { costOfUsage } from "../../hooks/lib/models";
9
+ import { planPriceFor } from "./plan-prices";
10
+ import { fmtCost } from "./token-report";
11
+
12
+ export interface AgentMonth {
13
+ calls: number;
14
+ unpriced: number;
15
+ apiEquivalent: number;
16
+ billing: Set<string>;
17
+ plans: Map<string, string>;
18
+ apiBilled: number;
19
+ extraUsage: boolean;
20
+ }
21
+
22
+ export type CostMonths = Map<string, Map<string, AgentMonth>>;
23
+
24
+ const numberOf = (value: unknown) => (typeof value === "number" ? value : 0);
25
+
26
+ /** Claude Code at its dated rates, opencode at the cost it recorded, anything else unknown. */
27
+ function apiPriceOf(event: LoggedEvent): number | null {
28
+ if (event.runtime === "opencode")
29
+ return typeof event.costUsd === "number" ? event.costUsd : null;
30
+ if (event.runtime !== "claude" || typeof event.model !== "string") return null;
31
+ return costOfUsage(
32
+ event.model,
33
+ {
34
+ input: numberOf(event.inputTokens),
35
+ output: numberOf(event.outputTokens),
36
+ cacheWrite5m: numberOf(event.cacheWrite5m),
37
+ cacheWrite1h: numberOf(event.cacheWrite1h),
38
+ cacheRead: numberOf(event.cacheRead),
39
+ },
40
+ event.ts
41
+ );
42
+ }
43
+
44
+ function emptyAgentMonth(): AgentMonth {
45
+ return {
46
+ calls: 0,
47
+ unpriced: 0,
48
+ apiEquivalent: 0,
49
+ billing: new Set(),
50
+ plans: new Map(),
51
+ apiBilled: 0,
52
+ extraUsage: false,
53
+ };
54
+ }
55
+
56
+ function addCall(month: AgentMonth, event: LoggedEvent): void {
57
+ const price = apiPriceOf(event);
58
+ const billing = typeof event.billing === "string" ? event.billing : "unknown";
59
+ month.calls++;
60
+ month.billing.add(billing);
61
+ if (price === null) month.unpriced++;
62
+ else month.apiEquivalent += price;
63
+ if (billing === "api") month.apiBilled += price ?? 0;
64
+ if (billing === "subscription" && typeof event.plan === "string")
65
+ month.plans.set(event.plan, month.plans.get(event.plan) ?? event.ts);
66
+ if (event.extraUsage === true) month.extraUsage = true;
67
+ }
68
+
69
+ export function costMonths(events: LoggedEvent[]): CostMonths {
70
+ const months: CostMonths = new Map();
71
+ for (const event of events) {
72
+ const month = event.ts.slice(0, 7);
73
+ const agents = months.get(month) ?? new Map<string, AgentMonth>();
74
+ months.set(month, agents);
75
+ const agent = agents.get(event.runtime) ?? emptyAgentMonth();
76
+ agents.set(event.runtime, agent);
77
+ addCall(agent, event);
78
+ }
79
+ return months;
80
+ }
81
+
82
+ /** Null when any part of what was paid is unknown. */
83
+ function paidFor(month: AgentMonth): number | null {
84
+ if (month.billing.has("unknown")) return null;
85
+ let paid = month.billing.has("api") ? month.apiBilled : 0;
86
+ if (!month.billing.has("subscription")) return paid;
87
+ if (month.plans.size === 0) return null;
88
+ for (const [plan, since] of month.plans) {
89
+ const price = planPriceFor(plan, since);
90
+ if (price === null) return null;
91
+ paid += price;
92
+ }
93
+ return paid;
94
+ }
95
+
96
+ /** Null unless both sides are known and something was paid. */
97
+ function leverageOf(month: AgentMonth): number | null {
98
+ const paid = paidFor(month);
99
+ if (paid === null || paid === 0 || month.unpriced > 0) return null;
100
+ return month.apiEquivalent / paid;
101
+ }
102
+
103
+ function apiEquivalentLabel(month: AgentMonth): string {
104
+ if (month.unpriced === month.calls) return "unknown";
105
+ const priced = fmtCost(month.apiEquivalent);
106
+ return month.unpriced > 0 ? `${priced} + ${month.unpriced} unpriced calls` : priced;
107
+ }
108
+
109
+ function paidLabel(month: AgentMonth): string {
110
+ const paid = paidFor(month);
111
+ if (paid === null) return "unknown";
112
+ const extra = month.extraUsage ? " + extra usage (unknown)" : "";
113
+ return `${fmtCost(paid)}${extra}`;
114
+ }
115
+
116
+ function agentLine(label: string, month: AgentMonth): string {
117
+ const leverage = leverageOf(month);
118
+ const bound = month.extraUsage ? "up to " : "";
119
+ const leverageText =
120
+ leverage === null ? "" : ` leverage ${bound}${leverage.toFixed(1)}x`;
121
+ return ` ${label.padEnd(16)} paid ${paidLabel(month)} API-equivalent ${apiEquivalentLabel(month)}${leverageText}`;
122
+ }
123
+
124
+ export function costLines(months: CostMonths, labelOf: (runtime: string) => string) {
125
+ if (months.size === 0) return ["\n No model calls in the event log yet\n"];
126
+ return [...months.keys()]
127
+ .sort()
128
+ .flatMap((month) => [
129
+ `\n ${month}\n`,
130
+ ...[...(months.get(month) ?? [])]
131
+ .sort(([a], [b]) => a.localeCompare(b))
132
+ .map(([runtime, agent]) => agentLine(labelOf(runtime), agent)),
133
+ ]);
134
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * What a subscription costs per month, keyed by the plan recorded on each
3
+ * model_call. Prices from support.claude.com's Max plan article, checked
4
+ * 2026-10-12; the Max launch date from Bloomberg, 2025-04-09.
5
+ */
6
+
7
+ interface PlanPrice {
8
+ from: string;
9
+ monthlyUsd: number;
10
+ }
11
+
12
+ export const PLAN_PRICES: Record<string, PlanPrice[]> = {
13
+ default_claude_max_5x: [{ from: "2025-04-09", monthlyUsd: 100 }],
14
+ default_claude_max_20x: [{ from: "2025-04-09", monthlyUsd: 200 }],
15
+ };
16
+
17
+ /** The monthly price in force at `at`, or null when the plan or its price then is unknown. */
18
+ export function planPriceFor(plan: string | null, at: string): number | null {
19
+ const when = Date.parse(at);
20
+ const inForce = (PLAN_PRICES[plan ?? ""] ?? []).filter(
21
+ (p) => Date.parse(p.from) <= when
22
+ );
23
+ return inForce.at(-1)?.monthlyUsd ?? null;
24
+ }
@@ -147,13 +147,17 @@ export function accumulateUsage(transcript: string, sessionId: string): SessionU
147
147
  const cacheRead = usage.cache_read_input_tokens ?? 0;
148
148
  const { cacheWrite5m, cacheWrite1h } = cacheWritesOf(usage);
149
149
 
150
- total.cost += costOfUsage(model, {
151
- input,
152
- output,
153
- cacheWrite5m,
154
- cacheWrite1h,
155
- cacheRead,
156
- });
150
+ total.cost += costOfUsage(
151
+ model,
152
+ {
153
+ input,
154
+ output,
155
+ cacheWrite5m,
156
+ cacheWrite1h,
157
+ cacheRead,
158
+ },
159
+ entry.timestamp
160
+ );
157
161
  total.input += input;
158
162
  total.output += output;
159
163
  total.cacheWrite5m += cacheWrite5m;
@@ -90,8 +90,13 @@ function addCall(bucket: Bucket, tokens: Tokens, cost: number | null, calls = 1)
90
90
  if (cost === null) bucket.unpriced += calls;
91
91
  }
92
92
 
93
- export function addToBucket(bucket: Bucket, model: string, tokens: Tokens): void {
94
- addCall(bucket, tokens, costOfUsage(model, tokens));
93
+ export function addToBucket(
94
+ bucket: Bucket,
95
+ model: string,
96
+ tokens: Tokens,
97
+ ts: string
98
+ ): void {
99
+ addCall(bucket, tokens, costOfUsage(model, tokens, ts));
95
100
  }
96
101
 
97
102
  /**
@@ -117,7 +122,7 @@ export function addToTimeBuckets(
117
122
  tokens: Tokens,
118
123
  horizons: Horizons
119
124
  ): void {
120
- addToWindows(buckets, ts, horizons, (bucket) => addToBucket(bucket, model, tokens));
125
+ addToWindows(buckets, ts, horizons, (bucket) => addToBucket(bucket, model, tokens, ts));
121
126
  }
122
127
 
123
128
  export function totalTokens(bucket: Bucket): number {
@@ -290,7 +295,7 @@ export function readClaudeCode(
290
295
  model: call.model,
291
296
  tokens: call.tokens,
292
297
  project: call.project,
293
- cost: costOfUsage(call.model, call.tokens),
298
+ cost: costOfUsage(call.model, call.tokens, call.ts),
294
299
  });
295
300
  }
296
301
  return tally.usage;
@@ -357,7 +362,7 @@ export function readPalInference(
357
362
  );
358
363
 
359
364
  result.byCaller[entry.caller] ??= emptyBucket();
360
- addToBucket(result.byCaller[entry.caller], entry.model, tokens);
365
+ addToBucket(result.byCaller[entry.caller], entry.model, tokens, entry.ts);
361
366
  }
362
367
 
363
368
  return result;