> ## Documentation Index
> Fetch the complete documentation index at: https://docs.morphllm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Prompt Caching

> Automatic prefix caching on every open source model. Cached input at $0.22/1M on GLM-5.2, with per-request TTL control.

Prefix caching is on for every open source model. No configuration, no cache-write surcharge. When a request shares a prefix with earlier traffic (system prompt, tool definitions, conversation history), those tokens skip prefill and bill at the cached rate.

| Model                 | ID                  | Input per 1M | Cached input per 1M | Output per 1M |
| --------------------- | ------------------- | ------------ | ------------------- | ------------- |
| **GLM-5.2 744B**      | `morph-glm52-744b`  | \$1.10       | **\$0.22**          | \$4.10        |
| **GLM-5.3-Flash**     | `morph-glm53flash`  | \$0.15       | **\$0.03**          | \$0.50        |
| **Kimi K3 2.8T**      | `morph-kimik3`      | \$2.90       | **\$0.29**          | \$14.00       |
| **Kimi K3 2.8T Fast** | `morph-kimik3-fast` | \$6.00       | **\$0.60**          | \$22.50       |

Cached input is 80% off on GLM-5.2 and GLM-5.3-Flash, and 90% off on Kimi K3. Other open source models cache automatically too; their cached tokens currently bill at the regular input rate.

## Reading cache hits

Every response reports how much of the prompt was served from cache:

```json theme={null}
{
  "usage": {
    "prompt_tokens": 18211,
    "completion_tokens": 512,
    "total_tokens": 18723,
    "prompt_tokens_details": { "cached_tokens": 17408 }
  }
}
```

`cached_tokens` billed at the cached rate, the remainder of `prompt_tokens` at the input rate.

## Getting hits

<Frame>
  <img src="https://mintcdn.com/morph-555d6c14/bcAHnPuhOK-ICSqu/images/prompt-caching-prefix.webp?fit=max&auto=format&n=bcAHnPuhOK-ICSqu&q=85&s=eff2a7be8ce831010a748e011cf07095" alt="Prompt caching: a request matching the stable prefix is a cache hit and reuses those tokens; changing any token in the prefix is a cache miss" width="1672" height="941" data-path="images/prompt-caching-prefix.webp" />
</Frame>

Matching is exact-prefix and block-aligned. To maximize hit rate:

* Put stable content first: system prompt, then tool definitions, then history. Variable content (the user's latest message, retrieved context) goes last.
* Keep the prefix byte-identical between turns. A timestamp or request ID in the system prompt kills every hit after it.
* Short prompts rarely hit. Caching operates on \~1k-token blocks, so a 300-token prompt has nothing to reuse.

Multi-turn agent loops get this for free: each turn re-sends the previous turns verbatim, so everything but the newest turn is a cache hit.

## Session key

Caching is automatic and needs no key. A key answers the other half of the question: which worker serves the turn. A conversation's prefix lives in the cache of the worker that prefilled it, so a follow-up that lands on a different worker re-prefills the whole thing at full price. Send one id per conversation and every turn routes to the worker that already holds its prefix.

Two carriers, both OpenAI convention, both passed through by OpenRouter:

| Field              | Where                | Type   | Meaning                                                                                                                                     |
| ------------------ | -------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `prompt_cache_key` | top-level body field | string | Per-conversation id. OpenCode and the Codex CLI already send it.                                                                            |
| `x-session-id`     | request header       | string | The same id, carried as a header. What OpenRouter sends on behalf of its callers, so traffic arriving through OpenRouter is tagged already. |

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST "https://api.morphllm.com/v1/chat/completions" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "morph-dsv4flash",
        "messages": [{"role": "user", "content": "..."}],
        "prompt_cache_key": "conv-8f2c1a"
      }'
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from openai import OpenAI

    client = OpenAI(
        api_key="YOUR_API_KEY",
        base_url="https://api.morphllm.com/v1",
    )

    # Non-standard fields go via extra_body
    response = client.chat.completions.create(
        model="morph-dsv4flash",
        messages=[{"role": "user", "content": "..."}],
        extra_body={"prompt_cache_key": "conv-8f2c1a"},
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import OpenAI from "openai";

    const client = new OpenAI({
      apiKey: "YOUR_API_KEY",
      baseURL: "https://api.morphllm.com/v1",
    });

    const response = await client.chat.completions.create({
      model: "morph-dsv4flash",
      messages: [{ role: "user", content: "..." }],
      // @ts-expect-error non-standard field
      prompt_cache_key: "conv-8f2c1a",
    });
    ```
  </Tab>
</Tabs>

A client that already carries its conversation id in a header sends the same value there instead:

```bash theme={null}
curl -X POST "https://api.morphllm.com/v1/chat/completions" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "x-session-id: conv-8f2c1a" \
  -H "Content-Type: application/json" \
  -d '{"model": "morph-dsv4flash", "messages": [{"role": "user", "content": "..."}]}'
```

Picking keys:

* **One key per conversation or agent session.** Generate it when the conversation starts and reuse it for every turn, including retries of the same turn.
* **Never a shared app-wide or canary value.** One key across unrelated traffic funnels all of it onto a single worker. That worker fills up, sheds the overflow with a 429, and you spend the cache savings on retries.
* **Keys are hashed server-side and never stored raw.** What we forward is derived from the hash, so it carries none of your value.
* **A key changes placement, never billing.** Rates and the cached-input discount are the same with or without it.

Values are opaque to us: any non-empty string up to 512 bytes. A value that is empty, the wrong type, or longer than that counts as absent, so the request is served without a pin rather than rejected. When a request carries both carriers, the header is the one used.

<Note>
  Rolling out per model, DeepSeek V4 Flash (`morph-dsv4flash`) first. Sending the key to any other model is harmless: it is read, recorded, and ignored until that model's rollout completes.
</Note>

`run_id` on [Agent Runs](/sdk/components/agent-programs) is the heavier version of the same idea. A run id schedules a whole tool-calling run as one unit: sticky placement, priority resume, and whole-run admission, which under load means whole runs pause rather than every run getting slow. A session key does placement and nothing else. Use a run id for an agent run on Kimi K3, a session key for any other multi-turn conversation. If a request carries both, `run_id` wins: you named the run yourself, and the session key is only our read of where its prefix lives.

## Cache TTL

By default cached prefixes persist under LRU eviction, with no fixed expiry. To control retention per request, pass `cache_ttl`:

| Value   | Retention  |
| ------- | ---------- |
| `"5m"`  | 5 minutes  |
| `"30m"` | 30 minutes |
| `"1h"`  | 1 hour     |
| `"6h"`  | 6 hours    |
| `"24h"` | 24 hours   |

Expiry is sliding, Anthropic-style: every cache hit refreshes the clock. Past the TTL the prefix stops hitting entirely (full recompute), and re-sending it caches it fresh. A prefix shared by multiple requests keeps the longest surviving TTL.

<Note>
  `cache_ttl` is rolling out now, GLM-5.2 first and Kimi K3 at launch. Requests that include it are accepted today; the field takes effect as each model's rollout completes.
</Note>

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST "https://api.morphllm.com/v1/chat/completions" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "morph-glm52-744b",
        "messages": [{"role": "user", "content": "..."}],
        "cache_ttl": "1h"
      }'
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from openai import OpenAI

    client = OpenAI(
        api_key="YOUR_API_KEY",
        base_url="https://api.morphllm.com/v1",
    )

    # Non-standard fields go via extra_body
    response = client.chat.completions.create(
        model="morph-glm52-744b",
        messages=[{"role": "user", "content": "..."}],
        extra_body={"cache_ttl": "1h"},
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import OpenAI from "openai";

    const client = new OpenAI({
      apiKey: "YOUR_API_KEY",
      baseURL: "https://api.morphllm.com/v1",
    });

    const response = await client.chat.completions.create({
      model: "morph-glm52-744b",
      messages: [{ role: "user", content: "..." }],
      // @ts-expect-error non-standard field
      cache_ttl: "1h",
    });
    ```
  </Tab>
</Tabs>

Fine print:

* Invalid `cache_ttl` values are rejected with a 400. Only the five tiers above are accepted.
* Expiry granularity is \~30 seconds: treat a TTL as "at least this long, expired within \~30s after."
* Omitting `cache_ttl` keeps the default behavior (LRU, no fixed expiry).
* A session key and `cache_ttl` are independent: the key picks the worker, the TTL controls how long that worker keeps the prefix.

## Pitfalls

<AccordionGroup>
  <Accordion title="cached_tokens is 0 on every request">
    Your prefix is changing between requests. Diff two consecutive prompts byte-for-byte; the first divergent token ends the cacheable prefix. Common culprits: timestamps, UUIDs, or shuffled tool order in the system prompt.
  </Accordion>

  <Accordion title="Hits stop after an edit mid-conversation">
    Editing an earlier message invalidates everything after it. Expected: caching is prefix-based, so append, don't rewrite.
  </Accordion>
</AccordionGroup>

## See Also

* [Standby Requests](/sdk/components/standby) — stack a 50% tier discount on top: cached standby input is \$0.11/1M
* [Agent Runs](/sdk/components/agent-programs) — `run_id`, the whole-run version of a session key
* [Open Source Models](/sdk/components/fast-models) — the models this page prices
* [Compact](/sdk/components/compact) — shrink context before caching it
