提示缓存
在五折的基础上进一步降低输入成本:使用各厂商自带的提示缓存,Half Off Dev 会把每段对话送回持有其缓存的渠道。
五折只是起点。对于反复发送同一段前缀的场景,比如长对话、编程助手、基于固定文档集的检索问答,提示缓存还能把输入成本再压低一大截。模型最近处理过的前缀可以直接复用,复用的 token 按缓存读取单价计费。以 claude-sonnet-5.5 为例,缓存读取是每百万 token $0.05,未缓存输入则是 $1.00。
在 Half Off Dev 上用好缓存,靠的是两件事:
- 厂商自己的缓存机制。 完全按各厂商文档使用,
cache_control、prompt_cache_key等字段原样转发。 - 对话亲和性。 缓存只存在于写入它的那个渠道上。网关能识别同一段对话的后续轮次,并把它们送回该渠道,下一轮到来时缓存还在。
Claude:标记要缓存的部分#
Claude 只缓存你标记过的内容。在不变的前缀(工具定义、系统提示词、之前的轮次)的最后一个块上加 cache_control。最多可以设四个断点;也可以在请求顶层放一个 cache_control,缓存到最后一个块为止的全部内容。
response = client.messages.create(
model="claude-sonnet-5.5",
max_tokens=1024,
system=[{
"type": "text",
"text": PRODUCT_MANUAL,
"cache_control": {"type": "ephemeral"},
}],
messages=chat_history,
)
print(response.usage)usage 中的 cache_creation_input_tokens 是写入量(按缓存写入单价计费),cache_read_input_tokens 是读取量(按缓存读取单价计费)。短于模型最小长度的前缀不会被缓存。断点放在哪里、哪些改动会让缓存失效,见 Anthropic 缓存指南。
GPT:自动缓存,可选缓存键#
GPT 模型会自动缓存较长的前缀,不需要做任何标记。把不变的内容放前面、变化的内容放后面即可。为每段对话或每个用户设置一个固定的 prompt_cache_key 能提高命中率,网关也会用这个值把该对话固定在同一个渠道上。
response = client.chat.completions.create(
model="gpt-6.1-sol",
messages=chat_history,
prompt_cache_key="thread_51c7d0",
)
print(response.usage.prompt_tokens_details.cached_tokens)usage.prompt_tokens_details.cached_tokens 按缓存读取单价计费,prompt_tokens 的其余部分按输入单价计费。GPT 模型没有单独的缓存写入费用。详见 OpenAI 缓存指南。
Gemini:隐式缓存#
Gemini 模型会自动缓存重复的前缀。命中的 token 记在 usageMetadata.cachedContentTokenCount 中,按缓存读取单价计费,没有缓存写入费用。不提供通过 cachedContents API 的显式上下文缓存。
如何把多轮请求认成同一段对话#
网关会给每段对话生成一个指纹,取下表中第一个存在的来源:
| 来源 | API 格式 |
|---|---|
x-session-id 请求头 | 全部 |
x-halfoff-session 请求头 | 全部 |
x-claude-code-session-id 请求头(Claude Code 会发送) | 全部 |
请求体中的 metadata.user_id | Anthropic |
请求体中的 prompt_cache_key | OpenAI |
请求体中的 user | OpenAI |
| 系统提示词加第一条用户消息 | 全部 |
最后一条无需你做任何事。对话是靠追加消息往下进行的,系统提示词和第一条用户消息不会变,所以每一轮得到的指纹都相同。比较时只取两者各自的前 4,000 个字符。
指纹的作用范围是同一个 API 密钥、同一个模型、同一个渠道分组。用另一个密钥或换一个模型发送同一段对话,会重新建立固定关系。
固定关系能维持多久#
对话的第一次成功调用会把它固定到服务该调用的渠道上,之后的轮次优先发往这个渠道。
- 时长。 固定关系最长保持一小时,对话活跃期间会不断续期。闲置 30 到 60 分钟后失效,下一轮按新对话处理。
- 缓存已过期。 在 Anthropic Messages 格式下,如果对话的闲置时间超过了缓存的存活时间(5 分钟;若
cache_control断点设了ttl: "1h"则为 1 小时),固定关系也会结束。此时缓存已经没了,下一轮会重新分配渠道,而不是去等原来那个。 - 渠道繁忙。 被固定的渠道满载时,这一轮会稍等片刻(短提示词约 1 秒,长提示词最多 10 秒),而不是立刻换渠道、重新付一次缓存写入的钱。等过之后仍然满载,才会换渠道。
- 渠道故障。 被固定的渠道不健康时,这一轮会换到健康的渠道,固定关系随之转移。这一轮会写入新的缓存,之后的轮次再从中读取。
显式指定对话 ID#
如果第一条用户消息每轮都会变,比如你的应用会压缩或改写旧的历史记录,自动指纹就会失效。这时请为每段对话发送一个固定的 ID。请求头在所有格式下都有效:
curl https://halfoff.dev/v1/messages \
-H "x-api-key: $HALFOFF_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "x-session-id: thread_51c7d0" \
-H "content-type: application/json" \
-d '{"model": "claude-sonnet-5.5", "max_tokens": 1024,
"messages": [{"role": "user", "content": "接着上次的内容继续。"}]}'用请求体字段也可以:Anthropic 格式用 metadata.user_id,OpenAI 格式用 prompt_cache_key。每段对话用一个 ID;让许多无关的对话共用一个 ID,会把它们全挤到同一个渠道上。
在日志和账单中查看缓存#
日志中每次调用都会显示:
| 字段 | 含义 |
|---|---|
| 输入 | 未缓存输入加上缓存写入。 |
| 已缓存 | 从缓存读取的输入 token。 |
| 缓存写入(详情) | 写入缓存的输入 token。 |
| 缓存命中(详情) | 缓存 token 占全部输入的比例。 |
| 会话亲和(详情) | explicit:你提供了会话 ID。sticky:对话已固定,并回到了原渠道。new:首次调用,或固定关系已过期、原渠道不可用。none:无法生成指纹。 |
控制台概览页显示你所有调用的缓存命中率。每类 token 都有各自的五折单价,见计费公式。
命中率偏低?
如果一段对话每一轮都显示 new,说明它的系统提示词或第一条用户消息在轮次之间有变化。最常见的原因是系统提示词里带了时间戳,这同样会让厂商的缓存失效:把它挪到最新一条用户消息里,或者发送显式的会话 ID。如果显示 sticky 但 已缓存 一直为空,请确认前缀达到了模型的最小长度;对 Claude 还要确认带了 cache_control。