Prompt caching

Overview

Anthropic's Messages API supports prompt caching: a stable prefix of the prompt (system block, tools, earlier messages) can be marked with cache_control so the provider stores it once and serves subsequent requests from cache. Cached reads are billed at 10% of the input rate and cache writes at 125%, so multi-turn and tool-heavy Claude chats become markedly cheaper once the cache is warm.

OpenAI-family providers cache automatically rather than through markers, and there the lever is a per-conversation routing key instead — see OpenAI-family cache routing.

mouaif enables this for every Anthropic model, including OAuth-authenticated models. The cached prefix is the system block plus the native tool definitions, with the breakpoint extended to the penultimate message: the combined system block (prompt profile + agent files + skills + feature summary + tagged files + custom prompt) is the most stable part of the prompt across turns, the last tool definition is marked with cache_control per Anthropic's own recommendation, and the second-to-last message carries a breakpoint too. The message breakpoint is what makes caching actually engage: Anthropic silently ignores a cache_control breakpoint whose prefix is below the per-model minimum cacheable length (1024 tokens for Sonnet 3.5/3.7, 4096 for the current generation — Sonnet 4 / Opus 4 / Haiku 4.5), and a system block alone — or even a small tool set — is usually below that. The penultimate message guarantees the cached prefix (system + tools + history) clears the minimum whenever there is any history to replay, so caching works from the second request of a conversation even with every tool switched off. The cache read/write token counts are surfaced in the chat usage line and priced at the discounted tiers.

Claude tool calls ride the same multi-turn loop as the OpenAI-shaped providers: the request carries native tools (converted from the OpenAI-shaped specs), tool_use blocks from the stream are parsed back into the loop, and tool results are sent back as tool_result blocks.

Usage

No user action is required. Every chat turn against an Anthropic API-key model sends:

// Request body, system field (single combined block, marked cacheable):
{
  "system": [
    {
      "type": "text",
      "text": "<profile + agent files + skills + feature summary + tagged files + custom prompt>",
      "cache_control": { "type": "ephemeral" }
    }
  ],
  "tools": [
    { "name": "shell", "description": "Run a shell command", "input_schema": { "type": "object", "properties": { "cmd": { "type": "string" } } } },
    // … every other enabled tool …
    {
      "name": "report_progress",
      "description": "Report progress",
      "input_schema": { /* … */ },
      "cache_control": { "type": "ephemeral" }   // LAST tool only
    }
  ]
}

API-key requests need no caching beta header because prompt caching is generally available. OAuth requests retain only their required anthropic-beta: oauth-2025-04-20 header; stale cache beta names are intentionally not combined with it. The system block is sent as an array because a plain string silently drops cache_control.

Every request also marks the penultimate message (the second-to-last, i.e. the deepest point of the stable, replayed prefix) with cache_control. The final message — the current turn — is never marked: a breakpoint there is ignored by the API, and the current turn is not part of the stable prefix anyway. On the first request of a conversation there is no penultimate message, so the breakpoint appears from the second request onward — which is also the first time there is any history to read back from cache.

Cache metrics

Anthropic reports cache usage on the message_start frame:

{ "message": { "usage": {
  "input_tokens": 1200,
  "cache_read_input_tokens": 900,
  "cache_creation_input_tokens": 300
} } }

Anthropic's three input fields are disjoint buckets: input_tokens is uncached input, while the two cache fields are read and newly-created input. The AI client normalizes them into the provider-neutral contract by setting promptTokens to their sum, then folds the result into per-round usage snapshots and the final done usage block:

{ "usage": { "promptTokens": 2400, "completionTokens": 400,
             "cacheReadTokens": 900, "cacheCreationTokens": 300 } }

The chat UI shows a cache 900 read · 300 written token in the per-turn meta line (only when the provider reported any cache activity), and the cost line prices the cached tokens at the discounted rates.

OpenAI-family cache routing

OpenAI-family endpoints cache prompt prefixes automatically and need no cache_control markers. What they do need is a routing hint: the cache is machine-local, so two requests of the same conversation only hit a warm cache if they land on the same machine. Every request carries prompt_cache_key, derived from the chat id:

// Request body, OpenAI-shaped providers.
{
  "model": "gpt-4o",
  "messages": [ /* … */ ],
  "stream": true,
  "prompt_cache_key": "mouaif-<chatId>"
}

The key is byte-identical for every request of one chat — every tool round and every follow-up turn — and different between chats, which is exactly what the hint is for. A request with no chat (a one-shot POST /api/chat) omits the field, because there is no conversation worth keeping warm. Hits come back as prompt_tokens_details.cached_tokens and are priced by usage-metrics.md.

The field is sent only to openai-compatible, azure, and openrouter. github-copilot is OpenAI-shaped but rides a gateway of its own, and the remaining OpenAI-shaped providers are non-OpenAI upstreams; a strict gateway that rejects unknown body fields would turn the optimisation into a 400. Anthropic is never sent the field — it uses cache_control instead.

ProviderCaching mechanismRouting key
anthropicexplicit cache_control breakpointsnot applicable
openai-compatibleautomaticprompt_cache_key
azureautomaticprompt_cache_key
openrouterautomatic, forwarded to the routed upstreamprompt_cache_key
github-copilotautomaticnot sent
geminiautomatic (implicit caching)not applicable
ollamanonenot applicable

The derivation lives in src/ai-stream.js (promptCacheKeyFor) and the provider gate in src/ai-endpoints.js (PROMPT_CACHE_KEY_PROVIDERS). scripts/test-prompt-cache-key.js locks both halves down: which providers receive the field, which must not, and that the key stays stable within a chat and distinct between chats.

Behavior