@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,
|
|
132
|
-
// Without cap: 20 tokens / 0.
|
|
133
|
-
// With cap: min(
|
|
131
|
+
// Turn 2: tool call with fallback TPS (2 updates, 250ms generationMs)
|
|
132
|
+
// Without cap: 20 tokens / 0.25s ≈ 80 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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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 &&
|
|
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);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-tps",
|
|
3
|
-
"version": "1.1.
|
|
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.
|
|
9
|
+
"version": "1.1.1",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"devDependencies": {
|