@pi-unipi/footer 2.12.0 → 2.12.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.
- package/package.json +1 -1
- package/src/index.ts +5 -14
- package/src/tps-tracker.ts +46 -35
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -235,12 +235,6 @@ function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterSta
|
|
|
235
235
|
// Branch-derived tool time: pending callId → assistant msg ts.
|
|
236
236
|
const pendingToolCalls = new Map<string, number>();
|
|
237
237
|
let branchToolMs = 0;
|
|
238
|
-
// Per-assistant-message duration (this assistant ts → next entry
|
|
239
|
-
// ts): the honest generation+tool window, replaces first-scan
|
|
240
|
-
// reconstruction for restart tok/s.
|
|
241
|
-
const recordDurations = new Map<number, number>();
|
|
242
|
-
let prevEntryTs = 0;
|
|
243
|
-
let prevAssistantIdx = -1;
|
|
244
238
|
for (const e of events) {
|
|
245
239
|
if (!e || typeof e !== "object") continue;
|
|
246
240
|
if (e.type !== "message") continue;
|
|
@@ -249,12 +243,6 @@ function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterSta
|
|
|
249
243
|
// Branch-derived turn/wall accounting: user messages delimit
|
|
250
244
|
// turns; assistant timestamps bound the session wall time.
|
|
251
245
|
const ts = Date.parse((e as any).timestamp ?? "") || 0;
|
|
252
|
-
// Close the previous assistant's window with this entry's ts.
|
|
253
|
-
if (prevAssistantIdx >= 0 && ts > 0 && prevEntryTs > 0) {
|
|
254
|
-
recordDurations.set(prevAssistantIdx, Math.min(ts - prevEntryTs, 600_000));
|
|
255
|
-
prevAssistantIdx = -1;
|
|
256
|
-
}
|
|
257
|
-
if (ts > 0) prevEntryTs = ts;
|
|
258
246
|
if (m.role === "user") {
|
|
259
247
|
userCount++;
|
|
260
248
|
continue;
|
|
@@ -274,7 +262,6 @@ function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterSta
|
|
|
274
262
|
if (ts > 0) {
|
|
275
263
|
if (firstAssistantTs === 0 || ts < firstAssistantTs) firstAssistantTs = ts;
|
|
276
264
|
if (ts > lastAssistantTs) lastAssistantTs = ts;
|
|
277
|
-
prevAssistantIdx = msgIndex; // close on next entry
|
|
278
265
|
}
|
|
279
266
|
const hasStop = !!m.stopReason;
|
|
280
267
|
// Pass the whole message: completed messages get anchored to
|
|
@@ -301,7 +288,11 @@ function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterSta
|
|
|
301
288
|
msgIndex++;
|
|
302
289
|
}
|
|
303
290
|
tpsTracker.syncBranchStats(userCount, msgIndex);
|
|
304
|
-
|
|
291
|
+
// NOTE: no per-record duration seeding here on purpose. Timestamps
|
|
292
|
+
// alone cannot mark stream END — a 'next-entry' delta would include
|
|
293
|
+
// tool runs + user think-time inside the rate window (the bug that
|
|
294
|
+
// made 100-tok/s models read ~7 tok/s). AVG now uses only
|
|
295
|
+
// hook-measured decode windows; see getSessionAvgTps().
|
|
305
296
|
if (lastAssistantTs > firstAssistantTs) {
|
|
306
297
|
tpsTracker.syncWallMs(lastAssistantTs - firstAssistantTs);
|
|
307
298
|
}
|
package/src/tps-tracker.ts
CHANGED
|
@@ -56,8 +56,16 @@ interface MessageTpsRecord {
|
|
|
56
56
|
requestAt: number;
|
|
57
57
|
/** When OUTPUT GENERATION started (ms). First non-empty delta. */
|
|
58
58
|
startedAt: number;
|
|
59
|
+
/** True once a live streaming delta was observed (window is hook-measured). */
|
|
60
|
+
sawFirstDelta: boolean;
|
|
59
61
|
/** When generation completed (ms), 0 if still generating. */
|
|
60
62
|
completedAt: number;
|
|
63
|
+
/**
|
|
64
|
+
* MEASURED output-only decode window (first delta → stream end, ms).
|
|
65
|
+
* 0 when never measured — such records are excluded from the session
|
|
66
|
+
* average (deepseek-harness rule: unmeasurable steps drop out).
|
|
67
|
+
*/
|
|
68
|
+
decodeMs: number;
|
|
61
69
|
/** Final TPS for this message (once completed). */
|
|
62
70
|
tps: number;
|
|
63
71
|
}
|
|
@@ -141,7 +149,10 @@ function estimateOutputTokens(message: unknown): number {
|
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
|
-
|
|
152
|
+
// FIXED: was estimateTokens(String(chars)) — that divided the DIGIT COUNT
|
|
153
|
+
// of the number (String(8000).length === 4) by 4, collapsing an 8000-char
|
|
154
|
+
// response into 1 "token". Divide the actual char count instead.
|
|
155
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
/**
|
|
@@ -202,7 +213,9 @@ export class TpsTracker {
|
|
|
202
213
|
anchored: false,
|
|
203
214
|
requestAt: this.lastTurnStart,
|
|
204
215
|
startedAt: 0,
|
|
216
|
+
sawFirstDelta: false,
|
|
205
217
|
completedAt: 0,
|
|
218
|
+
decodeMs: 0,
|
|
206
219
|
tps: 0,
|
|
207
220
|
});
|
|
208
221
|
}
|
|
@@ -215,7 +228,9 @@ export class TpsTracker {
|
|
|
215
228
|
// record inherits the current open-turn start as its TTFT bound.
|
|
216
229
|
requestAt: this.lastTurnStart,
|
|
217
230
|
startedAt: 0,
|
|
231
|
+
sawFirstDelta: false,
|
|
218
232
|
completedAt: 0,
|
|
233
|
+
decodeMs: 0,
|
|
219
234
|
tps: 0,
|
|
220
235
|
});
|
|
221
236
|
}
|
|
@@ -252,6 +267,7 @@ export class TpsTracker {
|
|
|
252
267
|
this.ttftHookSamples += 1;
|
|
253
268
|
}
|
|
254
269
|
}
|
|
270
|
+
record.sawFirstDelta = true; // window becomes hook-measured
|
|
255
271
|
record.estimatedTokens += deltaContribution(deltaText);
|
|
256
272
|
record.tokens = record.estimatedTokens;
|
|
257
273
|
}
|
|
@@ -279,10 +295,19 @@ export class TpsTracker {
|
|
|
279
295
|
// provider message timestamp so the window is still output-ish.
|
|
280
296
|
record.startedAt =
|
|
281
297
|
messageTimestamp(finalMessage) || record.completedAt - 500;
|
|
282
|
-
}
|
|
298
|
+
}
|
|
299
|
+
const durationSec = Math.max(
|
|
283
300
|
(record.completedAt - record.startedAt) / 1000,
|
|
284
301
|
0.05,
|
|
285
302
|
);
|
|
303
|
+
// decodeMs is the measured output-only window ONLY when live hooks saw
|
|
304
|
+
// both bounds; scan-reconstructed windows stay 0 → excluded from AVG.
|
|
305
|
+
if (record.sawFirstDelta && record.startedAt > 0) {
|
|
306
|
+
const ms = Math.max(1, record.completedAt - record.startedAt);
|
|
307
|
+
// Guard: a hook-measured window should never be absurdly long —
|
|
308
|
+
// keep an over-flow safety cap far above any real generation.
|
|
309
|
+
record.decodeMs = Math.min(ms, 3600_000);
|
|
310
|
+
}
|
|
286
311
|
record.tps = record.tokens > 0 ? record.tokens / durationSec : 0;
|
|
287
312
|
this.totalOutput += record.tokens;
|
|
288
313
|
this.pendingChars.delete(messageIndex);
|
|
@@ -392,7 +417,9 @@ export class TpsTracker {
|
|
|
392
417
|
anchored: false,
|
|
393
418
|
requestAt: this.lastTurnStart,
|
|
394
419
|
startedAt: 0,
|
|
420
|
+
sawFirstDelta: false,
|
|
395
421
|
completedAt: 0,
|
|
422
|
+
decodeMs: 0,
|
|
396
423
|
tps: 0,
|
|
397
424
|
});
|
|
398
425
|
}
|
|
@@ -436,49 +463,33 @@ export class TpsTracker {
|
|
|
436
463
|
return 0;
|
|
437
464
|
}
|
|
438
465
|
|
|
439
|
-
/** Session average TPS across completed + current generation windows. */
|
|
440
|
-
/**
|
|
441
|
-
* Honest per-record duration override from branch order: a persisted
|
|
442
|
-
* assistant message ended by the time the NEXT entry arrived. Feeding
|
|
443
|
-
* these durations replaces first-scan-sighting reconstruction (which made
|
|
444
|
-
* every historical message look 10 min long → tok/s ≈ 0).
|
|
445
|
-
*/
|
|
446
|
-
syncRecordDurations(durations: Map<number, number>): void {
|
|
447
|
-
for (const [idx, durMs] of durations) {
|
|
448
|
-
const r = this.records[idx];
|
|
449
|
-
if (!r || r.completedAt === 0 || !r.startedAt) continue;
|
|
450
|
-
const sec = Math.max(0.05, Math.min(durMs / 1000, TpsTracker.MAX_RECORD_DURATION_SEC));
|
|
451
|
-
// Recompute this record's tps contribution lazily via tokens/duration
|
|
452
|
-
r.tps = sec > 0 ? r.tokens / sec : r.tps;
|
|
453
|
-
(r as unknown as { forcedDurationSec?: number }).forcedDurationSec = sec;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
466
|
/** Session average TPS across completed + current generation windows.
|
|
458
467
|
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
468
|
+
* Deepseek-harness contract (projection.ts): throughput =
|
|
469
|
+
* Σ decodeTokens ÷ Σ decodeMs, sampled ONLY over steps whose decode
|
|
470
|
+
* window was actually measured (first delta → stream end by live hooks).
|
|
471
|
+
* Steps without a measured window drop out of the average entirely.
|
|
472
|
+
*
|
|
473
|
+
* Scan-reconciled OLD messages (after restart/reload) previously entered
|
|
474
|
+
* this average with fabricated durations: startedAt from the provider
|
|
475
|
+
* timestamp, completedAt from 'when our scanner first saw it' (minutes
|
|
476
|
+
* late), or worse a 'next-entry-ts' window that silently includes tool
|
|
477
|
+
* runs and user think-time. That made a 100 tok/s model read ~7 tok/s.
|
|
478
|
+
* We now honor the same rule as the harness: unmeasurable steps are
|
|
479
|
+
* EXCLUDED, never averaged in with invented durations.
|
|
465
480
|
*/
|
|
466
|
-
private static readonly MAX_RECORD_DURATION_SEC = 600;
|
|
467
|
-
|
|
468
481
|
getSessionAvgTps(): number {
|
|
469
|
-
const cap = TpsTracker.MAX_RECORD_DURATION_SEC;
|
|
470
482
|
let totalTokens = 0;
|
|
471
483
|
let totalDurationSec = 0;
|
|
472
484
|
for (const r of this.records) {
|
|
473
|
-
if (r.completedAt > 0 && r.startedAt > 0) {
|
|
485
|
+
if (r.completedAt > 0 && r.startedAt > 0 && r.decodeMs > 0) {
|
|
486
|
+
// Measured output-only window (live streaming hooks).
|
|
474
487
|
totalTokens += r.tokens;
|
|
475
|
-
|
|
476
|
-
// startedAt→completedAt clamped at the cap.
|
|
477
|
-
const forced = (r as unknown as { forcedDurationSec?: number }).forcedDurationSec;
|
|
478
|
-
totalDurationSec += forced ?? Math.min((r.completedAt - r.startedAt) / 1000, cap);
|
|
488
|
+
totalDurationSec += r.decodeMs / 1000;
|
|
479
489
|
} else if (r.completedAt === 0 && r.startedAt > 0) {
|
|
490
|
+
// Open generation window: contribute what's elapsed so far.
|
|
480
491
|
totalTokens += r.tokens;
|
|
481
|
-
totalDurationSec += Math.
|
|
492
|
+
totalDurationSec += Math.max(0.05, (Date.now() - r.startedAt) / 1000);
|
|
482
493
|
}
|
|
483
494
|
}
|
|
484
495
|
if (totalDurationSec <= 0) return 0;
|