Caching components¶
Two components touch the provider's prompt cache: cachesplit (in every caching preset) and
cacheinject (in none). Both are Reformat components whose real work happens body-level in
apply, not in the pipeline's per-message rewrite.
OpenAI's implicit cache¶
OpenAI requests are cache-aware in cache_mode: auto even without cache_control:
the provider caches eligible prefixes implicitly. Context-guru protects the
previous turn's prefix while rewriting a new tail. It does not add
prompt_cache_options.ttl to requests. For GPT-5.6 and later, OpenAI's
documented default is 30m after the latest write or reuse; that is a
minimum, not proof that the entry expires at 30 minutes. The
entry is classified warm throughout that guarantee: its actual expiry
cannot be inferred, so pre_expiry is not claimed. Past 30 minutes the cache
phase is unknown, not cold. Earlier
OpenAI models have different retention rules, so their phase remains unknown
unless a reliable lifetime can be established. GPT-5.6+ Responses sessions
can opt into a separate 28-minute keep-alive schedule; earlier OpenAI models
are not pinged.
See the OpenAI prompt-caching guide for the model-specific retention rules.
Split is measured, placement isn't¶
Claude Code appends a live environment snapshot to the end of its main system block:
Measured across 50 SWE-bench tasks that block is ~7,017 tokens, of which the first 6,921 (98.4%) are byte-identical across sessions — but a cache entry hashes everything before its breakpoint, and no breakpoint position can exclude part of a single content block. So the churning tail is one cacheable unit with its breakpoint at the end, and the shared 98.4% is re-written every session regardless of where a breakpoint is placed inside it.
The tail is real content: it cannot be moved or dropped without lying to the model about the
repo state. It can be split — [stable][volatile] as two text blocks with the same
concatenated text, breakpoint on the first. Adjacent text blocks concatenate, so the model sees
a byte-identical prompt while the provider gains a hash boundary that excludes the churn. This is
what cachesplit enables, and it is measured: −34.1% mean cost on one Terminal-Bench task
measured three times, and 0% → 96.7% cache hit in an isolated live A/B.
Breakpoint placement (cacheinject) is a different question — given a cache boundary,
where should the (up to 4) breakpoints go? — and it has never been shown to help: the one live
measurement is n=1 and mildly negative, with no mechanism established. cachesplit and
cacheinject are separate config entries because their evidence is not comparable, and keeping
them apart is what stops disabling placement from silently disabling the split too. Every preset
that touches caching carries cachesplit, never cacheinject.
Explicit-breakpoint providers only (Anthropic, Bedrock, Vertex). Under an implicit longest-prefix cache (OpenAI, Gemini) the match already ends at the divergence, so neither component buys anything there.
cachesplit¶
Reformat — lossless, and a marker component
Enables the volatile-tail split: the top-level system array's churning tail is
separated from its stable head so the provider's cache boundary excludes the churn.
The model sees byte-identical text.
cachesplit is in the default presets. It carries no logic of its own: the component's
Reformat always reports Skipped, and the actual rewrite is body-level — it edits the
top-level system array, which components never see — and lives in apply/prefixsplit.go,
gated on this name being present in the pipeline. apply still honours cacheinject as a gate
for the split, so an existing pipeline naming only cacheinject keeps working.
What the split does¶
[stable][volatile] as two text blocks with the same concatenated text, breakpoint on the first.
Adjacent text blocks concatenate, so the model sees a byte-identical prompt while the provider
gains a hash boundary that excludes the churn. Asserted by TestSplitIsConcatenationIdentical.
What it is worth¶
The full four-way measurement (structural target, mechanical verification, isolated live A/B,
end-to-end agent run) is under What the split is worth below,
because that is where the work was done. The headline: −34.1% mean cost on one Terminal-Bench
task measured three times, and $0.0205 saved per warm session on Sonnet 5 in the isolated A/B.
A second Terminal-Bench task was within noise and its numbers are not quoted.
Configuration¶
None. It is a name in the pipeline:
A components.cachesplit: block is an error, not a no-op. The constructor used to take
its raw block and discard it, so a document saying cachesplit: {ttl: 1h} looked configured
and was not; it now decodes strictly like every other component and refuses the block.
When it shines¶
Anthropic-family agents whose system prompt carries a volatile tail (an environment snapshot, a timestamp, a git status) in front of a cache breakpoint.
When it's inert¶
Explicit-breakpoint providers only (Anthropic, Bedrock, Vertex). Under an implicit longest-prefix cache (OpenAI, Gemini) the match already ends at the divergence, so a block boundary buys nothing. It is also inert when the system block has no separable volatile tail.
Because the component itself always skips, /stats lists it under top_passthrough — that
is expected, not dead weight. The split's saving is a provider-side cache effect, invisible
to content-token counts.
cacheinject¶
Reformat — lossless. In no preset: opt in explicitly.
Places Anthropic cache_control breakpoints at the positions that minimise billed input
cost, so the provider KV cache is read rather than re-processed. Placement has never been
shown to help, so no shipped preset enables it — the presets carry cachesplit,
which enables the measured volatile-tail split instead. See Configuration.
How it works¶
Placement is the solution to a cost minimisation, not a heuristic. With R = 0.1 (cache
read), W = 1.25 (5m cache write) and 1.0 (plain input) as multipliers on the base input
price, four facts fix the policy:
- Write anything resent even once. A token resent
kmore times costsW + kRwritten and1 + kunwritten, so writing wins fork > (W−1)/(1−R) = 0.28. Hence a breakpoint on the last block, every turn. - Writes bill as a span,
(highest breakpoint − read point), not per breakpoint. An extra breakpoint below the top costs exactly zero and adds a position a later turn's backward read-walk can land on, so none of the four slots is left idle. - Divergence is the large lever. If a turn differs from the previous one at block
d, every block abovedis unmatchable and the whole prefix bills at 1.0x instead of 0.1x. A breakpoint atd−1recovers it — worth 12.5x more than any positional tuning, because it restores read→read rather than read→write. - Anchors must be turn-stable. A position is readable on turn
t+1only if it was written on turnt, so anchors are counted up from the start, never back from the end.
before: [ tools ][ system ]…[ msg d−1 ][ mutated msg d ]…[ newest turn ]
after: [ tools ][ system ]…[ msg d−1 {cache_control} ]…[ newest turn {cache_control} ]
^ rescues the stable head ^ writes this turn's growth
The component tracks a per-message digest per session to find d, and keeps every
breakpoint the caller already set — an agent that places its own is usually already at the
optimum, and the provider's 4-breakpoint cap means a fifth is a 400.
5m TTL by default. A 1h write costs 2.0x instead of 1.25x and only pays when the chance of
an entry lapsing before its next reuse exceeds 65.2%, i.e. (2.0 − 1.25) / (1.25 − 0.1).
That denominator used to read (1 − 0.1), giving 83.3%, and it was wrong by 18 points: it
assumed a lapsed entry re-bills at the 1.0x FRESH rate. Measured, it re-bills as a 1.25x
cache-CREATION — those tokens still carry cache_control, so the provider writes a new entry
rather than charging fresh input. Production ttl_expiry rows say so directly: cache_write
averages 178,793 against fresh_input 1,712. The conclusion does not move — agent turns are
seconds apart (measured median 7.6 s over 1,905 real turns) and every read refreshes the TTL
for free — so set ttl: 1h only when reuse is genuinely sparse: a low-concurrency sweep with
task starts more than 5 minutes apart, or a deployed agent handling a few sessions per hour.
On the IBM gateway that decision is currently moot for most models: measured live, the 1h tier
is granted on aws/claude-haiku-4-5 (36,251 of 36,574 written tokens) and silently downgraded
to 5 minutes on aws/claude-sonnet-5 (0 of 48,212). See apply/headttl.go.
What placement is actually worth¶
Effectively unmeasured, and the one live reading is mildly negative — which is why it ships
off. One cacheonly vs off pair on SWE-bench Verified (aws/claude-sonnet-5, n=1 per arm,
one task completed in both arms):
| metric | off | cacheonly | delta |
|---|---|---|---|
| steps | 14 | 11 | −21.4% |
| billed cost | $0.17607 | $0.14931 | −15.2% |
| cache-read | 609,968 | 461,844 | −24.3% |
| cache-write | 8,695 | 11,062 | +27.2% |
| cache-hit | 98.59% | 96.66% | −1.93 pp |
The −15.2% is not a saving: the agent took 3 fewer steps, and on this traffic cost tracks steps at corr 0.95. Per step, cost is +7.9% and cache-write +61.9%. Read that as one task, once, with a degenerate control — and as a reason to leave placement off, not as a measurement of the policy.
Against an agent that already caches well, expect exactly 0%. Claude Code marks its own final message on 466 of 472 measured requests (98.7%), so rule 1 is already satisfied and every position this component would choose collides with an existing breakpoint. The component earns its keep on agents that do not mark their own tail, and when a prefix mutates mid-conversation.
Simulated comparison¶
A simulation over a captured 91-request SWE-bench stream, calibrated to within 2.10 pp of a live 50-task run's cache-hit rate. It models the billing rule credibly; it is not a measurement of the policy.
| policy | cost | hit | vs the agent's own placement |
|---|---|---|---|
| claude-code's own breakpoints | $0.9270 | 96.04% | — |
| a breakpoint before the newest turn | $0.9780 | 95.23% | +5.50% |
| this policy | $0.9315 | 96.04% | +0.49% |
Placing a single breakpoint before the newest turn shortens the cached prefix on every turn — a real cost increase, and the reason the policy marks the last block instead.
The 4-breakpoint budget is computed by the host¶
The provider caps cache_control at 4 across system + tools + messages together, and
a component sees none of the first two. So apply counts them structurally from the raw body
and passes the total as Ctx.ExistingBreakpoints for the component to budget against; the
count covers the Bedrock cachePoint spelling too. apply also counts the output and logs an
error if we pushed the request over 4 — a request that arrived over the cap is forwarded
as-is and is not reported as ours.
On real Claude Code traffic the agent spends 3 of the 4 slots itself (1,771 of 1,794 requests
carry exactly system=2, tools=0, messages=1), leaving this component one to place.
cache_control is metadata, not content, so apply writes it as a targeted sjson key on the
original raw bytes after verifying the component's only change to that message was adding
cache_control keys. Any wider change is discarded and counted in discarded_changes.
Lossiness¶
None. It attaches cache directives only; model-visible content is unchanged, asserted by comparing model-visible text byte-for-byte across all 91 captured requests.
Messages whose content is a bare string cannot carry a block-level directive; they are skipped rather than restructured, and the breakpoint falls back to the nearest markable block below so the prefix is still written.
Configuration¶
components:
cacheinject:
ttl: 5m # or 1h — see the TTL rule above. Anything else is rejected at load.
K=4 breakpoints and L=20 lookback blocks are provider facts, not tunables.
cacheinject is in no preset: placement has never been shown to help, and shipping it on
by default would enable an unmeasured policy on every request. The presets carry
cachesplit instead, which enables the volatile-tail split — the measured
part — without the placement. Add cacheinject by hand to run the placement study, or when
your agent does not set its own cache_control.
When it's inert¶
On OpenAI- and Gemini-shaped wires, where cache_control does not exist at all. Also when four
breakpoints are already present anywhere in the request (including system and tools), and on
string-content messages.
What the split is worth¶
Is there a target? Across 50 real Claude Code sessions the stable half of the volatile-tail block takes 1 distinct value while the volatile tail takes 50 — so without the split, the hash covers a value unique to each session and the 6,877-token stable half can never be read from another session's cache.
Does the provider actually cache it? Two sessions differing only in the git snapshot, same
breakpoints, same order, judged by cache_read_input_tokens from the API. Three runs,
byte-identical results:
| arm | session-2 read | session-2 write | hit |
|---|---|---|---|
| without split | 0 | 8,882 | 0.0% |
| with split | 8,597 | 0 | 96.7% |
At Sonnet 5 rates that first request of a warm session costs $0.022205 → $0.001719 — $0.0205 saved per session (−92.3%). Per model: Sonnet 5 $0.0205 ($0.0307 from 2026-09-01), Opus 5 $0.0512, Haiku 4.5 $0.0102.
The saving lands once per request carrying the system prompt, not once per turn: within a session, turns 2…n already read the prefix turn 1 wrote in both arms. A cold first session pays the same either way.
End to end in a real agent run — Terminal-Bench, fix-code-vulnerability, 3 trials:
| trial | without | with split | saved |
|---|---|---|---|
| 1 | $0.2003 | $0.1368 | $0.0636 (31.7%) |
| 2 | $0.1828 | $0.1198 | $0.0630 (34.5%) |
| 3 | $0.2003 | $0.1280 | $0.0723 (36.1%) |
| mean | $0.1945 | $0.1282 | $0.0663 (34.1%) |
The end-to-end saving is 3.2× the isolated per-session figure because Claude Code issues sub-agent and side requests that each re-send the same system prompt, and each was independently re-writing it.
Treat 34.1% as one task measured three times, not a fleet average: a second Terminal-Bench task was within noise, its numbers are not quoted, and all figures are Sonnet 5 rates on one benchmark. Placement contributes $0 of it — every dollar above is the split.
See also: Components overview · Choose a preset