@monotykamary/pi-tps 1.3.4 → 1.3.6
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.
- package/README.md +12 -5
- package/package.json +9 -4
- package/.github/FUNDING.yml +0 -4
- package/.github/workflows/test.yml +0 -57
- package/.pi/autoresearch/session-id +0 -1
- package/.pi/fabric/mesh/actors/actors.json +0 -4
- package/.prettierrc +0 -7
- package/commitlint.config.cjs +0 -1
- package/extensions/pi-tps/__tests__/cost-rate.test.ts +0 -503
- package/extensions/pi-tps/__tests__/dynamic-tps-cap.test.ts +0 -390
- package/extensions/pi-tps/__tests__/export-command.test.ts +0 -307
- package/extensions/pi-tps/__tests__/extension-setup.test.ts +0 -41
- package/extensions/pi-tps/__tests__/format-duration.test.ts +0 -83
- package/extensions/pi-tps/__tests__/helpers.ts +0 -177
- package/extensions/pi-tps/__tests__/precision-timing.test.ts +0 -701
- package/extensions/pi-tps/__tests__/rehydration.test.ts +0 -282
- package/extensions/pi-tps/__tests__/session-export.test.ts +0 -204
- package/extensions/pi-tps/__tests__/stall-detection.test.ts +0 -209
- package/extensions/pi-tps/__tests__/stall-reduction.test.ts +0 -139
- package/extensions/pi-tps/__tests__/telemetry-flow.test.ts +0 -654
- package/extensions/pi-tps/__tests__/volume-gate.test.ts +0 -372
- package/knip.json +0 -10
- package/npm-shrinkwrap.json +0 -6900
- package/pnpm-workspace.yaml +0 -9
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -15
|
@@ -1,372 +0,0 @@
|
|
|
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
|
-
});
|
package/knip.json
DELETED