@haisto/opencode-model-output-metrics
v1.0.0
Published
Per-request time-to-first-token (TTFT) latency in the OpenCode TUI prompt footer, with P99 and latency color coding
Maintainers
Readme
兼容性:仅支持 OpenCode v2.2.5 及以上(
@opencode/plugin2.x),经cli.json加载。包名:
@haisto/opencode-model-output-metrics(与 GitHub 仓库同名)。功能已收窄为只看首字延迟:不再有侧边栏面板、模型排行或 token 统计。
1. 它显示什么
首字延迟(TTFT,time to first token)= 从宿主发出这次模型请求,到模型吐出第一个 token 的时间。
每一次请求都会单独计时,包括同一轮里工具调用之后的再次请求,也包括一次对话里你后来又发的每一条消息。读数挂在输入框状态栏右侧(prompt.footer.status 之后),不占侧边栏高度,不抢正文空间:
刚提交,还在等首字 ttft 0.12s
出字瞬间固定下来 ttft 4.84s
工具跑完,下一次请求 ttft 0.08s
这一次出字 ttft 4.51s- 一行只有一个读数:每次请求直接替换上一次,不做并排累积 —— 状态栏回答的是「这一次等了多久」。
- 等待阶段读数每 200ms 刷新,从提交(或上一次工具结束)那一刻就开始涨;等待与出字是同一种形状,不加省略号之类的标记。
- 出字瞬间该次读数固定下来(精确到毫秒),下一次请求再从零开始。
- 没有任何可显示的内容时,插件渲染一个零宽空盒子 —— 不会留下占位的空白或残留数字。
2. 颜色
颜色只做一件事:让你不用读数字就知道这次快不快。
| 区间 | 颜色 |
|------|------|
| 快阈值以下 | 主题的 success(绿) |
| 两阈值之间 | 主题的 warning(黄) |
| 慢阈值以上 | 主题的 error(红) |
| 这一轮已结束(读数停留窗口) | 主题的 muted(灰) |
着色看当前显示的那个读数:等待时它随已等时长上涨,于是颜色会随着跨过阈值自己从绿走到黄再到红 —— 这是本插件最有用的一处反馈,你不需要盯着数字看它涨。
默认阈值:1.5s 内算快,5s 以上算慢。可用环境变量覆盖,两个都要给且 FAST < SLOW,否则整组回退默认值:
TTFT_FAST_MS=800
TTFT_SLOW_MS=3000不想染色就 /ttft-color 关掉,读数用正常前景色显示。
3. 安装
3.1 方式一:OpenCode 命令安装(推荐)
按 Ctrl + P 打开命令面板,搜索 install plugin,输入:
@haisto/opencode-model-output-metrics@latest3.2 方式二:手动安装
# 1. 安装
npm install -g @haisto/opencode-model-output-metrics@latest
# 2. 写入 TUI 插件配置(推荐,自动改 ~/.config/opencode/cli.json)
npx @haisto/opencode-model-output-metrics// 或手动编辑 ~/.config/opencode/cli.json(全局配置)
{
"$schema": "https://opencode.ai/v2/cli.json",
"plugins": ["@haisto/opencode-model-output-metrics@latest"]
}3.3 重启 OpenCode
进入任意 session,输入框右侧即可看到读数。
4. 命令
| 命令 | 功能 |
|------|------|
| /ttft | 显示 / 隐藏读数 |
| /ttft-color | 打开 / 关闭按延迟染色 |
| /ttft-stat | 切换统计口径:最近一次 / P99 |
两条命令同时出现在命令面板(Ctrl + P)里,分组为 TTFT。
5. 口径
| 环节 | 取什么 |
|------|--------|
| 起点 | session.step.started 的 data.started(宿主记录的请求真正发出时刻;事件本身懒发布,晚于请求,不能直接用事件时间) |
| 等待期起点 | 提交(session.execution.started)、模型要工具的步骤结束(step.ended 且 finish = tool-calls)、工具结束(tool.success / .failed)。只用来让读数在等待时先动起来,拿到精确起点后被替换;工具执行期间(tool.called / tool.progress)停表 |
| 终点 | 该次请求的第一个输出:session.step.streamed,兜底为 session.text.delta / session.reasoning.delta / session.tool.input.delta / session.tool.input.ended |
| 范围 | 每一次请求各算一次。同一轮里工具调用之后的再次请求、一次对话里后发的每条消息,都单独记一笔 |
| 结束 | session.execution.succeeded / .failed / .interrupted,或兜底的 session.idle;只影响「是否落定」与灰色,不影响已记录的读数 |
| 隔离 | 按会话独立计时。子代理会话有自己的请求序列,不与父会话混算 |
| 子代理 | 后代会话在跑时,父会话并没有在等模型:这一行改为代显子代理的读数(前缀 ttft> 区分),切进子代理窗口也能看到自己那一次;后代停下后从那一刻重新起算,精确值也扣掉这段重叠 |
| 并发子代理 | 多个后代同时在跑时显示最慢的那一个(有在等的优先),与派出顺序无关,不会在几个时钟之间来回跳 |
| 统计口径 | 默认「最近一次」;切到 P99 后显示最近 200 个样本的 99 分位(最近秩法,取到的永远是真实观测值)。P99 没有等待态,数字在两次请求之间保持稳定 |
几个刻意的选择:
- 推理与工具参数也算首字。只调工具、不写正文的请求,首字就发生在工具参数生成时;把它们排除会让这类请求永远等不到读数。
- 起点用步骤级事件而不是轮次级。轮次级的
session.execution.started一轮只发一次,只能测到每轮第一次请求;步骤级的session.step.started才是每次请求一对一的。 - 读数显示单值、逐次替换。并排多值是「历史」,而状态栏要回答的是「这一次」;一行一个数才读得快,宽度也不会随历史增长。
- 等待期另立「预备起点」。
step.started被宿主懒发布在首个输出时刻,只靠它计时,等待阶段根本看不见 —— 读数会凭空出现。于是从提交、模型要工具的步骤结束、或工具结束那一刻起就先起算,拿到精确值再替换。 - 工具执行期间停表。长工具(跑测试、装依赖)不是「等首字」,不停表会让读数先涨到十几秒、再在首字到达时突然缩回一秒多。
- 首字先于起点到达时按消息 ID 配对。两个事件谁先到不确定;用
assistantMessageID把先到的首字挂回起点,否则这次请求会被算成「一直在等」。 - 不显示 0ms 伪读数。请求结束却始终没有输出(中断、上游报错),说明这次没有首字可言,直接丢弃,而不是报一个假的
0ms。 - 事件时刻优先用宿主的
created,只在它缺失或与本地时钟相差超过一天时才回退本地时钟,避免坏数据算出天文数字。 - 一轮结束后读数降为灰色并保留 8 秒,够你在回答落定后回看一眼,又不会让过期数字长期占据状态栏。
6. 边界情况
| 情况 | 行为 |
|------|------|
| 插件在某次请求中途才挂载 | 这次请求没有起点,不记(不猜);下一次请求起正常 |
| 请求结束但从未出字 | 丢弃该次,不显示 |
| 同一会话里并发两次请求 | 按 assistantMessageID 区分,各自计时 |
| 子会话切换、多会话并行 | 各自独立,状态栏只显示当前可见会话 |
| 宿主在 home 路由等无会话场景 | 不显示 |
7. 设置存储
| 键 | 内容 |
|----|------|
| ttft_meter.mode | compact / hidden |
| ttft_meter.color | true / false |
读取端逐项收敛:任一项损坏只回退该项,不影响另一项,也不会让插件启动失败。
8. 日志与排障
默认不写日志:插件只往状态栏写一行读数,没有需要默认留痕的行为。排障时显式打开:
OPENCODE_MODEL_METRICS_LOG_LEVEL=debug打开后写入:
~/.opencode-model-output-metrics/model-output-metrics.log| 变量 | 作用 |
|------|------|
| OPENCODE_MODEL_METRICS_LOG_FILE | 指定日志文件路径(不设则用上面的默认路径) |
| OPENCODE_MODEL_METRICS_LOG_LEVEL | trace / debug / info / warn / error;留空或非法值都等于关闭 |
读数不出现时,先确认你正在一个 session 里(home 路由没有会话 ID),再确认这一轮确实已经触发过 session.execution.started。
9. 更新
由于 OpenCode 已知问题 #6774,插件缓存会锁死在首次安装的版本。
# 1. 清缓存(Windows)
Remove-Item -Recurse -Force "$env:USERPROFILE\.cache\opencode\npm\@haisto\opencode-model-output-metrics@latest"# 1. 清缓存(macOS / Linux)
rm -rf ~/.cache/opencode/npm/@haisto/opencode-model-output-metrics@latestCtrl + P→install plugin→@haisto/opencode-model-output-metrics@latest- 重启 OpenCode
10. 开发
npm install
npm test # vitest
npm run typecheck # tsc --noEmit
npm run typecheck:test # 含测试与脚本
npm run build # 清空 dist 后重建(含 tui.js)src/
index.tsx 插件入口:订阅事件、挂状态栏插槽、注册命令
host.ts 唯一感知 OpenCode v2 API 的适配层(事件 / 主题 / 存储 / 提示)
commands.tsx /ttft、/ttft-color 与 /ttft-stat
settings.ts 显示模式、染色开关、阈值(环境变量)
meter/
types.ts 事件与视图类型
meter.ts 首字状态机(纯函数,注入时钟)
format.ts 定宽时长格式化(5 列不抖动)
ui/
color.ts 主题归一化 + 阈值色带
meter.tsx 一行读数组件
tests/ 状态机、格式化、色带、设置、命令、适配层、安装器设计取舍见 docs/superpowers/specs/2026-10-31-model-output-metrics-design.md。
11. 兼容性
代码完全模型无关:只要宿主按 OpenCode 事件协议上报 session.execution.started 与输出片段,任何模型都能量出首字延迟。
12. License
MIT
