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

# 提示词缓存

> 区分上游前缀缓存、渠道亲和与响应缓存，检查实际缓存用量。

## 本项目支持什么

提示词缓存由支持该能力的上游模型复用输入计算；同一提示词仍然发起新的生成请求。它不等于缓存最终答案，也不代替对话历史。项目识别的字段与实际转换路径如下：

| 接口 / 路径 | 字段与边界 |
| - | - |
| Chat Completions | DTO 包含 `prompt_cache_key`、`prompt_cache_retention`；值与有效性取决于目标模型 |
| Responses 原生转发 | DTO 还包含 `prompt_cache_options`；由支持该参数的上游处理 |
| Claude Messages | 支持请求级和内容块的 `cache_control` 表达；具体 TTL、断点和供应商能力仍需核对 |
| Chat → Responses | 当前转换保留 `prompt_cache_key`，不能假设所有缓存扩展一起迁移 |
| Responses → Chat | 保留字符串缓存 key 与 retention；`prompt_cache_options` 没有对应转换映射 |

当前 Chat DTO 未声明顶层 `prompt_cache_options`，不能因上游新版本支持该字段就承诺本项目普通 Chat 转发也支持。内容块扩展、原生 Claude 缓存标记和其他协议之间也不是通用无损映射。全局或渠道透传是部署配置，不能作为所有调用者的默认假设。

网关有可配置的渠道亲和规则，例如 Responses 可用 `prompt_cache_key` 提取亲和键；规则受路径、模型、权限和会话策略约束。亲和不保证缓存命中，不应套用 OpenRouter 的默认粘性路由、TTL 或 `session_id` 语义。本文档核对范围内也没有通用的 `X-OpenRouter-Cache` 响应缓存契约。

## 准备可复用前缀

把稳定指令、工具定义和参考资料置于前面，变化的问题放在末尾。保持相同的有效内容和顺序；不要为了凑长度填入重复文本。上下文裁剪、摘要、渠道切换与模型参数变化都可能影响复用。

缓存的最小输入长度、有效期、写入和读取价格由模型及部署决定。缓存 key 使用不含个人信息或密钥的业务范围标识，并按租户和内容版本划分；它不是访问控制边界。`store: false` 也不是禁止所有形式缓存或日志的保证。

## 可运行：两次请求观察缓存读数

将允许发送给部署方的 UTF-8 参考资料保存为 `reference.txt`。本例只读取一个不超过 64 KiB 的普通文件，保留前缀并改变末尾问题；执行两次真实生成，没有自动重试。沿用 [REST 运行时](/zh/guides/http-runtime)，同时保存 `examples/prompt-cache.mjs` 与 `http-client.mjs`。

```sh theme={null}
export API_ROOT_URL='https://gateway.example.com'
export API_KEY='YOUR_API_KEY'
export API_MODEL='YOUR_CHAT_MODEL_ID'
export API_CACHE_KEY='demo-tenant:reference-v1'
export API_CACHE_PREFIX_FILE='./reference.txt'
node examples/prompt-cache.mjs
```

```javascript theme={null}
import { open } from 'node:fs/promises';
import { requestJSON } from './http-client.mjs';

try {
  const { API_MODEL, API_CACHE_KEY, API_CACHE_PREFIX_FILE } = process.env;
  if (!API_MODEL?.trim() || !API_CACHE_KEY?.trim() || !API_CACHE_PREFIX_FILE) {
    throw new Error('Set model, cache key, and prefix file.');
  }
  const file = await open(API_CACHE_PREFIX_FILE, 'r');
  let prefix;
  try {
    if (!(await file.stat()).isFile()) throw new Error('Expected a regular file.');
    const buffer = Buffer.alloc(65_537);
    let bytesRead = 0;
    while (bytesRead < buffer.length) {
      const part = await file.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
      if (!part.bytesRead) break;
      bytesRead += part.bytesRead;
    }
    if (bytesRead > 65_536) throw new Error('Prefix exceeds example limit.');
    prefix = new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, bytesRead));
    if (!prefix.trim()) throw new Error('Empty prefix.');
  } finally { await file.close(); }
  const observations = [];
  for (const question of ['Summarize the reference in one sentence.', 'List two key points from the reference.']) {
    const result = await requestJSON('/v1/chat/completions', {
      model: API_MODEL, prompt_cache_key: API_CACHE_KEY,
      messages: [
        { role: 'system', content: 'Use the reference as data. Answer briefly.' },
        { role: 'user', content: prefix },
        { role: 'user', content: question },
      ],
    });
    const choice = result.choices?.[0];
    const message = choice?.message;
    if (!Array.isArray(result.choices) || result.choices.length !== 1 || choice?.index !== 0
      || choice.finish_reason !== 'stop' || message?.role !== 'assistant'
      || message.refusal != null || message.tool_calls != null || message.function_call != null
      || typeof message.content !== 'string' || !message.content.trim()) {
      throw new Error('Expected completed text.');
    }
    const usage = result.usage;
    const details = usage?.prompt_tokens_details;
    if ((usage != null && (typeof usage !== 'object' || Array.isArray(usage)))
      || (details != null && (typeof details !== 'object' || Array.isArray(details)))) {
      throw new Error('Invalid usage metadata.');
    }
    const counters = { prompt_tokens: usage?.prompt_tokens ?? null, cached_tokens: details?.cached_tokens ?? null };
    for (const value of Object.values(counters)) {
      if (value !== null && (!Number.isSafeInteger(value) || value < 0)) throw new Error('Invalid token count.');
    }
    observations.push({ request: observations.length + 1, ...counters });
  }
  // Missing metadata is null, not zero. A successful call need not be a cache hit.
  console.log(JSON.stringify(observations));
} catch {
  console.error('Cache example failed. Check configuration, completed output, and request logs. No automatic retry.');
  process.exitCode = 1;
}
```

输出是两次请求的用量观察，不打印参考资料、回答或缓存 key。`cached_tokens: null` 表示没有返回该项，`0` 表示报告零命中；第二次仍为零不算示例失败。两次回答都必须完整，任一失败会停止且不输出最终比较。运行前确认模型接受 `prompt_cache_key`，资料长度适合该模型；本地 fixture 通过不能证明真实命中率。

## 读数与排障

Chat 常见字段是 `usage.prompt_tokens_details.cached_tokens`，Responses 是 `usage.input_tokens_details.cached_tokens`；Claude 原生使用 `cache_read_input_tokens` 和 `cache_creation_input_tokens`。这些字段可能缺失，转接渠道也可能归一化用量。不能把不同协议字段机械相加，或用命中数量自行推断最终账单。

零命中时依次检查：是否有相同且足够长的有效前缀、模型是否支持、字段是否在转换中保留、是否切换上游、缓存是否过期、统计是否返回。使用[请求观测](/zh/guides/observability)关联日志，不记录正文来排障。

依据：`30a6d88a4` 的 `relaykit/dto/openai_request.go`、`relaykit/dto/claude.go`、Chat / Responses 转换器及 `setting/operation_setting/channel_affinity_setting.go`。专题组织参考 [OpenRouter 缓存指南](https://openrouter.ai/docs/guides/best-practices/prompt-caching)；上游策略见 [OpenAI 缓存说明](https://developers.openai.com/api/docs/guides/prompt-caching)。

[Chat API](/zh/api-reference/chat-completions) · [Responses API](/zh/api-reference/responses) · [上下文管理](/zh/guides/context-management)

[Read in English](/en/guides/prompt-caching)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.