- utils/token_usage.py:usage 归一化补全缓存命中字段提取,覆盖 prompt_tokens_details/input_tokens_details.cached_tokens(OpenAI 系)、 顶层 prompt_cache_hit_tokens(DeepSeek)、顶层 cached_tokens(Kimi/Step)、 cache_read_input_tokens(Anthropic 系)、cachedContentTokenCount(Gemini); Anthropic 语义下把缓存读/写加回总输入以统一口径,normalize 保持幂等 - 对话级统计新增 total_cached_input_tokens 与 cache_exempt_input_tokens (首轮/深度压缩后首轮未命中缓存的输入视为冷启动成本,豁免出命中率分母; 压缩通过 cache_cold_start_pending 标记在下一次真实调用时判定) - token_update 广播与 token-statistics 接口同步携带新字段 - TokenDrawer 面板新增「累积缓存输入」「缓存命中率」(前端按 缓存/(总输入-豁免) 换算) - 深色模式下「当前上下文」数字由灰色 --accent 改为 --text-primary(白) - 附 cache_research/ 各厂商缓存字段调研文档(代码注释引用) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8.0 KiB
8.0 KiB
LLM API 缓存命中 Token 字段调研 · 总汇总
调研时间:2026-08-29 · 目的:为「验证各家 API 是否返回缓存命中 token」实验提供字段对照 详细报告:
official_overseas_v2/report.md(海外官方)、official_china/README.md(国内官方)、aggregators/report.md(聚合层) ⚠️ 全部为官方文档 + 社区证据调研结论,未经实请求验证;实验时以实际返回为准。
一、缓存命中字段总对照表
官方 API
| 提供商 | 命中字段完整路径 | 写入字段 | 自动/显式 | 最低门槛 | 命中价折扣 |
|---|---|---|---|---|---|
| OpenAI Chat Completions | usage.prompt_tokens_details.cached_tokens |
...cache_write_tokens(GPT-5.6+) |
自动(5.6+ 可显式断点) | 1024(5.5 及更早 2048) | 读 0.1×(5.6+)/ 0.5×(老模型) |
| OpenAI Responses API | usage.input_tokens_details.cached_tokens |
...cache_write_tokens |
同上 | 同上 | 同上 |
| Anthropic Claude | usage.cache_read_input_tokens(顶层) |
usage.cache_creation_input_tokens(另有 cache_creation.ephemeral_5m/1h_input_tokens 细分) |
显式 cache_control |
按模型 512/1024/2048/4096 | 读 0.1×、写 1.25×(5m)/ 2×(1h) |
| Google Gemini | usageMetadata.cachedContentTokenCount(SDK:cached_content_token_count) |
无 usage 内写入字段(显式缓存按资源 TTL 计费) | 隐式自动 + 显式 cachedContents | 隐式 2048(2.5)/ 4096(3.x) | 命中 ~0.1×(2.5+) |
| xAI Grok | usage.prompt_tokens_details.cached_tokens(Responses:input_tokens_details.cached_tokens) |
无 | 自动(建议 x-grok-conv-id/prompt_cache_key) |
未公布 | 有缓存价 |
| Mistral | usage.prompt_tokens_details.cached_tokens |
无 | 半显式(建议 prompt_cache_key) |
64 tokens 起,恒为 64 的倍数 | 读 0.1× |
| DeepSeek | usage.prompt_cache_hit_tokens(顶层!) + prompt_cache_miss_tokens |
无(自动) | 自动 | 未公布 | 读 ≈0.03×($0.014 vs $0.44,折扣最大) |
| Kimi / Moonshot | usage.cached_tokens(顶层);部分官方示例为 prompt_tokens_details.cached_tokens——两处都要读 |
无 | 自动(可用请求参数 prompt_cache_key 提命中率) |
未公布 | 读 0.1×~0.2×(k3 为 0.1×) |
| Qwen / DashScope | usage.prompt_tokens_details.cached_tokens;显式另有 cache_creation_input_tokens;Anthropic 兼容模式为 cache_read_input_tokens |
显式时上报创建量 | 隐式自动 + 显式 cache_control |
隐式 256(部分模型 2000)/ 显式块 1024 | 隐式读 0.2×;显式读 0.1×、写 1.25× |
| 智谱 GLM | usage.prompt_tokens_details.cached_tokens |
无 | 自动 | 512 | 读 0.5× |
| 豆包 / 火山方舟 | usage.prompt_tokens_details.cached_tokens |
创建接口响应同路径 | 仅显式(Context API / Responses API caching 参数) |
— | 缓存输入折扣价 + 存储费 |
| MiniMax | OpenAI 模式:prompt_tokens_details.cached_tokens;Anthropic 模式:cache_read_input_tokens |
Anthropic 模式:cache_creation_input_tokens |
自动 + 显式(Anthropic 模式) | 512 | 读 0.1×~0.2× |
| 阶跃 Step | usage.cached_tokens(顶层) |
无 | 自动 | 256 | 读 0.2× |
| 百度千帆 | usage.prompt_tokens_details.cached_tokens |
无 | 自动 | 未公布 | 读 0.4× |
聚合层 / 中转(实验时最容易踩坑的一层)
| 服务 | 缓存字段行为 | 关键坑 |
|---|---|---|
| OpenRouter | 规范化为 usage.prompt_tokens_details.cached_tokens + 扩展 cache_write_tokens / cache_discount / cost |
⚠️ 它另有「响应缓存」X-OpenRouter-Cache-Status: HIT——命中时 usage 全为 0,与 prompt 缓存是两回事;个别上游(如 DeepSeek)缓存不过网关 |
| opencode Zen / Go | Zen 价格表单列 Cached Read/Write(必然解析了上游缓存字段);「opencode go」= $10/月订阅服务,非 Go 语言版 | ⚠️ opencode 客户端流式解析有 bug(#33997):tokens_cache_read 恒 0——别看客户端展示值,抓原始 SSE |
| one-api / new-api / one-hub | 意图透传 cached_tokens,但流式渠道多个已证实 bug(字段清零/计费错误/负 token) |
⚠️ 客户端收到的 usage ≠ 网关账单;非流式作基线对照 |
| 国内中转站(packycode、灵眸AI 等) | 口碑「官转」站透传 Anthropic 原生 cache_creation/read_input_tokens 并按 5m cache write 计费;逆向接口站无缓存 |
社区验收标准=响应 usage 里有没有这两个字段 |
| LiteLLM / Portkey / CF AI Gateway | LiteLLM 双格式并存但 Anthropic 透传路径有 bug;Portkey 明确规范化;CF 未文档化(推测透传) | LiteLLM /v1/messages 路径不映射 cached_tokens(#27763) |
| 订阅制(Copilot/Cursor/Windsurf/Augment) | 无公开 per-request usage API;Cursor/Augment 面板展示 cache read/write(数据来自上游响应) | 无法从响应侧做本实验,跳过 |
二、实验用统一读取器(Python 伪代码)
def extract_cache_hit(usage: dict, body: dict | None = None) -> dict:
"""按优先级从各家 usage 中提取缓存命中 token 数。"""
u = usage or {}
details = u.get("prompt_tokens_details") or {}
in_details = u.get("input_tokens_details") or {}
candidates = [
("prompt_cache_hit_tokens", u.get("prompt_cache_hit_tokens")), # DeepSeek(顶层)
("cached_tokens@top", u.get("cached_tokens")), # Kimi / Step / 部分 DashScope(顶层)
("prompt_tokens_details", details.get("cached_tokens")), # OpenAI Chat / Qwen / GLM / MiniMax / 千帆 / xAI / Mistral / OpenRouter
("input_tokens_details", in_details.get("cached_tokens")), # OpenAI/xAI Responses API
("cache_read_input_tokens", u.get("cache_read_input_tokens")), # Anthropic / Bedrock / MiniMax-Anthropic / 中转站
]
hit = next(((k, v) for k, v in candidates if v), (None, 0))
# Gemini 走完全独立的 usageMetadata(camelCase),从响应体而非 usage 取
gemini = ((body or {}).get("usageMetadata") or {}).get("cachedContentTokenCount")
return {"hit_tokens": hit[1] or gemini or 0, "field": hit[0] or ("usageMetadata" if gemini else None)}
三、实验设计要点(三份报告的共同结论)
- 两轮法:第 1 轮建缓存(命中=0 或走写入字段),第 2 轮同前缀不同后缀(命中>0)。两轮间隔必须在缓存 TTL 内(Anthropic/Qwen 显式 = 5 分钟)。
- 前缀 ≥2048 tokens,避开各家阈值差异(256~4096 不等)。
- 流式必须
stream_options: {"include_usage": true},否则 OpenAI 系协议流式响应没有 usage chunk;Kimi 流式末 chunk 带 usage;Anthropic 看message_start事件。 - 语义差异:OpenAI 系
prompt_tokens包含缓存部分;Anthropicinput_tokens不含缓存部分(cache_read 另算)。对账时别混。 - 区分两种「缓存」:网关级响应缓存(result cache,命中时 usage 可能归零)≠ prompt 前缀缓存(KV cache,本实验目标)。
- 聚合层要抓三个视图:客户端响应 usage、网关账单/消费日志、可直连时的上游原生 usage——三者可能互不一致(new-api #6144 教训)。
- 首轮
cache_read=0是预期行为,不是字段丢失;写入字段(cache_creation_input_tokens/cache_write_tokens)>0 反而证明缓存机制在运作。
四、详细报告索引
| 报告 | 路径 | 覆盖 |
|---|---|---|
| 海外官方 | official_overseas_v2/report.md |
OpenAI / Anthropic / Gemini / xAI / Mistral / Bedrock / Azure |
| 国内官方 | official_china/README.md + usage_fields_reference.md |
DeepSeek / Kimi / Qwen / GLM / 豆包 / MiniMax / Step / 千帆 |
| 聚合层 | aggregators/report.md |
OpenRouter / opencode Zen·Go / one-api·new-api·one-hub / 中转站 / Copilot·Cursor·Windsurf·Augment / LiteLLM·Portkey·CF |