@monotykamary/pi-tps 1.3.2 → 1.3.4
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/.github/workflows/test.yml +9 -7
- package/.pi/fabric/mesh/actors/actors.json +4 -0
- package/extensions/pi-tps/__tests__/cost-rate.test.ts +112 -13
- package/extensions/pi-tps/__tests__/helpers.ts +1 -1
- package/extensions/pi-tps/__tests__/rehydration.test.ts +16 -0
- package/extensions/pi-tps/index.ts +61 -71
- package/npm-shrinkwrap.json +2 -2
- package/package.json +12 -12
- package/pnpm-workspace.yaml +9 -0
|
@@ -13,17 +13,18 @@ jobs:
|
|
|
13
13
|
steps:
|
|
14
14
|
- uses: actions/checkout@v4
|
|
15
15
|
|
|
16
|
+
- uses: pnpm/action-setup@v4
|
|
16
17
|
- name: Setup Node.js
|
|
17
18
|
uses: actions/setup-node@v4
|
|
18
19
|
with:
|
|
19
20
|
node-version: '22'
|
|
20
|
-
cache: '
|
|
21
|
+
cache: 'pnpm'
|
|
21
22
|
|
|
22
23
|
- name: Install dependencies
|
|
23
|
-
run:
|
|
24
|
+
run: pnpm install --frozen-lockfile
|
|
24
25
|
|
|
25
26
|
- name: Type check
|
|
26
|
-
run:
|
|
27
|
+
run: pnpm typecheck
|
|
27
28
|
|
|
28
29
|
test:
|
|
29
30
|
runs-on: ubuntu-latest
|
|
@@ -32,20 +33,21 @@ jobs:
|
|
|
32
33
|
steps:
|
|
33
34
|
- uses: actions/checkout@v4
|
|
34
35
|
|
|
36
|
+
- uses: pnpm/action-setup@v4
|
|
35
37
|
- name: Setup Node.js
|
|
36
38
|
uses: actions/setup-node@v4
|
|
37
39
|
with:
|
|
38
40
|
node-version: '22'
|
|
39
|
-
cache: '
|
|
41
|
+
cache: 'pnpm'
|
|
40
42
|
|
|
41
43
|
- name: Install dependencies
|
|
42
|
-
run:
|
|
44
|
+
run: pnpm install --frozen-lockfile
|
|
43
45
|
|
|
44
46
|
- name: Run tests
|
|
45
|
-
run:
|
|
47
|
+
run: pnpm test
|
|
46
48
|
|
|
47
49
|
- name: Run tests with coverage
|
|
48
|
-
run:
|
|
50
|
+
run: pnpm test:coverage
|
|
49
51
|
|
|
50
52
|
- name: Upload coverage
|
|
51
53
|
uses: actions/upload-artifact@v4
|
|
@@ -52,7 +52,8 @@ function makeMessageWithCost(opts: {
|
|
|
52
52
|
async function runBurstTurn(
|
|
53
53
|
fixture: ReturnType<typeof createTestFixture>,
|
|
54
54
|
message: AssistantMessage,
|
|
55
|
-
turnIndex = 0
|
|
55
|
+
turnIndex = 0,
|
|
56
|
+
beforeTurnEnd?: () => void
|
|
56
57
|
) {
|
|
57
58
|
const { handlers, mockCtx } = fixture;
|
|
58
59
|
handlers['turn_start']?.({ type: 'turn_start', turnIndex, timestamp: Date.now() });
|
|
@@ -65,6 +66,7 @@ async function runBurstTurn(
|
|
|
65
66
|
assistantMessageEvent: { type: 'text_delta', delta: 't' },
|
|
66
67
|
});
|
|
67
68
|
handlers['message_end']?.({ type: 'message_end', message });
|
|
69
|
+
beforeTurnEnd?.();
|
|
68
70
|
handlers['turn_end']?.({ type: 'turn_end', turnIndex, message, toolResults: [] }, mockCtx);
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -108,15 +110,15 @@ describe('pi-tps extension — blended $/M-tokens rate', () => {
|
|
|
108
110
|
});
|
|
109
111
|
|
|
110
112
|
// Simulate the neuralwatt provider's turn_end running BEFORE ours: it emits
|
|
111
|
-
// the per-turn energy event
|
|
112
|
-
fixture
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
// the per-turn energy event after streaming but before pi-tps handles turn_end.
|
|
114
|
+
await runBurstTurn(fixture, message, 0, () => {
|
|
115
|
+
fixture.emitEvent('neuralwatt:turn-energy', {
|
|
116
|
+
costUsd: 0.006,
|
|
117
|
+
energyJoules: 21.6,
|
|
118
|
+
turnIndex: 0,
|
|
119
|
+
});
|
|
116
120
|
});
|
|
117
121
|
|
|
118
|
-
await runBurstTurn(fixture, message, 0);
|
|
119
|
-
|
|
120
122
|
const { notifySpy, appendEntrySpy } = fixture;
|
|
121
123
|
expect(notifySpy).toHaveBeenCalledOnce();
|
|
122
124
|
const banner = notifySpy.mock.calls[0][0] as string;
|
|
@@ -157,6 +159,102 @@ describe('pi-tps extension — blended $/M-tokens rate', () => {
|
|
|
157
159
|
expect(data.cost).toBeNull();
|
|
158
160
|
});
|
|
159
161
|
|
|
162
|
+
it('omits the rate when pi reports an all-zero cost block for an unpriced model', async () => {
|
|
163
|
+
const message = makeMessageWithCost({
|
|
164
|
+
input: 500,
|
|
165
|
+
output: 500,
|
|
166
|
+
costTotal: 0,
|
|
167
|
+
provider: 'makora',
|
|
168
|
+
model: 'zai-org/GLM-5.2-NVFP4',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await runBurstTurn(fixture, message);
|
|
172
|
+
|
|
173
|
+
const { appendEntrySpy, notifySpy } = fixture;
|
|
174
|
+
const [, data] = appendEntrySpy.mock.calls[0];
|
|
175
|
+
expect(data.cost).toBeNull();
|
|
176
|
+
expect(data.rateUsdPerMTokens).toBeNull();
|
|
177
|
+
expect(notifySpy.mock.calls[0][0]).not.toMatch(/\$.*\/M/);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('does not reuse a late billed cost after /tree when a turn index repeats', async () => {
|
|
181
|
+
const neuralwattMessage = makeMessageWithCost({
|
|
182
|
+
input: 1178,
|
|
183
|
+
output: 1235,
|
|
184
|
+
costTotal: 0.0102352,
|
|
185
|
+
provider: 'neuralwatt',
|
|
186
|
+
model: 'glm-5.2-short',
|
|
187
|
+
});
|
|
188
|
+
neuralwattMessage.usage.cacheRead = 8192;
|
|
189
|
+
neuralwattMessage.usage.totalTokens = 10605;
|
|
190
|
+
|
|
191
|
+
await runBurstTurn(fixture, neuralwattMessage, 2);
|
|
192
|
+
fixture.emitEvent('neuralwatt:turn-energy', {
|
|
193
|
+
costUsd: 0.00116,
|
|
194
|
+
energyJoules: 835.2,
|
|
195
|
+
turnIndex: 2,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
fixture.handlers['session_tree']?.(
|
|
199
|
+
{ type: 'session_tree', newLeafId: null, oldLeafId: 'old-leaf' },
|
|
200
|
+
fixture.mockCtx
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const unpricedMessage = makeMessageWithCost({
|
|
204
|
+
input: 8460,
|
|
205
|
+
output: 717,
|
|
206
|
+
costTotal: 0,
|
|
207
|
+
provider: 'makora',
|
|
208
|
+
model: 'zai-org/GLM-5.2-NVFP4',
|
|
209
|
+
});
|
|
210
|
+
unpricedMessage.usage.cacheRead = 1216;
|
|
211
|
+
unpricedMessage.usage.totalTokens = 10393;
|
|
212
|
+
|
|
213
|
+
await runBurstTurn(fixture, unpricedMessage, 2);
|
|
214
|
+
|
|
215
|
+
const latestTelemetry = fixture.appendEntrySpy.mock.calls.at(-1)![1];
|
|
216
|
+
expect(latestTelemetry.model).toEqual({
|
|
217
|
+
provider: 'makora',
|
|
218
|
+
modelId: 'zai-org/GLM-5.2-NVFP4',
|
|
219
|
+
});
|
|
220
|
+
expect(latestTelemetry.cost).toBeNull();
|
|
221
|
+
expect(latestTelemetry.rateUsdPerMTokens).toBeNull();
|
|
222
|
+
|
|
223
|
+
const latestBanner = fixture.notifySpy.mock.calls.at(-1)![0] as string;
|
|
224
|
+
expect(latestBanner).not.toContain('$0.11/M');
|
|
225
|
+
expect(latestBanner).not.toMatch(/\$.*\/M/);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('does not apply an early current-turn event to a previous run with the same index', async () => {
|
|
229
|
+
const previousMessage = makeMessageWithCost({
|
|
230
|
+
input: 1000,
|
|
231
|
+
output: 1000,
|
|
232
|
+
costTotal: 0.008,
|
|
233
|
+
});
|
|
234
|
+
await runBurstTurn(fixture, previousMessage, 0);
|
|
235
|
+
|
|
236
|
+
const currentMessage = makeMessageWithCost({
|
|
237
|
+
input: 1000,
|
|
238
|
+
output: 1000,
|
|
239
|
+
costTotal: 0.01,
|
|
240
|
+
provider: 'neuralwatt',
|
|
241
|
+
model: 'glm-5.2',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await runBurstTurn(fixture, currentMessage, 0, () => {
|
|
245
|
+
fixture.emitEvent('neuralwatt:turn-energy', {
|
|
246
|
+
costUsd: 0.006,
|
|
247
|
+
energyJoules: 21.6,
|
|
248
|
+
turnIndex: 0,
|
|
249
|
+
});
|
|
250
|
+
expect(fixture.appendEntrySpy).toHaveBeenCalledTimes(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(fixture.appendEntrySpy).toHaveBeenCalledTimes(2);
|
|
254
|
+
const latestTelemetry = fixture.appendEntrySpy.mock.calls.at(-1)![1];
|
|
255
|
+
expect(latestTelemetry.rateUsdPerMTokens).toBe(3);
|
|
256
|
+
});
|
|
257
|
+
|
|
160
258
|
it('falls back to list-price rate when billed-cost event misses (out-of-order load)', async () => {
|
|
161
259
|
// Neuralwatt turn but the energy event never arrives (provider loaded after
|
|
162
260
|
// us). Must not block or crash — falls back to the list-price compute rate.
|
|
@@ -224,12 +322,13 @@ describe('pi-tps extension — blended $/M-tokens rate', () => {
|
|
|
224
322
|
model: 'moonshotai/Kimi-K2.5',
|
|
225
323
|
});
|
|
226
324
|
|
|
227
|
-
fixture
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
325
|
+
await runBurstTurn(fixture, message, 0, () => {
|
|
326
|
+
fixture.emitEvent('neuralwatt:turn-energy', {
|
|
327
|
+
costUsd: 0.006,
|
|
328
|
+
energyJoules: 21.6,
|
|
329
|
+
turnIndex: 0,
|
|
330
|
+
});
|
|
231
331
|
});
|
|
232
|
-
await runBurstTurn(fixture, message, 0);
|
|
233
332
|
|
|
234
333
|
// A stray duplicate event must not append a second corrected entry.
|
|
235
334
|
fixture.emitEvent('neuralwatt:turn-energy', {
|
|
@@ -108,7 +108,7 @@ export function createTestFixture(): TestFixture {
|
|
|
108
108
|
ui: { notify: notifySpy } as any,
|
|
109
109
|
sessionManager: {
|
|
110
110
|
getEntries: vi.fn().mockReturnValue(mockEntries),
|
|
111
|
-
getBranch: vi.fn(),
|
|
111
|
+
getBranch: vi.fn().mockReturnValue(mockEntries),
|
|
112
112
|
getSessionId: vi.fn(),
|
|
113
113
|
},
|
|
114
114
|
modelRegistry: undefined as any,
|
|
@@ -175,6 +175,22 @@ describe('pi-tps extension — rehydration', () => {
|
|
|
175
175
|
expect(notifySpy).toHaveBeenCalledOnce();
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
+
it('should restore from the active branch instead of an abandoned branch', async () => {
|
|
179
|
+
const { handlers, notifySpy, mockEntries, mockCtx } = fixture;
|
|
180
|
+
const activeEntry = makeTpsEntry({ tps: 10.0 });
|
|
181
|
+
const abandonedEntry = makeTpsEntry({ tps: 99.0 });
|
|
182
|
+
mockEntries.push(activeEntry, abandonedEntry);
|
|
183
|
+
(mockCtx.sessionManager.getBranch as ReturnType<typeof vi.fn>).mockReturnValue([activeEntry]);
|
|
184
|
+
|
|
185
|
+
handlers['session_tree']?.({ newLeafId: 'active', oldLeafId: 'abandoned' }, mockCtx);
|
|
186
|
+
await tick();
|
|
187
|
+
|
|
188
|
+
expect(notifySpy).toHaveBeenCalledOnce();
|
|
189
|
+
const msg = notifySpy.mock.calls[0][0] as string;
|
|
190
|
+
expect(msg).toContain('TPS 10.0');
|
|
191
|
+
expect(msg).not.toContain('TPS 99.0');
|
|
192
|
+
});
|
|
193
|
+
|
|
178
194
|
it('should rehydrate most recent entry, preferring structured over legacy', async () => {
|
|
179
195
|
const { handlers, notifySpy, mockEntries } = fixture;
|
|
180
196
|
|
|
@@ -74,8 +74,6 @@ const STALL_THRESHOLD_MS = 500;
|
|
|
74
74
|
|
|
75
75
|
/** Event name emitted by pi-neuralwatt-provider per turn with energy-billed cost data. */
|
|
76
76
|
const NEURALWATT_ENERGY_EVENT = 'neuralwatt:turn-energy';
|
|
77
|
-
/** Cap the per-turn billed-cost cache (turnIndex → costUsd) to avoid unbounded growth in long sessions. */
|
|
78
|
-
const NEURALWATT_ENERGY_CACHE_MAX = 32;
|
|
79
77
|
|
|
80
78
|
// ─── Data types ─────────────────────────────────────────────────────────────
|
|
81
79
|
|
|
@@ -115,6 +113,7 @@ interface TurnTelemetry {
|
|
|
115
113
|
|
|
116
114
|
/** In-memory state accumulated during one LLM turn */
|
|
117
115
|
interface TurnTiming {
|
|
116
|
+
turnIndex: number;
|
|
118
117
|
turnStartMs: number;
|
|
119
118
|
lastUpdateMs: number;
|
|
120
119
|
firstTokenMs: number | null;
|
|
@@ -489,7 +488,11 @@ function buildTelemetry(
|
|
|
489
488
|
// billed cost when present (energy-based, what the user actually pays),
|
|
490
489
|
// otherwise the list-price compute cost from message.usage.cost. Never both
|
|
491
490
|
// — billedCost wins outright when present, so there's no double-counting.
|
|
492
|
-
|
|
491
|
+
// Pi represents models without pricing with an all-zero cost block. Treat
|
|
492
|
+
// that as unavailable rather than displaying a misleading $0.00/M rate. An
|
|
493
|
+
// explicit billed cost remains valid even when it is zero.
|
|
494
|
+
const listPriceCost = hasCost && Number.isFinite(costTotal) && costTotal > 0 ? costTotal : null;
|
|
495
|
+
const effectiveCost = billedCost ?? listPriceCost;
|
|
493
496
|
const rateUsdPerMTokens = computeRateUsdPerM(effectiveCost, totalTokens);
|
|
494
497
|
|
|
495
498
|
return {
|
|
@@ -506,15 +509,16 @@ function buildTelemetry(
|
|
|
506
509
|
},
|
|
507
510
|
tps,
|
|
508
511
|
isPrimaryBranch,
|
|
509
|
-
cost:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
512
|
+
cost:
|
|
513
|
+
listPriceCost !== null
|
|
514
|
+
? {
|
|
515
|
+
input: costInput,
|
|
516
|
+
output: costOutput,
|
|
517
|
+
cacheRead: costCacheRead,
|
|
518
|
+
cacheWrite: costCacheWrite,
|
|
519
|
+
total: costTotal,
|
|
520
|
+
}
|
|
521
|
+
: null,
|
|
518
522
|
rateUsdPerMTokens,
|
|
519
523
|
timestamp: Date.now(),
|
|
520
524
|
};
|
|
@@ -533,24 +537,12 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
533
537
|
// Cached session entries for argument completion (captured on session_start / session_tree)
|
|
534
538
|
let cachedEntries: Array<{ type?: string; customType?: string; data?: unknown }> = [];
|
|
535
539
|
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
// event has already fired and the cache hit is immediate; if after us we miss it
|
|
543
|
-
// for that one turn and fall back to the list-price compute rate (the neuralwatt
|
|
544
|
-
// widget still shows billing separately). Zero latency, no awaiting, no duplicate
|
|
545
|
-
// capture — the raw energy/cost records stay solely in the neuralwatt provider's
|
|
546
|
-
// own session entries. Non-Neuralwatt turns never emit, so this stays empty.
|
|
547
|
-
const neuralwattBilledCostByTurn = new Map<number, number>();
|
|
548
|
-
|
|
549
|
-
// Tracks the most recently committed turn so a late-arriving
|
|
550
|
-
// `neuralwatt:turn-energy` event (provider loaded AFTER us) can retroactively
|
|
551
|
-
// correct a turn that we already persisted with the list-price fallback.
|
|
552
|
-
// Load-order-independent: when the provider loads before us the live cache
|
|
553
|
-
// hits and `billedApplied` is already true, so this never fires.
|
|
540
|
+
// pi-neuralwatt-provider emits billed cost during its turn_end handler. Keep
|
|
541
|
+
// an early event only for the active turn; a late event corrects only the
|
|
542
|
+
// turn that was just committed. turnIndex resets to zero on every agent run,
|
|
543
|
+
// so billed costs must never survive beyond either of those scopes.
|
|
544
|
+
let pendingNeuralwattBilledCost: { turnIndex: number; costUsd: number } | null = null;
|
|
545
|
+
|
|
554
546
|
let lastCommittedTurn: {
|
|
555
547
|
turnIndex: number;
|
|
556
548
|
telemetry: TurnTelemetry;
|
|
@@ -563,43 +555,34 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
563
555
|
const p = payload as Record<string, unknown>;
|
|
564
556
|
const turnIndex = typeof p.turnIndex === 'number' ? p.turnIndex : null;
|
|
565
557
|
const costUsd = typeof p.costUsd === 'number' ? p.costUsd : null;
|
|
566
|
-
if (turnIndex === null || costUsd === null)
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
//
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
// means a new turn is streaming; updating the banner then would be
|
|
575
|
-
// misleading). Idempotent: once `billedApplied` flips true we never
|
|
576
|
-
// re-correct, so repeated or duplicate events are safe.
|
|
577
|
-
const committed = lastCommittedTurn;
|
|
578
|
-
if (
|
|
579
|
-
committed &&
|
|
580
|
-
!committed.billedApplied &&
|
|
581
|
-
committed.turnIndex === turnIndex &&
|
|
582
|
-
!Number.isNaN(costUsd)
|
|
583
|
-
) {
|
|
584
|
-
const totalTokens = committed.telemetry.tokens.total;
|
|
585
|
-
const correctedRate = computeRateUsdPerM(costUsd, totalTokens);
|
|
586
|
-
if (correctedRate !== null && correctedRate !== committed.telemetry.rateUsdPerMTokens) {
|
|
587
|
-
const corrected = { ...committed.telemetry, rateUsdPerMTokens: correctedRate };
|
|
588
|
-
committed.telemetry = corrected;
|
|
589
|
-
committed.billedApplied = true;
|
|
590
|
-
pi.appendEntry('tps', corrected);
|
|
591
|
-
pi.events?.emit('tps:telemetry', corrected);
|
|
592
|
-
cachedEntries.push({ type: 'custom', customType: 'tps' });
|
|
593
|
-
if (committed.ctx.hasUI && !currentTiming) {
|
|
594
|
-
committed.ctx.ui.notify(composeDisplayString(corrected), 'info');
|
|
595
|
-
}
|
|
558
|
+
if (turnIndex === null || costUsd === null || !Number.isFinite(costUsd) || costUsd < 0) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Provider loaded before pi-tps: turn_end has not reached us yet.
|
|
563
|
+
if (currentTiming) {
|
|
564
|
+
if (currentTiming.turnIndex === turnIndex) {
|
|
565
|
+
pendingNeuralwattBilledCost = { turnIndex, costUsd };
|
|
596
566
|
}
|
|
567
|
+
return;
|
|
597
568
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
569
|
+
|
|
570
|
+
// Provider loaded after pi-tps: correct the turn we just committed. Do not
|
|
571
|
+
// cache this event; the same turnIndex can belong to the next agent run.
|
|
572
|
+
const committed = lastCommittedTurn;
|
|
573
|
+
if (!committed || committed.billedApplied || committed.turnIndex !== turnIndex) return;
|
|
574
|
+
|
|
575
|
+
committed.billedApplied = true;
|
|
576
|
+
const correctedRate = computeRateUsdPerM(costUsd, committed.telemetry.tokens.total);
|
|
577
|
+
if (correctedRate === null || correctedRate === committed.telemetry.rateUsdPerMTokens) return;
|
|
578
|
+
|
|
579
|
+
const corrected = { ...committed.telemetry, rateUsdPerMTokens: correctedRate };
|
|
580
|
+
committed.telemetry = corrected;
|
|
581
|
+
pi.appendEntry('tps', corrected);
|
|
582
|
+
pi.events?.emit('tps:telemetry', corrected);
|
|
583
|
+
cachedEntries.push({ type: 'custom', customType: 'tps' });
|
|
584
|
+
if (committed.ctx.hasUI) {
|
|
585
|
+
committed.ctx.ui.notify(composeDisplayString(corrected), 'info');
|
|
603
586
|
}
|
|
604
587
|
});
|
|
605
588
|
|
|
@@ -613,7 +596,7 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
613
596
|
*/
|
|
614
597
|
function restoreTPSNotification(ctx: ExtensionContext) {
|
|
615
598
|
if (!ctx.hasUI) return;
|
|
616
|
-
const entries =
|
|
599
|
+
const entries = ctx.sessionManager.getBranch();
|
|
617
600
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
618
601
|
const entry = entries[i];
|
|
619
602
|
if (entry.type === 'custom' && entry.customType === 'tps') {
|
|
@@ -646,6 +629,8 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
646
629
|
|
|
647
630
|
// Restore notification after /tree navigation (same session, different branch)
|
|
648
631
|
pi.on('session_tree', (_event: SessionTreeEvent, ctx: ExtensionContext) => {
|
|
632
|
+
pendingNeuralwattBilledCost = null;
|
|
633
|
+
lastCommittedTurn = null;
|
|
649
634
|
cachedEntries = ctx.sessionManager.getEntries();
|
|
650
635
|
restoreTPSNotification(ctx);
|
|
651
636
|
});
|
|
@@ -654,7 +639,10 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
654
639
|
|
|
655
640
|
// Track when a turn starts (request sent to LLM)
|
|
656
641
|
pi.on('turn_start', (_event: TurnStartEvent) => {
|
|
642
|
+
pendingNeuralwattBilledCost = null;
|
|
643
|
+
lastCommittedTurn = null;
|
|
657
644
|
currentTiming = {
|
|
645
|
+
turnIndex: _event.turnIndex,
|
|
658
646
|
turnStartMs: performance.now(),
|
|
659
647
|
turnStartTimestamp: typeof _event.timestamp === 'number' ? _event.timestamp : Date.now(),
|
|
660
648
|
lastUpdateMs: performance.now(),
|
|
@@ -764,9 +752,8 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
764
752
|
// ── Persist telemetry ───────────────────────────────────────────────────
|
|
765
753
|
|
|
766
754
|
// Calculate, display, and persist telemetry at the end of each LLM turn.
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
// synchronously. No awaiting, no added latency to turn dispatch.
|
|
755
|
+
// The Neuralwatt listener stores an early billed cost only while this exact
|
|
756
|
+
// turn is active, so the handoff remains synchronous and adds no latency.
|
|
770
757
|
pi.on('turn_end', (event: TurnEndEvent, ctx: ExtensionContext) => {
|
|
771
758
|
if (!currentTiming) return;
|
|
772
759
|
|
|
@@ -782,8 +769,11 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
782
769
|
// energy source is available do we fall back to the compute rate.
|
|
783
770
|
// Prefer the live energy event, fall back to the persisted energy entry,
|
|
784
771
|
// and finally to the list-price compute cost.
|
|
785
|
-
let billedCost =
|
|
786
|
-
|
|
772
|
+
let billedCost =
|
|
773
|
+
pendingNeuralwattBilledCost?.turnIndex === event.turnIndex
|
|
774
|
+
? pendingNeuralwattBilledCost.costUsd
|
|
775
|
+
: null;
|
|
776
|
+
pendingNeuralwattBilledCost = null;
|
|
787
777
|
|
|
788
778
|
if (billedCost === null && timing.turnStartTimestamp) {
|
|
789
779
|
billedCost = findEnergyCostFromSession(ctx, timing.turnStartTimestamp);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monotykamary/pi-tps",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@monotykamary/pi-tps",
|
|
9
|
-
"version": "1.3.
|
|
9
|
+
"version": "1.3.4",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"devDependencies": {
|
package/package.json
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monotykamary/pi-tps",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "Tokens-per-second tracker for pi — see your LLM generation speed after every agent turn",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
7
7
|
],
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "vitest run",
|
|
11
|
-
"test:watch": "vitest",
|
|
12
|
-
"test:coverage": "vitest run --coverage",
|
|
13
|
-
"typecheck": "tsc --noEmit",
|
|
14
|
-
"lint:dead": "knip --no-gitignore",
|
|
15
|
-
"postinstall": "simple-git-hooks 2>/dev/null || true"
|
|
16
|
-
},
|
|
17
9
|
"pi": {
|
|
18
10
|
"extensions": [
|
|
19
11
|
"./extensions"
|
|
@@ -32,8 +24,8 @@
|
|
|
32
24
|
"devDependencies": {
|
|
33
25
|
"@commitlint/cli": "21.0.1",
|
|
34
26
|
"@commitlint/config-conventional": "21.0.1",
|
|
35
|
-
"@earendil-works/pi-ai": "0.
|
|
36
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
27
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
28
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
37
29
|
"@types/node": "25.9.1",
|
|
38
30
|
"@vitest/coverage-v8": "4.1.7",
|
|
39
31
|
"knip": "6.14.1",
|
|
@@ -52,5 +44,13 @@
|
|
|
52
44
|
"fast-xml-builder": "1.2.0",
|
|
53
45
|
"protobufjs": "8.4.0",
|
|
54
46
|
"ws": "8.20.1"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:watch": "vitest",
|
|
51
|
+
"test:coverage": "vitest run --coverage",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"lint:dead": "knip --no-gitignore",
|
|
54
|
+
"postinstall": "simple-git-hooks 2>/dev/null || true"
|
|
55
55
|
}
|
|
56
|
-
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
allowBuilds:
|
|
2
|
+
'@google/genai': false
|
|
3
|
+
protobufjs: false
|
|
4
|
+
simple-git-hooks: false
|
|
5
|
+
minimumReleaseAgeExclude:
|
|
6
|
+
- '@earendil-works/pi-agent-core@0.83.0'
|
|
7
|
+
- '@earendil-works/pi-ai@0.83.0'
|
|
8
|
+
- '@earendil-works/pi-coding-agent@0.83.0'
|
|
9
|
+
- '@earendil-works/pi-tui@0.83.0'
|