메뉴
HN
Hacker News 5일 전

클로드 캐시 만료 방지 도구 'Claude-thermos'

IMP
8/10
핵심 요약

클로드 코드(Claude Code)를 사용할 때 하위 에이전트 작업 등으로 5분 이상 대기하면 메인 에이전트의 프롬프트 캐시가 만료되어 막대한 재인코딩 비용이 발생합니다. Claude-thermos는 로컬 프록시를 통해 백그라운드에서 캐시를 주기적으로 따뜻하게 유지(warming)하여, 전체 요금의 약 22%에 달하는 불필요한 비용 지출을 방지해 줍니다. 개발자는 기존 CLI 명령어 대신 uvx claude-thermos를 실행하는 것만으로 쉽게 적용할 수 있습니다.

번역된 본문

claude-thermos: 더 이상 클로드 코드 캐시를 재구성하느라 돈을 낭비하지 마세요.

메인 에이전트가 하위 에이전트(subagent)를 기다리느라 5분 이상 대기하게 되면, 프롬프트 캐시가 조용히 만료됩니다. 그리고 다음 차례에서는 저렴한 읽기 요금으로 불러올 수 있었던 전체 대화 내용을 비싼 쓰기(write) 요금으로 다시 인코딩하게 됩니다. 여러 하위 에이전트를 사용하는 긴 세션의 경우, 이 과정에서 전체 요금의 약 20%가 낭비됩니다. claude-thermos가 캐시를 따뜻하게 유지해 이 불필요한 비용(세금)을 지불하지 않도록 도와줍니다.

사용법은 평소처럼 클로드 코드를 실행하면 되지만, claude 대신 uvx로 claude-thermos를 실행하기만 하면 됩니다:

uvx claude-thermos

대신 사용: claude

uvx claude-thermos -p "버그 수정"

클로드의 모든 인자(arguments)를 그대로 전달할 수 있습니다.

Python 3.11 이상과 PATH에 설정된 claude CLI가 필요합니다. 준비물은 이게 전부입니다. 캐시 워밍 작업은 백그라운드에서 자동으로 실행됩니다. 명령어를 바꾸지 않고 특정 실행에만 워밍을 비활성화하려면 CLAUDE_WARMER_DISABLE=1 환경변수를 설정하면 됩니다.

세부 조정 (모두 선택 사항): 플래그 | 기본값 | 의미 --idle | 270 | 워밍이 시작되기 전까지 메인 에이전트가 유휴 상태로 대기해야 하는 시간(초) --interval | 270 | 워밍 사이클 간격(초) --max-cycles | 4 | 유휴 상태당 최대 워밍 횟수 (무제한은 auto) --subagent-window | 540 | 하위 에이전트가 '여전히 활성 상태'로 간주되는 시간(초)

왜 캐시가 계속 만료될까요? 클로드 코드의 프롬프트 캐시는 5분의 TTL(Time-To-Live)을 사용합니다. 캐시가 살아있는 한 매 차례마다 전체 대화 기록을 전체 요금으로 다시 보내는 대신, 입력 가격의 0.1배(0.1x)에 해당하는 저렴한 캐시 가격으로 불러올 수 있습니다. 동일한 접두사(prefix)를 가진 요청 사이에 5분 이상이 지나면 캐시가 만료됩니다. 이 5분의 공백을 유발하는 주된 이유는 사용자가 생각하는 시간이 아닙니다. 바로 메인 에이전트가 5분 이상 실행되는 하위 에이전트 때문에 차단되어 있기 때문입니다.

하위 에이전트는 다른 시스템 프롬프트와 도구 세트를 가지고 있으므로, 해당 요청은 다른 캐시 접두사를 사용하며 메인 에이전트의 캐시를 절대 새로고침하지 않습니다. 하위 에이전트가 작동하는 동안 메인 에이전트의 캐시된 기록은 아무도 건드리지 않은 채로 오래되고, 5분이 지나면 사라집니다. 하위 에이전트가 반환되면 메인 에이전트는 바이트가 완전히 동일한 추가 전용(append-only) 기록으로 작업을 재개하지만 캐시가 사라진 것을 발견하게 됩니다. 결국 1.25배의 높은 쓰기 요금으로 전체를 다시 인코딩해야 합니다.

이 시점이 되면 기록의 크기가 매우 크기 때문에 재인코딩 비용이 비쌉니다. 개별적으로 무너진 캐시를 복구할 때마다 20만~50만 개의 토큰을 다시 작성해야 합니다. 약 185개의 로컬 세션에서 측정한 결과, 이러한 재구성 작업이 전체 요금의 약 22%를 차지했으며, 이는 불과 몇 순간 전까지만 해도 캐시되어 있던 콘텐츠를 다시 인코딩하는 데 낭비된 돈입니다.

작동 원리 claude-thermos는 작은 로컬 역방향 프록시(reverse proxy) 뒤에서 클로드 코드를 실행합니다(ANTHROPIC_BASE_URL을 로컬 루프백 포트로 지정하지만, 모든 트래픽은 여전히 실제 Anthropic API로 전달됩니다).

관찰 (Observe). 프록시는 /v1/messages 트래픽을 감시하여 이를 세션과 계통(lineage)으로 그룹화합니다. 계통은 하나의 캐시 접두사를 의미하며 모델 + 도구 세트 + 시스템 텍스트를 키로 사용합니다. 도구를 포함하는 첫 번째 계통이 메인 에이전트이고 나머지는 하위 에이전트입니다.

위험 구간 감지 (Detect the danger window). 메인 계통이 유휴 상태이고 하위 에이전트가 활발하게 실행 중일 때, 메인 접두사은 만료될 위험이 있습니다.

워밍 (Warm). 5분의 TTL 이내의 간격으로 메인 에이전트의 마지막 실제 요청을 워밍 요청으로 재생합니다. 이때 캐시 가능한 접두사은 동일하지만, max_tokens를 1로 설정하고 스트리밍을 끕니다. 반환된 단일 토큰은 버려집니다. 핵심은 전체 캐시된 접두사을 읽고 새로고침하는 사전 채우기(prefill)에 있습니다. 워밍 요청은 실제 트래픽을 방해하지 않도록 프록시를 거치지 않고 API로 직접 전송됩니다.

결과 (Result). 하위 에이전트가 작업을 마치면 메인 에이전트의 캐시가 여전히 따뜻하게 유지됩니다. 전체를 다시 작성하는 대신 저렴한 읽기 비용만 지불하면 됩니다. 각 워밍은 캐시 읽기 비용(0.1배)만 발생시킵니다. 반면 이것이 막아주는 재작성은 훨씬 더 큰 접두사에 대해 쓰기 비용(1.25배)을 발생시켰을 것이므로, 이 교환은 사용자에게 압도적으로 유리합니다.

이벤트 로그 및 비용 절감 모든 세션은 다음 경로에 기록됩니다: ~/.claude-thermos/logs// ├── events.jsonl # 추가 전용 구조화된 이벤트 스트림 └── summary.json # 롤업(rollup) 총계, 세션이 끝날 때 작성됨

events.jsonl은 각 요청/응답의 토큰 사용량과 모든 워밍 결정을 기록합니다 (warm_fired, warm_result, cap_reached, resume_detected 등). summary.json은 주로 사용자가 읽게 되는 롤업 파일입니다: 필드 | 의미 warms_fired | 전송된 워밍 요청 수 cache_read_total | 해당 워밍으로 다시 읽어온 토큰 수 episodes | 하위 에이전트를 동반한 유휴 상태 에피소드 수

원문 보기
원문 보기 (영어)
claude-thermos 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/messages traffic 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: 1 and 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.