Skip to Content
GatewaysOpenRouterPrompt caching

OpenRouter prompt caching: fix cache misses and check costs

OpenRouter cache not working? Use this page to find out whether your model supports the caching behavior you expect, why cached_tokens is zero or missing, what to change in requests or routing, and whether a cache hit actually reduced total cost.

Jump to troubleshooting failure modes or run the verification script (three paid API calls — optional).

How do I verify that a request actually used the cache?

Cache usage is provider prompt caching — not response caching  (identical full-body replay) and not semantic caching (your middleware). You are looking for prefix reuse reported in the API usage object or Activity detail.

Evidence to inspect

SourceWhat to look forPass signal
Chat response usage.prompt_tokens_detailscached_tokens, cache_write_tokenscached_tokens > 0 on a follow-up with the same reusable prefix
Top-level cache_discount (when present)Dollar effect of cache reads/writesNon-zero on hits; may be negative on cache writes (Anthropic)
Activity  detail rowSame fields + billed costCache fields populated; cost lower than an equivalent uncached call
GET /api/v1/generation?id=...Full generation recordMatches response usage; use generation id from the API response
{ "usage": { "prompt_tokens": 10339, "completion_tokens": 60, "cost": 0.0123, "prompt_tokens_details": { "cached_tokens": 10318, "cache_write_tokens": 0 } }, "cache_discount": 0.0041 }
FieldMeaning
cached_tokensTokens read from cache on this request
cache_write_tokensTokens written to cache (often on the first request that establishes the prefix)
cache_discountCost adjustment from caching (can be negative when writes cost more than base input)

Missing field vs explicit zero

ObservationLikely meaning
prompt_tokens_details absentClient stripped usage, older SDK, or non-chat endpoint — fetch GET /api/v1/generation or Activity
cached_tokens: 0 presentProvider processed the request but did not read from cache on that call
cache_write_tokens: 0 and cached_tokens: 0No cache write and no read — prefix too short, unsupported model, cache expired, or prefix changed

A cache hit (cached_tokens > 0) is evidence of prefix reuse, not proof of net savings. Compare usage.cost and Activity rows before and after your fix.

Does my model and provider support caching?

OpenRouter forwards to upstream providers. Behavior is not universal — verify your exact model slug in the OpenRouter prompt caching guide  (verified 2026-09-09).

Provider / familyCaching mode on OpenRouterMinimum prefix (documented)Cache writes
OpenAI (many models)Automatic; explicit breakpoints on GPT-5.6+1,024 tokens (automatic)Free on pre–GPT-5.6; 1.25× input on GPT-5.6+ writes
Anthropic ClaudeAutomatic top-level cache_control or per-block breakpoints1,024–4,096 tokens (model-dependent)1.25× input (5m TTL); 2× for 1h TTL
DeepSeekAutomaticProvider-managedSame as input pricing
Google Gemini 2.5+Implicit automatic; explicit cache_control for large bodiesModel-dependent (e.g. thousands of tokens for Pro/Flash)Implicit: no separate write fee; explicit Gemini writes include storage
Alibaba QwenExplicit cache_control onlyLarge cached blocks1.25× input writes
Grok, Groq, Moonshot, Z.AIAutomatic (model-dependent)Check provider section in OpenRouter docsMostly no write surcharge

Router models (openrouter/auto, Pareto, Fusion) may resolve to different upstream models between turns. Sticky routing reuses the resolved model only while it stays in the candidate set — not a guarantee of cache continuity.

If you are unsure whether caching is enabled for your slug, run the verification script on that model or inspect one generation in Activity before changing application code.

Why is cached_tokens zero or missing?

Each row is symptom → evidence → fix → verification. Provider rules come from OpenRouter prompt caching docs ; observed API output is authoritative for your account.

Prefix shorter than provider minimum

Symptomcached_tokens and cache_write_tokens stay 0; prompt_tokens is small
EvidenceCompare prompt_tokens to provider minimum (e.g. 1,024 for Claude Sonnet 4, 4,096 for some Opus variants, ~1k+ for OpenAI automatic)
FixMove large stable content (system rules, tool JSON, RAG chunks) into a fixed prefix; keep user questions in the suffix
VerifySecond request with identical prefix shows cache_write_tokens > 0 on first call, then cached_tokens > 0 on follow-up (provider-dependent timing)

Reusable prefix changes every request

SymptomEvery turn looks like a cold start; cache_write_tokens repeats
EvidenceDiff system messages between requests — timestamps, shuffled tools, per-request IDs in the system block
FixStatic block first (system, tools, rules); dynamic content last (user text, retrieved chunks, tool results). Stabilize AGENTS.md, MCP schemas, and rules files
VerifyLog a hash of the cached prefix bytes; unchanged hash across turns → cached_tokens should rise after the warm request

Cache expired (TTL)

SymptomHits worked earlier; after idle gap, cached_tokens returns to 0
EvidenceGap longer than provider TTL (Anthropic default 5 minutes; Gemini implicit ~3–5 minutes; OpenAI explicit prefixes 30 minutes when configured)
FixResend the same prefix to re-warm; for long sessions use Anthropic "ttl": "1h" on cache_control (higher write cost)
VerifyImmediate retry with same prefix shows cache_write_tokens > 0 again, then hits resume

Provider drift between turns

SymptomIntermittent zeros despite stable prefix
EvidenceActivity shows different upstream providers for the same model slug; missing session_id
FixSet stable session_id (body) or x-session-id (header), max 256 chars. Clients sending prompt_cache_key get sticky routing without changes
VerifyActivity provider column stable across the session; cached_tokens more consistent on turn 2+

OpenRouter activates sticky routing on any successful request when session_id is set — even before a cache hit — so later turns route to the same provider (sticky routing docs ).

Manual provider.order or forced routing

SymptomCache never warms across turns
EvidenceRequest body includes provider.order or provider restrictions
FixRemove manual ordering for cache-sensitive workloads, or pin a single provider explicitly and accept no stickiness
VerifyWithout provider.order, same session_id + prefix produces non-zero cached_tokens

Sticky routing is disabled when you set provider.order — OpenRouter docs, 2026-09-09.

Explicit caching required but not set

SymptomAlibaba Qwen or similar never caches
EvidenceModel slug in the explicit-caching list; no cache_control on content blocks
FixAdd cache_control: { "type": "ephemeral" } on large content blocks (Anthropic-style). Gemini large bodies need explicit breakpoints too — implicit caching alone may not apply to your payload shape
Verifycache_write_tokens > 0 on first qualifying request

Client-side request transformation

SymptomYour logged payload looks stable; API still misses
EvidenceMiddleware reorders messages, injects headers, strips cache_control, or re-serializes tool JSON
FixLog the wire JSON sent to https://openrouter.ai/api/v1/chat/completions; disable reshaping for the stable prefix
VerifyByte-identical prefix in outbound JSON → provider metrics move as expected

Wrong cache type diagnosed

SymptomYou enabled response caching but cached_tokens is still zero
EvidencePrompt changed slightly between calls; expecting semantic similarity
FixResponse caching  needs identical full requests. Prefix caching needs identical opening messages. Semantic layer is separate — semantic caching
VerifyMatch the correct layer’s requirements; inspect the correct field (cached_tokens vs your app cache metrics)

What should I change in my request or routing configuration?

Stable prefix layout

┌─────────────────────────────────────┐ │ REUSABLE PREFIX (cache candidate) │ │ system prompt, tools, rules, RAG │ ├─────────────────────────────────────┤ │ DYNAMIC SUFFIX │ │ user message, session tool results │ └─────────────────────────────────────┘
  1. Static first — system prompt, tool definitions, stable rules
  2. Dynamic last — user messages, retrieved chunks, run-specific state
  3. Do not edit the system block every agent turn in IDE or agent clients

session_id for agents and automation

{ "model": "anthropic/claude-sonnet-4", "session_id": "workflow-invoice-review-42", "cache_control": { "type": "ephemeral" }, "messages": [ { "role": "system", "content": "<STABLE RULES AND REFERENCE TEXT>" }, { "role": "user", "content": "<TASK FOR THIS STEP>" } ] }

Sticky sessions expire after ~10 minutes of inactivity; each successful request resets the timer. If the sticky provider errors, the next request may re-route (OpenRouter docs ).

For routing and fallback arrays that affect provider choice, see Model routing.

Explicit breakpoints (Anthropic, Gemini large bodies, Alibaba)

{ "model": "anthropic/claude-sonnet-4", "messages": [ { "role": "system", "content": [ { "type": "text", "text": "You are a code reviewer. Rules: ..." }, { "type": "text", "text": "<LARGE STABLE CORPUS>", "cache_control": { "type": "ephemeral" } } ] }, { "role": "user", "content": "Review this diff..." } ] }

Gemini treats the first system message as a single immutable systemInstruction — put dynamic tail content in a later user message, not after a cached block inside system (OpenRouter Gemini section ).

How do I check whether the cache reduced my total cost?

Cache reads usually lower input cost; cache writes can increase it on the warm request.

Compare billed cost, not just cached_tokens

StepAction
1Export or filter Activity  for the workflow
2Note cost, cached_tokens, cache_write_tokens, and cache_discount per row
3Compare before/after stabilizing the prefix — or run A/B with and without session_id
4For one row, GET /api/v1/generation?id=<generation_id> reconciles client logs

Illustrative cost math (not measured here)

Assumptions (Anthropic Claude Sonnet-class, OpenRouter docs 2026-09-09): base input 3/MTok∗∗,cacheread∗∗0.1×∗∗input(∗∗3 / MTok**, cache read **0.1×** input (**0.30 / MTok), cache write 1.25× input ($3.75 / MTok). Rates vary by model — check openrouter.ai/models .

RequestTokensRate appliedInput cost (illustrative)
Warm (10k prefix write)10,000 write$3.75 / MTok$0.0375
Hit (10k read)10,000 cached$0.30 / MTok$0.0030
Miss (10k uncached)10,000 input$3.00 / MTok$0.0300

A single warm request can cost more than an uncached one. Savings appear when the same prefix is reused enough times before TTL expiry. Always use Activity cost as billing truth — see Activity & keys.

Run a minimal cache verification example

The repo ships a bounded script that makes three chat completions (charges apply). It uses anthropic/claude-sonnet-4 with top-level automatic cache_control and a >1,024-token stable system prefix — a configuration documented in OpenRouter’s Anthropic section .

Download or copy: openrouter-cache-verify.mjs  (also at public/examples/openrouter-cache-verify.mjs in the wiki repo).

# From a clone of https://github.com/narevai/tokenminning-wiki export OPENROUTER_API_KEY="sk-or-..." # never commit or paste into the wiki node public/examples/openrouter-cache-verify.mjs

Optional environment variables:

VariableDefaultPurpose
OPENROUTER_API_KEY(required)Bearer token
OPENROUTER_MODELanthropic/claude-sonnet-4Model slug to test
OPENROUTER_SESSION_IDauto-generatedSticky routing key

The script runs, in order:

  1. Warm — stable prefix, question A (may show cache_write_tokens > 0)
  2. Reuse — same prefix, question B (often shows cached_tokens > 0 — not guaranteed)
  3. Compare — changed prefix (expect lower or zero cache reads)

It prints generation id, model, provider (when returned), token counts, cache fields, cost, and a curl template for GET /api/v1/generation.

Live API verification is opt-in. CI and normal pnpm check do not call OpenRouter. Without your API key, validate syntax locally only — runtime cache behavior remains unverified on your account until you run the script.

Agent and automation checklist

  1. Confirm the model slug supports your caching mode (automatic vs explicit cache_control)
  2. Pin system prompt + tool schemas across steps in the workflow
  3. Set session_id per conversation or job
  4. Inspect cached_tokens after the warm request — not only on turn 1
  5. Compare Activity cost before and after prefix stabilization
  6. Trim MCP/rules bloat in IDE clients — OpenCode MCP
Last updated on