oh-my-tps
v0.3.1
Published
Tiny live TTFT and TPS readouts for the Pi coding agent.
Maintainers
Readme
Oh My TPS
English | 简体中文
安装
npm package
pi install npm:oh-my-tpsGit repository
pi install git:github.com/EnderLiquid/oh-my-tps功能速览
oh-my-tps 只做一件事:
给 Pi TUI 加一组实时速度读数,测量 LLM 首字延迟和输出速度。
τ:TTFT,首个 token 到达前等了多久,单位秒Δ:TPS,每秒输出多少 token
显示效果:
τ0.8 Δ48.6就这么多。
十个字符的空间,开箱即用的体验。
感兴趣可以继续往下看,但到这里你其实已经会用了。
读数详解
你会在 TUI 底部状态区域看到这样的读数:
τ0.8 Δ48.6
τ1.1 Δ49.7L
τ0.8A Δ52.4A这里后缀的含义是:
A:平均值(Average)。TTFT 和 TPS 分别维护自己的历史样本。L:上一轮有效的最终 TPS(Last),仅用于Δ。
可以这样理解:
τ0.8 Δ48.6:响应正在流式传输,TTFT 约为 0.8 秒,并且已经产生了当前有效的实时 TPS。τ1.1 Δ49.7L:请求已经发出,但还没有收到首个 token;TTFT 正在计时,Δ暂时显示上一轮最终有效 TPS。τ0.8A Δ52.4A:当前处于空闲状态,显示近期 TTFT 和 TPS 的平均值。
流式传输时的 Δ 仍可能显示为上一轮 TPS、平均 TPS 或未知值,直到实时 TPS 满足首次计算条件。响应结束后的 Δ 优先使用服务提供方返回的 usage.output;只有没有可用的 usage.output 时,才使用最后一次有效的实时 TPS。本轮没有有效最终 TPS 时,最终状态回退到本轮开始前的上一轮 TPS,再回退到平均 TPS。
配置
默认开箱即用。要更改读数颜色,创建 ~/.pi/agent/oh-my-tps.json:
{
"builtinRenderer": {
"color": "muted"
}
}同时,插件支持自行绘制并替换内置读数。做法是把状态数据发布到 Pi 的事件总线,实现测量与渲染解耦,内置读数只是其中一个事件订阅者。你可以关闭内置读数,订阅同一份数据并渲染到任何位置。详见 CONFIGURATION.zh-CN.md。
原理说明
下面这部分面向希望了解插件原理的用户。
状态机
内部分为四个阶段:
- 等待阶段:请求已发出,等待首个 token
- 流式输出阶段:首个 token 已到达,响应正在流式输出
- 最终阶段:响应结束,已完成本轮 TPS 的结算尝试
- 空闲状态:当前没有正在处理的服务提供方请求
示例:
空闲 τ… Δ? (还没有历史样本)
-> 等待 τ0.2 Δ? (等待首个 token,每200ms更新τ)
-> 流式输出 τ1.3 Δ? (首个 token 可能来自思考,τ已锁定,实时 TPS 尚未就绪)
-> 流式输出 τ1.3 Δ51.0 (新的非思考增量到达,实时 TPS 已满足观察条件)
-> 最终 τ1.3 Δ52.0 (本轮有效最终 TPS 来自 `usage.output` 或实时 TPS 回退值)
-> 空闲 τ1.3A Δ52.0A
-> 等待 τ0.2 Δ52.0L (优先显示上一轮有效最终 TPS)τ 的来源
TTFT 的定义是:从请求发出到首个 token 到达所等待的时间。
插件使用首个携带 token 的非空流式增量作为可观测信号。以下事件在 delta.length > 0 时会触发 TTFT:
text_deltathinking_deltatoolcall_delta
以下事件不会触发 TTFT:
text_startthinking_starttoolcall_start- 空增量以及其他元数据事件
收到首个有效增量后,插件把它与请求开始时间的差值锁定为本轮 TTFT。
因此:
- 等待阶段的
τ会一直增加; - 首个正文、思考或工具调用 token 到达后,进入流式输出阶段,
τ就锁定; thinking_start等元数据不会结束等待,但非空thinking_delta会结束等待;- 如果服务提供方把思考内容加密并且不传输思考增量,插件只能测量首个可观察 token 的 TTFT,无法获得隐藏思考阶段内部的时间轴。
TTFT 样本与 TPS 样本独立维护。只要首个 token 到达,本轮就可以贡献 TTFT 平均值,即使本轮最终没有有效 TPS。
实时 Δ 的来源
服务提供方不会持续告诉 Pi “刚刚又生成了多少个 token”,所以实时 TPS 只能在本地估算。当前实现使用非思考增量的滚动队列:
- 纳入非空的
text_delta; - 纳入非空的
toolcall_delta; - 排除所有
thinking_delta,包括思考摘要。
默认窗口是最近 5 秒。每当新的非思考增量到达时,插件会:
- 将原始增量和到达时间加入队列;
- 移除窗口之外的旧增量;
- 按到达顺序拼接窗口内的增量;
- 使用
tokenx估算拼接内容的 token 数; - 用窗口内 token 数除以观察时长,得到新的实时 TPS。
公式可以表示为:
实时 TPS = 最近窗口内的非思考增量估算 token
/
min(5秒,首个非思考增量后的观察时长)从首个非思考增量到第一次计算实时 TPS,至少需要观察 2 秒。这个等待时间用于避免流式刚开始时分母过小,或者服务提供方一次性发送初始积压内容而产生异常高值。
实时 TPS 是最近窗口内正文和工具参数的传输速率,只在新的非思考增量到达时重算,不使用后台定时器。
最终 Δ 的来源
响应结束时,插件按以下顺序结算 TPS。
1. 服务提供方返回的 usage.output
当服务提供方返回有限正数 usage.output,并且从首个非空的正文、思考或工具调用增量到响应结束至少经过 2 秒时,使用:
最终 TPS = usage.output
/
(响应结束时间 - 首个有效内容增量时间)usage.output 通常是服务提供方报告的真实输出 token 数,可能包含推理 token。因此,这个来源的分子可能包含思考 token,分母则从首个可观察的内容增量开始。
对于加密思考模型,隐藏思考可能在首个可观察增量之前已经发生。客户端无法知晓这段隐藏推理时长,因此 usage.output 来源的最终 TPS 可能略高于真实值。
2. 实时 TPS 回退值
只有当服务提供方没有可用的 usage.output 时,才使用本轮最后一次有效的实时 TPS:
最终 TPS = 本轮最后一次有效的实时 TPS这个来源不包含思考,只反映正文和工具参数在最近窗口中的输出速率。
如果服务提供方返回了可用的 usage.output,但从首个有效内容增量到响应结束的时长不足 2 秒,插件不会改用实时 TPS 回退值;本轮没有有效最终 TPS。
实时值与最终值存在偏差的原因
1. token 估算是启发式的
tokenx 不是精确 tokenizer,而是一个轻量、偏启发式的估算库。它的优势是小而快,适合实时 UI 刷新。代价也很明确:它不是为所有模型都精确对齐而设计的。
tokenx 的设计与基准测试更偏向 GPT tokenizer / 英文文本 场景。当接入其他模型家族的 LLM,或者输出内容包含非英文字符时,偏差往往会更大一些。
2. 流式输出节奏不均匀
模型输出不是严格按“每个 token 匀速到达”展示给 UI 的。实际过程中还会受到这些因素影响:
- 服务提供方自己的 SSE / chunk 刷新策略;
- Pi 发布事件的节奏;
- 思考、工具调用、正文混在一起时的内容结构变化。
3. 两种 TPS 的统计范围不同
实时 TPS 和 usage.output 来源的最终 TPS 的定义本来就不同:
- 实时 TPS 只统计最近窗口内的非思考正文和工具调用;
usage.output来源的最终 TPS 使用服务提供方报告的输出 token,并统计从首个正文、思考或工具调用增量到响应结束的时长;- 实时 TPS 不包含思考,而
usage.output来源的最终 TPS 可能包含推理 token。
所以二者即使都没有估算误差,也不一定相等。实时值反映当前响应末段的输出节奏,最终值反映本轮可观察流式阶段的整体结果。
平均值 A
当前实现分别维护最近最多 5 条 TTFT 样本和 TPS 样本:
- 首个 token 到达后,TTFT 就可以写入 TTFT 历史;
- 只有本轮产生有效最终 TPS,才会写入 TPS 历史;
- 使用服务提供方
usage.output计算出的最终 TPS,以及实时 TPS 回退值来源的最终 TPS,都会纳入平均 TPS; - 因此一轮请求可能贡献 TTFT,但不贡献 TPS。
A 表示近期有效样本的平均值,不保证 TTFT 平均值和 TPS 平均值来自完全相同的请求集合,也不保证平均 TPS 中每条样本都具有相同的 token 统计范围。
数据参考指导
经验上可以这样看:
τ:参考价值很高,适合观察请求延迟;- 最终 / 平均
Δ:适合观察近期整体输出速度表现; - 实时
Δ:适合观察当前正文或工具参数的实时输出速度;
插件适用范围
- 适用于为 LLM 速度与延迟提供粗略量化参考
- 适用于快速发现长会话中某次明显偏慢的请求
- 不适用于严格的模型性能对比与基准测试
许可证
MIT License
