@monotykamary/pi-tps 1.3.0 → 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
|
|
@@ -249,4 +319,86 @@ describe('pi-tps extension — blended $/M-tokens rate', () => {
|
|
|
249
319
|
const banner = notifySpy.mock.calls[0][0] as string;
|
|
250
320
|
expect(banner).not.toMatch(/\$.*\/M/);
|
|
251
321
|
});
|
|
322
|
+
|
|
323
|
+
it('falls back to a persisted neuralwatt-energy entry when the live event is missed', async () => {
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
const message = makeMessageWithCost({
|
|
326
|
+
input: 1000,
|
|
327
|
+
output: 1000,
|
|
328
|
+
costTotal: 0.008, // list price: $4.00/M
|
|
329
|
+
provider: 'neuralwatt',
|
|
330
|
+
model: 'moonshotai/Kimi-K2.5',
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Provider appended its energy entry before pi-tps handled turn_end.
|
|
334
|
+
fixture.mockEntries.push({
|
|
335
|
+
type: 'custom',
|
|
336
|
+
customType: 'neuralwatt-energy',
|
|
337
|
+
data: { energy_joules: 21.6, cost_usd: 0.006 }, // $3.00/M for 2000 tokens
|
|
338
|
+
timestamp: now,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const { handlers, notifySpy, appendEntrySpy } = fixture;
|
|
342
|
+
handlers['turn_start']?.({ type: 'turn_start', turnIndex: 0, timestamp: now - 100 });
|
|
343
|
+
await tick(50);
|
|
344
|
+
handlers['message_start']?.({ type: 'message_start', message });
|
|
345
|
+
await tick(50);
|
|
346
|
+
handlers['message_update']?.({
|
|
347
|
+
type: 'message_update',
|
|
348
|
+
message,
|
|
349
|
+
assistantMessageEvent: { type: 'text_delta', delta: 'H' },
|
|
350
|
+
});
|
|
351
|
+
handlers['message_end']?.({ type: 'message_end', message });
|
|
352
|
+
handlers['turn_end']?.(
|
|
353
|
+
{ type: 'turn_end', turnIndex: 0, message, toolResults: [] },
|
|
354
|
+
fixture.mockCtx
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
expect(notifySpy).toHaveBeenCalledOnce();
|
|
358
|
+
const banner = notifySpy.mock.calls[0][0] as string;
|
|
359
|
+
expect(banner).toContain('$3.00/M');
|
|
360
|
+
expect(banner).not.toContain('$4.00/M');
|
|
361
|
+
|
|
362
|
+
const [, data] = appendEntrySpy.mock.calls[0];
|
|
363
|
+
expect(data.rateUsdPerMTokens).toBe(3.0);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('ignores stale neuralwatt-energy entries from before this turn', async () => {
|
|
367
|
+
const now = Date.now();
|
|
368
|
+
const message = makeMessageWithCost({
|
|
369
|
+
input: 1000,
|
|
370
|
+
output: 1000,
|
|
371
|
+
costTotal: 0.008, // list price: $4.00/M
|
|
372
|
+
provider: 'neuralwatt',
|
|
373
|
+
model: 'moonshotai/Kimi-K2.5',
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
fixture.mockEntries.push({
|
|
377
|
+
type: 'custom',
|
|
378
|
+
customType: 'neuralwatt-energy',
|
|
379
|
+
data: { energy_joules: 1, cost_usd: 9999 }, // absurd cost, should be ignored
|
|
380
|
+
timestamp: now - 1000,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const { handlers, notifySpy } = fixture;
|
|
384
|
+
handlers['turn_start']?.({ type: 'turn_start', turnIndex: 0, timestamp: now });
|
|
385
|
+
await tick(50);
|
|
386
|
+
handlers['message_start']?.({ type: 'message_start', message });
|
|
387
|
+
await tick(50);
|
|
388
|
+
handlers['message_update']?.({
|
|
389
|
+
type: 'message_update',
|
|
390
|
+
message,
|
|
391
|
+
assistantMessageEvent: { type: 'text_delta', delta: 'H' },
|
|
392
|
+
});
|
|
393
|
+
handlers['message_end']?.({ type: 'message_end', message });
|
|
394
|
+
handlers['turn_end']?.(
|
|
395
|
+
{ type: 'turn_end', turnIndex: 0, message, toolResults: [] },
|
|
396
|
+
fixture.mockCtx
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
expect(notifySpy).toHaveBeenCalledOnce();
|
|
400
|
+
const banner = notifySpy.mock.calls[0][0] as string;
|
|
401
|
+
expect(banner).toContain('$4.00/M');
|
|
402
|
+
expect(banner).not.toContain('$9999');
|
|
403
|
+
});
|
|
252
404
|
});
|
|
@@ -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:
|
|
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>;
|
|
@@ -511,6 +546,18 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
511
546
|
// own session entries. Non-Neuralwatt turns never emit, so this stays empty.
|
|
512
547
|
const neuralwattBilledCostByTurn = new Map<number, number>();
|
|
513
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
|
+
|
|
514
561
|
pi.events?.on(NEURALWATT_ENERGY_EVENT, (payload: unknown) => {
|
|
515
562
|
if (!payload || typeof payload !== 'object') return;
|
|
516
563
|
const p = payload as Record<string, unknown>;
|
|
@@ -518,6 +565,36 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
518
565
|
const costUsd = typeof p.costUsd === 'number' ? p.costUsd : null;
|
|
519
566
|
if (turnIndex === null || costUsd === null) return; // require turnIndex correlation
|
|
520
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
|
+
}
|
|
521
598
|
// Bound the cache (oldest = lowest turnIndex)
|
|
522
599
|
if (neuralwattBilledCostByTurn.size > NEURALWATT_ENERGY_CACHE_MAX) {
|
|
523
600
|
let oldest = Infinity;
|
|
@@ -579,6 +656,7 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
579
656
|
pi.on('turn_start', (_event: TurnStartEvent) => {
|
|
580
657
|
currentTiming = {
|
|
581
658
|
turnStartMs: performance.now(),
|
|
659
|
+
turnStartTimestamp: typeof _event.timestamp === 'number' ? _event.timestamp : Date.now(),
|
|
582
660
|
lastUpdateMs: performance.now(),
|
|
583
661
|
firstTokenMs: null,
|
|
584
662
|
currentMessageStartMs: null,
|
|
@@ -698,14 +776,19 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
698
776
|
const turnEndMs = performance.now();
|
|
699
777
|
|
|
700
778
|
// Pick the effective cost for the blended $/M-tokens rate: Neuralwatt's
|
|
701
|
-
// billed cost when present
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
//
|
|
706
|
-
|
|
779
|
+
// billed cost when present, otherwise the list-price compute cost from
|
|
780
|
+
// message.usage.cost. The live event is the primary source; if it missed,
|
|
781
|
+
// we read the persisted neuralwatt-energy session entry. Only if neither
|
|
782
|
+
// energy source is available do we fall back to the compute rate.
|
|
783
|
+
// Prefer the live energy event, fall back to the persisted energy entry,
|
|
784
|
+
// and finally to the list-price compute cost.
|
|
785
|
+
let billedCost = neuralwattBilledCostByTurn.get(event.turnIndex) ?? null;
|
|
707
786
|
neuralwattBilledCostByTurn.delete(event.turnIndex);
|
|
708
787
|
|
|
788
|
+
if (billedCost === null && timing.turnStartTimestamp) {
|
|
789
|
+
billedCost = findEnergyCostFromSession(ctx, timing.turnStartTimestamp);
|
|
790
|
+
}
|
|
791
|
+
|
|
709
792
|
const telemetry = buildTelemetry(timing, turnEndMs, billedCost);
|
|
710
793
|
if (!telemetry) return;
|
|
711
794
|
|
|
@@ -731,6 +814,17 @@ export default function tpsExtension(pi: ExtensionAPI) {
|
|
|
731
814
|
}
|
|
732
815
|
}
|
|
733
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
|
+
|
|
734
828
|
// Persist structured telemetry to session for export and rehydration
|
|
735
829
|
pi.appendEntry('tps', telemetry);
|
|
736
830
|
|