SLOPSHOPPER

llm-speed

Band showing avg time to first token and output tokens/s

newspinner
★ 1v0.3.4no licenseupdated 2026-10-03Lunik/gmz-claude-marketplace/llm-speed
A shopper browsing a rack in a slop shop
README

llm-speed

Shows how fast the model answers in the current session.

LLM speed ┃ TTFT 2.17s ┃ Throughput 195.2 tok/s ┃ Requests 3

Cells

CellMeaning
TTFTAverage time to first token (text, thinking or tool call) over all requests.
ThroughputOutput tokens divided by generation time (first token to end of response), over all requests.
RequestsNumber of model requests counted. A tool-use loop makes several per turn.

Behavior

  • Row in the bottom-right grid. Shows - until the first request completes.
  • A request with no usage report or no first token is skipped.
  • Numbers are averages for the session; they reset when the plugin reloads.
Source 2 files
hooks/register.tsx 81 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Speed } from '../types'
5
6const speed = atom(
7  { plugin: 'llm-speed', key: 'speed' } as const,
8  { steps: 0, ttftMs: 0, tokens: 0, genMs: 0 } as Speed,
9)
10
11export const register: Register = on => {
12  on('turn.step', async function* ($, e, next) {
13    const start = await $.clock.now()
14    let first = 0
15
16    const result = yield* (async function* () {
17      const stream = next(e)
18      for await (const c of stream) {
19        if (!first && (c.kind === 'text' || c.kind === 'thinking' || c.kind === 'tool')) {
20          first = await $.clock.now()
21        }
22        yield c
23      }
24      return stream.result
25    })()
26
27    // ponytail: steps without usage or a first token are skipped
28    const tokens = result?.usage?.output_tokens ?? 0
29    if (first && tokens) {
30      const end = await $.clock.now()
31      await update($, speed, s => ({
32        steps: s.steps + 1,
33        ttftMs: s.ttftMs + (first - start),
34        tokens: s.tokens + tokens,
35        genMs: s.genMs + (end - first),
36      }))
37      $.ui.invalidate('ui.render')
38    }
39    return result
40  })
41
42  // stacked above the usage bar (SessionMode, bottom right)
43  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
44    const s = await read($, speed)
45    const other = await next(e)
46    const { Box, Text } = $.ui.resolve(e)
47    const line = (
48      <Box>
49        <Box width={11}><Text bold color="#7dcfff">LLM speed</Text></Box>
50        <Box>
51          <Text color="#565f89"> ┃ </Text>
52          <Box width={24}>
53            <Text dimColor>TTFT </Text>
54            <Text bold color={s.steps ? '#c0caf5' : '#565f89'}>{s.steps ? `${(s.ttftMs / s.steps / 1000).toFixed(2)}s` : '-'}</Text>
55          </Box>
56        </Box>
57        <Box>
58          <Text color="#565f89"> ┃ </Text>
59          <Box width={24}>
60            <Text dimColor>Throughput </Text>
61            <Text bold color={s.genMs ? '#c0caf5' : '#565f89'}>{s.genMs ? `${(s.tokens / (s.genMs / 1000)).toFixed(1)} tok/s` : '-'}</Text>
62          </Box>
63        </Box>
64        <Box>
65          <Text color="#565f89"> ┃ </Text>
66          <Box>
67            <Text dimColor>Requests </Text>
68            <Text bold color={s.steps ? '#c0caf5' : '#565f89'}>{s.steps || '-'}</Text>
69          </Box>
70        </Box>
71      </Box>
72    )
73    return other.type === 'engine' ? line : (
74      <Box flexDirection="column" alignItems="flex-start">
75        {other}
76        {line}
77      </Box>
78    )
79  })
80}
81
types/index.d.ts 8 lines
1export type Speed = { steps: number; ttftMs: number; tokens: number; genMs: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'llm-speed': { speed: Speed }
6  }
7}
8