@monotykamary/pi-tps 1.3.0 → 1.3.1

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.
@@ -249,4 +249,86 @@ describe('pi-tps extension — blended $/M-tokens rate', () => {
249
249
  const banner = notifySpy.mock.calls[0][0] as string;
250
250
  expect(banner).not.toMatch(/\$.*\/M/);
251
251
  });
252
+
253
+ it('falls back to a persisted neuralwatt-energy entry when the live event is missed', async () => {
254
+ const now = Date.now();
255
+ const message = makeMessageWithCost({
256
+ input: 1000,
257
+ output: 1000,
258
+ costTotal: 0.008, // list price: $4.00/M
259
+ provider: 'neuralwatt',
260
+ model: 'moonshotai/Kimi-K2.5',
261
+ });
262
+
263
+ // Provider appended its energy entry before pi-tps handled turn_end.
264
+ fixture.mockEntries.push({
265
+ type: 'custom',
266
+ customType: 'neuralwatt-energy',
267
+ data: { energy_joules: 21.6, cost_usd: 0.006 }, // $3.00/M for 2000 tokens
268
+ timestamp: now,
269
+ });
270
+
271
+ const { handlers, notifySpy, appendEntrySpy } = fixture;
272
+ handlers['turn_start']?.({ type: 'turn_start', turnIndex: 0, timestamp: now - 100 });
273
+ await tick(50);
274
+ handlers['message_start']?.({ type: 'message_start', message });
275
+ await tick(50);
276
+ handlers['message_update']?.({
277
+ type: 'message_update',
278
+ message,
279
+ assistantMessageEvent: { type: 'text_delta', delta: 'H' },
280
+ });
281
+ handlers['message_end']?.({ type: 'message_end', message });
282
+ handlers['turn_end']?.(
283
+ { type: 'turn_end', turnIndex: 0, message, toolResults: [] },
284
+ fixture.mockCtx
285
+ );
286
+
287
+ expect(notifySpy).toHaveBeenCalledOnce();
288
+ const banner = notifySpy.mock.calls[0][0] as string;
289
+ expect(banner).toContain('$3.00/M');
290
+ expect(banner).not.toContain('$4.00/M');
291
+
292
+ const [, data] = appendEntrySpy.mock.calls[0];
293
+ expect(data.rateUsdPerMTokens).toBe(3.0);
294
+ });
295
+
296
+ it('ignores stale neuralwatt-energy entries from before this turn', async () => {
297
+ const now = Date.now();
298
+ const message = makeMessageWithCost({
299
+ input: 1000,
300
+ output: 1000,
301
+ costTotal: 0.008, // list price: $4.00/M
302
+ provider: 'neuralwatt',
303
+ model: 'moonshotai/Kimi-K2.5',
304
+ });
305
+
306
+ fixture.mockEntries.push({
307
+ type: 'custom',
308
+ customType: 'neuralwatt-energy',
309
+ data: { energy_joules: 1, cost_usd: 9999 }, // absurd cost, should be ignored
310
+ timestamp: now - 1000,
311
+ });
312
+
313
+ const { handlers, notifySpy } = fixture;
314
+ handlers['turn_start']?.({ type: 'turn_start', turnIndex: 0, timestamp: now });
315
+ await tick(50);
316
+ handlers['message_start']?.({ type: 'message_start', message });
317
+ await tick(50);
318
+ handlers['message_update']?.({
319
+ type: 'message_update',
320
+ message,
321
+ assistantMessageEvent: { type: 'text_delta', delta: 'H' },
322
+ });
323
+ handlers['message_end']?.({ type: 'message_end', message });
324
+ handlers['turn_end']?.(
325
+ { type: 'turn_end', turnIndex: 0, message, toolResults: [] },
326
+ fixture.mockCtx
327
+ );
328
+
329
+ expect(notifySpy).toHaveBeenCalledOnce();
330
+ const banner = notifySpy.mock.calls[0][0] as string;
331
+ expect(banner).toContain('$4.00/M');
332
+ expect(banner).not.toContain('$9999');
333
+ });
252
334
  });
@@ -140,7 +140,7 @@ export function createTestFixture(): TestFixture {
140
140
  };
141
141
 
142
142
  const mockPi: Partial<ExtensionAPI> = {
143
- on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
143
+ on: vi.fn((event: string, handler: any) => {
144
144
  handlers[event] = handler;
145
145
  return mockPi as ExtensionAPI;
146
146
  }),
@@ -132,6 +132,7 @@ interface TurnTiming {
132
132
  messageCount: number;
133
133
  isToolCall: boolean; // tool_execution_start fired during this turn
134
134
  isPrimaryBranch: boolean; // TPS came from primary-branch (reliable) measurement
135
+ turnStartTimestamp: number; // wall-clock ms at turn_start, for correlating session energy entries
135
136
  }
136
137
 
137
138
  // ─── Helpers ────────────────────────────────────────────────────────────────
@@ -150,6 +151,40 @@ function computeRateUsdPerM(costUsd: number | null, totalTokens: number): number
150
151
  return Math.round(rate * 100) / 100;
151
152
  }
152
153
 
154
+ /**
155
+ * Fallback: find a Neuralwatt energy entry that was persisted in this session
156
+ * during the current turn. This covers the case where pi-neuralwatt-provider
157
+ * loads before pi-tps, so the energy entry exists before our turn_end runs,
158
+ * but the live event cache missed (e.g. malformed payload, event ordering, or
159
+ * a race).
160
+ *
161
+ * Scans backward from the most recent entry and requires the entry timestamp
162
+ * to be >= turnStartTimestamp so a previous turn's energy data is not reused.
163
+ */
164
+ function findEnergyCostFromSession(
165
+ ctx: ExtensionContext,
166
+ turnStartTimestamp: number
167
+ ): number | null {
168
+ if (!ctx.sessionManager?.getEntries) return null;
169
+
170
+ const entries = ctx.sessionManager.getEntries();
171
+ for (let i = entries.length - 1; i >= 0; i--) {
172
+ const e = entries[i];
173
+ if (e.type !== 'custom' || e.customType !== 'neuralwatt-energy') continue;
174
+
175
+ const entryTimestamp = typeof e.timestamp === 'number' ? e.timestamp : 0;
176
+ if (entryTimestamp < turnStartTimestamp) return null;
177
+
178
+ const data = e.data as Record<string, unknown> | null | undefined;
179
+ if (!data) continue;
180
+
181
+ const costUsd = typeof data.cost_usd === 'number' ? data.cost_usd : null;
182
+ if (costUsd !== null && Number.isFinite(costUsd) && costUsd >= 0) return costUsd;
183
+ }
184
+
185
+ return null;
186
+ }
187
+
153
188
  function isAssistantMessage(message: unknown): message is AssistantMessage {
154
189
  if (!message || typeof message !== 'object') return false;
155
190
  const msg = message as Record<string, unknown>;
@@ -579,6 +614,7 @@ export default function tpsExtension(pi: ExtensionAPI) {
579
614
  pi.on('turn_start', (_event: TurnStartEvent) => {
580
615
  currentTiming = {
581
616
  turnStartMs: performance.now(),
617
+ turnStartTimestamp: typeof _event.timestamp === 'number' ? _event.timestamp : Date.now(),
582
618
  lastUpdateMs: performance.now(),
583
619
  firstTokenMs: null,
584
620
  currentMessageStartMs: null,
@@ -698,14 +734,19 @@ export default function tpsExtension(pi: ExtensionAPI) {
698
734
  const turnEndMs = performance.now();
699
735
 
700
736
  // Pick the effective cost for the blended $/M-tokens rate: Neuralwatt's
701
- // billed cost when present (cache hit), otherwise the list-price compute
702
- // cost from message.usage.cost. The neuralwatt provider emits its event
703
- // in its own turn_end, which runs before or after ours depending on load
704
- // order a cache miss here just falls back to the compute rate for this
705
- // one turn (no double-counting: only one source contributes to the rate).
706
- const billedCost = neuralwattBilledCostByTurn.get(event.turnIndex) ?? null;
737
+ // billed cost when present, otherwise the list-price compute cost from
738
+ // message.usage.cost. The live event is the primary source; if it missed,
739
+ // we read the persisted neuralwatt-energy session entry. Only if neither
740
+ // energy source is available do we fall back to the compute rate.
741
+ // Prefer the live energy event, fall back to the persisted energy entry,
742
+ // and finally to the list-price compute cost.
743
+ let billedCost = neuralwattBilledCostByTurn.get(event.turnIndex) ?? null;
707
744
  neuralwattBilledCostByTurn.delete(event.turnIndex);
708
745
 
746
+ if (billedCost === null && timing.turnStartTimestamp) {
747
+ billedCost = findEnergyCostFromSession(ctx, timing.turnStartTimestamp);
748
+ }
749
+
709
750
  const telemetry = buildTelemetry(timing, turnEndMs, billedCost);
710
751
  if (!telemetry) return;
711
752