TUI footer shows the session cache hit rate
TUI 页脚展示会话缓存命中率
The footer summed the session's token usage as `↑<input> ↓<output>`, where `↑` is the uncached input reported by the model. `TokenUsage` counts are disjoint: billed prompt tokens are `inputTokens` (uncached) plus `cacheReadTokens` and `cacheWriteTokens`. With only the uncached number visible, a user could not tell how much of each turn's prompt the provider cache served — the signal that most directly reflects whethe
English
Problem
The footer summed the session's token usage as ↑<input> ↓<output>, where ↑ is the uncached input reported by the model. TokenUsage counts are disjoint: billed prompt tokens are inputTokens (uncached) plus cacheReadTokens and cacheWriteTokens. With only the uncached number visible, a user could not tell how much of each turn's prompt the provider cache served — the signal that most directly reflects whether the reused request prefix is paying off. On a long session dominated by cache reads the ↑ figure stays small and hides that the prompt is large but cheap.
Decision
The footer appends cache <rate>% after ↑<input> ↓<output>, where the rate is the share of billed prompt tokens served from the provider cache.
TokenTotalsaccumulates the four disjoint buckets (input,output,cacheRead,cacheWrite).addUsagefolds one call'sTokenUsageinto the totals, treating a missingcacheReadTokens/cacheWriteTokensas zero.cacheHitRate(totals)isround(cacheRead / (input + cacheRead + cacheWrite) * 100), andundefinedbefore any input is billed.FooterComponentomits the wholecache N%segment while the rate isundefined, so an empty session shows no meaningless zero.↑keeps meaning uncached input, not billed input: the disjoint-bucket convention holds across the footer, and the cache percent supplies the reuse signal the raw counts cannot.- Totals are rebuilt on mount by
sessionTokens, which sums usage overassistant/messageevents (neverassistant/chunk, to avoid double counting), and updated live from eachassistant/messageevent that carries usage.
Alternatives considered
Show billed input (input + cacheRead + cacheWrite) as ↑ instead of a separate percent. Rejected: it would redefine ↑ away from the disjoint inputTokens bucket the rest of the harness reports, and it would still hide the reuse share the user actually wants; a derived percent adds the signal without overloading the count.
Compute the rate against all tokens (input + output + cache). Rejected: output tokens are never cache-served, so folding them into the denominator understates the rate for no meaning; cache hit rate is a property of the prompt.
Drop cacheWrite from the denominator. Rejected: cache writes are billed input the provider spent to populate the cache, so excluding them overstates the hit rate on a writing turn. DeepSeek reports no cache-write metric today, but the formula stays general and the write path is covered.
Render cache 0% on an empty session. Rejected: the billed input is 0, the ratio is 0/0, and a 0% badge on a fresh session is a lie about a value that does not exist yet; the segment stays hidden until input is billed.
Give the metric its own right-aligned footer element beside tools:. Rejected: it derives from the adjacent token counts and reads best in the input → output → cache order; grouping it left also keeps the lower-priority tools: indicator as the element that clips first under width pressure, matching the footer's existing layout priority.
Consequences
- The left group grew by
cache N%, so on a narrow footer the right-sidetools:state clips sooner. This follows the footer's pre-existing left-priority truncation and is an accepted trade-off. - The metric is best-effort live UI state derived from
assistant/messageusage: rebuilt from the session on mount, updated live, and never persisted. packages/ui/tui/src/index.tsstays at 100 % per-file coverage.- The
examples/tui-agentterminal snapshots carry the segment: a turn with cache reads renders e.g.cache 49%, and a first cold turn renderscache 0%.
Testing
packages/ui/tui/tests/tui.spec.ts drives the footer through the real createTuiChat: an empty session renders ↑0 ↓0 with no cache segment (the hidden path), a cold turn (inputTokens only) renders cache 0%, and a live warm turn carrying cacheReadTokens and cacheWriteTokens updates it to cache 60% while no longer showing cache 0%. The examples/tui-agent snapshot suite replays green against the recorded expected output.
中文
Problem
页脚原本把会话的 token 用量汇总为 ↑<input> ↓<output>,其中 ↑ 是模型上报的未缓存输入。TokenUsage 的各项计数互不重叠:计费的输入 token 由 inputTokens(未缓存)加上 cacheReadTokens 与 cacheWriteTokens 构成。只暴露未缓存的那个数字,用户就无从判断每轮提示词有多少由提供方缓存承接——而这恰是最能反映复用的请求前缀是否奏效的信号。在以缓存读取为主的长会话里,↑ 始终很小,掩盖了提示词其实很大但很便宜的事实。
Decision
页脚在 ↑<input> ↓<output> 之后追加 cache <rate>%,该比率是计费输入 token 中由提供方缓存承接的占比。
TokenTotals累加四个互不重叠的桶(input、output、cacheRead、cacheWrite)。addUsage把单次调用的TokenUsage折入总量,缺失的cacheReadTokens/cacheWriteTokens视为零。cacheHitRate(totals)为round(cacheRead / (input + cacheRead + cacheWrite) * 100),在尚无输入计费前返回undefined。比率为undefined时FooterComponent整段略去cache N%,因此空会话不会显示无意义的零。↑仍表示未缓存输入,而非计费输入:页脚全程遵守互不重叠的桶约定,缺失的复用信号由缓存百分比补足。- 挂载时由
sessionTokens重建总量,它对带 usage 的assistant/message事件求和(绝不用assistant/chunk,以免重复计数);此后每条携带 usage 的assistant/message事件都会实时更新。
Alternatives considered
把计费输入(input + cacheRead + cacheWrite)作为 ↑,不单列百分比。 否决:这会让 ↑ 偏离 harness 其余部分上报的互不重叠 inputTokens 桶,且仍旧藏住用户真正想要的复用占比;派生一个百分比既补上信号,又不给计数加载额外含义。
用全部 token(input + output + cache)作分母计算比率。 否决:输出 token 从不由缓存承接,把它折进分母只会无意义地拉低比率;缓存命中率是提示词的属性。
从分母里去掉 cacheWrite。 否决:缓存写入是提供方为填充缓存而付费的计费输入,剔除它会在写入的那一轮高估命中率。DeepSeek 目前不上报缓存写入指标,但公式保持通用,写入路径也有覆盖。
在空会话上渲染 cache 0%。 否决:此时计费输入为 0,比值是 0/0,在全新会话上打出 0% 是对一个尚不存在的值撒谎;在输入计费之前该段一直隐藏。
给该指标单独一个右对齐的页脚元素,紧挨 tools:。 否决:它派生自相邻的 token 计数,按 input → output → cache 的顺序阅读最顺;左置分组还让优先级更低的 tools: 指示成为宽度紧张时最先被裁剪的元素,与页脚既有的布局优先级一致。
Consequences
- 左段增加了
cache N%,因此窄终端上右侧的tools:状态更早被裁剪。这沿用页脚既有的左段优先裁剪策略,是可接受的取舍。 - 该指标是从
assistant/message的 usage 派生的尽力而为实时 UI 状态:挂载时从会话重建、随后实时更新、从不持久化。 packages/ui/tui/src/index.ts保持 100% 单文件覆盖率。examples/tui-agent终端快照带有该段:有缓存读取的一轮渲染为如cache 49%,首个冷启动轮次渲染为cache 0%。
Testing
packages/ui/tui/tests/tui.spec.ts 通过真实的 createTuiChat 驱动页脚:空会话渲染 ↑0 ↓0 且无缓存段(隐藏路径),冷启动一轮(仅 inputTokens)渲染 cache 0%,随后实时的热轮次携带 cacheReadTokens 与 cacheWriteTokens,把它更新为 cache 60% 且不再显示 cache 0%。examples/tui-agent 快照套件对已录制的预期输出回放通过。