# Oh My TPS

[English](./README.md) | 简体中文

## 安装

### npm package

```bash
pi install npm:oh-my-tps
```

### Git repository

```bash
pi install git:github.com/EnderLiquid/oh-my-tps
```

## 功能速览

`oh-my-tps` 只做一件事：
给 Pi TUI 加一组实时速度读数，测量 LLM 首字延迟和输出速度。

- `τ`：TTFT，首个 token 到达前等了多久，单位秒
- `Δ`：TPS，每秒输出多少 token

显示效果：

```
τ0.8 Δ48.6
```

就这么多。

十个字符的空间，开箱即用的体验。

感兴趣可以继续往下看，但到这里你其实已经会用了。

## 读数详解

你会在 TUI 底部状态区域看到这样的读数：

```text
τ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。

## 原理说明

下面这部分面向希望了解插件原理的用户。

### 状态机

内部分为四个阶段：

1. **等待阶段**：请求已发出，等待首个 token
2. **流式输出阶段**：首个 token 已到达，响应正在流式输出
3. **最终阶段**：响应结束，已完成本轮 TPS 的结算尝试
4. **空闲状态**：当前没有正在处理的服务提供方请求

示例：

```text
空闲 τ… Δ? (还没有历史样本)
    -> 等待 τ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_delta`
- `thinking_delta`
- `toolcall_delta`

以下事件不会触发 TTFT：

- `text_start`
- `thinking_start`
- `toolcall_start`
- 空增量以及其他元数据事件

收到首个有效增量后，插件把它与请求开始时间的差值锁定为本轮 TTFT。

因此：

- 等待阶段的 `τ` 会一直增加；
- 首个正文、思考或工具调用 token 到达后，进入流式输出阶段，`τ` 就锁定；
- `thinking_start` 等元数据不会结束等待，但非空 `thinking_delta` 会结束等待；
- 如果服务提供方把思考内容加密并且不传输思考增量，插件只能测量首个可观察 token 的 TTFT，无法获得隐藏思考阶段内部的时间轴。

TTFT 样本与 TPS 样本独立维护。只要首个 token 到达，本轮就可以贡献 TTFT 平均值，即使本轮最终没有有效 TPS。

### 实时 `Δ` 的来源

服务提供方不会持续告诉 Pi “刚刚又生成了多少个 token”，所以实时 TPS 只能在本地估算。当前实现使用非思考增量的滚动队列：

- 纳入非空的 `text_delta`；
- 纳入非空的 `toolcall_delta`；
- 排除所有 `thinking_delta`，包括思考摘要。

默认窗口是最近 5 秒。每当新的非思考增量到达时，插件会：

1. 将原始增量和到达时间加入队列；
2. 移除窗口之外的旧增量；
3. 按到达顺序拼接窗口内的增量；
4. 使用 [`tokenx`](https://github.com/johannschopplich/tokenx) 估算拼接内容的 token 数；
5. 用窗口内 token 数除以观察时长，得到新的实时 TPS。

公式可以表示为：

```text
实时 TPS = 最近窗口内的非思考增量估算 token
           /
           min(5秒，首个非思考增量后的观察时长)
```

从首个非思考增量到第一次计算实时 TPS，至少需要观察 2 秒。这个等待时间用于避免流式刚开始时分母过小，或者服务提供方一次性发送初始积压内容而产生异常高值。

实时 TPS 是最近窗口内正文和工具参数的传输速率，只在新的非思考增量到达时重算，不使用后台定时器。

### 最终 `Δ` 的来源

响应结束时，插件按以下顺序结算 TPS。

#### 1. 服务提供方返回的 `usage.output`

当服务提供方返回有限正数 `usage.output`，并且从首个非空的正文、思考或工具调用增量到响应结束至少经过 2 秒时，使用：

```text
最终 TPS = usage.output
           /
           (响应结束时间 - 首个有效内容增量时间)
```

`usage.output` 通常是服务提供方报告的真实输出 token 数，可能包含推理 token。因此，这个来源的分子可能包含思考 token，分母则从首个可观察的内容增量开始。

对于加密思考模型，隐藏思考可能在首个可观察增量之前已经发生。客户端无法知晓这段隐藏推理时长，因此 `usage.output` 来源的最终 TPS 可能略高于真实值。

#### 2. 实时 TPS 回退值

只有当服务提供方没有可用的 `usage.output` 时，才使用本轮最后一次有效的实时 TPS：

```text
最终 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
