useful-pi-extensions 1.1.1 → 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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "useful-pi-extensions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A small collection of pi extensions, installed with one command — a labelled status line with context pressure, cache, cost, effort, TTFT and tokens/sec.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
pair,
|
|
40
40
|
row,
|
|
41
41
|
shortenPath,
|
|
42
|
+
ttftDisplay,
|
|
43
|
+
ttftMs,
|
|
42
44
|
USD,
|
|
43
45
|
type Currency,
|
|
44
46
|
} from './render.ts'
|
|
@@ -46,6 +48,8 @@ import {
|
|
|
46
48
|
const LIVE_RENDER_MS = 200
|
|
47
49
|
const WINDOW_MS = 2000
|
|
48
50
|
const MIN_SAMPLE_MS = 200
|
|
51
|
+
/** How often the waiting TTFT slot ticks while a request is in flight. */
|
|
52
|
+
const TICK_MS = 250
|
|
49
53
|
/** Pi's own estimateTokens() heuristic, used only until a real ratio is known. */
|
|
50
54
|
const FALLBACK_TOKENS_PER_CHAR = 0.25
|
|
51
55
|
|
|
@@ -185,8 +189,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
185
189
|
let reading: { rate: number; exact: boolean; ttftMs: number | null } | null = null
|
|
186
190
|
|
|
187
191
|
let chars = 0
|
|
188
|
-
let
|
|
192
|
+
let requestAt: number | null = null
|
|
189
193
|
let firstTokenAt: number | null = null
|
|
194
|
+
let ticker: ReturnType<typeof setInterval> | null = null
|
|
190
195
|
let windowAt = 0
|
|
191
196
|
let windowTokens = 0
|
|
192
197
|
let renderedAt = 0
|
|
@@ -199,11 +204,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
199
204
|
renderedAt = 0
|
|
200
205
|
}
|
|
201
206
|
|
|
202
|
-
function publish(rate: number, exact: boolean,
|
|
203
|
-
reading = { rate, exact, ttftMs }
|
|
207
|
+
function publish(rate: number, exact: boolean, ttft: number | null): void {
|
|
208
|
+
reading = { rate, exact, ttftMs: ttft }
|
|
204
209
|
requestRender?.()
|
|
205
210
|
}
|
|
206
211
|
|
|
212
|
+
/** Stops the live count; a frozen TTFT needs no clock. */
|
|
213
|
+
function stopTicker(): void {
|
|
214
|
+
if (ticker !== null) {
|
|
215
|
+
clearInterval(ticker)
|
|
216
|
+
ticker = null
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Ticks the footer while a request is in flight, until the first token lands or the turn ends. */
|
|
221
|
+
function startTicker(): void {
|
|
222
|
+
if (ticker !== null) return
|
|
223
|
+
ticker = setInterval(() => {
|
|
224
|
+
if (requestAt === null || firstTokenAt !== null) {
|
|
225
|
+
stopTicker()
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
requestRender?.()
|
|
229
|
+
}, TICK_MS)
|
|
230
|
+
}
|
|
231
|
+
|
|
207
232
|
function installFooter(ctx: ExtensionContext): void {
|
|
208
233
|
ctx.ui.setFooter((tui, theme, footerData: ReadonlyFooterDataProvider) => {
|
|
209
234
|
requestRender = () => tui.requestRender()
|
|
@@ -235,7 +260,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
235
260
|
const model = ctx.model?.id ?? 'no model'
|
|
236
261
|
const row2Parts = [theme.fg('accent', model)]
|
|
237
262
|
if (ctx.thinkingLevel) row2Parts.push(pair(theme, 'Effort', ctx.thinkingLevel, 'muted'))
|
|
238
|
-
|
|
263
|
+
const waiting = ttftDisplay(requestAt, firstTokenAt, Date.now())
|
|
264
|
+
if (waiting !== null) {
|
|
265
|
+
// The clock is running: this wait has no reading yet, so the previous turn's
|
|
266
|
+
// throughput would only be mistaken for the current one.
|
|
267
|
+
row2Parts.push(pair(theme, 'TTFT', waiting.text, 'muted'))
|
|
268
|
+
} else if (reading) {
|
|
239
269
|
if (reading.ttftMs !== null)
|
|
240
270
|
row2Parts.push(pair(theme, 'TTFT', formatLatency(reading.ttftMs), 'muted'))
|
|
241
271
|
row2Parts.push(
|
|
@@ -283,9 +313,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
283
313
|
})
|
|
284
314
|
}
|
|
285
315
|
|
|
316
|
+
// A run that ends without a first token (abort, provider error) must not leave a clock counting
|
|
317
|
+
// against a request that is no longer in flight.
|
|
318
|
+
pi.on('turn_end', async () => {
|
|
319
|
+
requestAt = null
|
|
320
|
+
stopTicker()
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
pi.on('agent_end', async () => {
|
|
324
|
+
requestAt = null
|
|
325
|
+
stopTicker()
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
pi.on('agent_settled', async () => {
|
|
329
|
+
requestAt = null
|
|
330
|
+
stopTicker()
|
|
331
|
+
})
|
|
332
|
+
|
|
286
333
|
pi.on('session_start', async (_event, ctx) => {
|
|
287
334
|
ratio = seedRatio(ctx)
|
|
288
335
|
reading = null
|
|
336
|
+
requestAt = null
|
|
337
|
+
stopTicker()
|
|
289
338
|
resetStream()
|
|
290
339
|
currency = await loadCurrency((message) => ctx.ui.notify(message, 'warning'))
|
|
291
340
|
installFooter(ctx)
|
|
@@ -294,7 +343,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
294
343
|
pi.on('message_start', async (event) => {
|
|
295
344
|
if (event.message.role !== 'assistant') return
|
|
296
345
|
resetStream()
|
|
297
|
-
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// TTFT's request anchor, from the agent loop in pi's docs: turn_start opens the LLM call and
|
|
349
|
+
// before_provider_request is the last thing pi does before the wire. message_start is not that
|
|
350
|
+
// moment, and anchoring there made a first token that arrived early read as a clamped 0ms.
|
|
351
|
+
pi.on('turn_start', async () => {
|
|
352
|
+
requestAt = null
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
pi.on('before_provider_request', async () => {
|
|
356
|
+
requestAt = Date.now()
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
pi.on('turn_start', async () => {
|
|
360
|
+
requestAt = null
|
|
361
|
+
stopTicker()
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
pi.on('before_provider_request', async () => {
|
|
365
|
+
requestAt = Date.now()
|
|
366
|
+
startTicker()
|
|
298
367
|
})
|
|
299
368
|
|
|
300
369
|
pi.on('message_update', async (event) => {
|
|
@@ -310,7 +379,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
310
379
|
}
|
|
311
380
|
|
|
312
381
|
const now = Date.now()
|
|
313
|
-
if (firstTokenAt === null)
|
|
382
|
+
if (firstTokenAt === null) {
|
|
383
|
+
firstTokenAt = now
|
|
384
|
+
stopTicker()
|
|
385
|
+
}
|
|
314
386
|
chars += delta.delta.length
|
|
315
387
|
|
|
316
388
|
const tokens = chars * (ratio ?? FALLBACK_TOKENS_PER_CHAR)
|
|
@@ -326,7 +398,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
326
398
|
const rate =
|
|
327
399
|
windowMs > 0 ? ((tokens - windowTokens) / windowMs) * 1000 : (tokens / decodeMs) * 1000
|
|
328
400
|
renderedAt = now
|
|
329
|
-
publish(rate, false, firstTokenAt
|
|
401
|
+
publish(rate, false, ttftMs(requestAt, firstTokenAt))
|
|
330
402
|
})
|
|
331
403
|
|
|
332
404
|
pi.on('message_end', async (event) => {
|
|
@@ -341,10 +413,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
341
413
|
}
|
|
342
414
|
|
|
343
415
|
const decodeMs = firstTokenAt !== null ? Date.now() - firstTokenAt : 0
|
|
344
|
-
const
|
|
416
|
+
const measured = ttftMs(requestAt, firstTokenAt)
|
|
345
417
|
const tokens = output > 0 ? output : totalChars * (ratio ?? FALLBACK_TOKENS_PER_CHAR)
|
|
346
|
-
if (
|
|
347
|
-
publish((tokens / decodeMs) * 1000, output > 0,
|
|
418
|
+
if (measured !== null && decodeMs >= MIN_SAMPLE_MS)
|
|
419
|
+
publish((tokens / decodeMs) * 1000, output > 0, measured)
|
|
420
|
+
// Null it with the stream: a request that has produced its message is no longer in flight, and
|
|
421
|
+
// a stale anchor would let the live branch count against nothing until the next turn.
|
|
422
|
+
requestAt = null
|
|
348
423
|
resetStream()
|
|
349
424
|
})
|
|
350
425
|
}
|
|
@@ -66,6 +66,51 @@ export function formatLatency(ms: number): string {
|
|
|
66
66
|
return `${seconds < 10 ? Math.round(seconds * 10) / 10 : Math.round(seconds)}s`
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* TTFT from its two wall-clock anchors: the moment the request was dispatched, and the first
|
|
71
|
+
* streamed delta.
|
|
72
|
+
*
|
|
73
|
+
* The guard is the point. Both anchors come from extension events whose ordering around a stream is
|
|
74
|
+
* not guaranteed, and a first token that appears to precede its own request yields a negative that
|
|
75
|
+
* `formatLatency` would clamp to `0ms` — a confident fake where an honest absence belongs.
|
|
76
|
+
*
|
|
77
|
+
* @returns The milliseconds between the two, or null when they do not line up.
|
|
78
|
+
*/
|
|
79
|
+
export function ttftMs(requestAt: number | null, firstTokenAt: number | null): number | null {
|
|
80
|
+
if (requestAt === null || firstTokenAt === null) return null
|
|
81
|
+
return firstTokenAt > requestAt ? firstTokenAt - requestAt : null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** What the TTFT slot renders, and whether the clock is still running. */
|
|
85
|
+
export interface TtftDisplay {
|
|
86
|
+
text: string
|
|
87
|
+
live: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The TTFT slot, for all three phases of a request.
|
|
92
|
+
*
|
|
93
|
+
* While the request is in flight the slot counts up (`~2s`, the `~` marking an unfinished wait, the
|
|
94
|
+
* same convention as the tok/s estimate) — a slow first byte is something you watch happen, not a
|
|
95
|
+
* number you are told about afterwards. The moment the first token lands the count freezes into the
|
|
96
|
+
* exact value. With no request in flight there is nothing to show.
|
|
97
|
+
*
|
|
98
|
+
* @param now - The clock, passed in so every branch stays a function of its arguments.
|
|
99
|
+
*/
|
|
100
|
+
export function ttftDisplay(
|
|
101
|
+
requestAt: number | null,
|
|
102
|
+
firstTokenAt: number | null,
|
|
103
|
+
now: number,
|
|
104
|
+
): TtftDisplay | null {
|
|
105
|
+
if (requestAt === null) return null
|
|
106
|
+
if (firstTokenAt === null) {
|
|
107
|
+
if (now < requestAt) return null
|
|
108
|
+
return { text: `~${formatLatency(now - requestAt)}`, live: true }
|
|
109
|
+
}
|
|
110
|
+
const measured = ttftMs(requestAt, firstTokenAt)
|
|
111
|
+
return measured === null ? null : { text: formatLatency(measured), live: false }
|
|
112
|
+
}
|
|
113
|
+
|
|
69
114
|
/** Home-relative path, or the absolute path when it is outside the home directory. */
|
|
70
115
|
export function formatCwd(cwd: string): string {
|
|
71
116
|
const home = homedir()
|