npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@focksor/pi-token-speed

v1.6.0

Published

pi extension: show assistant output token speed and TTFT in the footer, aggregated across in-process subagents

Readme

@focksor/pi-token-speed

在 pi 的默认 footer 中显示 assistant 输出速度;运行 subagent(如 pi-subagents)时,footer 会实时显示所有正在流式的 subagent 的聚合速度。

npm

使用

从 npm 安装(推荐):

pi install npm:@focksor/pi-token-speed

临时从源码加载:

pi -e ~/workSpace/pi-token-speed/token-speed.ts

本地开发长期加载:把下面路径加入 ~/.pi/agent/settings.jsonextensions 数组:

{
  "extensions": [
    "/home/focksor/workSpace/pi-token-speed/token-speed.ts"
  ]
}

修改配置后重新启动 pi;已经运行的 pi 可以使用 /reload 重新加载扩展。

显示口径

  • 响应生成中:footer 每约 100ms 刷新一次,使用当前文本、思考内容和工具调用参数的字符数除以 4 估算 token 数。

  • 流式速度在到达周期上测量,并经由历史导出的可行域闸门过滤(见下),响应完成后沿用同一个估计器,因此读到的数字在收尾时不会跳变(历史不足 12 样本时不启用)。

  • TTFT 不计入速度:这是“已产出 token / 产出耗时”的瞬时速率,不含首字延迟。

  • TTFT(Time To First Token,首字延迟)从 provider 请求发出(before_provider_request)计到收到首个流式内容(首次 message_update),小于 1 秒显示毫秒,否则显示秒。

  • 请求发出后、首个流式内容到达前,footer 实时刷新等待计时(约 10Hz),锚点与最终 TTFT 相同,等待中的数字会精确收敛到定格值;尾部的 表示仍在等待,首个 token 到达后计时停止并定格为最终 TTFT。

  • 速度不回退:一次会话中只要显示过一次真实速度,之后的等待期和“样本不足”期都继续显示最近一次测得/最终的速度,不再退回 ... 占位;⚡ ... tok/s 只出现在从未显示过速度的新会话:

    ⚡ 32.4 tok/s · TTFT 2.4s…   ← 已显示过速度:等待期保留上次速度
    ⚡ ... tok/s · TTFT 2.4s…    ← 从未显示过速度的新会话

示例:

⚡ 32.4 tok/s · TTFT 830ms

速度估计算法

直接对"累计 token / 已用时间"求平均或做差都不行:provider 经常把大量 token 攒在一个 delta 里到达(首个大 chunk、整个工具调用参数 blob、代理缓冲),这一个批次会让朴素算法显示上千 tok/s,并且长时间缓慢衰减。

所以速度在到达周期上测量,并叠加一个由历史导出的可行域闸门:

  1. 周期 = 相邻两个"带 token 的到达"之间,周期速率 = Δtoken / (t_end − t_start)。零 token 的事件(role-only、finish_reason、usage-only、心跳)不构成周期,因此天然不会污染估计——既不会闪到 0,也不会因为丢弃它们而虚高。

  2. 取最近 16 个周期速率,按每个周期的时长加权取中位数(窗口不足 7 个时取最小值)。

    加权是必要的:纯按个数取中位数会把 1ms 的周期与 50ms 的周期同等对待,而背靠背 delta 的突发(长时间 TTFT 后的首 token flush、代理攒批一次性释放)能在窗口里占到一半、却只代表几毫秒的实际生成。实测(你机器上的真实 footer):5 个间隔 1ms 的 delta 把显示拉到 49764 tok/s(真实约 100),同一 subagent 段独立复现(31784)。而且不止 1ms:实测 2ms 间隔→12550、3ms→16667、5ms→5050、10ms→2550。所以不要用“绝对时长阈值”去滤——那只能盖住 1ms;按 span 加权才能全谱系抑制。

    权重相等时加权中位数严格退化为普通中位数(含“偶数样本取中间两个均值”的行为),因此已有的、已验证的行为不变;不等权才是加权的用途。

    窗口小时必须保守:中位数只在离群值占少数时才有效,3 个周期里 2 个被污染就能带跑它(实测 2600 tok/s,而真实是 100)。取最小值是数据稀缺时唯一诚实的读数,代价是窗口填满期间可能读偏低、绝不会读偏高

    窗口取 16 而非更小,是为了显示平滑:每周期 token 数在 3–8 波动、间隔在 45–80ms 波动时,8 周期的显示会在约 60 tok/s 范围内摆动、最大偏离真值约 38%;16 周期降到约 36 与 22%,且顺带把“无历史 + 4/8 多数污染”从 5100 修到 100。代价是真实速率发生阶跃时,显示追上来的时间从约 200ms 变为约 400ms(均实测)。为什么不更宽:超过 16 后抗毛刺不再获益,只损失跟手性(24 周期→600ms,32→800ms)。

  3. 可行域闸门:按 (provider, model) 在内存中积累该模型的历史速率分布(32 样本环形缓冲,每个响应写入最多 4 个分段中位数)。边界为

    中位数 + 6 × max(1.4826 × MAD, 0.3 × 中位数)

    超出边界的周期速率视为到达伪影,不参与估计;若没有可信周期可用,则静默(继续显示上一次的速度)。用 MAD 而非标准差:实测 20% 样本为 20 倍 spike 时边界仅从 212 动到 230 tok/s。

  4. 历史不足 12 样本(任何交付粒度下都是 3 个响应)时闸门不启用——没有依据就不做判断,未知模型绝不被压低、不做夹紧。注意"不启用"只指不做剔除与夹紧,不等于显示序列与旧实现完全相同:周期规则本身仍然生效,且在"响应早期 + 少数肥 chunk"这类场景下优于旧实现(实测旧实现显示 5100,新规则显示 100)。

这样还顺带解决了两个实际问题:

  • 零 token 的 delta:见第 1 条,不再需要特判。
  • 流式间隔不均:5ms 一个 delta 和 2s 一个 delta 都能得到正确速率,不需要知道 provider 的节奏。

与其他开源实现的关系

调研了两个同类项目,它们都用时间窗 + 公式而非滤波:

| 项目 | 平台 | 公式 | 辅助 | |---|---|---|---| | EnderLiquid/oh-my-tps | pi 扩展 | 5 秒滚动窗内 Σtokens / 窗口时长 | 2 秒最短采样门槛 | | f8hgkao02a/opencode-tps | OpenCode | 5 秒滚动窗内 token 增量 | EMA α=0.35 |

它们的做法在本扩展的场景下不适用,因为 Σtok/Σtime均值语义,会丢掉中位数的抗离群能力:实测它在“4/8 污染”和“思考突发巨块”下失效,而且对短于 2 秒的响应完全不显示(你的历史数据里约 39% 的响应 ≤200 token)。本扩展选择“周期中位数 + 16 周期窗口”:平滑度靠窗口宽度换取,抗毛刺靠中位数——两者不可互相替代。

代价(实测):

  • 如果模型的真实速率超过历史中位数约 2.8 倍(例如换到快得多的模型),会有最多约 5 个响应的"暂无新数字"期(继续显示上一次的速度),之后恢复正常。
  • 持续粗粒度交付(例如代理批量转发)不会被永久静默:闸门无法区分"模型快"与"传输批量化",因此均匀的批量交付会被历史吸收(显示该速率);只有高于该模型历史分布的交付会先静默、并在约 5 个响应后随历史适应而恢复显示。这是"不再显示高于常理的读数"的代价,单靠时间规则无法还原真实速率。
  • 一个响应只有包含至少 4 个周期(5 个带 token 到达)才会写入历史;更短的响应(如只有 3 个周期)不参与历史积累。门槛按周期数而非 chunk 数计,因此交付粒度粗的 provider(代理攒批,每 700ms+ 一个 delta)也能在 3 个响应内训练出闸门。
  • 无历史时若窗口内污染占多数(16 个周期中 ≥9 个),中位数仍可能被带跑;闸门在 3 个响应后关闭该缺口。例如“思考突发”:前 6 个周期每周期 300 tok、随后正常 100 tok/s,冷启动下仍会读到 6100,闸门训练后降到 100。
  • 按帧时长加权的边界:加权中位数解决了“突发恰好占半个窗口”的中位数退化,但解决不了“突发持续得够久”。当突发本身在时间上已不是零头时,它就是那段时间的真实速率,加权无法也不应该再把它当成离群值——这是信息论下限(无法区分“代理正在快速度交付”与“模型真的很快”)。实测剩余穿透完全由突发自身的交付速率决定,与 delta 个数无关:50 tok / 20ms 间隔 → 2500 tok/s(6/8/10 个 delta 结果相同),50 tok / 50ms → 1000,50 tok / 100ms → 500。实际影响很小:这需要代理长时间攒批后一次性 flush,且真实 footer 实测(两轮共约 6 万 token)未再出现任何 >1000 的读数。
  • 短抖动窗口偏保守:取最小值的规则覆盖前 6 个可信周期,因此响应刚开始时遇到“50/200 交替”这类诚实抖动会读到较低的那一档(50),而真实是 125。窗口达到 7 个周期后改用中位数,但交替流的中位数还取决于窗口长度的奇偶:偶数窗口读 125(实测 8/10/12 周期均读 125),奇数窗口读 50(实测 7/9 周期读 50,因为 50 在窗口中占多数)。这是刻意的取舍:宁可短期读低,也不读高。
  • 整个响应只有一个 delta(例如 200ms 内结束的短回复)时,沿用 usage.output / 耗时 作为最终值,但已夹紧到该模型的历史可行域;无历史时不夹紧。

闸门与历史只存内存(挂在进程级共享 store 上,/reload 存活、进程退出即丢),不写任何文件

全局速度(subagent 聚合)

pi-subagents 把每个 subagent 作为同进程内的独立会话运行,且子会话拿到的是 no-op UI(ctx.ui.setStatus 无效),所以 subagent 的速度默认无法到达主 footer。本扩展的做法是:每个会话的扩展实例都把自身流式状态上报到一个进程级共享 store(globalThis),由唯一拥有真实 UI 的主会话实例聚合渲染。

显示规则:

  • 标签跟随执行中的 agent 数量(从 spawn 到 agent_end),在它们的流式间隙(工具执行、思考)保持稳定,不会在名字和数量之间翻转。

  • 速度始终显示(与主会话同规则):每个 agent 有当前流式速度就显示当前值,停止流式后短时间内继续显示它最后一次测得的速度;聚合为各 agent 当前值或(3 秒内的)最后值之和。这个 3 秒时效是必要的——否则一个结束了大输出、进入长工具阶段的 agent 会把它冻结的速度一直加进总和,让聚合值长期虚高:

    ⚡ 32.4 tok/s · TTFT 830ms · 3 sub · 45.2 tok/s   ← 主会话流式 + 3 个 agent
    ⚡ 28.1 tok/s · TTFT 620ms · 3 sub · 45.2 tok/s   ← agent 全部在跑工具/思考:保留最后速度
    ⚡ 3 sub                                          ← 全部刚启动 / 最后速度已过期(样本不足期)
  • 主会话自己的速度信息始终显示:流式时实时速度+TTFT,空闲时保留最近一次的最终速度;聚合段只是追加在后,不会取代它。新会话还没跑过任何主消息时才可能只显示聚合段。

  • 只剩 1 个 agent 时显示会话名(去掉 pi-subagents 的 #id 后缀、超长截断):⚡ 28.1 tok/s · TTFT 620ms · sub Explore · 18.0 tok/s

  • agent 结束(agent_end)后其速度与数量一并移除;会话被丢弃(约 10 分钟后)时自动清理,异常退出的残留由 5 分钟 TTL 兜底。

已知限制:

  • 仅覆盖同进程会话:isolated: trueextensions: false 的 agent 类型不会加载本扩展、无法上报;独立 pi 进程(另开的终端、pi -p 一次性任务、--mode json 子进程)互相不可见。
  • 聚合段不显示各 subagent 的 TTFT;逐 agent 的明细可看 pi-subagents 的 widget / FleetView。

开发

node token-speed.ts    # 加载冒烟(pi 导入均为 type-only)
node test-estimator.ts    # 估计器回归:周期采样、可行域闸门、超短回复夹紧
node test-global.ts    # 多会话全局速度功能测试
node demo-timeline.ts  # footer 时间轴演示(真实时序,双响应衔接预览)
npx tsc --module nodenext --moduleResolution nodenext --target es2022 \
  --strict --noEmit --skipLibCheck --types node token-speed.ts