useful-pi-extensions 1.1.2 → 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,7 @@ import {
|
|
|
39
39
|
pair,
|
|
40
40
|
row,
|
|
41
41
|
shortenPath,
|
|
42
|
+
ttftDisplay,
|
|
42
43
|
ttftMs,
|
|
43
44
|
USD,
|
|
44
45
|
type Currency,
|
|
@@ -47,6 +48,8 @@ import {
|
|
|
47
48
|
const LIVE_RENDER_MS = 200
|
|
48
49
|
const WINDOW_MS = 2000
|
|
49
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
|
|
50
53
|
/** Pi's own estimateTokens() heuristic, used only until a real ratio is known. */
|
|
51
54
|
const FALLBACK_TOKENS_PER_CHAR = 0.25
|
|
52
55
|
|
|
@@ -188,6 +191,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
188
191
|
let chars = 0
|
|
189
192
|
let requestAt: number | null = null
|
|
190
193
|
let firstTokenAt: number | null = null
|
|
194
|
+
let ticker: ReturnType<typeof setInterval> | null = null
|
|
191
195
|
let windowAt = 0
|
|
192
196
|
let windowTokens = 0
|
|
193
197
|
let renderedAt = 0
|
|
@@ -205,6 +209,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
205
209
|
requestRender?.()
|
|
206
210
|
}
|
|
207
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
|
+
|
|
208
232
|
function installFooter(ctx: ExtensionContext): void {
|
|
209
233
|
ctx.ui.setFooter((tui, theme, footerData: ReadonlyFooterDataProvider) => {
|
|
210
234
|
requestRender = () => tui.requestRender()
|
|
@@ -236,7 +260,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
236
260
|
const model = ctx.model?.id ?? 'no model'
|
|
237
261
|
const row2Parts = [theme.fg('accent', model)]
|
|
238
262
|
if (ctx.thinkingLevel) row2Parts.push(pair(theme, 'Effort', ctx.thinkingLevel, 'muted'))
|
|
239
|
-
|
|
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) {
|
|
240
269
|
if (reading.ttftMs !== null)
|
|
241
270
|
row2Parts.push(pair(theme, 'TTFT', formatLatency(reading.ttftMs), 'muted'))
|
|
242
271
|
row2Parts.push(
|
|
@@ -284,10 +313,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
284
313
|
})
|
|
285
314
|
}
|
|
286
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
|
+
|
|
287
333
|
pi.on('session_start', async (_event, ctx) => {
|
|
288
334
|
ratio = seedRatio(ctx)
|
|
289
335
|
reading = null
|
|
290
336
|
requestAt = null
|
|
337
|
+
stopTicker()
|
|
291
338
|
resetStream()
|
|
292
339
|
currency = await loadCurrency((message) => ctx.ui.notify(message, 'warning'))
|
|
293
340
|
installFooter(ctx)
|
|
@@ -309,6 +356,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
309
356
|
requestAt = Date.now()
|
|
310
357
|
})
|
|
311
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()
|
|
367
|
+
})
|
|
368
|
+
|
|
312
369
|
pi.on('message_update', async (event) => {
|
|
313
370
|
const delta = event.assistantMessageEvent
|
|
314
371
|
// Narrowing on the discriminant is what keeps the payload typed; membership in
|
|
@@ -322,7 +379,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
322
379
|
}
|
|
323
380
|
|
|
324
381
|
const now = Date.now()
|
|
325
|
-
if (firstTokenAt === null)
|
|
382
|
+
if (firstTokenAt === null) {
|
|
383
|
+
firstTokenAt = now
|
|
384
|
+
stopTicker()
|
|
385
|
+
}
|
|
326
386
|
chars += delta.delta.length
|
|
327
387
|
|
|
328
388
|
const tokens = chars * (ratio ?? FALLBACK_TOKENS_PER_CHAR)
|
|
@@ -357,6 +417,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
357
417
|
const tokens = output > 0 ? output : totalChars * (ratio ?? FALLBACK_TOKENS_PER_CHAR)
|
|
358
418
|
if (measured !== null && decodeMs >= MIN_SAMPLE_MS)
|
|
359
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
|
|
360
423
|
resetStream()
|
|
361
424
|
})
|
|
362
425
|
}
|
|
@@ -81,6 +81,36 @@ export function ttftMs(requestAt: number | null, firstTokenAt: number | null): n
|
|
|
81
81
|
return firstTokenAt > requestAt ? firstTokenAt - requestAt : null
|
|
82
82
|
}
|
|
83
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
|
+
|
|
84
114
|
/** Home-relative path, or the absolute path when it is outside the home directory. */
|
|
85
115
|
export function formatCwd(cwd: string): string {
|
|
86
116
|
const home = homedir()
|