DSH / Atlas
2026-07-21implementedfeaturearchived 2026-07-26

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.

  • TokenTotals accumulates the four disjoint buckets (input, output, cacheRead, cacheWrite). addUsage folds one call's TokenUsage into the totals, treating a missing cacheReadTokens/cacheWriteTokens as zero.
  • cacheHitRate(totals) is round(cacheRead / (input + cacheRead + cacheWrite) * 100), and undefined before any input is billed. FooterComponent omits the whole cache N% segment while the rate is undefined, 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 over assistant/message events (never assistant/chunk, to avoid double counting), and updated live from each assistant/message event 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-side tools: 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/message usage: rebuilt from the session on mount, updated live, and never persisted.
  • packages/ui/tui/src/index.ts stays at 100 % per-file coverage.
  • The examples/tui-agent terminal snapshots carry the segment: a turn with cache reads renders e.g. cache 49%, and a first cold turn renders cache 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(未缓存)加上 cacheReadTokenscacheWriteTokens 构成。只暴露未缓存的那个数字,用户就无从判断每轮提示词有多少由提供方缓存承接——而这恰是最能反映复用的请求前缀是否奏效的信号。在以缓存读取为主的长会话里, 始终很小,掩盖了提示词其实很大但很便宜的事实。

Decision

页脚在 ↑<input> ↓<output> 之后追加 cache <rate>%,该比率是计费输入 token 中由提供方缓存承接的占比。

  • TokenTotals 累加四个互不重叠的桶(inputoutputcacheReadcacheWrite)。addUsage 把单次调用的 TokenUsage 折入总量,缺失的 cacheReadTokens/cacheWriteTokens 视为零。
  • cacheHitRate(totals)round(cacheRead / (input + cacheRead + cacheWrite) * 100),在尚无输入计费前返回 undefined。比率为 undefinedFooterComponent 整段略去 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%,随后实时的热轮次携带 cacheReadTokenscacheWriteTokens,把它更新为 cache 60% 且不再显示 cache 0%examples/tui-agent 快照套件对已录制的预期输出回放通过。