@ilya-lesikov/pi-pi 0.6.0 → 0.7.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/3p/pi-ask-user/index.ts +1 -1
- package/extensions/orchestrator/config.ts +3 -2
- package/extensions/orchestrator/custom-footer.ts +5 -2
- package/extensions/orchestrator/doctor.test.ts +3 -1
- package/extensions/orchestrator/doctor.ts +40 -2
- package/extensions/orchestrator/event-handlers.ts +47 -6
- package/extensions/orchestrator/flant-infra.test.ts +287 -0
- package/extensions/orchestrator/flant-infra.ts +316 -44
- package/extensions/orchestrator/integration.test.ts +205 -6
- package/extensions/orchestrator/model-registry.test.ts +76 -12
- package/extensions/orchestrator/model-registry.ts +48 -32
- package/extensions/orchestrator/orchestrator.ts +13 -2
- package/extensions/orchestrator/phases/review.test.ts +54 -0
- package/extensions/orchestrator/phases/review.ts +54 -12
- package/extensions/orchestrator/pp-menu.test.ts +74 -1
- package/extensions/orchestrator/pp-menu.ts +199 -41
- package/extensions/orchestrator/state.ts +41 -20
- package/extensions/orchestrator/test-helpers.ts +14 -2
- package/extensions/orchestrator/usage-tracker.test.ts +131 -3
- package/extensions/orchestrator/usage-tracker.ts +74 -11
- package/package.json +1 -1
|
@@ -96,6 +96,7 @@ describe("usage-tracker", () => {
|
|
|
96
96
|
cacheWriteTokens: 0,
|
|
97
97
|
cacheSupported: false,
|
|
98
98
|
turns: 1,
|
|
99
|
+
subscription: false,
|
|
99
100
|
},
|
|
100
101
|
"anthropic/claude-opus-4-6": {
|
|
101
102
|
inputTokens: 7,
|
|
@@ -104,6 +105,7 @@ describe("usage-tracker", () => {
|
|
|
104
105
|
cacheWriteTokens: 0,
|
|
105
106
|
cacheSupported: false,
|
|
106
107
|
turns: 1,
|
|
108
|
+
subscription: false,
|
|
107
109
|
},
|
|
108
110
|
});
|
|
109
111
|
});
|
|
@@ -186,6 +188,7 @@ describe("usage-tracker", () => {
|
|
|
186
188
|
cost: 0.2,
|
|
187
189
|
durationMs: 900,
|
|
188
190
|
toolUses: 3,
|
|
191
|
+
subscription: false,
|
|
189
192
|
},
|
|
190
193
|
]);
|
|
191
194
|
});
|
|
@@ -226,12 +229,21 @@ describe("usage-tracker", () => {
|
|
|
226
229
|
expect(tracker.getTotalCost()).toBe(2);
|
|
227
230
|
});
|
|
228
231
|
|
|
229
|
-
it("getCacheHitRate
|
|
232
|
+
it("getCacheHitRate is cacheRead over processed input (uncached + read + write)", () => {
|
|
230
233
|
const tracker = createUsageTracker();
|
|
231
234
|
|
|
232
|
-
|
|
235
|
+
// uncached 30, cacheRead 10, cacheWrite 10 → 10 / (30+10+10) = 0.2
|
|
236
|
+
tracker.recordTurn("openai/gpt-5", "openai", 30, 0, 10, 10, 0, true);
|
|
233
237
|
|
|
234
|
-
expect(tracker.getCacheHitRate()).toBeCloseTo(10 /
|
|
238
|
+
expect(tracker.getCacheHitRate()).toBeCloseTo(10 / 50);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("getCacheHitRate includes subagent cache tokens", () => {
|
|
242
|
+
const tracker = createUsageTracker();
|
|
243
|
+
tracker.recordTurn("openai/gpt-5", "openai", 10, 0, 0, 0, 0, true);
|
|
244
|
+
tracker.recordSubagentCompletion({ input: 0, output: 0, cacheRead: 30, cacheWrite: 10 } as any);
|
|
245
|
+
// cacheRead 30 / processed (10 + 30 + 10) = 0.6
|
|
246
|
+
expect(tracker.getCacheHitRate()).toBeCloseTo(30 / 50);
|
|
235
247
|
});
|
|
236
248
|
|
|
237
249
|
it("getCacheHitRate returns zero when denominator is zero", () => {
|
|
@@ -239,6 +251,22 @@ describe("usage-tracker", () => {
|
|
|
239
251
|
expect(tracker.getCacheHitRate()).toBe(0);
|
|
240
252
|
});
|
|
241
253
|
|
|
254
|
+
it("getTotalProcessedInputTokens sums uncached + cache read + cache write across main and subagents", () => {
|
|
255
|
+
const tracker = createUsageTracker();
|
|
256
|
+
tracker.recordTurn("openai/gpt-5", "openai", 10, 0, 20, 5, 0, true);
|
|
257
|
+
tracker.recordSubagentCompletion({ input: 3, output: 0, cacheRead: 7, cacheWrite: 2 } as any);
|
|
258
|
+
// main: 10+20+5=35, subagent: 3+7+2=12 → 47
|
|
259
|
+
expect(tracker.getTotalProcessedInputTokens()).toBe(47);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("getTotalCacheReadTokens and getTotalCacheWriteTokens include subagents", () => {
|
|
263
|
+
const tracker = createUsageTracker();
|
|
264
|
+
tracker.recordTurn("openai/gpt-5", "openai", 0, 0, 20, 5, 0, true);
|
|
265
|
+
tracker.recordSubagentCompletion({ input: 0, output: 0, cacheRead: 7, cacheWrite: 2 } as any);
|
|
266
|
+
expect(tracker.getTotalCacheReadTokens()).toBe(27);
|
|
267
|
+
expect(tracker.getTotalCacheWriteTokens()).toBe(7);
|
|
268
|
+
});
|
|
269
|
+
|
|
242
270
|
it("isCacheSupported true when any model or subagent has cache", () => {
|
|
243
271
|
const tracker = createUsageTracker();
|
|
244
272
|
expect(tracker.isCacheSupported()).toBe(false);
|
|
@@ -382,6 +410,106 @@ describe("usage-tracker", () => {
|
|
|
382
410
|
expect(roundTrip.subagents).toEqual((mid as any).subagents);
|
|
383
411
|
});
|
|
384
412
|
|
|
413
|
+
it("subscription main turns count tokens but contribute zero cost", () => {
|
|
414
|
+
const tracker = createUsageTracker();
|
|
415
|
+
|
|
416
|
+
// Detected via the sub/ model id prefix...
|
|
417
|
+
tracker.recordTurn("sub/claude-opus-4-6", "anthropic", 100, 50, 10, 5, 1.23, true);
|
|
418
|
+
// ...or via the subscription provider with a bare id (keyed under sub/).
|
|
419
|
+
tracker.recordTurn("claude-opus-4-6", "pp-flant-anthropic-sub", 20, 10, 0, 0, 0.5, true);
|
|
420
|
+
|
|
421
|
+
expect(tracker.getMainInputTokens()).toBe(120);
|
|
422
|
+
expect(tracker.getMainOutputTokens()).toBe(60);
|
|
423
|
+
expect(tracker.getMainCacheReadTokens()).toBe(10);
|
|
424
|
+
expect(tracker.getMainCost()).toBe(0);
|
|
425
|
+
expect(tracker.getTotalCost()).toBe(0);
|
|
426
|
+
expect(tracker.getPerModelUsage()["sub/claude-opus-4-6"]?.subscription).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("provider-detected subscription turns key under sub/ so paid rows stay separate", () => {
|
|
430
|
+
const tracker = createUsageTracker();
|
|
431
|
+
|
|
432
|
+
// Same underlying model id: one paid, one subscription (provider-only).
|
|
433
|
+
tracker.recordTurn("claude-opus-4-6", "pp-flant-anthropic", 10, 5, 0, 0, 0.4, false);
|
|
434
|
+
tracker.recordTurn("claude-opus-4-6", "pp-flant-anthropic-sub", 20, 10, 0, 0, 2.0, false);
|
|
435
|
+
|
|
436
|
+
const usage = tracker.getPerModelUsage();
|
|
437
|
+
expect(usage["claude-opus-4-6"]).toMatchObject({ inputTokens: 10, outputTokens: 5, subscription: false });
|
|
438
|
+
expect(usage["sub/claude-opus-4-6"]).toMatchObject({ inputTokens: 20, outputTokens: 10, subscription: true });
|
|
439
|
+
expect(tracker.getMainCost()).toBeCloseTo(0.4);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("subscription subagents count tokens but contribute zero cost", () => {
|
|
443
|
+
const tracker = createUsageTracker();
|
|
444
|
+
|
|
445
|
+
tracker.recordSubagentCompletion({ input: 30, output: 15 } as any, 0.9, {
|
|
446
|
+
description: "Explore", agentType: "explore", modelId: "sub/claude-haiku-4-5",
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(tracker.getSubagentTotals()).toEqual({ inputTokens: 30, outputTokens: 15, cost: 0 });
|
|
450
|
+
expect(tracker.getSubagentList()[0]?.subscription).toBe(true);
|
|
451
|
+
expect(tracker.getSubagentList()[0]?.cost).toBe(0);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("mixed paid and subscription session totals only the paid portion", () => {
|
|
455
|
+
const tracker = createUsageTracker();
|
|
456
|
+
|
|
457
|
+
tracker.recordTurn("openai/gpt-5", "openai", 10, 5, 0, 0, 0.4, false);
|
|
458
|
+
tracker.recordTurn("sub/claude-opus-4-6", "pp-flant-anthropic-sub", 10, 5, 0, 0, 2.0, false);
|
|
459
|
+
tracker.recordSubagentCompletion({ input: 5, output: 2 } as any, 0.1, { modelId: "openai/gpt-5" });
|
|
460
|
+
tracker.recordSubagentCompletion({ input: 5, output: 2 } as any, 3.0, { modelId: "sub/claude-haiku-4-5" });
|
|
461
|
+
|
|
462
|
+
expect(tracker.getMainCost()).toBeCloseTo(0.4);
|
|
463
|
+
expect(tracker.getSubagentTotals().cost).toBeCloseTo(0.1);
|
|
464
|
+
expect(tracker.getTotalCost()).toBeCloseTo(0.5);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("subscription marker survives JSON round-trip and stays paid-only", () => {
|
|
468
|
+
const source = createUsageTracker();
|
|
469
|
+
source.recordTurn("sub/claude-opus-4-6", "pp-flant-anthropic-sub", 10, 5, 0, 0, 2.0, true);
|
|
470
|
+
source.recordSubagentCompletion({ input: 5, output: 2 } as any, 3.0, { modelId: "sub/claude-haiku-4-5" });
|
|
471
|
+
|
|
472
|
+
const mid = source.toSummary() as Record<string, unknown>;
|
|
473
|
+
const restored = createUsageTracker();
|
|
474
|
+
restored.loadFromSummary(mid);
|
|
475
|
+
|
|
476
|
+
const roundTrip = restored.toSummary() as any;
|
|
477
|
+
expect(restored.getTotalCost()).toBe(0);
|
|
478
|
+
expect(restored.getPerModelUsage()["sub/claude-opus-4-6"]?.subscription).toBe(true);
|
|
479
|
+
expect(restored.getSubagentList()[0]?.subscription).toBe(true);
|
|
480
|
+
expect(roundTrip.totals).toEqual((mid as any).totals);
|
|
481
|
+
expect(roundTrip.models).toEqual((mid as any).models);
|
|
482
|
+
expect(roundTrip.subagents).toEqual((mid as any).subagents);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("legacy summaries without subscription field default to non-subscription", () => {
|
|
486
|
+
const tracker = createUsageTracker();
|
|
487
|
+
tracker.loadFromSummary({
|
|
488
|
+
totals: { inputTokens: 10, outputTokens: 5, cost: 1.5, turns: 1 },
|
|
489
|
+
models: { "openai/gpt-5": { inputTokens: 10, outputTokens: 5, turns: 1 } },
|
|
490
|
+
subagents: [{ description: "a", agentType: "x", modelId: "m", inputTokens: 3, outputTokens: 1, cost: 0.2 }],
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
expect(tracker.getPerModelUsage()["openai/gpt-5"]?.subscription).toBe(false);
|
|
494
|
+
expect(tracker.getSubagentList()[0]?.subscription).toBe(false);
|
|
495
|
+
expect(tracker.getMainCost()).toBe(1.5);
|
|
496
|
+
expect(tracker.getSubagentTotals().cost).toBeCloseTo(0.2);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("loadFromSummary recovers subscription from sub/ prefix when flag is absent", () => {
|
|
500
|
+
const tracker = createUsageTracker();
|
|
501
|
+
tracker.loadFromSummary({
|
|
502
|
+
totals: { inputTokens: 10, outputTokens: 5, cost: 0, turns: 1 },
|
|
503
|
+
models: { "sub/claude-opus-4-6": { inputTokens: 10, outputTokens: 5, turns: 1 } },
|
|
504
|
+
subagents: [{ description: "a", agentType: "x", modelId: "sub/claude-haiku-4-5", inputTokens: 3, outputTokens: 1, cost: 2.5 }],
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
expect(tracker.getPerModelUsage()["sub/claude-opus-4-6"]?.subscription).toBe(true);
|
|
508
|
+
expect(tracker.getSubagentList()[0]?.subscription).toBe(true);
|
|
509
|
+
expect(tracker.getSubagentList()[0]?.cost).toBe(0);
|
|
510
|
+
expect(tracker.getSubagentTotals().cost).toBe(0);
|
|
511
|
+
});
|
|
512
|
+
|
|
385
513
|
it("reset clears all state", () => {
|
|
386
514
|
const tracker = createUsageTracker();
|
|
387
515
|
tracker.recordTurn("openai/gpt-5", "openai", 1, 2, 3, 4, 0.2, true);
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { SUB_MODEL_PREFIX, SUB_PROVIDER } from "./flant-infra.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A usage unit is subscription-routed (flat-rate personal Claude subscription,
|
|
8
|
+
* not per-token billed) when its main-turn provider is the subscription
|
|
9
|
+
* provider, or when the registered model id carries the `sub/` prefix. The
|
|
10
|
+
* prefix is the only signal available for subagents (subagents:completed has no
|
|
11
|
+
* provider field), so we always check it in addition to the provider.
|
|
12
|
+
*/
|
|
13
|
+
export function isSubscriptionRouted(modelId?: string, provider?: string): boolean {
|
|
14
|
+
if (provider === SUB_PROVIDER) return true;
|
|
15
|
+
return typeof modelId === "string" && modelId.startsWith(SUB_MODEL_PREFIX);
|
|
16
|
+
}
|
|
4
17
|
|
|
5
18
|
export interface ModelUsage {
|
|
6
19
|
inputTokens: number;
|
|
@@ -9,6 +22,8 @@ export interface ModelUsage {
|
|
|
9
22
|
cacheWriteTokens: number;
|
|
10
23
|
cacheSupported: boolean;
|
|
11
24
|
turns: number;
|
|
25
|
+
/** Flat-rate personal subscription: dollars are excluded from cost totals, tokens are not. */
|
|
26
|
+
subscription: boolean;
|
|
12
27
|
}
|
|
13
28
|
|
|
14
29
|
export interface UsageTracker {
|
|
@@ -19,6 +34,8 @@ export interface UsageTracker {
|
|
|
19
34
|
getTotalOutputTokens(): number;
|
|
20
35
|
getTotalCacheReadTokens(): number;
|
|
21
36
|
getTotalCacheWriteTokens(): number;
|
|
37
|
+
/** Total input the model actually processed: uncached input + cache read + cache write (main + subagents). */
|
|
38
|
+
getTotalProcessedInputTokens(): number;
|
|
22
39
|
getTotalCost(): number;
|
|
23
40
|
getMainInputTokens(): number;
|
|
24
41
|
getMainOutputTokens(): number;
|
|
@@ -46,6 +63,8 @@ export interface SubagentUsage {
|
|
|
46
63
|
cost: number;
|
|
47
64
|
durationMs: number;
|
|
48
65
|
toolUses: number;
|
|
66
|
+
/** Flat-rate personal subscription: dollars are excluded from cost totals, tokens are not. */
|
|
67
|
+
subscription: boolean;
|
|
49
68
|
}
|
|
50
69
|
|
|
51
70
|
interface TrackerState {
|
|
@@ -58,6 +77,8 @@ interface TrackerState {
|
|
|
58
77
|
totalTurns: number;
|
|
59
78
|
subagentInputTokens: number;
|
|
60
79
|
subagentOutputTokens: number;
|
|
80
|
+
subagentCacheReadTokens: number;
|
|
81
|
+
subagentCacheWriteTokens: number;
|
|
61
82
|
subagentCost: number;
|
|
62
83
|
models: Map<string, ModelUsage>;
|
|
63
84
|
subagents: SubagentUsage[];
|
|
@@ -89,6 +110,8 @@ function createInitialState(): TrackerState {
|
|
|
89
110
|
totalTurns: 0,
|
|
90
111
|
subagentInputTokens: 0,
|
|
91
112
|
subagentOutputTokens: 0,
|
|
113
|
+
subagentCacheReadTokens: 0,
|
|
114
|
+
subagentCacheWriteTokens: 0,
|
|
92
115
|
subagentCost: 0,
|
|
93
116
|
models: new Map<string, ModelUsage>(),
|
|
94
117
|
subagents: [],
|
|
@@ -99,12 +122,15 @@ export function createUsageTracker(): UsageTracker {
|
|
|
99
122
|
const state = createInitialState();
|
|
100
123
|
|
|
101
124
|
return {
|
|
102
|
-
recordTurn(modelId: string,
|
|
125
|
+
recordTurn(modelId: string, provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number, cacheSupported?: boolean): void {
|
|
103
126
|
const safeInput = toFiniteNumber(input);
|
|
104
127
|
const safeOutput = toFiniteNumber(output);
|
|
105
128
|
const safeCacheRead = toFiniteNumber(cacheRead);
|
|
106
129
|
const safeCacheWrite = toFiniteNumber(cacheWrite);
|
|
107
|
-
const
|
|
130
|
+
const subscription = isSubscriptionRouted(modelId, provider);
|
|
131
|
+
// Subscription-routed turns are flat-rate: keep the tokens but exclude the
|
|
132
|
+
// fictitious per-token dollars so totals stay paid-only by construction.
|
|
133
|
+
const safeCost = subscription ? 0 : toFiniteNumber(cost);
|
|
108
134
|
|
|
109
135
|
state.totalInputTokens += safeInput;
|
|
110
136
|
state.totalOutputTokens += safeOutput;
|
|
@@ -113,7 +139,11 @@ export function createUsageTracker(): UsageTracker {
|
|
|
113
139
|
state.totalCost += safeCost;
|
|
114
140
|
state.totalTurns += 1;
|
|
115
141
|
|
|
116
|
-
|
|
142
|
+
// Key subscription turns under the sub/ prefix even when detected only by
|
|
143
|
+
// provider (bare model id), so a paid and a subscription turn for the same
|
|
144
|
+
// underlying model never share a row and the paid dollars stay visible.
|
|
145
|
+
const baseKey = modelId || "unknown-model";
|
|
146
|
+
const key = subscription && !baseKey.startsWith(SUB_MODEL_PREFIX) ? `${SUB_MODEL_PREFIX}${baseKey}` : baseKey;
|
|
117
147
|
const usage = state.models.get(key) ?? {
|
|
118
148
|
inputTokens: 0,
|
|
119
149
|
outputTokens: 0,
|
|
@@ -121,6 +151,7 @@ export function createUsageTracker(): UsageTracker {
|
|
|
121
151
|
cacheWriteTokens: 0,
|
|
122
152
|
cacheSupported: false,
|
|
123
153
|
turns: 0,
|
|
154
|
+
subscription: false,
|
|
124
155
|
};
|
|
125
156
|
|
|
126
157
|
usage.inputTokens += safeInput;
|
|
@@ -128,6 +159,7 @@ export function createUsageTracker(): UsageTracker {
|
|
|
128
159
|
usage.cacheReadTokens += safeCacheRead;
|
|
129
160
|
usage.cacheWriteTokens += safeCacheWrite;
|
|
130
161
|
if (cacheSupported) usage.cacheSupported = true;
|
|
162
|
+
if (subscription) usage.subscription = true;
|
|
131
163
|
usage.turns += 1;
|
|
132
164
|
state.models.set(key, usage);
|
|
133
165
|
},
|
|
@@ -142,12 +174,17 @@ export function createUsageTracker(): UsageTracker {
|
|
|
142
174
|
const safeTotal = toFiniteNumber(tokens.total);
|
|
143
175
|
const safeCacheRead = toFiniteNumber(tokens.cacheRead);
|
|
144
176
|
const safeCacheWrite = toFiniteNumber(tokens.cacheWrite);
|
|
145
|
-
|
|
177
|
+
// Subagents carry no provider field, so detect subscription routing from
|
|
178
|
+
// the registered model id prefix. Zero the dollars when subscription.
|
|
179
|
+
const subscription = isSubscriptionRouted(meta?.modelId);
|
|
180
|
+
const safeCost = subscription ? 0 : toFiniteNumber(tokens.cost ?? cost);
|
|
146
181
|
|
|
147
182
|
const effectiveInput = safeInput === 0 && safeOutput === 0 ? safeTotal : safeInput;
|
|
148
183
|
|
|
149
184
|
state.subagentInputTokens += effectiveInput;
|
|
150
185
|
state.subagentOutputTokens += safeOutput;
|
|
186
|
+
state.subagentCacheReadTokens += safeCacheRead;
|
|
187
|
+
state.subagentCacheWriteTokens += safeCacheWrite;
|
|
151
188
|
state.subagentCost += safeCost;
|
|
152
189
|
|
|
153
190
|
const cacheSupported = typeof tokens.cacheRead === "number" || typeof tokens.cacheWrite === "number";
|
|
@@ -164,6 +201,7 @@ export function createUsageTracker(): UsageTracker {
|
|
|
164
201
|
cost: safeCost,
|
|
165
202
|
durationMs: toFiniteNumber(meta?.durationMs),
|
|
166
203
|
toolUses: toFiniteNumber(meta?.toolUses),
|
|
204
|
+
subscription,
|
|
167
205
|
});
|
|
168
206
|
},
|
|
169
207
|
|
|
@@ -183,24 +221,33 @@ export function createUsageTracker(): UsageTracker {
|
|
|
183
221
|
state.subagents = [];
|
|
184
222
|
state.subagentInputTokens = 0;
|
|
185
223
|
state.subagentOutputTokens = 0;
|
|
224
|
+
state.subagentCacheReadTokens = 0;
|
|
225
|
+
state.subagentCacheWriteTokens = 0;
|
|
186
226
|
state.subagentCost = 0;
|
|
187
227
|
for (const sa of summary.subagents as Record<string, unknown>[]) {
|
|
228
|
+
const modelId = typeof sa.modelId === "string" ? sa.modelId : "unknown";
|
|
229
|
+
// Pre-change summaries lack the flag; recover it from the sub/ prefix
|
|
230
|
+
// so legacy subscription rows are not restored as paid.
|
|
231
|
+
const subscription = sa.subscription === true || isSubscriptionRouted(modelId);
|
|
188
232
|
const entry: SubagentUsage = {
|
|
189
233
|
description: typeof sa.description === "string" ? sa.description : "unknown",
|
|
190
234
|
agentType: typeof sa.agentType === "string" ? sa.agentType : "unknown",
|
|
191
|
-
modelId
|
|
235
|
+
modelId,
|
|
192
236
|
inputTokens: toFiniteNumber(sa.inputTokens),
|
|
193
237
|
outputTokens: toFiniteNumber(sa.outputTokens),
|
|
194
238
|
cacheReadTokens: toFiniteNumber(sa.cacheReadTokens),
|
|
195
239
|
cacheWriteTokens: toFiniteNumber(sa.cacheWriteTokens),
|
|
196
240
|
cacheSupported: sa.cacheSupported === true,
|
|
197
|
-
cost: toFiniteNumber(sa.cost),
|
|
241
|
+
cost: subscription ? 0 : toFiniteNumber(sa.cost),
|
|
198
242
|
durationMs: toFiniteNumber(sa.durationMs),
|
|
199
243
|
toolUses: toFiniteNumber(sa.toolUses),
|
|
244
|
+
subscription,
|
|
200
245
|
};
|
|
201
246
|
state.subagents.push(entry);
|
|
202
247
|
state.subagentInputTokens += entry.inputTokens;
|
|
203
248
|
state.subagentOutputTokens += entry.outputTokens;
|
|
249
|
+
state.subagentCacheReadTokens += entry.cacheReadTokens;
|
|
250
|
+
state.subagentCacheWriteTokens += entry.cacheWriteTokens;
|
|
204
251
|
state.subagentCost += entry.cost;
|
|
205
252
|
}
|
|
206
253
|
}
|
|
@@ -213,6 +260,7 @@ export function createUsageTracker(): UsageTracker {
|
|
|
213
260
|
cacheWriteTokens: toFiniteNumber(usage.cacheWriteTokens),
|
|
214
261
|
cacheSupported: (usage as any).cacheSupported === true,
|
|
215
262
|
turns: toFiniteNumber(usage.turns),
|
|
263
|
+
subscription: (usage as any).subscription === true || isSubscriptionRouted(modelId),
|
|
216
264
|
});
|
|
217
265
|
}
|
|
218
266
|
}
|
|
@@ -230,11 +278,22 @@ export function createUsageTracker(): UsageTracker {
|
|
|
230
278
|
},
|
|
231
279
|
|
|
232
280
|
getTotalCacheReadTokens(): number {
|
|
233
|
-
return state.totalCacheReadTokens;
|
|
281
|
+
return state.totalCacheReadTokens + state.subagentCacheReadTokens;
|
|
234
282
|
},
|
|
235
283
|
|
|
236
284
|
getTotalCacheWriteTokens(): number {
|
|
237
|
-
return state.totalCacheWriteTokens;
|
|
285
|
+
return state.totalCacheWriteTokens + state.subagentCacheWriteTokens;
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
getTotalProcessedInputTokens(): number {
|
|
289
|
+
return (
|
|
290
|
+
state.totalInputTokens +
|
|
291
|
+
state.subagentInputTokens +
|
|
292
|
+
state.totalCacheReadTokens +
|
|
293
|
+
state.subagentCacheReadTokens +
|
|
294
|
+
state.totalCacheWriteTokens +
|
|
295
|
+
state.subagentCacheWriteTokens
|
|
296
|
+
);
|
|
238
297
|
},
|
|
239
298
|
|
|
240
299
|
getTotalCost(): number {
|
|
@@ -262,9 +321,11 @@ export function createUsageTracker(): UsageTracker {
|
|
|
262
321
|
},
|
|
263
322
|
|
|
264
323
|
getCacheHitRate(): number {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
324
|
+
const totalCacheRead = state.totalCacheReadTokens + state.subagentCacheReadTokens;
|
|
325
|
+
// Session-wide, token-weighted average: cache reads over all processed
|
|
326
|
+
// input (uncached input + cache read + cache write). Including cache
|
|
327
|
+
// writes keeps first-touch (uncached) content from inflating the rate.
|
|
328
|
+
const denominator = this.getTotalProcessedInputTokens();
|
|
268
329
|
if (denominator <= 0) return 0;
|
|
269
330
|
return totalCacheRead / denominator;
|
|
270
331
|
},
|
|
@@ -324,6 +385,8 @@ export function createUsageTracker(): UsageTracker {
|
|
|
324
385
|
state.totalTurns = 0;
|
|
325
386
|
state.subagentInputTokens = 0;
|
|
326
387
|
state.subagentOutputTokens = 0;
|
|
388
|
+
state.subagentCacheReadTokens = 0;
|
|
389
|
+
state.subagentCacheWriteTokens = 0;
|
|
327
390
|
state.subagentCost = 0;
|
|
328
391
|
state.models.clear();
|
|
329
392
|
state.subagents = [];
|