提示缓存在 OpenRouter 上怎么用:缓存断点打在哪

2026-08-18

一段几千 token 的系统提示、一份 RAG 检索出来的长文、一个塞满工具定义的前缀,每一轮对话都要原样再发一次。你知道提示缓存能救这个场面,也知道 OpenRouter 支持它,但真到写请求体的时候就卡住了——那个”断点”到底写在哪儿?写在消息里,还是写在请求根上?写一个还是写好几个?

这篇只解决这一件事:缓存断点的声明方式。依据是 OpenRouter 官方文档 openrouter.ai/docs/guides/best-practices/prompt-caching 这一页。这一页里价格内容很多,本文一个价格数字都不抄——折扣倍率与各家的读写计费方式都会变,要算账请直接看官方定价页。

一、先分清”要不要你声明”

文档开头就把整件事分成了两类:多数上游是自动开启提示缓存的,不需要你在请求里加任何东西;但有一部分要求你逐条消息地显式开启。文档原话是 “Most providers automatically enable prompt caching, but note that some … require you to enable it on a per-message basis.”

这个分法决定了你该不该动请求体。属于自动那一类的,你能做的只是把提示的前半段写稳定——文档在讲隐式缓存的那一节给过一条明确建议(原文是个 Tip):让消息数组的开头部分在多次请求之间保持一致,把会变的内容(用户提问、动态上下文)推到提示末尾。属于需要显式声明那一类的,不写断点就完全不会缓存,不是命中率低,是压根不进缓存。

哪些上游属于哪一类,文档是按小节列出来的,会随平台调整。本文不抄这份名单,你按自己实际在用的那个模型去查那一页对应小节。

二、前置条件

动手之前有三件事要先确认,这一段最容易被跳过。

第一,确认你调的是哪个接口。 文档明确写了,Anthropic 风格的逐块 cache_control没有通过 Responses API 暴露出来——原文是 “Anthropic-style per-block cache_control inside input items is not exposed through the Responses API”。Responses API 上只支持顶层 cache_control 那种自动形态,要做逐块控制得改用 OpenAI 风格的 prompt_cache_breakpoint。所以”我照着文档写了断点却没生效”,第一个要查的不是字段拼错没有,是你用的接口支不支持这个写法。

第二,确认你的提示够不够长。 文档写明短于最小可缓存长度的提示不会被缓存。各模型的阈值不一样,文档里列了一张表——那张表里的数值会随模型更新变动,本文不抄,你按当时的文档看。要记住的是这条边界存在:内容太短的时候你写了断点也不会有缓存,这时候排查方向不在语法上。

第三,确认你没有把粘性路由关掉。 缓存是绑在具体 provider 端点上的,OpenRouter 用 provider sticky routing 把同一会话的后续请求送回同一个端点来保住缓存。文档写明:当你用 provider.order 指定了手动的 provider 顺序时,粘性路由不会启用,你的显式排序优先。另外文档写明粘性会话在一段无活动时间之后过期(落盘时那一页写的是 10 分钟,这类平台侧参数可能调整,以官方文档当时的写法为准),每次成功请求会重置这个计时;如果粘性的那个端点返回错误,缓存记录不会更新,下一次请求会被重新路由。

三、三种声明方式,分别写在哪一层

形态一:顶层 cache_control(自动推进的断点)

在请求体根上放一个 cache_control 字段,系统会自动把缓存断点打在最后一个可缓存的块上,并且随着对话变长自动往后推进。文档给的例子:

{
  "model": "~anthropic/claude-sonnet-latest",
  "cache_control": { "type": "ephemeral" },
  "messages": [
    {
      "role": "system",
      "content": "You are a historian studying the fall of the Roman Empire. You know the following book very well: HUGE TEXT BODY"
    },
    {
      "role": "user",
      "content": "What triggered the collapse?"
    }
  ]
}

例子里的 model 值是官方文档当时写的示例值,平台上有哪些模型、哪些端点随时在变,别拿它当清单用。

这个形态的关键在于”断点会自己走”。文档写它适合多轮对话(“Best for multi-turn conversations”),因为你不用在每一轮重新计算该把断点挪到哪条消息上。另外文档提到,有的上游接口不接受这个顶层字段,OpenRouter 会把它翻译成一个位于末尾的缓存断点再发出去。

形态二:逐块 cache_control(自己划边界)

cache_control 直接放在某个 content 块上,缓存的边界就由你划。语法是 cache_control: { "type": "ephemeral" },默认 TTL 是 5 分钟;加上 "ttl": "1h" 可以延长到 1 小时。文档给的用户消息示例:

{
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Given the book below:"
        },
        {
          "type": "text",
          "text": "HUGE TEXT BODY",
          "cache_control": {
            "type": "ephemeral",
            "ttl": "1h"
          }
        },
        {
          "type": "text",
          "text": "Name all the characters in the above book"
        }
      ]
    }
  ]
}

注意这个例子的排布:稳定的大块内容夹在中间,断点打在它上面,会变的那句提问放在断点之后。这就是断点的语义——断点标记的是”可复用前缀到此为止”,它后面的东西不进缓存。

文档对这个形态给了一条使用建议:把断点留给大块文本,比如角色卡、CSV 数据、RAG 数据、书籍章节这类。还有一条硬约束:显式断点的数量有上限,文档写明是四个(“There is a limit of four explicit breakpoints”)。这一条在 Gemini 那节不一样——文档说 cache_control 断点的数量没有限制,但对 Gemini 而言 OpenRouter 只会用最后一个断点,多写几个是安全的、也有助于保持与 Anthropic 写法的兼容,但只有最后那个起作用。

形态三:OpenAI 风格的 prompt_cache_breakpoint + prompt_cache_options

这一套是两个控制项配合的,分别在两层:

  • prompt_cache_breakpoint:放在单个文本内容块上(Responses 里是 input_text,Chat Completions 里是 text),标记可复用前缀的结束位置,到这个块为止的内容成为候选缓存前缀。文档写明此时自动缓存仍然是开着的。
  • prompt_cache_options:放在请求根上。把 mode 设成 "explicit" 会关掉平台自动放置的断点,只有你标了 prompt_cache_breakpoint 的块参与缓存;ttl 用来请求一个缓存时长(文档示例写的是 "30m")。

Chat Completions 的写法:

{
  "model": "openai/...",
  "prompt_cache_key": "my-session-key",
  "prompt_cache_options": {
    "mode": "explicit",
    "ttl": "30m"
  },
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "<REUSABLE_PREFIX>",
          "prompt_cache_breakpoint": {
            "mode": "explicit"
          }
        },
        {
          "type": "text",
          "text": "<TASK_SPECIFIC_SUFFIX>"
        }
      ]
    }
  ]
}

文档在这一节用一个 Info 框限定了支持范围:这套显式缓存只在 OpenAI 较新的模型系列上受支持,具体是哪一代请以官方文档当时的写法为准,本文不抄型号。

附带一件事:session_id 决定路由粘不粘

断点写对了、缓存写进去了,下一次请求被路由到别的端点,缓存照样白写。文档给了显式的控制手段:在请求体顶层传 session_id,或者设 x-session-id HTTP 头;两个都给时请求体里的值优先。文档还写明 session_id 有长度上限,超了要截断或换更短的键,具体上限值以官方文档当时的写法为准。

{
  "model": "anthropic/claude-sonnet-4",
  "session_id": "my-agent-session-abc123",
  "messages": [
    {
      "role": "user",
      "content": "Continue our conversation..."
    }
  ]
}

(这段同样是官方文档的示例,里面的 model 值是文档当时写的示例值,不是可用模型清单。)

两处细节值得记:一是不传 session_id 时,平台默认对第一条 system(或 developer)消息与第一条非 system 消息做哈希来识别会话——开头消息一变,会话就被认成另一个,这正是文档推荐多轮 Agent 显式传 session_id 的场景。二是传了 session_id 时,粘性路由在任何一次成功请求之后就激活;不传则要等到检测到缓存命中之后才激活。两者都没设时,文档写明平台会退回用 prompt_cache_key 请求字段当粘性路由的键。

以上片段均按官方文档原样抄录;把多个字段组合进同一个请求时,属于按官方文档中的参数语义组合的示例,未经实测,以官方文档最新内容为准。

四、边界:这些地方文档明说了不支持或不保证

  • TTL 不做跨风格翻译。 文档写明块级标记本身是可互换的:标了 cache_control 的文本块在路由到支持的 OpenAI 模型时会得到一个 prompt_cache_breakpoint,反过来标了 prompt_cache_breakpoint 的块在路由到 Anthropic 或 Google 时会得到一个默认(5 分钟)的 cache_control。但 TTL 不翻译——cache_control 上的 ttl 朝 OpenAI 方向会被丢掉,请求级的 prompt_cache_options 只对 OpenAI 生效。你想设 TTL 又走 Responses API 的话,文档指的路是改用 Chat Completions 或 Anthropic Messages 接口配 cache_control
  • prompt_cache_breakpoint 本身不带 ttl 这是上一条的直接后果,文档单独点了出来。
  • Gemini 的 systemInstruction 是不可变的。 文档写 Gemini 只有单个 systemInstruction 字段,缓存内容把它当作不可变。落到 OpenRouter 上的后果是:第一条 systemdeveloper 消息里的 cache_control 可以缓存规范化后的系统提示,但没法在同一条消息里保留一段未缓存的动态尾巴。需要动态部分就把它挪到后面的 user 消息里。
  • 批量接口里的缓存不保证互相可见。 文档写明 cache_control 断点在 :batch 端点上的行为与同步接口一致,但同一个 batch 里的请求可能并发、乱序处理,某一行写入的缓存不保证对同一批里的其它行可见。文档给的做法是用 "ttl": "1h" 的断点配一个共享前缀,跨多个批次复用,或者先用一次同步请求把缓存热起来。
  • Batch API 目前不按 session_id 分组。 文档的 Note 用的是 “does not yet”,照实标出来。
  • 部分快照式端点不支持显式缓存。 文档在讲某一家上游的显式缓存时列了支持与不支持的端点,快照端点属于不支持的那一类。具体是哪些端点会随模型更新变,本文不抄,请查那一页当时的列表。
  • 非同步的那几类端点只用 session_id 分组,不做粘性路由。 文档写明 embeddings、reranking、语音转文字、文字转语音、图像生成、视频生成这些端点只接受 x-session-id 头(不接受请求体里的 session_id),而且这个值只用于在日志里归组,粘性路由对它们不适用。

关于操作系统:这一页讲的全是请求体 JSON,官方文档没有区分操作系统,Windows 与 Linux/macOS 在这里没有差别。下面这条是通用工程做法,不是该产品官方文档的内容:在 Windows 上用命令行 curl 手拼这段 JSON 时,cmd.exe 与 PowerShell 对双引号的处理跟 POSIX shell 不一样,很容易把 cache_control 那层嵌套引号转义坏;把请求体写进一个 .json 文件再用 --data @<你的项目目录>/body.json 发送,能省掉一整类”字段明明写了却像没写”的假象。密钥走环境变量,别写进命令行,示例里写 <YOUR_API_KEY> 占位。

五、怎么确认真的配对了

不要靠”感觉快了”来判断——那不是证据。文档给了三条查看途径:控制台的 Activity 页面上每条生成记录的详情、/api/v1/generation 这个 API,以及每次 API 响应里都带的 usage 对象。

最直接的是 usage 对象里的 prompt_tokens_details,文档给的结构长这样:

{
  "usage": {
    "prompt_tokens": 10339,
    "completion_tokens": 60,
    "total_tokens": 10399,
    "prompt_tokens_details": {
      "cached_tokens": 10318,
      "cache_write_tokens": 0
    }
  }
}

(这段是文档里的示例响应,里面的 token 数只是示例,不代表你会看到的数值。)

两个字段的语义,文档写得很清楚:

字段含义
cached_tokens从缓存读出的 token 数,大于零就说明命中了缓存
cache_write_tokens写入缓存的 token 数,在建立新缓存条目的那一次请求上出现

把这两条字段语义并在一起读,一次正常的配置应该呈现出这样的序列:第一次请求 cache_write_tokens 大于零、cached_tokens 为零;紧接着的同前缀请求反过来。(这一步是把上面两条字段释义拼起来得到的读法,文档没有把它写成一条排查流程。)如果你连发两次相同前缀的请求,两次的 cached_tokens 都是零,那说明这段前缀没有进缓存,回头查第二节那三个前置条件。

接口不同,字段挂的位置也不同:文档写明 Responses API 报在 usage.input_tokens_details,Chat Completions 报在 usage.prompt_tokens_details。另外响应体里还有一个 cache_discount 字段,具体数值本文不涉及;只提醒一点:文档明说某些上游的缓存写入会是负的 discount,缓存读取才是正的,看到一次负值不代表你配错了。

最后一句老话:这个平台的上游、模型与路由策略变得很勤,字段语义也在增补。上面每一条都请以官方文档最新内容为准。


本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。 该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。 该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单; 价格、额度与限流的具体数值请以官方定价页与用量说明为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。