@saccolabs/pi-claude-cli 0.4.9 → 0.4.11
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 +1 -1
- package/src/event-bridge.ts +89 -12
- package/src/types.ts +24 -1
package/package.json
CHANGED
package/src/event-bridge.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ClaudeApiEvent,
|
|
3
3
|
ClaudeAssistantEnvelope,
|
|
4
|
+
ClaudeModelUsage,
|
|
4
5
|
ClaudeResultMessage,
|
|
5
6
|
ClaudeUsage,
|
|
6
7
|
TrackedContentBlock,
|
|
@@ -131,9 +132,69 @@ export function createEventBridge(
|
|
|
131
132
|
cache_creation_input_tokens: 0,
|
|
132
133
|
};
|
|
133
134
|
let cycleUsage: ClaudeUsage = {};
|
|
135
|
+
/**
|
|
136
|
+
* Context size of the most recent cycle — what the model actually saw on
|
|
137
|
+
* its last call, which is NOT the episode's summed usage.
|
|
138
|
+
*
|
|
139
|
+
* pi reads `usage.totalTokens` as the conversation's context size
|
|
140
|
+
* (`calculateContextTokens` in pi's compaction module short-circuits on it)
|
|
141
|
+
* and uses it for both the context gauge and the auto-compaction trigger.
|
|
142
|
+
* Every cycle re-sends the same cached prefix, so summing cycles counts
|
|
143
|
+
* that prefix once per cycle: a captured 3-cycle episode sums to 82,174
|
|
144
|
+
* while the model's real context never exceeded 28,243. The host then
|
|
145
|
+
* showed 41% of a 200k window instead of 14%, and a long enough turn
|
|
146
|
+
* crossed pi's compaction threshold at a fraction of true occupancy.
|
|
147
|
+
*
|
|
148
|
+
* So the four component figures stay cumulative — they are what gets
|
|
149
|
+
* billed — and `totalTokens` carries the last cycle's context instead.
|
|
150
|
+
*/
|
|
151
|
+
let lastCycleContext = 0;
|
|
134
152
|
/** Tool ids already surfaced as markers (envelope arrives once per block). */
|
|
135
153
|
const markedToolIds = new Set<string>();
|
|
136
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Fold per-model spend into one `ClaudeUsage`.
|
|
157
|
+
*
|
|
158
|
+
* Every entry counts, including models the session did not pick: a
|
|
159
|
+
* sub-agent may run a different model, and the auto-titler always does.
|
|
160
|
+
* Returns undefined when the CLI sent no `modelUsage` (older versions), so
|
|
161
|
+
* the caller can fall back to the main-agent-only `usage`.
|
|
162
|
+
*
|
|
163
|
+
* The folded tokens are priced at the SESSION's model rates by
|
|
164
|
+
* `calculateCost`. That is exact for sub-agents, which inherit the session
|
|
165
|
+
* model, and slightly off for a cheaper helper model — a known skew worth
|
|
166
|
+
* far less than the tokens it stops hiding.
|
|
167
|
+
*/
|
|
168
|
+
function sumModelUsage(
|
|
169
|
+
modelUsage: Record<string, ClaudeModelUsage> | undefined,
|
|
170
|
+
): ClaudeUsage | undefined {
|
|
171
|
+
if (!modelUsage) return undefined;
|
|
172
|
+
const entries = Object.values(modelUsage);
|
|
173
|
+
if (entries.length === 0) return undefined;
|
|
174
|
+
const total = {
|
|
175
|
+
input_tokens: 0,
|
|
176
|
+
output_tokens: 0,
|
|
177
|
+
cache_read_input_tokens: 0,
|
|
178
|
+
cache_creation_input_tokens: 0,
|
|
179
|
+
};
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
total.input_tokens += entry.inputTokens ?? 0;
|
|
182
|
+
total.output_tokens += entry.outputTokens ?? 0;
|
|
183
|
+
total.cache_read_input_tokens += entry.cacheReadInputTokens ?? 0;
|
|
184
|
+
total.cache_creation_input_tokens += entry.cacheCreationInputTokens ?? 0;
|
|
185
|
+
}
|
|
186
|
+
return total;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Prompt size of one cycle: everything the model read, excluding output. */
|
|
190
|
+
function contextOf(usage: ClaudeUsage): number {
|
|
191
|
+
return (
|
|
192
|
+
(usage.input_tokens ?? 0) +
|
|
193
|
+
(usage.cache_read_input_tokens ?? 0) +
|
|
194
|
+
(usage.cache_creation_input_tokens ?? 0)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
137
198
|
function recomputeUsage(): void {
|
|
138
199
|
output.usage.input =
|
|
139
200
|
cumulativeUsage.input_tokens + (cycleUsage.input_tokens ?? 0);
|
|
@@ -145,11 +206,18 @@ export function createEventBridge(
|
|
|
145
206
|
output.usage.cacheWrite =
|
|
146
207
|
cumulativeUsage.cache_creation_input_tokens +
|
|
147
208
|
(cycleUsage.cache_creation_input_tokens ?? 0);
|
|
209
|
+
// Latch the newest cycle we have numbers for. `applyResult` clears
|
|
210
|
+
// cycleUsage, so a zero here means "no fresher figure", never "empty
|
|
211
|
+
// context" — the last latched value must survive.
|
|
212
|
+
const cycleContext = contextOf(cycleUsage);
|
|
213
|
+
if (cycleContext > 0) lastCycleContext = cycleContext;
|
|
148
214
|
output.usage.totalTokens =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
215
|
+
lastCycleContext > 0
|
|
216
|
+
? lastCycleContext
|
|
217
|
+
: output.usage.input +
|
|
218
|
+
output.usage.output +
|
|
219
|
+
output.usage.cacheRead +
|
|
220
|
+
output.usage.cacheWrite;
|
|
153
221
|
calculateCost(model, output.usage);
|
|
154
222
|
}
|
|
155
223
|
|
|
@@ -536,19 +604,28 @@ export function createEventBridge(
|
|
|
536
604
|
}
|
|
537
605
|
|
|
538
606
|
function applyResult(result: ClaudeResultMessage): void {
|
|
539
|
-
// Authoritative
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
607
|
+
// Authoritative spend for the whole episode. `modelUsage` is preferred
|
|
608
|
+
// over `usage` because `usage` is the MAIN AGENT ONLY: sub-agents run
|
|
609
|
+
// inside the CLI and never appear in the parent stream, so their tokens
|
|
610
|
+
// — often the majority — were simply missing from what the host billed.
|
|
611
|
+
// Same for helper models like the haiku auto-titler.
|
|
612
|
+
//
|
|
613
|
+
// Measured 2026-08-27: a lane's turn reported $2.34 while seven
|
|
614
|
+
// sub-agents spent 28.6M cache-read tokens on top of it, a 10x
|
|
615
|
+
// under-report. On a captured single-sub-agent episode `modelUsage`
|
|
616
|
+
// summed to exactly main + sub-agent (102,641 cache-read / 56,920
|
|
617
|
+
// cache-write), which is why it is trusted here.
|
|
618
|
+
const totals = sumModelUsage(result.modelUsage) ?? result.usage;
|
|
619
|
+
if (totals) {
|
|
543
620
|
cumulativeUsage.input_tokens =
|
|
544
|
-
|
|
621
|
+
totals.input_tokens ?? cumulativeUsage.input_tokens;
|
|
545
622
|
cumulativeUsage.output_tokens =
|
|
546
|
-
|
|
623
|
+
totals.output_tokens ?? cumulativeUsage.output_tokens;
|
|
547
624
|
cumulativeUsage.cache_read_input_tokens =
|
|
548
|
-
|
|
625
|
+
totals.cache_read_input_tokens ??
|
|
549
626
|
cumulativeUsage.cache_read_input_tokens;
|
|
550
627
|
cumulativeUsage.cache_creation_input_tokens =
|
|
551
|
-
|
|
628
|
+
totals.cache_creation_input_tokens ??
|
|
552
629
|
cumulativeUsage.cache_creation_input_tokens;
|
|
553
630
|
cycleUsage = {};
|
|
554
631
|
recomputeUsage();
|
package/src/types.ts
CHANGED
|
@@ -7,14 +7,37 @@ export interface ClaudeStreamEventMessage {
|
|
|
7
7
|
event: ClaudeApiEvent;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Per-model spend for the episode, keyed by model id.
|
|
12
|
+
*
|
|
13
|
+
* This is the ONLY place the CLI accounts for work done outside the main
|
|
14
|
+
* agent's own transcript: sub-agents (which run inside the CLI and never
|
|
15
|
+
* appear in the parent stream) and helper models such as the haiku
|
|
16
|
+
* auto-titler. `result.usage` covers the main agent alone.
|
|
17
|
+
*
|
|
18
|
+
* Verified 2026-08-27 on a captured episode with one synchronous sub-agent:
|
|
19
|
+
* main agent 74,562 cache-read / 26,808 cache-write, sub-agent 28,079 /
|
|
20
|
+
* 30,112, and `modelUsage` reported exactly the two summed — 102,641 /
|
|
21
|
+
* 56,920.
|
|
22
|
+
*/
|
|
23
|
+
export interface ClaudeModelUsage {
|
|
24
|
+
inputTokens?: number;
|
|
25
|
+
outputTokens?: number;
|
|
26
|
+
cacheReadInputTokens?: number;
|
|
27
|
+
cacheCreationInputTokens?: number;
|
|
28
|
+
costUSD?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
export interface ClaudeResultMessage {
|
|
11
32
|
type: "result";
|
|
12
33
|
subtype: "success" | "error";
|
|
13
34
|
result?: string;
|
|
14
35
|
error?: string;
|
|
15
36
|
session_id?: string;
|
|
16
|
-
/** Cumulative usage across every cycle of the episode
|
|
37
|
+
/** Cumulative usage for the MAIN agent across every cycle of the episode. */
|
|
17
38
|
usage?: ClaudeUsage;
|
|
39
|
+
/** Per-model spend, including sub-agents and helper models. */
|
|
40
|
+
modelUsage?: Record<string, ClaudeModelUsage>;
|
|
18
41
|
num_turns?: number;
|
|
19
42
|
total_cost_usd?: number;
|
|
20
43
|
}
|