@monotykamary/pi-tps 1.1.0 → 1.2.0

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.
@@ -128,15 +128,15 @@ describe('pi-tps extension — dynamic TPS cap', () => {
128
128
  messageEnd: 900,
129
129
  });
130
130
 
131
- // Turn 2: tool call with fallback TPS (2 updates, 100ms generationMs)
132
- // Without cap: 20 tokens / 0.055s363 TPS (inflated)
133
- // With cap: min(363, 50) = 50 TPS
131
+ // Turn 2: tool call with fallback TPS (2 updates, 250ms generationMs)
132
+ // Without cap: 20 tokens / 0.25s80 TPS (feasible but from short window)
133
+ // With cap: min(80, 50) = 50 TPS
134
134
  const { appendEntrySpy, notifySpy } = driveTurn({
135
135
  turnStart: 0,
136
136
  messageStart: 100,
137
137
  firstUpdate: 100.1,
138
138
  streamUpdates: [100.15, 100.3],
139
- messageEnd: 200,
139
+ messageEnd: 350,
140
140
  isToolCall: true,
141
141
  });
142
142
 
@@ -147,20 +147,20 @@ describe('pi-tps extension — dynamic TPS cap', () => {
147
147
  expect(data.tps).toBeGreaterThan(0);
148
148
  });
149
149
 
150
- // ── Tool calls do not set the cap ────────────────────────────────────────
150
+ // ── Tool calls do not set the cap from fallback ─────────────────────────────
151
151
 
152
- it('should not let tool-call turns set the cap', () => {
153
- // Turn 1: tool call no cap exists yet, TPS is null
152
+ it('should not let fallback-branch tool-call turns set the cap', () => {
153
+ // Turn 1: tool call with fallback TPS should NOT set the cap
154
154
  const { appendEntrySpy: spy1 } = driveTurn({
155
155
  turnStart: 0,
156
156
  messageStart: 100,
157
157
  firstUpdate: 100.1,
158
158
  streamUpdates: [100.15, 100.3],
159
- messageEnd: 200,
159
+ messageEnd: 350,
160
160
  isToolCall: true,
161
161
  });
162
162
  const [, data1] = spy1.mock.calls[0];
163
- // No cap → tool call TPS is null
163
+ // No cap → fallback tool call TPS is null
164
164
  expect(data1.tps).toBeNull();
165
165
 
166
166
  // Turn 2: reliable streaming response at ~50 TPS → sets the cap
@@ -175,13 +175,13 @@ describe('pi-tps extension — dynamic TPS cap', () => {
175
175
  expect(data2.tps).toBeGreaterThanOrEqual(40);
176
176
  expect(data2.tps).toBeLessThanOrEqual(60);
177
177
 
178
- // Turn 3: another tool call — should now be clamped to 50
178
+ // Turn 3: another fallback tool call — should now be clamped to 50
179
179
  const { appendEntrySpy: spy3 } = driveTurn({
180
180
  turnStart: 0,
181
181
  messageStart: 100,
182
182
  firstUpdate: 100.1,
183
183
  streamUpdates: [100.15, 100.3],
184
- messageEnd: 200,
184
+ messageEnd: 350,
185
185
  isToolCall: true,
186
186
  });
187
187
  const [, data3] = spy3.mock.calls[2];
@@ -189,6 +189,40 @@ describe('pi-tps extension — dynamic TPS cap', () => {
189
189
  expect(data3.tps).toBeLessThanOrEqual(55);
190
190
  });
191
191
 
192
+ // ── Primary-branch tool calls (reasoning) set the cap ──────────────────────
193
+
194
+ it('should let primary-branch tool-call turns set the cap (e.g. reasoning before tool call)', () => {
195
+ // Turn 1: tool call with PRIMARY-branch TPS (reasoning + tool call, enough updates/time)
196
+ // 20 tokens / 0.4s = 50 TPS from primary branch, isToolCall = true
197
+ const { appendEntrySpy: spy1 } = driveTurn({
198
+ turnStart: 0,
199
+ messageStart: 200,
200
+ firstUpdate: 200.123,
201
+ streamUpdates: [400, 500, 600, 700, 800],
202
+ messageEnd: 900,
203
+ isToolCall: true,
204
+ });
205
+ const [, data1] = spy1.mock.calls[0];
206
+ // Primary branch + isToolCall → TPS is still computed (not null/capped)
207
+ expect(data1.tps).toBeGreaterThanOrEqual(40);
208
+ expect(data1.tps).toBeLessThanOrEqual(60);
209
+ expect(data1.isPrimaryBranch).toBe(true);
210
+
211
+ // Turn 2: fallback tool call — should be clamped to the cap from turn 1
212
+ const { appendEntrySpy: spy2 } = driveTurn({
213
+ turnStart: 0,
214
+ messageStart: 100,
215
+ firstUpdate: 100.1,
216
+ streamUpdates: [100.15, 100.3],
217
+ messageEnd: 350,
218
+ isToolCall: true,
219
+ });
220
+ const [, data2] = spy2.mock.calls[1];
221
+ expect(data2.tps).not.toBeNull();
222
+ // Clamped to ~50 cap set by the primary-branch tool call in turn 1
223
+ expect(data2.tps).toBeLessThanOrEqual(55);
224
+ });
225
+
192
226
  // ── Cold start: no cap yet ────────────────────────────────────────────────
193
227
 
194
228
  it('should show null TPS for tool calls when no cap exists yet', () => {
@@ -197,7 +231,7 @@ describe('pi-tps extension — dynamic TPS cap', () => {
197
231
  messageStart: 100,
198
232
  firstUpdate: 100.1,
199
233
  streamUpdates: [100.15, 100.3],
200
- messageEnd: 200,
234
+ messageEnd: 350,
201
235
  isToolCall: true,
202
236
  });
203
237
 
@@ -228,7 +262,7 @@ describe('pi-tps extension — dynamic TPS cap', () => {
228
262
  messageStart: 100,
229
263
  firstUpdate: 100.1,
230
264
  streamUpdates: [100.15, 100.3],
231
- messageEnd: 200,
265
+ messageEnd: 350,
232
266
  isToolCall: false,
233
267
  });
234
268
 
@@ -344,7 +378,7 @@ describe('pi-tps extension — dynamic TPS cap', () => {
344
378
  messageStart: 100,
345
379
  firstUpdate: 100.1,
346
380
  streamUpdates: [100.15, 100.3],
347
- messageEnd: 200,
381
+ messageEnd: 350,
348
382
  isToolCall: true,
349
383
  });
350
384
 
@@ -0,0 +1,372 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import type { AssistantMessage } from '@earendil-works/pi-ai';
3
+ import { createTestFixture, activateExtension } from './helpers';
4
+
5
+ describe('pi-tps extension — volume-based TPS gate', () => {
6
+ let fixture: ReturnType<typeof createTestFixture>;
7
+
8
+ beforeEach(async () => {
9
+ fixture = createTestFixture();
10
+ await activateExtension(fixture);
11
+ });
12
+
13
+ afterEach(() => {
14
+ vi.restoreAllMocks();
15
+ });
16
+
17
+ /**
18
+ * Drive a full turn with mocked performance.now() timestamps.
19
+ * Customizable output token count for volume-based testing.
20
+ *
21
+ * Timestamp mapping (one performance.now() per handler call):
22
+ * [0] turn_start → turnStartMs, lastUpdateMs
23
+ * [1] turn_start → (second call — same init)
24
+ * [2] message_start → currentMessageStartMs, lastUpdateMs reset
25
+ * [3] message_update (TTFT) → firstTokenMs, lastUpdateMs
26
+ * [4..4+n-1] message_update (stream) → each streamUpdate timestamp
27
+ * [4+n] message_end → generation time end
28
+ * [5+n] turn_end → total time
29
+ *
30
+ * streamMs = last streamUpdate - first streamUpdate (not total window!)
31
+ * updateCount = streamUpdates.length (post-TTFT events)
32
+ */
33
+ function driveTurn(clocks: {
34
+ turnStart: number;
35
+ messageStart: number;
36
+ firstUpdate: number;
37
+ streamUpdates: number[];
38
+ messageEnd: number;
39
+ turnEnd?: number;
40
+ output: number;
41
+ input?: number;
42
+ isToolCall?: boolean;
43
+ }) {
44
+ const { handlers, notifySpy, appendEntrySpy } = fixture;
45
+
46
+ const timestamps = [
47
+ clocks.turnStart,
48
+ clocks.turnStart,
49
+ clocks.messageStart,
50
+ clocks.firstUpdate,
51
+ ...clocks.streamUpdates,
52
+ clocks.messageEnd,
53
+ clocks.turnEnd ?? clocks.messageEnd,
54
+ ];
55
+
56
+ let callIdx = 0;
57
+ const spy = vi.spyOn(performance, 'now').mockImplementation(() => {
58
+ return timestamps[Math.min(callIdx++, timestamps.length - 1)];
59
+ });
60
+
61
+ const input = clocks.input ?? 50;
62
+ const assistantMessage: AssistantMessage = {
63
+ role: 'assistant',
64
+ content: [{ type: 'text', text: 'Response' }],
65
+ api: 'openai-completions',
66
+ provider: 'openai',
67
+ model: 'gpt-4',
68
+ usage: {
69
+ input,
70
+ output: clocks.output,
71
+ cacheRead: 0,
72
+ cacheWrite: 0,
73
+ totalTokens: input + clocks.output,
74
+ cost: {
75
+ input: 0.001,
76
+ output: 0.002,
77
+ cacheRead: 0,
78
+ cacheWrite: 0,
79
+ total: 0.003,
80
+ },
81
+ },
82
+ stopReason: clocks.isToolCall ? 'toolUse' : 'stop',
83
+ timestamp: Date.now(),
84
+ };
85
+
86
+ handlers['turn_start']?.({ type: 'turn_start', turnIndex: 0, timestamp: Date.now() });
87
+ handlers['message_start']?.({ type: 'message_start', message: assistantMessage });
88
+ handlers['message_update']?.({
89
+ type: 'message_update',
90
+ message: assistantMessage,
91
+ assistantMessageEvent: { type: 'text_delta', delta: 't' },
92
+ });
93
+ for (const _ts of clocks.streamUpdates) {
94
+ handlers['message_update']?.({
95
+ type: 'message_update',
96
+ message: assistantMessage,
97
+ assistantMessageEvent: { type: 'text_delta', delta: 't' },
98
+ });
99
+ }
100
+
101
+ if (clocks.isToolCall) {
102
+ handlers['tool_execution_start']?.({
103
+ type: 'tool_execution_start',
104
+ toolCallId: 'call_123',
105
+ toolName: 'bash',
106
+ args: { command: 'ls' },
107
+ });
108
+ }
109
+
110
+ handlers['message_end']?.({ type: 'message_end', message: assistantMessage });
111
+ handlers['turn_end']?.(
112
+ { type: 'turn_end', turnIndex: 0, message: assistantMessage, toolResults: [] },
113
+ fixture.mockCtx
114
+ );
115
+
116
+ spy.mockRestore();
117
+ return { notifySpy, appendEntrySpy };
118
+ }
119
+
120
+ // ── Primary branch: volume gate ────────────────────────────────────────
121
+
122
+ it('should null TPS when primary-branch TPS exceeds plausibility ceiling', () => {
123
+ // streamUpdates: [400, 450, 500, 550, 600] → streamMs = 200ms
124
+ // 3000 tokens / 0.2s = 15,000 TPS — exceeds 10,000 ceiling
125
+ const { appendEntrySpy } = driveTurn({
126
+ turnStart: 0,
127
+ messageStart: 200,
128
+ firstUpdate: 200.123,
129
+ streamUpdates: [400, 450, 500, 550, 600],
130
+ messageEnd: 700,
131
+ output: 3000,
132
+ });
133
+
134
+ const [, data] = appendEntrySpy.mock.calls[0];
135
+ expect(data.tps).toBeNull();
136
+ expect(data.isPrimaryBranch).toBe(false);
137
+ });
138
+
139
+ it('should preserve primary-branch TPS when it is within plausibility', () => {
140
+ // streamUpdates: [400, 500, 600, 700, 800] → streamMs = 400ms
141
+ // 1000 tokens / 0.4s = 2,500 TPS — well within 10,000 ceiling
142
+ const { appendEntrySpy } = driveTurn({
143
+ turnStart: 0,
144
+ messageStart: 200,
145
+ firstUpdate: 200.123,
146
+ streamUpdates: [400, 500, 600, 700, 800],
147
+ messageEnd: 900,
148
+ output: 1000,
149
+ });
150
+
151
+ const [, data] = appendEntrySpy.mock.calls[0];
152
+ expect(data.tps).not.toBeNull();
153
+ expect(data.tps).toBeGreaterThanOrEqual(2000);
154
+ expect(data.tps).toBeLessThanOrEqual(3000);
155
+ expect(data.isPrimaryBranch).toBe(true);
156
+ });
157
+
158
+ it('should preserve primary-branch TPS for high volume with long enough window', () => {
159
+ // streamUpdates: [500, 600, 700, 800, 1000] → streamMs = 500ms
160
+ // 3000 tokens / 0.5s = 6,000 TPS — within ceiling
161
+ const { appendEntrySpy } = driveTurn({
162
+ turnStart: 0,
163
+ messageStart: 200,
164
+ firstUpdate: 200.123,
165
+ streamUpdates: [500, 600, 700, 800, 1000],
166
+ messageEnd: 1100,
167
+ output: 3000,
168
+ });
169
+
170
+ const [, data] = appendEntrySpy.mock.calls[0];
171
+ expect(data.tps).not.toBeNull();
172
+ expect(data.tps).toBeGreaterThanOrEqual(5500);
173
+ expect(data.tps).toBeLessThanOrEqual(6500);
174
+ expect(data.isPrimaryBranch).toBe(true);
175
+ });
176
+
177
+ it('should null TPS for very large token volume over minimum effective span', () => {
178
+ // streamUpdates: [400, 450, 500, 550, 600] → streamMs = 200ms
179
+ // 5000 tokens / 0.2s = 25,000 TPS — well beyond ceiling
180
+ const { appendEntrySpy } = driveTurn({
181
+ turnStart: 0,
182
+ messageStart: 200,
183
+ firstUpdate: 200.123,
184
+ streamUpdates: [400, 450, 500, 550, 600],
185
+ messageEnd: 700,
186
+ output: 5000,
187
+ });
188
+
189
+ const [, data] = appendEntrySpy.mock.calls[0];
190
+ expect(data.tps).toBeNull();
191
+ expect(data.isPrimaryBranch).toBe(false);
192
+ });
193
+
194
+ // ── Fallback branch: volume gate ───────────────────────────────────────
195
+
196
+ it('should null TPS when fallback-branch TPS exceeds plausibility ceiling', () => {
197
+ // 2 updates (fallback branch), generationMs = 200ms
198
+ // 3000 tokens / 0.2s = 15,000 TPS — exceeds ceiling
199
+ const { appendEntrySpy } = driveTurn({
200
+ turnStart: 0,
201
+ messageStart: 50,
202
+ firstUpdate: 50.1,
203
+ streamUpdates: [50.15, 50.3],
204
+ messageEnd: 250,
205
+ output: 3000,
206
+ });
207
+
208
+ const [, data] = appendEntrySpy.mock.calls[0];
209
+ expect(data.tps).toBeNull();
210
+ });
211
+
212
+ it('should preserve fallback-branch TPS when it is within plausibility', () => {
213
+ // 2 updates (fallback branch), generationMs = 200ms
214
+ // 400 tokens / 0.2s = 2,000 TPS — within ceiling
215
+ const { appendEntrySpy } = driveTurn({
216
+ turnStart: 0,
217
+ messageStart: 50,
218
+ firstUpdate: 50.1,
219
+ streamUpdates: [50.15, 50.3],
220
+ messageEnd: 250,
221
+ output: 400,
222
+ });
223
+
224
+ const [, data] = appendEntrySpy.mock.calls[0];
225
+ expect(data.tps).not.toBeNull();
226
+ expect(data.tps).toBeGreaterThanOrEqual(1500);
227
+ expect(data.tps).toBeLessThanOrEqual(2500);
228
+ });
229
+
230
+ // ── Boundary: exactly at ceiling ──────────────────────────────────────
231
+
232
+ it('should preserve TPS when exactly at the plausibility ceiling (not exceeded)', () => {
233
+ // streamUpdates: [400, 450, 500, 550, 600] → streamMs = 200ms
234
+ // 2000 tokens / 0.2s = 10,000 TPS — exactly at ceiling, not > ceiling
235
+ const { appendEntrySpy } = driveTurn({
236
+ turnStart: 0,
237
+ messageStart: 200,
238
+ firstUpdate: 200.123,
239
+ streamUpdates: [400, 450, 500, 550, 600],
240
+ messageEnd: 700,
241
+ output: 2000,
242
+ });
243
+
244
+ const [, data] = appendEntrySpy.mock.calls[0];
245
+ expect(data.tps).not.toBeNull();
246
+ expect(data.tps).toBeGreaterThanOrEqual(9900);
247
+ expect(data.tps).toBeLessThanOrEqual(10100);
248
+ expect(data.isPrimaryBranch).toBe(true);
249
+ });
250
+
251
+ it('should null TPS just above the plausibility ceiling', () => {
252
+ // streamUpdates: [400, 450, 500, 550, 600] → streamMs = 200ms
253
+ // 2100 tokens / 0.2s = 10,500 TPS — just above threshold
254
+ const { appendEntrySpy } = driveTurn({
255
+ turnStart: 0,
256
+ messageStart: 200,
257
+ firstUpdate: 200.123,
258
+ streamUpdates: [400, 450, 500, 550, 600],
259
+ messageEnd: 700,
260
+ output: 2100,
261
+ });
262
+
263
+ const [, data] = appendEntrySpy.mock.calls[0];
264
+ expect(data.tps).toBeNull();
265
+ expect(data.isPrimaryBranch).toBe(false);
266
+ });
267
+
268
+ // ── Notification display ──────────────────────────────────────────────
269
+
270
+ it('should show TPS dash when volume gate nulls TPS', () => {
271
+ const { notifySpy } = driveTurn({
272
+ turnStart: 0,
273
+ messageStart: 200,
274
+ firstUpdate: 200.123,
275
+ streamUpdates: [400, 450, 500, 550, 600],
276
+ messageEnd: 700,
277
+ output: 3000,
278
+ });
279
+
280
+ const notification = notifySpy.mock.calls[0][0] as string;
281
+ expect(notification).toContain('TPS —');
282
+ });
283
+
284
+ // ── Interaction with dynamic TPS cap ──────────────────────────────────
285
+
286
+ it('should not let volume-gated turns set the dynamic TPS cap', () => {
287
+ // Turn 1: 3000 tokens / 0.2s = 15,000 TPS — volume gates to null.
288
+ // The cap condition requires isPrimaryBranch && tps !== null,
289
+ // both of which are false after the volume gate. So the cap
290
+ // should NOT be set from this turn.
291
+ driveTurn({
292
+ turnStart: 0,
293
+ messageStart: 200,
294
+ firstUpdate: 200.123,
295
+ streamUpdates: [400, 450, 500, 550, 600],
296
+ messageEnd: 700,
297
+ output: 3000,
298
+ });
299
+
300
+ const [, data1] = fixture.appendEntrySpy.mock.calls[0];
301
+ expect(data1.tps).toBeNull();
302
+ expect(data1.isPrimaryBranch).toBe(false);
303
+
304
+ // Turn 2: reliable non-tool-call streaming at ~50 TPS (20 tokens / 0.4s)
305
+ // This should set the cap at ~50 TPS, not at 15,000.
306
+ // If the volume-gated turn had set the cap at 15,000, the cap
307
+ // would be 15,000 and a subsequent tool call would be allowed
308
+ // up to 15,000 — which is inflated.
309
+ const { appendEntrySpy } = driveTurn({
310
+ turnStart: 0,
311
+ messageStart: 200,
312
+ firstUpdate: 200.123,
313
+ streamUpdates: [400, 500, 600, 700, 800],
314
+ messageEnd: 900,
315
+ output: 20,
316
+ });
317
+
318
+ const [, data2] = appendEntrySpy.mock.calls[1];
319
+ expect(data2.tps).not.toBeNull();
320
+ expect(data2.tps).toBeGreaterThanOrEqual(40);
321
+ expect(data2.tps).toBeLessThanOrEqual(60);
322
+ expect(data2.isPrimaryBranch).toBe(true);
323
+ });
324
+
325
+ // ── Volume gate doesn't affect normal token counts ────────────────────
326
+
327
+ it('should not affect TPS for normal token counts even at short effective span', () => {
328
+ // streamUpdates: [400, 450, 500, 550, 600] → streamMs = 200ms
329
+ // 20 tokens / 0.2s = 100 TPS — well within ceiling
330
+ const { appendEntrySpy } = driveTurn({
331
+ turnStart: 0,
332
+ messageStart: 200,
333
+ firstUpdate: 200.123,
334
+ streamUpdates: [400, 450, 500, 550, 600],
335
+ messageEnd: 700,
336
+ output: 20,
337
+ });
338
+
339
+ const [, data] = appendEntrySpy.mock.calls[0];
340
+ expect(data.tps).not.toBeNull();
341
+ expect(data.tps).toBeGreaterThanOrEqual(80);
342
+ expect(data.tps).toBeLessThanOrEqual(120);
343
+ expect(data.isPrimaryBranch).toBe(true);
344
+ });
345
+
346
+ // ── High-output `read`-style burst ────────────────────────────────────
347
+
348
+ it('should null TPS when provider dumps 1000+ tokens in a burst that passes timing gates', () => {
349
+ // Real-world scenario: a provider spits out 1500 tokens in a single
350
+ // fast burst. The timing gates pass (5+ updates with ≥1ms gaps,
351
+ // 200ms+ effective span), but the rate is inflated because the
352
+ // generation window is too short relative to the volume.
353
+ // streamUpdates: [400, 450, 500, 550, 600] → streamMs = 200ms
354
+ // 1500 tokens / 0.2s = 7,500 TPS — below 10,000 ceiling, passes gate
355
+ // This is actually plausible for a very fast provider, so it should
356
+ // NOT be nulled. Test that the gate is not overly aggressive.
357
+ const { appendEntrySpy } = driveTurn({
358
+ turnStart: 0,
359
+ messageStart: 200,
360
+ firstUpdate: 200.123,
361
+ streamUpdates: [400, 450, 500, 550, 600],
362
+ messageEnd: 700,
363
+ output: 1500,
364
+ });
365
+
366
+ const [, data] = appendEntrySpy.mock.calls[0];
367
+ expect(data.tps).not.toBeNull();
368
+ // 1500 / 0.2 = 7,500 TPS — plausible, within ceiling
369
+ expect(data.tps).toBeGreaterThanOrEqual(7000);
370
+ expect(data.tps).toBeLessThanOrEqual(8000);
371
+ });
372
+ });
@@ -303,11 +303,24 @@ function buildTelemetry(timing: TurnTiming, turnEndMs: number): TurnTelemetry |
303
303
  const MIN_STREAM_MS = 1;
304
304
  const MIN_STREAM_UPDATES = 5;
305
305
  const MIN_INTER_CHUNK_MS = 1;
306
- const MIN_GENERATION_MS = 50;
306
+ const MIN_GENERATION_MS = 200;
307
307
  const ACTIVE_TIME_THRESHOLD_MS = 200;
308
308
  const STALL_REDUCTION_DENOM = 2;
309
309
  const STALL_DOMINANCE_RATIO = 0.85;
310
310
 
311
+ // Maximum plausible generation speed (tokens/second). Beyond this, the
312
+ // measured TPS is almost certainly a measurement artifact — the effective
313
+ // generation window is too short relative to the token volume to
314
+ // distinguish genuine inference from a buffer-flush dispatch of
315
+ // pre-generated tokens. At 10_000 TPS this is 5× the fastest known
316
+ // commercial inference (Cerebras ~2_000 tok/s). The gate is phrased as
317
+ // "extraordinary claims require extraordinary evidence": for X output
318
+ // tokens, the minimum reliable measurement window is X / MAX_PLAUSIBLE_TPS
319
+ // seconds. Below that, the volume of tokens cannot be reliably timed, and
320
+ // TPS is set to null. This is mathematically equivalent to nulling out
321
+ // TPS when computed TPS > MAX_PLAUSIBLE_TPS.
322
+ const MAX_PLAUSIBLE_TPS = 10_000;
323
+
311
324
  const streamMs =
312
325
  timing.updateCount > 0 && timing.firstStreamUpdateMs !== null
313
326
  ? timing.lastStreamUpdateMs - timing.firstStreamUpdateMs
@@ -385,6 +398,23 @@ function buildTelemetry(timing: TurnTiming, turnEndMs: number): TurnTelemetry |
385
398
  tps = null;
386
399
  }
387
400
 
401
+ // Volume-based sanity gate: extraordinary TPS claims require
402
+ // proportionally longer measurement windows. When a provider emits a
403
+ // large volume of tokens in a short time window, the measured rate is
404
+ // dominated by dispatch/buffer-flush timing rather than actual inference
405
+ // speed. For X output tokens, the minimum reliable generation window is
406
+ // X / MAX_PLAUSIBLE_TPS seconds — below that, the volume of tokens cannot
407
+ // be reliably timed, and TPS is set to null.
408
+ //
409
+ // Mathematically equivalent to: computed TPS > MAX_PLAUSIBLE_TPS → null.
410
+ // But phrased as a measurement-reliability principle: the evidence
411
+ // (effective window duration) must be proportional to the claim (token
412
+ // volume ÷ inferred rate).
413
+ if (tps !== null && tps > MAX_PLAUSIBLE_TPS) {
414
+ tps = null;
415
+ isPrimaryBranch = false;
416
+ }
417
+
388
418
  return {
389
419
  model,
390
420
  tokens: { input, output, cacheRead, cacheWrite, total: totalTokens },
@@ -601,7 +631,7 @@ export default function tpsExtension(pi: ExtensionAPI) {
601
631
  // short outputs over tiny time windows.
602
632
  const modelKey = `${telemetry.model.provider}:${telemetry.model.modelId}`;
603
633
 
604
- if (telemetry.isPrimaryBranch && !timing.isToolCall && telemetry.tps !== null) {
634
+ if (telemetry.isPrimaryBranch && telemetry.tps !== null) {
605
635
  const currentCap = tpsCaps.get(modelKey);
606
636
  if (currentCap === undefined || telemetry.tps > currentCap) {
607
637
  tpsCaps.set(modelKey, telemetry.tps);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-tps",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-tps",
9
- "version": "1.1.0",
9
+ "version": "1.1.1",
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.1.0",
3
+ "version": "1.2.0",
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"