[BUG] `DISABLE_PROMPT_CACHING_HAIKU` is silently ignored when Haiku is the main model — undocumented main-model guard, absent on `_SONNET`/`_OPUS`
Preflight
- [x] I searched existing issues —
DISABLE_PROMPT_CACHING_HAIKUappears in exactly 6 issues repo-wide (#7095, #8632, #42309, #48082, #48090, #91091) and none reports this behavior. - [x] Single bug report.
- [x] Reproduced on the latest version at time of filing (2.1.267), and on 2.1.247.
Summary
DISABLE_PROMPT_CACHING_HAIKU is documented on three official pages as disabling prompt caching "for Haiku models". In practice it is a no-op whenever Haiku is the selected main model — the flag is silently skipped and the request still writes a prompt cache. DISABLE_PROMPT_CACHING_SONNET and DISABLE_PROMPT_CACHING_OPUS carry no equivalent exemption, so the four documented per-model flags do not behave as one mechanism.
Because the guard fires only when Haiku is the main model, and the main conversation is exactly the bucket that receives the 1-hour TTL by default on a subscription, the failure mode is structurally guaranteed to produce the most expensive possible cache write.
Environment
- Claude Code 2.1.247 and 2.1.267 (both reproduce)
- Native installer, macOS 26.6.2, arm64
- First-party API (no Bedrock/Vertex,
ANTHROPIC_BASE_URLunset) - Claude subscription
Reproduction
~/.claude/settings.json:
{ "env": { "DISABLE_PROMPT_CACHING_HAIKU": "1" } }
Then /model → Haiku 4.5, and send one short prompt.
Interactive transcript usage from the session that first surfaced this, v2.1.247:
"model": "claude-haiku-4-5-20251001",
"cache_creation_input_tokens": 41576,
"cache_creation": { "ephemeral_1h_input_tokens": 41576, "ephemeral_5m_input_tokens": 0 },
"cache_read_input_tokens": 0
I can't prove after the fact that the env var was set in that particular session, so the load-bearing
evidence is this non-interactive repro plus a control, both run fresh on 2.1.267 withDISABLE_PROMPT_CACHING_HAIKU=1 confirmed present in process.env:
| # | Command | cache_creation_input_tokens | cache_read_input_tokens | input_tokens |
|---|---|---:|---:|---:|
| B | claude -p --model haiku --output-format json "…" | 8102 (ephemeral_1h) | 14074 | 10 |
| A | DISABLE_PROMPT_CACHING=1 claude -p --model haiku --output-format json "…" | 0 | 0 | 22186 |
Control A shows the env plumbing is fine — the global flag disables caching completely. Only the _HAIKU branch fails to fire.
Expected vs actual
- Expected (per docs): with
DISABLE_PROMPT_CACHING_HAIKU=1, requests to a Haiku model carry nocache_control, socache_creation_input_tokens == 0. - Actual: caching is fully active, including a 1-hour cache write.
Root cause
From the bundled JS in ~/.local/share/claude/versions/2.1.267 (offset 168137155):
function RJe(e){ // e = request model id
if(Ie(process.env.DISABLE_PROMPT_CACHING)) return !1;
if(Ie(process.env.DISABLE_PROMPT_CACHING_HAIKU) && Mnt()){
let n = rg(); // resolved small-fast / Haiku model
if(n !== et() && e === n) return !1; // <-- et() = resolved MAIN model
}
if(Ie(process.env.DISABLE_PROMPT_CACHING_SONNET)){ let n=Rp(); if(e===n) return !1 }
if(Ie(process.env.DISABLE_PROMPT_CACHING_OPUS)){ let n=Wl(); if(e===n) return !1 }
if(Ie(process.env.DISABLE_PROMPT_CACHING_FABLE)){ if(E1(e)||sM(e)) return !1 }
if(a.DISABLE_PROMPT_CACHING_MYTHOS){ if(Unt(e)) return !1 }
return !0;
}
RJe is the gate on the main request path — both request builders readp.enablePromptCaching ?? RJe(...) (offsets 168161354, 168170458).
The HAIKU branch alone carries two extra conditions:
function Mnt(){ // TRUE on a plain first-party install (confirmed, see below)
if(a.ANTHROPIC_SMALL_FAST_MODEL !== void 0) return !0;
let e = He(), // "firstParty"
n = e === "firstParty" && (go() || fmr()) || jO(e); // go() -> true when ANTHROPIC_BASE_URL is unset
return a.ANTHROPIC_DEFAULT_HAIKU_MODEL !== void 0 || n;
}
function rg(){ /* -> q8() -> claude-haiku-4-5-20251001 */ }
function et(){ let e = um(); if(e != null) return kt(e); return ul(); } // -> the /model selection
Mnt() returning true is confirmed statically, not assumed: the chunk containing Mnt
(160444341–161393292) imports He, go, jO from chunk-xsncbnja.js and defines none of them locally;
that He() is the provider resolver returning "firstParty", and go() returns true whenANTHROPIC_BASE_URL is unset. So the n !== et() guard is the sole cause — the branch is live, not dead.
When Haiku is the main model, rg() === et(), so n !== et() is false, the branch never reachesreturn !1, and caching stays on. No combination of ANTHROPIC_SMALL_FAST_MODEL /ANTHROPIC_DEFAULT_HAIKU_MODEL can satisfy the guard, because n !== et() and e === n are mutually
exclusive for a main-model request.
Identical shape in 2.1.247 (H6e):
if(zr(process.env.DISABLE_PROMPT_CACHING_HAIKU)&&gNe()){let t=Oc();if(t!==Xo()&&e===t)return!1}
The startup warning asserts the opposite
function yet(){ return ["DISABLE_PROMPT_CACHING","DISABLE_PROMPT_CACHING_HAIKU",
"DISABLE_PROMPT_CACHING_OPUS","DISABLE_PROMPT_CACHING_SONNET",
"DISABLE_PROMPT_CACHING_FABLE"].filter(w => Ie(process.env[w])) }
var LXt = { id:"prompt-caching-disabled", tier:"warning", type:"warning",
isActive: () => yet().length > 0,
render: () => ["Prompt caching off (", yet().join(", "),
"), requests will be slower and cost more", " · unset it to re-enable"] };
yet() filters on env-var presence only — no rg()/et() comparison, no Mnt(). So the warning gate and the cache
gate disagree by construction: the banner activates on flag presence while RJe() returns true and writes
a 1-hour cache. (I verified the code paths; I did not capture a screenshot of the banner.) That
divergence is a defect on its own, whichever semantics are intended.
Docs contradiction
All four per-model rows are worded identically, scoped by tier, with no role qualifier anywhere:
- env-vars —
| DISABLE_PROMPT_CACHING_HAIKU | Set to 1 to disable prompt caching for Haiku models | - model-config — under "disable prompt caching globally or for specific model tiers":
| DISABLE_PROMPT_CACHING_HAIKU | Set to 1 to disable prompt caching for Haiku models only | - prompt-caching —
| DISABLE_PROMPT_CACHING_HAIKU | Disable for Haiku only |
Two further points from the docs' own text:
- prompt-caching says disabling is "useful when debugging caching behavior with a specific model or provider" — the guard defeats exactly that use case for Haiku.
- prompt-caching says these can go "in the
envblock of managed settings" to set org-wide policy. An administrator doing so gets silent non-compliance on every machine where a developer selects Haiku.
The docs demonstrably have vocabulary for role scoping — ANTHROPIC_DEFAULT_HAIKU_MODEL is described as "background functionality", the deprecated ANTHROPIC_SMALL_FAST_MODEL as a "Haiku-class model for background tasks" — and deliberately do not apply it to these four rows. Selecting haiku is itself a documented first-class main-model choice (model-config: "haiku | Uses the fast and efficient Haiku model for simple tasks").
No release note has ever named _HAIKU/_SONNET/_OPUS/_FABLE. DISABLE_PROMPT_CACHING appears exactly once in the whole CHANGELOG (390 versions, back to 0.2.21), at 2.1.108: "Added a warning at startup when prompt caching is disabled via DISABLE_PROMPT_CACHING* environment variables". So the docs site is the sole public specification of these flags' semantics.
Cost impact
Per prompt-caching's TTL table, the main conversation gets a one-hour TTL by default on a subscription within included usage, and 1-hour writes are "billed at a higher rate". Since the guard misfires only for main-model requests, the ignored flag and the expensive TTL always coincide.
For the single interactive turn above, Claude Code's own cost-state estimate was $0.088957, of which the
41,576-token 1-hour write accounts for roughly 93% — with cache_read_input_tokens: 0, i.e. the cache was
never read because the session ended. (On a subscription within included usage this is Claude Code's estimate,
not an invoice; please confirm current per-MTok rates against the live pricing page.)
On "working as intended"
I want to pre-empt this, because the guard looks deliberate. In #7095 a collaborator wrote:
v1.0.113 added a few env vars that should resolve this for you -DISABLE_PROMPT_CACHING_HAIKU,DISABLE_PROMPT_CACHING_SONNET,DISABLE_PROMPT_CACHING_OPUS. This allows you to keep prompt caching enabled for your main loop model, while disabling for your unsupported Haiku version specifically.
n !== et() faithfully implements that sentence. But the scenario there was main = Sonnet with an unsupported Haiku 3 in the small/fast slot — a case a plain tier-scoped flag satisfies identically, since the main model isn't Haiku. It says nothing about a user deliberately selecting Haiku as the main model, which the linked thread never contemplates, and /model haiku is a documented first-class option today.
Additional data point, offered as corroboration rather than proof: a third-party decompiled mirror of v2.1.88 (chauncygu/collection-claude-code-source-code, src/services/api/claude.ts L333-356) shows the gate with no main-model guard and all branches structurally identical:
if (isEnvTruthy(process.env.DISABLE_PROMPT_CACHING_HAIKU)) {
const smallFastModel = getSmallFastModel();
if (model === smallFastModel) return false;
}
If accurate, the flag did fire for Haiku-as-main at 2.1.88, and the guard was added later without an announcement.
Requested resolution
Either of these closes it; the current state is inconsistent whichever way you go.
- Honor the flag by tier, as documented — drop the
n !== et()guard so_HAIKUmatches_SONNET/_OPUS. If the guard exists for the real Bedrock/Vertex case where the Haiku slot collapses onto the main model by fallback, key it on that fallback condition rather than on model-id equality. - Or keep the guard and fix the contract — correct all three doc pages to state the main-model exemption, and make the startup warning suppress or qualify itself when the flag is being skipped.
Side notes
DISABLE_PROMPT_CACHING_MYTHOSis honored by the gate (readinga.DISABLE_PROMPT_CACHING_MYTHOS, notprocess.env) but appears in no doc table and is missing fromyet(), so setting it produces no startup warning.Mnt()is a second undocumented condition on the HAIKU branch alone; on provider configurations where it returns false,_HAIKUwould no-op for a different reason.- Related but distinct: #48090 (docs omit the startup warning; same doc rows), #8632 (
DISABLE_PROMPT_CACHING→ 401).