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.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/cli/cost.ts +18 -0
- package/src/cli/events.ts +22 -0
- package/src/cli/tree.ts +2 -0
- package/src/hooks/lib/events.ts +44 -3
- package/src/hooks/lib/interaction.ts +25 -5
- package/src/hooks/lib/invocation-context.ts +11 -3
- package/src/hooks/lib/ledger.ts +61 -2
- package/src/hooks/lib/models.ts +42 -10
- package/src/hooks/lib/opencode-events.ts +49 -0
- package/src/hooks/lib/remote.ts +3 -0
- package/src/hooks/lib/signals.ts +54 -4
- package/src/hooks/lib/work-tracking.ts +39 -2
- package/src/targets/lib.ts +1 -3
- package/src/targets/opencode/plugin.ts +3 -0
- package/src/tools/lib/backfill/git.ts +184 -0
- package/src/tools/lib/backfill/index.ts +32 -0
- package/src/tools/lib/backfill/ledger.ts +28 -0
- package/src/tools/lib/backfill/ratings.ts +21 -0
- package/src/tools/lib/backfill/rows.ts +33 -0
- package/src/tools/lib/backfill/sessions.ts +56 -0
- package/src/tools/lib/backfill/turns.ts +32 -0
- package/src/tools/lib/collectors/billing.ts +105 -0
- package/src/tools/lib/collectors/claude.ts +14 -6
- package/src/tools/lib/collectors/codex.ts +6 -3
- package/src/tools/lib/collectors/copilot.ts +2 -0
- package/src/tools/lib/collectors/index.ts +7 -1
- package/src/tools/lib/collectors/opencode.ts +10 -2
- package/src/tools/lib/cost-report.ts +134 -0
- package/src/tools/lib/plan-prices.ts +24 -0
- package/src/tools/lib/session-usage.ts +11 -7
- package/src/tools/lib/usage-buckets.ts +10 -5
|
@@ -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(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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(
|
|
94
|
-
|
|
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;
|