SLOPSHOPPER

⚡

Status line showing the model's output speed (tokens/sec), TTFT, total time and session average

newstatus
v0.4.0MITupdated 2026-10-15Kiyliy/claude-code-tps
A shopper browsing a rack in a slop shop
README

⚡ tps

English · 简体中文

A tiny Claude Code mod that shows how fast the model is talking, right in the status line. Works in the terminal and in the desktop Code tab.

⚡  92.3 tps · TTFT 2.1s · total 22.0s · avg 88.0 tps

license claude code size

FieldMeaning
tpsOutput speed of the last request: usage.output_tokens ÷ generation time (first streamed chunk → end, TTFT excluded)
TTFTTime to first token: request sent → first streamed chunk
totalWall time of the request: sent → last chunk
avgAverage speed over every request in this session
  • While a response is streaming you see a live estimate such as ~120 tps (≈4 chars/token for Latin text, ≈1 char/token for CJK). When the response ends it switches to the exact value from the API usage.
  • Requests made by sub-agents are tagged (sub).

Install

  1. Clone it somewhere stable:
   git clone https://github.com/Kiyliy/claude-code-tps.git ~/.claude/mods/tps
  1. Add it to ~/.claude/settings.json (merge into an existing env block; separate several directories with ; on Windows):
   "env": { "CLAUDE_CODE_PLUGIN_DIRS": "~/.claude/mods/tps" }
  1. Start a new session.

How it works

The mod hooks the turn.step stream, timestamps every chunk, reads usage.output_tokens when the step stops, and writes the result with $.ui.status(). The whole thing is hooks/register.ts.

Notes:

  • The plugin is deliberately named ⚡, because the engine always prefixes the status line with the plugin name. claude plugin validate warns that the name isn't kebab-case; that only matters for marketplace installs, local loading is fine.
  • The name is read when a session first loads the plugin, so a rename needs a new session. Code changes hot-reload in open sessions.

Validate & test

claude plugin validate ~/.claude/mods/tps
claude plugin test ~/.claude/mods/tps

The mod API is early access and needs a recent Claude Code; tested on 2.1.293.

License

MIT

Source 1 files
hooks/register.ts 53 lines
1import type { Register } from 'claude-code'
2
3// Rough live estimate: ~4 chars/token for Latin text, ~1 char/token for CJK.
4const estimateTokens = (s: string) => {
5  let cjk = 0
6  for (const ch of s) if (ch.charCodeAt(0) > 0x2e80) cjk += 1
7  return cjk + (s.length - cjk) / 4
8}
9
10const fmt = (n: number) => (n >= 100 ? n.toFixed(0) : n.toFixed(1))
11
12// The plugin is named ⚡: the engine leads the status line with that name.
13export const register: Register = on => {
14  // Session totals (reset on reload, which is fine for a speed meter).
15  let totalTokens = 0
16  let totalMs = 0
17
18  on('turn.step', async function* ($, e, next) {
19    const sentAt = performance.now()
20    let firstAt = 0
21    let lastDraw = 0
22    let estimate = 0
23
24    const stream = next(e)
25    for await (const chunk of stream) {
26      const now = performance.now()
27      if (chunk.kind !== 'engine') {
28        if (firstAt === 0) firstAt = now
29        if (chunk.kind === 'text' || chunk.kind === 'thinking') estimate += estimateTokens(chunk.text)
30        else if (chunk.kind === 'input') estimate += estimateTokens(chunk.json)
31
32        if (chunk.kind === 'stop') {
33          const out = chunk.usage?.output_tokens ?? 0
34          const genMs = now - firstAt
35          if (out > 0 && genMs > 50) {
36            totalTokens += out
37            totalMs += genMs
38            const ttft = (firstAt - sentAt) / 1000
39            $.ui.status(
40              `${fmt(out / (genMs / 1000))} tps · TTFT ${ttft.toFixed(1)}s · total ${((now - sentAt) / 1000).toFixed(1)}s · avg ${fmt(totalTokens / (totalMs / 1000))} tps` +
41                (e.agentId ? ' (sub)' : ''),
42            )
43          }
44        } else if (now - lastDraw > 500 && now - firstAt > 300) {
45          lastDraw = now
46          $.ui.status(`~${fmt(estimate / ((now - firstAt) / 1000))} tps`)
47        }
48      }
49      yield chunk
50    }
51  })
52}
53