- 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/ 各厂商缓存字段调研文档(代码注释引用)
85 lines
8.0 KiB
Markdown
85 lines
8.0 KiB
Markdown
# 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 伪代码)
|
||
|
||
```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. **两轮法**:第 1 轮建缓存(命中=0 或走写入字段),第 2 轮同前缀不同后缀(命中>0)。两轮间隔必须在缓存 TTL 内(Anthropic/Qwen 显式 = 5 分钟)。
|
||
2. **前缀 ≥2048 tokens**,避开各家阈值差异(256~4096 不等)。
|
||
3. **流式必须 `stream_options: {"include_usage": true}`**,否则 OpenAI 系协议流式响应没有 usage chunk;Kimi 流式末 chunk 带 usage;Anthropic 看 `message_start` 事件。
|
||
4. **语义差异**:OpenAI 系 `prompt_tokens` **包含**缓存部分;Anthropic `input_tokens` **不含**缓存部分(cache_read 另算)。对账时别混。
|
||
5. **区分两种「缓存」**:网关级响应缓存(result cache,命中时 usage 可能归零)≠ prompt 前缀缓存(KV cache,本实验目标)。
|
||
6. **聚合层要抓三个视图**:客户端响应 usage、网关账单/消费日志、可直连时的上游原生 usage——三者可能互不一致(new-api #6144 教训)。
|
||
7. **首轮 `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 |
|