Research notes behind Permafrost. The question: what does Claude Code do, request to request, that changes the leading bytes of the prompt and therefore resets DeepSeek's automatic prefix cache? — and which of those can a passthrough proxy + a little env hygiene neutralize.
DeepSeek's context cache is automatic and prefix-anchored at byte 0. From their docs:
"A subsequent request can only hit the cache if it fully matches a cache prefix unit." Partial matches in the middle do not count.
The cache is read-only input: the first byte that differs from a previously-seen
request ends the hit; everything after it is billed at the full (miss) price —
about 50× more than a hit on V4 Flash. Render order is tools → system →
messages, so the cache anchor is tools + system. Keep those bytes
frozen across turns and the whole conversation up to the newest turn is served
from cache. Let one byte near the front wobble and you pay full price for
everything.
So the enemy is prefix instability. Here is where Claude Code introduces it.
| # | What CC does | Where it lands | Why it resets the cache | Permafrost's fix |
|---|---|---|---|---|
| 1 | MCP tools finish connecting mid-session, changing the tool set or its order | tools (position 0) |
Any add/remove/reorder of a tool changes byte 0 of the anchor → total miss | sort_tools: emit tools in a deterministic (name, canonical-json) order, independent of arrival timing |
| 2 | ENABLE_TOOL_SEARCH defaults off under a custom ANTHROPIC_BASE_URL |
tools + system |
CC stops deferring tools and re-inlines the entire tool set every turn — tens of KB of churn at the front of the prefix | Forward tool_reference blocks faithfully; ship ENABLE_TOOL_SEARCH=true in the env block / wrap |
| 3 | The environment block (cwd, platform, today's date, a git status snapshot) |
system |
git status changes the moment you edit a file; the date changes daily — each change resets everything after it |
relocate_volatile: lift the env block out of the anchor and re-attach it to the latest turn (aggressive mode) |
| 4 | cache_control breakpoints slide to the newest turn each request |
system + messages |
DeepSeek ignores the markers, but their bytes move, perturbing serialization around them | strip_cache_control: remove every cache_control key (no-op for DeepSeek's auto cache) |
| 5 | Claude Code's TodoWrite reminder appears after enough tool loops | messages (role=system) |
The mid-conversation nudge is non-semantic but creates a fresh tail prefix; live traffic showed a 12.9% hit turn recovering to 99.7% after stripping | strip_todowrite_reminders: drop only the exact TodoWrite reminder, preserving other system messages |
| 6 | anthropic-beta header flips as feature flags toggle (interleaved thinking, fine-grained streaming, tool search…) |
request header | Header churn can change how the upstream frames/keys the request mid-session | sticky_beta: pin the first-seen beta value per cache anchor for the session |
| 7 | Model-name churn — the main model vs. ANTHROPIC_SMALL_FAST_MODEL for cheap calls |
model field |
Caches are model-scoped; a different model is a different cache namespace, and DeepSeek maps claude-opus*→deepseek-v4-pro, claude-sonnet*/haiku*→deepseek-v4-flash |
Pin ANTHROPIC_MODEL / ANTHROPIC_SMALL_FAST_MODEL to names that map to one DeepSeek SKU |
| 8 | Non-essential background traffic (telemetry, title-gen, summaries) on different prefixes | separate requests | Each one-off prefix is a pure cache write, never read | CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 |
| 9 | Re-sending reasoning_content in history (only on the OpenAI-style endpoint) |
messages |
DeepSeek bills re-sent reasoning as input (~500 tok/turn in Reasonix's measurements) | N/A for the Anthropic passthrough (thinking blocks are append-only in the tail); documented for the OpenAI path |
| 10 | Non-deterministic JSON — object-key reordering, \uXXXX escaping, whitespace |
whole body | Byte-different serialization of identical content → prefix differs | canonical_dumps: compact separators, ensure_ascii=False, insertion-order (optional key sort) |
| 11 | The x-anthropic-billing-header system block with a per-request cch=<nonce> (found on real CC traffic) |
system (first block) |
A 5-char nonce at the front of system changes the anchor fingerprint every request |
stabilize_metadata: pin the cch value to a constant (telemetry the model ignores) |
Two distinct things get called "the header":
-
The HTTP
anthropic-betaheader. It carries feature flags and can change mid-session. For DeepSeek's body-anchored cache this header is not part of the cached prefix, so it rarely matters — but it does for real Anthropic caching and for any gateway that keys on it. Permafrost pins it (sticky_beta, on by default) so it can't wobble. It is not something a plugin can stop CC from generating; the only safe interception point is a proxy, which is why Permafrost is one. -
The request "header" in the colloquial sense — the front of the prompt body (
tools+system). This is the real battleground, and busters 1–4 and 9 above are exactly "CC randomly modifying the head of the request." Permafrost rewrites this head into a stable, canonical form on every request.
The crucial constraint: CC reads its env once, at process start, and never
re-checks it. So ANTHROPIC_BASE_URL, ENABLE_TOOL_SEARCH, and the model
pins must be set before claude launches — permafrost wrap and the
settings.example.json block both do this. Changing them in a running session
does nothing.
A Claude Code plugin runs hooks, commands and skills — it never sees or rewrites
the bytes CC puts on the wire. So the request-body fixes (1, 3, 4, 9) are only
possible from a proxy sitting at ANTHROPIC_BASE_URL. That is the whole
reason Permafrost ships a proxy and not just hooks. The plugin layer handles
what it can own: the env hardening (2, 6, 7), live diagnostics (/doctor),
and the savings statusline.