@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.
@@ -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: 'npm'
21
+ cache: 'pnpm'
21
22
 
22
23
  - name: Install dependencies
23
- run: npm ci
24
+ run: pnpm install --frozen-lockfile
24
25
 
25
26
  - name: Type check
26
- run: npm run typecheck
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: 'npm'
41
+ cache: 'pnpm'
40
42
 
41
43
  - name: Install dependencies
42
- run: npm ci
44
+ run: pnpm install --frozen-lockfile
43
45
 
44
46
  - name: Run tests
45
- run: npm test
47
+ run: pnpm test
46
48
 
47
49
  - name: Run tests with coverage
48
- run: npm run test:coverage
50
+ run: pnpm test:coverage
49
51
 
50
52
  - name: Upload coverage
51
53
  uses: actions/upload-artifact@v4
@@ -0,0 +1,4 @@
1
+ {
2
+ "format": 1,
3
+ "actors": []
4
+ }
@@ -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, which our listener stashes keyed by turnIndex.
112
- fixture.emitEvent('neuralwatt:turn-energy', {
113
- costUsd: 0.006,
114
- energyJoules: 21.6,
115
- turnIndex: 0,
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
- const effectiveCost = billedCost ?? (hasCost ? costTotal : null);
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: hasCost
510
- ? {
511
- input: costInput,
512
- output: costOutput,
513
- cacheRead: costCacheRead,
514
- cacheWrite: costCacheWrite,
515
- total: costTotal,
516
- }
517
- : null,
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
- // ── Neuralwatt per-turn billed-cost integration ─────────────────────────
537
- // pi-neuralwatt-provider emits NEURALWATT_ENERGY_EVENT in its turn_end handler
538
- // (after its tee reader drains) with { costUsd, energyJoules, turnIndex }. We stash
539
- // only the one number we need billed costUsd keyed by turnIndex, and read it
540
- // synchronously in our own turn_end. pi awaits turn_end handlers sequentially in
541
- // registration order, so if the neuralwatt provider was registered before us the
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>();
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) return; // require turnIndex correlation
555
- neuralwattBilledCostByTurn.set(turnIndex, costUsd);
556
- // Bound the cache (oldest = lowest turnIndex)
557
- if (neuralwattBilledCostByTurn.size > NEURALWATT_ENERGY_CACHE_MAX) {
558
- let oldest = Infinity;
559
- for (const k of neuralwattBilledCostByTurn.keys()) if (k < oldest) oldest = k;
560
- if (oldest !== Infinity) neuralwattBilledCostByTurn.delete(oldest);
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 = cachedEntries.length > 0 ? cachedEntries : ctx.sessionManager.getEntries();
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
- // Synchronous: the Neuralwatt billed cost (if any) was stashed by our
726
- // NEURALWATT_ENERGY_EVENT listener, keyed by turnIndex, and is read here
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 = neuralwattBilledCostByTurn.get(event.turnIndex) ?? null;
744
- neuralwattBilledCostByTurn.delete(event.turnIndex);
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
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-tps",
3
- "version": "1.3.1",
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.1",
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.1",
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
+ }
@@ -0,0 +1,4 @@
1
+ allowBuilds:
2
+ '@google/genai': false
3
+ protobufjs: false
4
+ simple-git-hooks: false