@monotykamary/pi-tps 1.3.1 → 1.3.3
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 +177 -8
- 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 +80 -37
- package/npm-shrinkwrap.json +2 -2
- package/package.json +10 -10
- package/pnpm-workspace.yaml +4 -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.
|
|
@@ -175,6 +273,77 @@ describe('pi-tps extension — blended $/M-tokens rate', () => {
|
|
|
175
273
|
expect(banner).toContain('$4.00/M');
|
|
176
274
|
});
|
|
177
275
|
|
|
276
|
+
it('corrects the banner + persisted entry when the energy event arrives late', async () => {
|
|
277
|
+
// Provider loads AFTER pi-tps: our turn_end runs first on the list-price
|
|
278
|
+
// fallback, then the provider's turn_end emits the energy event, which
|
|
279
|
+
// must retroactively correct the rate without a second turn_end.
|
|
280
|
+
const message = makeMessageWithCost({
|
|
281
|
+
input: 1000,
|
|
282
|
+
output: 1000,
|
|
283
|
+
costTotal: 0.008, // list price: $4.00/M; billed: $3.00/M
|
|
284
|
+
provider: 'neuralwatt',
|
|
285
|
+
model: 'moonshotai/Kimi-K2.5',
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await runBurstTurn(fixture, message, 0);
|
|
289
|
+
|
|
290
|
+
const { notifySpy, appendEntrySpy } = fixture;
|
|
291
|
+
// Initially committed on the list-price fallback.
|
|
292
|
+
expect(appendEntrySpy.mock.calls[0][0]).toBe('tps');
|
|
293
|
+
expect(appendEntrySpy.mock.calls[0][1].rateUsdPerMTokens).toBe(4.0);
|
|
294
|
+
expect(notifySpy.mock.calls[0][0]).toContain('$4.00/M');
|
|
295
|
+
|
|
296
|
+
// Provider's turn_end fires the energy event after ours.
|
|
297
|
+
fixture.emitEvent('neuralwatt:turn-energy', {
|
|
298
|
+
costUsd: 0.006, // $3.00/M for 2000 tokens
|
|
299
|
+
energyJoules: 21.6,
|
|
300
|
+
turnIndex: 0,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// A corrected `tps` entry is appended in place of the list-price one.
|
|
304
|
+
expect(appendEntrySpy.mock.calls).toHaveLength(2);
|
|
305
|
+
expect(appendEntrySpy.mock.calls[1][0]).toBe('tps');
|
|
306
|
+
expect(appendEntrySpy.mock.calls[1][1].rateUsdPerMTokens).toBe(3.0);
|
|
307
|
+
|
|
308
|
+
// Banner is refreshed to the billed rate.
|
|
309
|
+
const lastBanner = notifySpy.mock.calls.at(-1)![0] as string;
|
|
310
|
+
expect(lastBanner).toContain('$3.00/M');
|
|
311
|
+
expect(lastBanner).not.toContain('$4.00/M');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('does not double-correct a turn already billed at commit time', async () => {
|
|
315
|
+
// Provider loads BEFORE pi-tps: the event fires first, our turn_end reads
|
|
316
|
+
// it from the live cache (billedApplied=true), so no late correction.
|
|
317
|
+
const message = makeMessageWithCost({
|
|
318
|
+
input: 1000,
|
|
319
|
+
output: 1000,
|
|
320
|
+
costTotal: 0.008,
|
|
321
|
+
provider: 'neuralwatt',
|
|
322
|
+
model: 'moonshotai/Kimi-K2.5',
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await runBurstTurn(fixture, message, 0, () => {
|
|
326
|
+
fixture.emitEvent('neuralwatt:turn-energy', {
|
|
327
|
+
costUsd: 0.006,
|
|
328
|
+
energyJoules: 21.6,
|
|
329
|
+
turnIndex: 0,
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// A stray duplicate event must not append a second corrected entry.
|
|
334
|
+
fixture.emitEvent('neuralwatt:turn-energy', {
|
|
335
|
+
costUsd: 0.006,
|
|
336
|
+
energyJoules: 21.6,
|
|
337
|
+
turnIndex: 0,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const { appendEntrySpy, notifySpy } = fixture;
|
|
341
|
+
expect(appendEntrySpy.mock.calls.filter((c) => c[0] === 'tps')).toHaveLength(1);
|
|
342
|
+
expect(notifySpy).toHaveBeenCalledOnce();
|
|
343
|
+
const banner = notifySpy.mock.calls[0][0] as string;
|
|
344
|
+
expect(banner).toContain('$3.00/M');
|
|
345
|
+
});
|
|
346
|
+
|
|
178
347
|
it('ignores neuralwatt:turn-energy payloads lacking a numeric turnIndex', async () => {
|
|
179
348
|
// Defensive: malformed event must not pollute the cache.
|
|
180
349
|
fixture.emitEvent('neuralwatt:turn-energy', { costUsd: 0.006, energyJoules: 21.6 }); // no turnIndex
|
|
@@ -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,31 +537,52 @@ 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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+
|
|
546
|
+
let lastCommittedTurn: {
|
|
547
|
+
turnIndex: number;
|
|
548
|
+
telemetry: TurnTelemetry;
|
|
549
|
+
billedApplied: boolean;
|
|
550
|
+
ctx: ExtensionContext;
|
|
551
|
+
} | null = null;
|
|
548
552
|
|
|
549
553
|
pi.events?.on(NEURALWATT_ENERGY_EVENT, (payload: unknown) => {
|
|
550
554
|
if (!payload || typeof payload !== 'object') return;
|
|
551
555
|
const p = payload as Record<string, unknown>;
|
|
552
556
|
const turnIndex = typeof p.turnIndex === 'number' ? p.turnIndex : null;
|
|
553
557
|
const costUsd = typeof p.costUsd === 'number' ? p.costUsd : null;
|
|
554
|
-
if (turnIndex === null || costUsd === null)
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (
|
|
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 };
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
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');
|
|
561
586
|
}
|
|
562
587
|
});
|
|
563
588
|
|
|
@@ -571,7 +596,7 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
571
596
|
*/
|
|
572
597
|
function restoreTPSNotification(ctx: ExtensionContext) {
|
|
573
598
|
if (!ctx.hasUI) return;
|
|
574
|
-
const entries =
|
|
599
|
+
const entries = ctx.sessionManager.getBranch();
|
|
575
600
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
576
601
|
const entry = entries[i];
|
|
577
602
|
if (entry.type === 'custom' && entry.customType === 'tps') {
|
|
@@ -604,6 +629,8 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
604
629
|
|
|
605
630
|
// Restore notification after /tree navigation (same session, different branch)
|
|
606
631
|
pi.on('session_tree', (_event: SessionTreeEvent, ctx: ExtensionContext) => {
|
|
632
|
+
pendingNeuralwattBilledCost = null;
|
|
633
|
+
lastCommittedTurn = null;
|
|
607
634
|
cachedEntries = ctx.sessionManager.getEntries();
|
|
608
635
|
restoreTPSNotification(ctx);
|
|
609
636
|
});
|
|
@@ -612,7 +639,10 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
612
639
|
|
|
613
640
|
// Track when a turn starts (request sent to LLM)
|
|
614
641
|
pi.on('turn_start', (_event: TurnStartEvent) => {
|
|
642
|
+
pendingNeuralwattBilledCost = null;
|
|
643
|
+
lastCommittedTurn = null;
|
|
615
644
|
currentTiming = {
|
|
645
|
+
turnIndex: _event.turnIndex,
|
|
616
646
|
turnStartMs: performance.now(),
|
|
617
647
|
turnStartTimestamp: typeof _event.timestamp === 'number' ? _event.timestamp : Date.now(),
|
|
618
648
|
lastUpdateMs: performance.now(),
|
|
@@ -722,9 +752,8 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
722
752
|
// ── Persist telemetry ───────────────────────────────────────────────────
|
|
723
753
|
|
|
724
754
|
// Calculate, display, and persist telemetry at the end of each LLM turn.
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
// 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.
|
|
728
757
|
pi.on('turn_end', (event: TurnEndEvent, ctx: ExtensionContext) => {
|
|
729
758
|
if (!currentTiming) return;
|
|
730
759
|
|
|
@@ -740,8 +769,11 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
740
769
|
// energy source is available do we fall back to the compute rate.
|
|
741
770
|
// Prefer the live energy event, fall back to the persisted energy entry,
|
|
742
771
|
// and finally to the list-price compute cost.
|
|
743
|
-
let billedCost =
|
|
744
|
-
|
|
772
|
+
let billedCost =
|
|
773
|
+
pendingNeuralwattBilledCost?.turnIndex === event.turnIndex
|
|
774
|
+
? pendingNeuralwattBilledCost.costUsd
|
|
775
|
+
: null;
|
|
776
|
+
pendingNeuralwattBilledCost = null;
|
|
745
777
|
|
|
746
778
|
if (billedCost === null && timing.turnStartTimestamp) {
|
|
747
779
|
billedCost = findEnergyCostFromSession(ctx, timing.turnStartTimestamp);
|
|
@@ -772,6 +804,17 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
772
804
|
}
|
|
773
805
|
}
|
|
774
806
|
|
|
807
|
+
// Record this turn so a late-arriving neuralwatt:turn-energy event
|
|
808
|
+
// (provider loaded after us) can retroactively correct the rate. Billed
|
|
809
|
+
// cost wins when present, so a turn already billed at commit time is
|
|
810
|
+
// never re-corrected.
|
|
811
|
+
lastCommittedTurn = {
|
|
812
|
+
turnIndex: event.turnIndex,
|
|
813
|
+
telemetry,
|
|
814
|
+
billedApplied: billedCost !== null,
|
|
815
|
+
ctx,
|
|
816
|
+
};
|
|
817
|
+
|
|
775
818
|
// Persist structured telemetry to session for export and rehydration
|
|
776
819
|
pi.appendEntry('tps', telemetry);
|
|
777
820
|
|
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.3",
|
|
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.3",
|
|
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.3",
|
|
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"
|
|
@@ -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
|
+
}
|