@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:
|
|
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
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
//
|
|
706
|
-
|
|
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
|
|