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
| Source | What to look for | Pass signal |
|---|---|---|
Chat response usage.prompt_tokens_details | cached_tokens, cache_write_tokens | cached_tokens > 0 on a follow-up with the same reusable prefix |
Top-level cache_discount (when present) | Dollar effect of cache reads/writes | Non-zero on hits; may be negative on cache writes (Anthropic) |
| Activity detail row | Same fields + billed cost | Cache fields populated; cost lower than an equivalent uncached call |
GET /api/v1/generation?id=... | Full generation record | Matches 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
}| Field | Meaning |
|---|---|
cached_tokens | Tokens read from cache on this request |
cache_write_tokens | Tokens written to cache (often on the first request that establishes the prefix) |
cache_discount | Cost adjustment from caching (can be negative when writes cost more than base input) |
Missing field vs explicit zero
| Observation | Likely meaning |
|---|---|
prompt_tokens_details absent | Client stripped usage, older SDK, or non-chat endpoint — fetch GET /api/v1/generation or Activity |
cached_tokens: 0 present | Provider processed the request but did not read from cache on that call |
cache_write_tokens: 0 and cached_tokens: 0 | No 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 / family | Caching mode on OpenRouter | Minimum 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 Claude | Automatic top-level cache_control or per-block breakpoints | 1,024–4,096 tokens (model-dependent) | 1.25× input (5m TTL); 2× for 1h TTL |
| DeepSeek | Automatic | Provider-managed | Same as input pricing |
| Google Gemini 2.5+ | Implicit automatic; explicit cache_control for large bodies | Model-dependent (e.g. thousands of tokens for Pro/Flash) | Implicit: no separate write fee; explicit Gemini writes include storage |
| Alibaba Qwen | Explicit cache_control only | Large cached blocks | 1.25× input writes |
| Grok, Groq, Moonshot, Z.AI | Automatic (model-dependent) | Check provider section in OpenRouter docs | Mostly 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
| Symptom | cached_tokens and cache_write_tokens stay 0; prompt_tokens is small |
| Evidence | Compare prompt_tokens to provider minimum (e.g. 1,024 for Claude Sonnet 4, 4,096 for some Opus variants, ~1k+ for OpenAI automatic) |
| Fix | Move large stable content (system rules, tool JSON, RAG chunks) into a fixed prefix; keep user questions in the suffix |
| Verify | Second 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
| Symptom | Every turn looks like a cold start; cache_write_tokens repeats |
| Evidence | Diff system messages between requests — timestamps, shuffled tools, per-request IDs in the system block |
| Fix | Static block first (system, tools, rules); dynamic content last (user text, retrieved chunks, tool results). Stabilize AGENTS.md, MCP schemas, and rules files |
| Verify | Log a hash of the cached prefix bytes; unchanged hash across turns → cached_tokens should rise after the warm request |
Cache expired (TTL)
| Symptom | Hits worked earlier; after idle gap, cached_tokens returns to 0 |
| Evidence | Gap longer than provider TTL (Anthropic default 5 minutes; Gemini implicit ~3–5 minutes; OpenAI explicit prefixes 30 minutes when configured) |
| Fix | Resend the same prefix to re-warm; for long sessions use Anthropic "ttl": "1h" on cache_control (higher write cost) |
| Verify | Immediate retry with same prefix shows cache_write_tokens > 0 again, then hits resume |
Provider drift between turns
| Symptom | Intermittent zeros despite stable prefix |
| Evidence | Activity shows different upstream providers for the same model slug; missing session_id |
| Fix | Set stable session_id (body) or x-session-id (header), max 256 chars. Clients sending prompt_cache_key get sticky routing without changes |
| Verify | Activity 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
| Symptom | Cache never warms across turns |
| Evidence | Request body includes provider.order or provider restrictions |
| Fix | Remove manual ordering for cache-sensitive workloads, or pin a single provider explicitly and accept no stickiness |
| Verify | Without 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
| Symptom | Alibaba Qwen or similar never caches |
| Evidence | Model slug in the explicit-caching list; no cache_control on content blocks |
| Fix | Add 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 |
| Verify | cache_write_tokens > 0 on first qualifying request |
Client-side request transformation
| Symptom | Your logged payload looks stable; API still misses |
| Evidence | Middleware reorders messages, injects headers, strips cache_control, or re-serializes tool JSON |
| Fix | Log the wire JSON sent to https://openrouter.ai/api/v1/chat/completions; disable reshaping for the stable prefix |
| Verify | Byte-identical prefix in outbound JSON → provider metrics move as expected |
Wrong cache type diagnosed
| Symptom | You enabled response caching but cached_tokens is still zero |
| Evidence | Prompt changed slightly between calls; expecting semantic similarity |
| Fix | Response caching needs identical full requests. Prefix caching needs identical opening messages. Semantic layer is separate — semantic caching |
| Verify | Match 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 │
└─────────────────────────────────────┘- Static first — system prompt, tool definitions, stable rules
- Dynamic last — user messages, retrieved chunks, run-specific state
- 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
| Step | Action |
|---|---|
| 1 | Export or filter Activity for the workflow |
| 2 | Note cost, cached_tokens, cache_write_tokens, and cache_discount per row |
| 3 | Compare before/after stabilizing the prefix — or run A/B with and without session_id |
| 4 | For 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 0.30 / MTok), cache write 1.25× input ($3.75 / MTok). Rates vary by model — check openrouter.ai/models .
| Request | Tokens | Rate applied | Input 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.mjsOptional environment variables:
| Variable | Default | Purpose |
|---|---|---|
OPENROUTER_API_KEY | (required) | Bearer token |
OPENROUTER_MODEL | anthropic/claude-sonnet-4 | Model slug to test |
OPENROUTER_SESSION_ID | auto-generated | Sticky routing key |
The script runs, in order:
- Warm — stable prefix, question A (may show
cache_write_tokens> 0) - Reuse — same prefix, question B (often shows
cached_tokens> 0 — not guaranteed) - 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
- Confirm the model slug supports your caching mode (automatic vs explicit
cache_control) - Pin system prompt + tool schemas across steps in the workflow
- Set
session_idper conversation or job - Inspect
cached_tokensafter the warm request — not only on turn 1 - Compare Activity cost before and after prefix stabilization
- Trim MCP/rules bloat in IDE clients — OpenCode MCP
Related
- OpenRouter hub — spending checklist
- Activity & keys — billing truth and generation IDs
- Model routing — fallbacks and
provider.order - Prompt caching — provider-agnostic prefix rules
- Semantic caching — separate from
cached_tokens - OpenRouter: Prompt caching (source of provider tables above, verified 2026-09-09)
- Response caching