缓存与幂等
提示词缓存与响应缓存的命中条件、请求头与计费,以及用 Idempotency-Key 避免重试重复扣费。
更新于 2026-10-23·阅读约 10 分钟
SciRouter 上有两层缓存,作用不同:
| 项目 | 上游提示词缓存 | SciRouter 响应缓存 |
|---|---|---|
| 缓存位置 | 模型厂商 | SciRouter 网关 |
| 命中条件 | 请求开头一段与之前的请求相同 | 整个请求与之前的请求完全相同 |
| 计费 | 命中部分按较低的缓存命中价计费,模型仍会推理 | 不调用模型,不计费 |
| 调用方操作 | 多数情况无需操作;单轮批量调用见单轮批量调用 | 对话将 temperature 设为 0 即自动缓存;其他情况带请求头 |
上游提示词缓存
#同一段长前缀(系统提示、参考文档、工具定义)重复发送时,上游会缓存该前缀。命中部分按模型详情页的缓存命中价计费,通常远低于输入价。
部分上游首次写入缓存时按缓存写入价计费,略高于输入价,之后几轮的命中可抵消这部分差价。Anthropic 协议的 1 小时缓存写入单独计价(cache_write_1h_input_tokens,通常更贵):模型未单独定价时按缓存写入价计,缓存写入价也未设置时按输入价计。模型未单独设置缓存价时,对应部分按输入价计费。
网关在以下两种上游上自动处理缓存:
| 上游类型 | 网关行为 |
|---|---|
| 需要显式标记缓存位置 | 输入较长(约 1000 token 起)且为多轮对话(system 之外至少两条消息)时,在工具定义、系统提示、上一轮末尾与最后一块内容上加标记 |
| 支持缓存分组键 | 计算分组键后发送。默认按 API Key 隔离;请求带 prompt_cache_key 时使用该键,仍按账号隔离 |
用量与计费
#| 位置 | 内容 |
|---|---|
usage.prompt_tokens_details.cached_tokens | 命中缓存的输入 token 数 |
usage.prompt_tokens_details.cache_write_tokens | 写入缓存的输入 token 数,仅在有写入时返回 |
响应体 scirouter.prompt_cache | 单次调用的缓存明细,见可观测性 |
| 控制台用量页的「上游缓存命中」一栏 | 一段时间内的命中率、命中与写入的 token 数、命中节省的配额 |
走 OpenAI 兼容入口时,usage 由上游原样返回;个别兼容服务只在顶层返回 prompt_cache_hit_tokens,两种写法均按其计费。
命中部分按缓存命中价计费,写入部分按写入价计费,均不免费。用量页的命中率 = 命中 ÷ 全部输入 token(普通输入、命中与两种写入之和)。
单轮批量调用
#一批请求共用同一段很长的系统提示、但每条只有一个问题时,网关不自动加标记。在需要缓存的内容段上加 cache_control:
json{
"model": "YOUR_MODEL_ID",
"messages": [
{
"role": "system",
"content": [
{ "type": "text", "text": "(很长的评测规则与参考资料…)", "cache_control": { "type": "ephemeral" } }
]
},
{ "role": "user", "content": "第 17 题:…" }
]
}- 同一请求最多保留 4 个标记,超出时保留最后 4 个。
- 需要标记的线路按标记缓存;其他线路按线路画像保留或去掉该字段(自动缓存的上游一律去掉,不影响缓存效果)。
- Anthropic 格式的请求经转换时,断点同样按画像带或不带,见 Messages「请求的转发方式」。
- 上游明确不支持该字段时,平台去掉后重发(响应头
X-SciRouter-Param-Actions中为healed=cache_control),调用方不会收到 400,见参数如何被处理。
提高命中率
#将不变的内容放在前面,变化的内容放在后面。系统提示开头写入当前时间、随机 id 等内容时,每次请求的前缀都不同,无法命中。
SciRouter 响应缓存
#对话的 temperature 显式为 0 时自动缓存:1 小时内完全相同的请求,第二次起直接返回上一次的结果,无需带请求头。对话接口的缓存规则:
| 条件 | 缓存行为 |
|---|---|
temperature 显式为 0,未带 X-SciRouter-Cache | 缓存 |
带 X-SciRouter-Cache: enabled | 缓存,不论 temperature 取值 |
带 X-SciRouter-Cache: disabled | 本次不缓存 |
未写 temperature(或不为 0),且未带请求头 | 不涉及缓存 |
向量接口(/embeddings)默认缓存,且按条缓存,见向量按条缓存。不需要时带 X-SciRouter-Cache: disabled。
以下示例带请求头,不依赖 temperature:
bashcurl https://api.scirouter.cn/v1/chat/completions \
-H "Authorization: Bearer $SCIROUTER_API_KEY" \
-H "Content-Type: application/json" \
-H "X-SciRouter-Cache: enabled" \
-d '{
"model": "YOUR_MODEL_ID",
"temperature": 0,
"messages": [{"role": "user", "content": "列出这段序列里所有的跨膜区段"}]
}'pythonimport os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SCIROUTER_API_KEY"], base_url="https://api.scirouter.cn/v1")
response = client.chat.completions.create(
model="YOUR_MODEL_ID",
temperature=0,
messages=[{"role": "user", "content": "列出这段序列里所有的跨膜区段"}],
extra_headers={"X-SciRouter-Cache": "enabled"},
)适用场景:批量评测反复运行同一套题、向量化含重复文本的批次、开发时反复调试同一请求。要求每次重新生成的场景不适用:命中时返回的回答与首次完全相同。
带 X-SciRouter-Cache: enabled 时不论 temperature 取值都缓存;温度不为 0 时,之后每次得到的都是首次那一份随机结果。
请求头
#| 请求头 | 取值 | 说明 |
|---|---|---|
X-SciRouter-Cache | enabled / disabled | 开启或关闭本次请求的缓存。对话 temperature 显式为 0 时不带该请求头也开启 |
X-SciRouter-Cache-TTL | 正整数(秒) | 本次结果的保存时长。对话默认 1 小时、向量默认 24 小时;超过上限(默认 24 小时)时按上限计 |
Cache-Control | no-cache | 不读取旧结果,调用模型后用新结果覆盖缓存 |
Cache-Control | no-store | 本次既不读取也不写入 |
X-SciRouter-Cache 不是上述两个取值、X-SciRouter-Cache-TTL 不是正整数时,请求直接返回 400。Cache-Control 的其他取值(如 max-age)被忽略。
命中状态
#响应头 X-SciRouter-Cache 的取值:
| 取值 | 含义 |
|---|---|
HIT | 来自缓存,未调用模型,不扣配额 |
MISS | 未命中,正常调用并计费;成功的结果会被缓存 |
PARTIAL | 仅向量接口:一批输入中部分命中,只有未命中的条目发给模型并计费,见向量按条缓存 |
BYPASS | 本应走缓存,但调用方要求不读取(Cache-Control: no-cache / no-store,或对向量、temperature 为 0 的对话带 X-SciRouter-Cache: disabled),或请求形状不可缓存,见不缓存的情况 |
| 无该响应头 | 本次不涉及缓存。例如对话未带 X-SciRouter-Cache: enabled,且 temperature 不是显式的 0(对话只带 Cache-Control 或 disabled 不会开启缓存) |
其他相关的响应头与响应体字段:
| 响应头 / 字段 | 说明 |
|---|---|
X-SciRouter-Cache-Age | 命中时返回:该结果产生至今的秒数 |
X-SciRouter-Cache-TTL | 结果将被保存时返回:实际保存的秒数。请求的时长超过上限时为截断后的值 |
scirouter.cache、scirouter.cache_age_s、scirouter.cache_ttl_s | 与上述响应头的结论相同,取值小写 |
scirouter.saved_quota | 命中时返回:按当前倍率计算的节省配额 |
scirouter.cache_items | 仅向量接口:{"hit": 命中条数, "total": 总条数} |
流式响应在 data: [DONE] 之前、choices 为空的扩展分片中也带这些字段。排队较久、心跳已先发出响应头时,响应头中不带这些信息,以扩展分片或请求日志为准。
命中时响应中的 usage 是首次调用的真实用量(与无缓存时相同),但本次不扣费;本次消耗见 scirouter.cost_quota(命中时为 0)。控制台请求日志中命中的记录带「缓存」标记,token 与费用记为 0;用量页的请求数、Tokens、配额均不含命中,命中次数单独列在「缓存命中」。
相同请求的判定
#按规范化之后的请求体比较:
- 键的顺序、空白、
1与1.0等写法差异不影响比较; stream、stream_options、user、safety_identifier、metadata、prompt_cache_key、store不参与比较。流式与非流式共用一份缓存,非流式保存的结果也可以流式回放;max_completion_tokens与max_tokens视为同一参数,n: 1与不写相同;- 其余任何字段不同(更换模型、修改一个标点、
temperature不同)均视为另一个请求。
向量按条缓存
#向量接口按每一条输入分别缓存,不按整批缓存:同一段文字无论出现在哪一批、排在第几条,都能命中。一批中只有部分命中时(PARTIAL),网关只将未命中的条目发给模型,再按原顺序拼回。计费只计发给模型的条目,命中部分记为节省的配额(scirouter.saved_quota);响应中的 usage 仍为整批输入的 token 数。
不缓存的情况
#- 要求多个候选(
n大于 1)或逐 token 概率(logprobs/top_logprobs)的对话:回放无法提供这些内容,响应头为BYPASS; - 带平台托管工具的请求(结果随网页内容变化);
- 失败的调用,以及未正常结束的回答:只保存
finish_reason为stop/tool_calls且有内容的结果,被截断(length)、被过滤(content_filter)或内容为空的均不保存; - 过大的结果:对话单条超过 256 KB,或向量中单条输入的结果超过 2 MB(按压缩前的大小计);
- 缓存服务自身故障时,一律按未命中处理,调用照常进行。
隔离与计费
#- 缓存按 API Key 隔离:同一账号下的不同 Key 互相看不到对方的缓存。
- 命中计入每分钟请求数(RPM),不计入每分钟 token 数与并发;命中不扣配额,因此不受 Key 月度上限、项目预算与套餐每日上限的约束。
- 平台更换该模型背后的上游,或管理员清空该模型的缓存后,旧结果不再命中。
- 响应缓存只作用于
/v1的调用;Playground 与 Chat 不使用响应缓存。
幂等
#网络抖动时客户端可能重试。为避免一次重试产生两次计费,在请求中带调用方自行生成的 Idempotency-Key:
pythonimport os
import uuid
from openai import OpenAI
client = OpenAI(api_key=os.environ["SCIROUTER_API_KEY"], base_url="https://api.scirouter.cn/v1")
key = str(uuid.uuid4()) # 同一个逻辑请求的所有重试用同一个值
response = client.chat.completions.create(
model="YOUR_MODEL_ID",
messages=[{"role": "user", "content": "帮我判断这段序列的二级结构倾向"}],
extra_headers={"Idempotency-Key": key},
)/chat/completions、/messages、/responses 与 /embeddings 支持该请求头。键最长 128 字节,按 API Key 隔离,各接口各自独立,保留 24 小时。下表与下方说明适用于这四个接口;图片接口同样接受该请求头,但按规整后的请求判定,b64_json 与流式的结果不保存,规则见图片「幂等」。
| 情况 | 结果 |
|---|---|
| 同一个键、同一个请求体 | 直接返回首次的响应体,带响应头 Idempotent-Replayed: true,不再调用模型、不再计费 |
| 首次请求尚未处理完,同一个键再次请求 | 409 idempotency_conflict |
| 同一个键、不同的请求体 | 409 idempotency_conflict,通常是代码将键复用到了其他请求 |
| 首次请求失败(非 2xx) | 不保存,可用同一个键修正后重试 |
| 首次请求被客户端先断开 | 服务端取消该次调用、不保存结果;之后同一个键的重试会重新调用模型并照常计费 |
| 流式请求 | 流未结束时,同一个键的重试返回 409;流结束后,同一个键重新执行一次并照常计费。流式结果无法重放 |
- 请求体按原始字节比较。重试时发送完全相同的字节(SDK 的自动重试即如此);重新拼接 JSON 导致键的顺序变化时返回 409。
- 重放的响应体中
scirouter.request_id仍为首次的编号,按它查询日志。X-SciRouter-*与x-ratelimit-*响应头不重放,重放也不产生新的日志记录。 - 两种 409 的
code相同,只有message不同。确定是重试同一个请求时,退避等待后再试,并设次数上限。 - 客户端超时应长于模型的正常耗时;回答可能很长时使用流式。
- 幂等存储自身故障时平台放行请求,不拒绝;极少数情况下可能重复执行一次。
幂等与响应缓存相互独立:幂等只识别调用方提供的键,保证同一个请求只计费一次;响应缓存按请求内容识别,使不同时间发出的相同请求共用结果。两者可同时使用。