别再花钱重建你的 Claude Code 缓存了。当你的主智能体等待子智能体超过 5 分钟时,它的提示词缓存会悄然过期,下一轮对话就会以写入费率重新编码你的整个对话,而不是以低价读回。在包含许多子智能体的长会话中,这大约占你账单的 20%。claude-thermos 能让缓存保持温热,让你永远不用付这笔冤枉钱。
使用方法
像平常一样运行 Claude Code,但通过 uvx 使用 claude-thermos:
uvx claude-thermos # instead of: claude
uvx claude-thermos -p "fix the bug" # any claude args pass straight through
需要 Python 3.11+ 以及 PATH 中的 claude CLI。
就这么简单。预热会在后台自动运行。如果要在某次运行中禁用它而不改变命令,请设置 CLAUDE_WARMER_DISABLE=1。
调优(全部可选):
| 标志 | 默认值 | 含义 |
|---|---|---|
| --idle | 270 | 主智能体在预热启动前必须空闲的秒数 |
| --interval | 270 | 预热周期之间的秒数 |
| --max-cycles | 4 | 每次空闲时段的最大预热次数(设为 auto 表示无限) |
| --subagent-window | 540 | 子智能体被视为“仍处于活动状态”的秒数 |
为什么你的缓存会不断过期
Claude Code 的提示词缓存使用 5 分钟 TTL。只要缓存保持存活,每一轮对话的整个历史记录都会以输入价格 0.1 倍的费率从缓存中读取,而不是以全价重新发送。
如果同一前缀的请求之间间隔超过 5 分钟,缓存就会过期。造成这种间隔的主要触发因素不是你在思考,而是主智能体被运行超过 5 分钟的子智能体阻塞。子智能体有不同的系统提示词和工具集,因此它的请求有不同的缓存前缀,永远不会刷新主智能体的缓存。在子智能体工作时,主智能体的缓存历史记录会原封不动地老化;超过 5 分钟就消失了。当子智能体返回时,主智能体以字节相同、仅追加的历史记录恢复运行,却发现缓存丢失,被迫以 1.25 倍的写入费率进行完整重新编码。
到那时历史记录已经很大,所以重新编码代价高昂:单次崩溃会重写 200K 到 500K 个 token。根据大约 185 次本地会话的测量,这些重建约占账单总额的 22%,这些钱花在了重新编码片刻之前就已缓存的内容上。
工作原理
claude-thermos 在本地小型反向代理后面启动 Claude Code(它将 ANTHROPIC_BASE_URL 指向一个回环端口;所有流量仍然流向真实的 Anthropic API)。
- 观察。代理监控 /v1/messages 流量,并将其分组为会话和谱系(lineage),一个谱系即一个缓存前缀,以模型 + 工具集 + 系统文本为键。第一个携带工具的谱系是主智能体;其余的是子智能体。
- 检测危险窗口。当主谱系进入空闲状态而子智能体正在运行时,主前缀就有过期风险。
- 预热。在 5 分钟 TTL 之内的一个时间间隔上,它重放主智能体最后一次真实请求作为预热请求:相同的可缓存前缀,但 max_tokens 设为 1 且不启用流式传输。这单个 token 会被丢弃;关键在于预填充(prefill),它会读取并刷新完整的缓存前缀。预热请求直接发送到 API,绝不经过代理,因此不会干扰真实流量。
- 结果。当子智能体完成时,主智能体的缓存仍然是热的。它只需支付一次廉价的读取,而不是一次完整的重写。
每次预热花费一次缓存读取(0.1 倍);它每次阻止的重写原本会在大得多的前缀上花费一次写入(1.25 倍),所以这笔交易对你极为有利。
事件日志与节省
每个会话都会写入:
~/.claude-thermos/logs/<session_id>/
├── events.jsonl # append-only structured event stream
└── summary.json # rollup totals, written when the session ends
events.jsonl 记录每个请求/响应的 token 用量以及每一个预热决策(warm_fired、warm_result、cap_reached、resume_detected 等)。summary.json 是你通常会读取的汇总文件:
| 字段 | 含义 |
|---|---|
| warms_fired | 已发送的预热请求数 |
| cache_read_total | 这些预热请求读回的 token 数 |
| episodes | 以成功恢复(即实际避免了重写)告终的空闲-带-子智能体事件数 |
| rewrite_avoided_tokens | 原本会被重写的 token 数,跨所有事件求和 |
| warm_cost | 预热花费了你多少:0.1 × cache_read_total |
| rewrite_avoided_cost | 它节省了多少:1.25 × rewrite_avoided_tokens |
| net_savings | rewrite_avoided_cost − warm_cost |
这三个成本数字均以基础输入 token 单位计(token 数已按其缓存倍数加权)。要把 net_savings 换算成美元,乘以你所用模型每输入 token 的价格即可。
dollars saved ≈ net_savings × (input token price)
例如,在输入价格为 $3 / 1M tokens 的情况下,net_savings 为 1_200_000 时,大约相当于 1_200_000 × $3 / 1_000_000 = 该会话节省了 $3.60。
Stop paying to rebuild your Claude Code cache. When your main agent waits on a subagent for more than 5 minutes, its prompt cache silently expires, and the next turn re-encodes your entire conversation at the write rate instead of reading it back cheap. On long sessions with many subagents that's roughly 20% of your bill. claude-thermos keeps the cache warm so you never pay that tax.
Use
Run Claude Code exactly as you normally would, but through claude-thermos with uvx:
uvx claude-thermos # instead of: claude
uvx claude-thermos -p "fix the bug" # any claude args pass straight through
Requires Python 3.11+ and the claude CLI on your PATH.
That's it. Warming runs automatically in the background. To disable it for a run without changing the command, set CLAUDE_WARMER_DISABLE=1.
Tuning (all optional):
| Flag | Default | Meaning |
|---|---|---|
--idle | 270 | Seconds the main agent must be idle before warming kicks in |
--interval | 270 | Seconds between warming cycles |
--max-cycles | 4 | Max warms per idle episode (auto for unlimited) |
--subagent-window | 540 | Seconds a subagent counts as "still active" |
Why your cache keeps expiring
Claude Code's prompt cache uses a 5-minute TTL. Every turn, your whole conversation history is served from cache at 0.1x the input price instead of being re-sent at full price, as long as the cache stays alive.
The cache expires if more than 5 minutes pass between requests on the same prefix. The dominant trigger for that gap is not you thinking. It's the main agent blocked on a subagent that runs longer than 5 minutes. A subagent has a different system prompt and tool set, so its requests have a different cache prefix and never refresh the main agent's. While the subagent works, the main agent's cached history ages untouched; past 5 minutes it's gone. When the subagent returns, the main agent resumes with a byte-identical, append-only history, and finds its cache missing, forcing a full re-encode at the 1.25x write rate.
By then the history is large, so the re-encode is expensive: individual collapses re-write 200K to 500K tokens. Measured across roughly 185 local sessions, these rebuilds accounted for about 22% of the total bill, money spent re-encoding content that was already cached moments earlier.
How it works
claude-thermos launches Claude Code behind a small local reverse proxy (it points ANTHROPIC_BASE_URL at a loopback port; all traffic still goes to the real Anthropic API).
- Observe. The proxy watches
/v1/messagestraffic and groups it into sessions and lineages, a lineage being one cache prefix, keyed by model + tool set + system text. The first tool-bearing lineage is the main agent; the rest are subagents. - Detect the danger window. When the main lineage goes idle and a subagent is actively running, the main prefix is at risk of expiring.
- Warm. On an interval under the 5-minute TTL, it replays the main agent's last real request as a warm request: identical cacheable prefix, but
max_tokens: 1and no streaming. The single token is thrown away; the point is the prefill, which reads and refreshes the full cached prefix. Warm requests go directly to the API, never through the proxy, so they can't disturb real traffic. - Result. When the subagent finishes, the main agent's cache is still warm. It pays a cheap read instead of a full rewrite.
Each warm costs a cache read (0.1x); each rewrite it prevents would have cost a write (1.25x) on a much larger prefix, so the trade is heavily in your favor.
Event logs & savings
Every session writes to:
~/.claude-thermos/logs/<session_id>/
├── events.jsonl # append-only structured event stream
└── summary.json # rollup totals, written when the session ends
events.jsonl records each request/response's token usage plus every warming decision (warm_fired, warm_result, cap_reached, resume_detected, and so on). summary.json is the rollup you'll usually read:
| Field | Meaning |
|---|---|
warms_fired | Warm requests sent |
cache_read_total | Tokens read back by those warms |
episodes | Idle-with-subagent episodes that ended in a successful resume (a rewrite actually avoided) |
rewrite_avoided_tokens | Tokens that would have been re-written, summed across episodes |
warm_cost | What warming cost you: 0.1 × cache_read_total |
rewrite_avoided_cost | What it saved: 1.25 × rewrite_avoided_tokens |
net_savings | rewrite_avoided_cost − warm_cost |
All three cost figures are in base-input-token units (token counts already weighted by their cache multiplier). To turn net_savings into dollars, multiply it by your model's price per input token:
dollars saved ≈ net_savings × (input token price)
For example, at an input price of $3 / 1M tokens, a net_savings of 1_200_000 is about 1_200_000 × $3 / 1_000_000 = $3.60 saved that session.