@monotykamary/pi-tps 1.3.1 → 1.3.2

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.
@@ -175,6 +175,76 @@ describe('pi-tps extension — blended $/M-tokens rate', () => {
175
175
  expect(banner).toContain('$4.00/M');
176
176
  });
177
177
 
178
+ it('corrects the banner + persisted entry when the energy event arrives late', async () => {
179
+ // Provider loads AFTER pi-tps: our turn_end runs first on the list-price
180
+ // fallback, then the provider's turn_end emits the energy event, which
181
+ // must retroactively correct the rate without a second turn_end.
182
+ const message = makeMessageWithCost({
183
+ input: 1000,
184
+ output: 1000,
185
+ costTotal: 0.008, // list price: $4.00/M; billed: $3.00/M
186
+ provider: 'neuralwatt',
187
+ model: 'moonshotai/Kimi-K2.5',
188
+ });
189
+
190
+ await runBurstTurn(fixture, message, 0);
191
+
192
+ const { notifySpy, appendEntrySpy } = fixture;
193
+ // Initially committed on the list-price fallback.
194
+ expect(appendEntrySpy.mock.calls[0][0]).toBe('tps');
195
+ expect(appendEntrySpy.mock.calls[0][1].rateUsdPerMTokens).toBe(4.0);
196
+ expect(notifySpy.mock.calls[0][0]).toContain('$4.00/M');
197
+
198
+ // Provider's turn_end fires the energy event after ours.
199
+ fixture.emitEvent('neuralwatt:turn-energy', {
200
+ costUsd: 0.006, // $3.00/M for 2000 tokens
201
+ energyJoules: 21.6,
202
+ turnIndex: 0,
203
+ });
204
+
205
+ // A corrected `tps` entry is appended in place of the list-price one.
206
+ expect(appendEntrySpy.mock.calls).toHaveLength(2);
207
+ expect(appendEntrySpy.mock.calls[1][0]).toBe('tps');
208
+ expect(appendEntrySpy.mock.calls[1][1].rateUsdPerMTokens).toBe(3.0);
209
+
210
+ // Banner is refreshed to the billed rate.
211
+ const lastBanner = notifySpy.mock.calls.at(-1)![0] as string;
212
+ expect(lastBanner).toContain('$3.00/M');
213
+ expect(lastBanner).not.toContain('$4.00/M');
214
+ });
215
+
216
+ it('does not double-correct a turn already billed at commit time', async () => {
217
+ // Provider loads BEFORE pi-tps: the event fires first, our turn_end reads
218
+ // it from the live cache (billedApplied=true), so no late correction.
219
+ const message = makeMessageWithCost({
220
+ input: 1000,
221
+ output: 1000,
222
+ costTotal: 0.008,
223
+ provider: 'neuralwatt',
224
+ model: 'moonshotai/Kimi-K2.5',
225
+ });
226
+
227
+ fixture.emitEvent('neuralwatt:turn-energy', {
228
+ costUsd: 0.006,
229
+ energyJoules: 21.6,
230
+ turnIndex: 0,
231
+ });
232
+ await runBurstTurn(fixture, message, 0);
233
+
234
+ // A stray duplicate event must not append a second corrected entry.
235
+ fixture.emitEvent('neuralwatt:turn-energy', {
236
+ costUsd: 0.006,
237
+ energyJoules: 21.6,
238
+ turnIndex: 0,
239
+ });
240
+
241
+ const { appendEntrySpy, notifySpy } = fixture;
242
+ expect(appendEntrySpy.mock.calls.filter((c) => c[0] === 'tps')).toHaveLength(1);
243
+ expect(notifySpy).toHaveBeenCalledOnce();
244
+ const banner = notifySpy.mock.calls[0][0] as string;
245
+ expect(banner).toContain('$3.00/M');
246
+ });
247
+
178
248
  it('ignores neuralwatt:turn-energy payloads lacking a numeric turnIndex', async () => {
179
249
  // Defensive: malformed event must not pollute the cache.
180
250
  fixture.emitEvent('neuralwatt:turn-energy', { costUsd: 0.006, energyJoules: 21.6 }); // no turnIndex
@@ -546,6 +546,18 @@ export default function tpsExtension(pi: ExtensionAPI) {
546
546
  // own session entries. Non-Neuralwatt turns never emit, so this stays empty.
547
547
  const neuralwattBilledCostByTurn = new Map<number, number>();
548
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.
554
+ let lastCommittedTurn: {
555
+ turnIndex: number;
556
+ telemetry: TurnTelemetry;
557
+ billedApplied: boolean;
558
+ ctx: ExtensionContext;
559
+ } | null = null;
560
+
549
561
  pi.events?.on(NEURALWATT_ENERGY_EVENT, (payload: unknown) => {
550
562
  if (!payload || typeof payload !== 'object') return;
551
563
  const p = payload as Record<string, unknown>;
@@ -553,6 +565,36 @@ export default function tpsExtension(pi: ExtensionAPI) {
553
565
  const costUsd = typeof p.costUsd === 'number' ? p.costUsd : null;
554
566
  if (turnIndex === null || costUsd === null) return; // require turnIndex correlation
555
567
  neuralwattBilledCostByTurn.set(turnIndex, costUsd);
568
+
569
+ // Late-arrival correction: the event reached us after our turn_end already
570
+ // committed this turn on the list-price fallback. Recompute the blended
571
+ // rate from the billed cost, persist a corrected `tps` entry (which later
572
+ // resume/export reads in place of the stale list-price one), and refresh
573
+ // the banner — but only if no newer turn is mid-flight (currentTiming set
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
+ }
596
+ }
597
+ }
556
598
  // Bound the cache (oldest = lowest turnIndex)
557
599
  if (neuralwattBilledCostByTurn.size > NEURALWATT_ENERGY_CACHE_MAX) {
558
600
  let oldest = Infinity;
@@ -772,6 +814,17 @@ export default function tpsExtension(pi: ExtensionAPI) {
772
814
  }
773
815
  }
774
816
 
817
+ // Record this turn so a late-arriving neuralwatt:turn-energy event
818
+ // (provider loaded after us) can retroactively correct the rate. Billed
819
+ // cost wins when present, so a turn already billed at commit time is
820
+ // never re-corrected.
821
+ lastCommittedTurn = {
822
+ turnIndex: event.turnIndex,
823
+ telemetry,
824
+ billedApplied: billedCost !== null,
825
+ ctx,
826
+ };
827
+
775
828
  // Persist structured telemetry to session for export and rehydration
776
829
  pi.appendEntry('tps', telemetry);
777
830
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-tps",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
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.2",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "devDependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-tps",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
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"