@sven1103-agent/pi-metrics0.1.1
Live LLM latency HUD for the Pi coding agent: TTFT, inter-token latency and decode throughput.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-metrics
Live LLM latency HUD for the Pi coding agent: time to first token, inter-token latency and decode throughput — per call and per session. No network calls, no telemetry, no runtime dependencies.

The same session in graphStyle: "block":

The animations are generated by scripts/demo.mjs, which drives the real renderer over a
virtual clock. They play at real time — one second of animation is one second of decoding —
so the cadence you see is the cadence the HUD actually has. The figures are illustrative:
they exercise the states and the colour ramp the HUD can show, and are not a benchmark.
Regenerate with scripts/record-demo.sh.
Contents
- Install
- What the HUD shows
- Commands
- Settings
- What it measures
- Supported Pi versions
- What it does not do
- Maintained by
Install
pi install npm:@sven1103-agent/pi-metrics
Or pin a release from git:
pi install git:github.com/sven1103-agent/pi-metrics@v0.1.1
Pre-1.0: no compatibility promise
Per Semantic Versioning, a 0.y.z release makes no compatibility promise — behaviour,
settings keys and the rendered layout can all change, in any version including a patch.
1.0.0 is the first release that will define a stable API, and it is the target this is
being built towards. Until then, pin a tag if you need something that does not move.
What the HUD shows
A static frame, as text
─ pi-metrics · demo-model ─────────────────────────────────────────────────────────────── 3 calls ─
○ waiting ● writing ‖ paused ✔ done [██████░░░░░░] 14.9 tok/s (est) 30 tok waiting 900ms
TPS tok/s · fast 100 · avg 32 · p95 79 20s · max 100 auto
⢸⣿⣿⣿⣿⡇
⢸⣿⣿⣿⣿⡇ ⢀⣀⣀⣀⣀
⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⢸⣿⣿⣿⣿⡇⠒⠒⠒⠒⣼⣿⣿⣿⣿⣦⣤⣤⣤⣤⣄⣀⣀⣀⣀⠒⠒⠒⠒⠒
⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⢸⣿⣿⣿⣿⣷⣶⣶⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⣀⣀⣀⣀
───────────────────────────────────────────────────────────────────────────────────────────────────
TTFT ms med 400ms max 500ms │ ITL ms med 22ms max 38ms
⡇⡀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀ │ ⡇
⣧⡇ │ ⢠⡇⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤
⣿⡇ │ ⣼⡇
⣿⡇⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂ │ ⣿⡇⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂
───────────────────────────────────────────────────────────────────────────────────────────────────
STATS med conn/pre 100ms/300ms med first 400ms med ITL 22ms p95 ITL 38ms 3 calls
The panel sits above the editor by default. From the top:
- Header — the model name and the number of calls recorded this session.
- State row — the live state, a pressure bar, throughput, and provenance.
- TPS chart — session throughput over time.
- HIST charts — TTFT and ITL, one column per call.
- STATS row — session medians and the call count.
style: "compact" collapses the TPS and HIST charts to one line each, instead of the tall
multi-row graphs.
The state row
All four states are drawn on every frame with the active one highlighted, so there is nothing to re-read mid-response:
| Glyph | State | Meaning |
|---|---|---|
○ |
waiting | No token yet — connection and prompt reading, one wait. |
● |
writing | Tokens are flowing. |
‖ |
paused | A token is overdue; waiting Xs appears at the end of the row. |
✔ |
done | The call finished; the values freeze. |
The rest of the row, left to right:
| Element | Meaning |
|---|---|
| Pressure bar | Throughput against the tpsGood target — full at the target. |
12.3 tok/s |
Current decode throughput. |
(usage) / (est) |
Where the figure came from — see What it measures. |
30 tok |
Output tokens for the call. |
waiting 900ms |
Only while paused: time since the last token. |
The TPS and HIST charts
The TPS chart is a session timeline, one column per completed writing second, height =
tokens generated that second. It is never reset per call, so separate writing actions stay
comparable. The title's left side is measurement (avg, p95); its right side is the chart's
own axes — the recording window in seconds, then the tok/s ceiling, marked auto when derived
from the session.
The HIST charts plot TTFT and ITL one column per call. In style: "normal" they sit side
by side, with the session median drawn as a reference line and the median and max in the title;
in compact, each gets a one-line sparkline beside its median.
Commands
| Command | Effect |
|---|---|
/metrics |
toggle the HUD |
/metrics on · /metrics off |
explicit toggle |
/metrics report |
append a table of recent calls to the transcript |
/metrics reset |
forget the calls recorded in this session |
/metrics placement above · below |
move the panel |
/metrics style compact · normal |
four lines, or multi-row graphs |
/metrics graph braille · block |
graph glyphs |
/metrics color calm · vivid · mono |
colour intensity |
Display commands take effect immediately and last until Pi exits. Each one prints the
settings.json line that makes the change permanent — the extension cannot write that file
itself, because Pi's extension API exposes no settings-write call, and editing the file
underneath Pi's settings manager risks dropping unrelated keys.
Settings
All optional, under the pi-metrics key in ~/.pi/agent/settings.json:
{
"pi-metrics": {
"enabled": true,
"placement": "aboveEditor",
"style": "normal",
"graphStyle": "braille",
"colorMode": "calm",
"timeline": true,
"liveTimeline": true,
"history": 120,
"tpsWindow": 120,
"tpsMax": "auto",
"tpsGood": 30,
"tpsBad": 15,
"tpsFast": 100,
"charsPerToken": "auto",
"cost": true,
"persist": false
}
}
| Key | Values | Default | Effect |
|---|---|---|---|
enabled |
true / false |
true |
Show the HUD at startup. |
placement |
aboveEditor / belowEditor, or above / below |
aboveEditor |
Panel above or below the editor. |
style |
compact / normal |
normal |
Four lines, or the multi-row charts. |
graphStyle |
braille / block |
braille |
Chart glyphs. |
colorMode |
calm / vivid / mono |
calm |
Colour intensity. |
timeline |
true / false |
true |
Draw the TPS and HIST timeline rows (compact style only). |
liveTimeline |
true / false |
true |
Also draw the live TPS row (compact style only, needs timeline). |
history |
number | 120 |
Calls kept for the HIST timelines and /metrics report. |
tpsWindow |
number, seconds | 120 |
Seconds of history in the TPS chart. |
tpsMax |
number / auto |
auto |
TPS chart ceiling; auto = session p90 plus headroom, never below tpsFast. |
tpsGood |
number, tok/s | 30 |
"Good" target line; fills the pressure bar and is the cold-start colour baseline. |
tpsBad |
number, tok/s | 15 |
"Bad" target line drawn on the TPS chart. |
tpsFast |
number, tok/s | 100 |
"Fast" target line; also floors the automatic chart ceiling. |
charsPerToken |
number / auto |
auto |
Divisor for the character token estimate; auto calibrates from the session. |
cost |
true / false |
true |
Per-call cost column in /metrics report and the persisted card. |
persist |
true / false |
false |
Append a one-line card per call to the transcript. |
placement accepts aboveEditor / belowEditor and, because a natural guess is otherwise
silently ignored, also above / below. A value that is unusable in any other way is
reported at startup as a warning and replaced by the default, so a typo cannot degrade
the HUD quietly.
The pre-0.1.0 names rateWindow / rateMax / rateGood / rateBad / rateFast are
still read as aliases, so an existing config keeps working. When both a legacy and a new
key are set, the new key wins.
Why cost is not on the HUD
cost adds a per-call column to /metrics report and the persisted card. It is not on the
HUD: Pi's own footer already carries the session cost, and that total also includes compaction
and branch-summary usage this extension never sees, so a second session total here would
disagree with the one directly above it.
Why tpsGood and tpsBad are not printed
They are targets, drawn as reference lines on the TPS chart, and configuration rather than
measurement — printing them invited reading them as results. tpsGood also fills the pressure
bar and is the colour baseline until the session has a few seconds of its own history; colour is
otherwise relative (see below), so these thresholds do not decide it. tpsFast is the one
that is printed, because it also floors the automatic chart ceiling.
What it measures
| Term | Meaning |
|---|---|
| TTFT | Time to first token: request → first streamed token, as a session median. |
| ITL | Inter-token latency: gap between streamed deltas, as a median and p95. |
| tok/s | Decode throughput: output tokens over the decode window (first token → end). |
| TPS | The session timeline of tok/s, one column per completed second. |
| TTOUT | Time to output end: wall time of the whole response. |
A few properties are worth stating explicitly, because the figures can mislead without them:
conn/prefill—/metrics reportsplits TTFT intoconn(request → response headers) andprefill(headers → first token) when the provider exposes the boundary. They are two separate medians, and medians are not additive, so they need not sum to themed firstbeside them.connfolds in server-side queueing, so the split compares calls to the same provider rather than across providers.(usage)vs(est)— throughput is tagged with its source:(usage)when the provider reportedusage.output(the call is finished and its TPS buckets were rescaled to the exact count),(est)in amber while the figure still comes from streamed characters. Estimates are never shown as if they were measured.- Latency is the median; throughput is the mean. Latency has a long right tail (cold connections, provider queueing) where single outliers drag a mean away from the typical call. Throughput has no such noise: a stalled second is delivered throughput, and a median would flatter it.
- Colour is relative, not absolute. One cool-to-hot hue ramp carries severity as the
ratio to the session's own typical value, so the same palette means the same thing on an
8 tok/s local model and a 200 tok/s hosted one. A third of typical is the cool end, typical
is the middle, three times typical is the warm end. In the charts the ramp is applied
vertically: a bar's top edge carries the colour of its own value and the fill fades toward
the axis baseline below.
colorModepicks how loud it is:calmkeeps the theme's own colours,vividuses saturated anchors,monodrops hue entirely. TTOUTis not on the HUD panel. It appears in/metrics report, in the persisted card, and in the one-line status shown when the HUD is toggled off — alongside the per-call cost.
About the character estimate
When a provider reports no usage, tokens are estimated from streamed characters divided by
charsPerToken. Measured across 39,000 real calls against provider-reported counts, the true
characters-per-token ranges from ~3.0 (DeepSeek) to ~3.8 (Kimi), so a fixed 4 is off by ~19%
typically and ~49% at p90. The default auto calibrates from the session's own completed
calls; set a number for reproducibility.
Why the automatic TPS ceiling uses p90
The auto ceiling is the session p90 plus headroom, floored at tpsFast. At the sample
counts this chart has, p98 is the maximum — percentile takes the nearest index, so p98 of
ten seconds is the tenth — so one outlier second used to move the ceiling and rescale every
other bar with it. p95 does not fix that either: it is still the maximum until about a dozen
samples. p90 survives both, and a bimodal (half cached) session as well. The deliberate trade:
a genuine peak exceeds the ceiling and renders at full height.
Supported Pi versions
Each version is its own CI job, so the Checks tab names the exact release that broke:
| Pi | Typechecks clean | Loads |
|---|---|---|
| 1.3.0 · 1.2.0 · 1.1.0 | ✅ gating | ✅ gating |
| 1.0.4 · 1.0.0 · 0.99.2 | ⚠️ errors reported, not asserted | ✅ gating |
| < 0.85.1 | ❌ | ❌ does not load |
Below 1.1.0 the published typings are incomplete: the extension loads, but does not
typecheck. That lane asserts the load gate and only reports the error count as evidence
for the floor — it never requires errors to be present, so an upstream backport lowers the
floor without breaking the build. The @earendil-works/pi-ai peer floor matches at
>=1.1.0; the other two stay at * as Pi requires.
Interactive rendering is verified manually. The widget is TUI-only, so a headless run cannot exercise it. Do not read these badges as covering what the HUD looks like.
What it does not do
- It does not send anything anywhere. No network calls, no telemetry, no credential access. See SECURITY.md for the full scope, written against the source, and for how to report a problem privately.
- It installs nothing.
package.jsondeclares no runtimedependencies, so adding this package adds no code for you to audit —npm audithas no tree to report on for it. - It does not show a session cost. Pi's footer already carries that total; this extension only
adds a per-call cost column to
/metrics reportand the persisted card. - It does not measure prompt tokens per second; it measures decode throughput.
- It does not persist history across sessions unless
persistis enabled.
Maintained by
Sven Fillinger, Research Software Engineer, University of Tübingen, through my agent alias @sven1103-agent — the account that authors the commits here. Same person; the alias exists so the automation is labelled as automation.
The npm package is published from an npm account of the same name, for the same reason:
npm view @sven1103-agent/pi-metrics maintainers names the agent account, not a human
one, so the automation is visible in the registry too rather than being fronted. I own
that account and its recovery factors, so it stays accountable to a person.
License
MIT