feat(stats): token 统计新增缓存命中追踪与命中率展示

- 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>
This commit is contained in:
JOJO 2026-08-29 12:16:48 +08:00
parent e4fdc82319
commit 3a4ea67e26
18 changed files with 8178 additions and 13 deletions

84
cache_research/SUMMARY.md Normal file
View File

@ -0,0 +1,84 @@
# 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+ 可显式断点) | 10245.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 | 隐式 20482.5/ 40963.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 透传路径有 bugPortkey 明确规范化CF 未文档化(推测透传) | LiteLLM `/v1/messages` 路径不映射 `cached_tokens`#27763 |
| **订阅制Copilot/Cursor/Windsurf/Augment** | 无公开 per-request usage APICursor/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 走完全独立的 usageMetadatacamelCase从响应体而非 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 chunkKimi 流式末 chunk 带 usageAnthropic 看 `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 |

View File

@ -0,0 +1,297 @@
# 聚合层调研报告:聚合 API / 中转服务 / coding plan 的「缓存命中 token」字段透传情况
- 撰写时间2026-08-29
- 调研人:子智能体 #3(聚焦聚合层)
- 配套调研(其他子智能体负责):官方海外 APIOpenAI/Anthropic/DeepSeek 等)、官方国内 API
- **重要说明**:本领域大量结论来自 GitHub issue、论坛/社区讨论而非官方文档。每条结论都标注了证据等级:
- **官方文档**:服务方官方文档/博客
- **官方源码**:服务方开源仓库源码(本文直接读取了 new-api 的 `relay/channel/openai/helper.go`
- **Issue 讨论**GitHub issue / 论坛讨论(含用户实测)
- **第三方调研**:独立第三方测评/文档(如 cuihuan/awesome-ai-gateway 的逐 commit 源码审查)
- **社区讨论**LINUX DO、Cursor 论坛、Reddit 等社区帖子
- **推测**:无直接证据,基于已有事实的合理推断;此类结论已明确标注「推测」
- 未找到明确证据的,一律写「未找到证据」。
---
## 1. 总览对照表
| 服务 | 是否透传/保留缓存字段 | 字段格式 / 重命名情况 | 流式中的表现 | 计费显示 | 证据等级 | 来源 |
|---|---|---|---|---|---|---|
| **OpenRouter** | ✅ 保留并**统一规范化**为 OpenAI 风格 | `usage.prompt_tokens_details.cached_tokens` + 自有扩展 `cache_write_tokens`、`cache_discount`、`cost`、`cost_details` | 需 `stream_options.include_usage=true`;末 chunk 带回 usage官方格式**OpenRouter 自身的响应缓存 HIT 时 usage 全为 0** | ✅ `usage.cost` 会按缓存读取折扣计价;`cache_discount` 表示本 generation 的缓存折扣Activity 页与 `/api/v1/generation` 可查 | 官方文档 | [OpenRouter chat completion 文档](https://openrouter.ai/docs/api/api-reference/chat/create-a-chat-completion)、[Prompt Caching 教程博客](https://openrouter.ai/blog/tutorials/prompt-caching-sticky-routing)、[Response caching 文档](https://openrouter.ai/docs/guides/features/response-caching) |
| **opencode开源 agent** | 客户端**解析**用法字段(含缓存),但 TUI 默认不显示 | `session.tokens_cache_read` / `info.tokens.cache.read` | 已知 bugOpenAI-compatible 流式路径下 `tokens_cache_read` 恒为 0上游明明返回了 `cached_tokens`#33997 | opencode 内部按模型计费TUI 不展示缓存明细(有多个第三方插件补足) | 官方源码(基于 issue 定位) + Issue 讨论 | [anomalyco/opencode#33997](https://github.com/anomalyco/opencode/issues/33997)、[#34296](https://github.com/anomalyco/opencode/issues/34296)、[#13003](https://github.com/anomalyco/opencode/issues/13003) |
| **opencode ZenPAUG 网关)** | 见下;同时提供 OpenAI 兼容 / Anthropic 兼容 / Gemini 兼容端点;**官方价格表单独列出 Cached Read / Cached Write 两列**(按模型计费,说明其必然解析上游缓存字段) | 端点协议原生格式(`v1/chat/completions` 走 OpenAI 格式,`v1/messages` 走 Anthropic 格式) | 未找到官方对流式 usage 的专门描述 | ✅ 官方按 Cached Read/Write 定价 | 官方文档(价格表)+ 第三方Bifrost 文档,见下) | [opencode.ai/docs/zen](https://opencode.ai/docs/zen)、[docs.getbifrost.ai OpenCode 页](https://docs.getbifrost.ai/providers/supported-providers/opencode) |
| **opencode Go订阅** | 见下「opencode go」= OpenCode Go 订阅服务($5 首月/$10 每月),**不是**「Go 语言版本」 | 同上 | 同上 | 订阅制,固定月费 + 用量限额,**不按缓存计费** | 官方文档 | [opencode.ai/docs/go](https://opencode.ai/docs/go)、[opencode.ai/zh/go](https://opencode.ai/zh/go) |
| **one-apisongquanpeng** | 大体透传上游 OpenAI 格式 usage**计费模型不含缓存折扣**`额度 = 分组倍率 × 模型倍率 × (提示 token + 补全 token × 补全倍率)` | OpenAI 风格(其主干只做 OpenAI 兼容转发) | 依赖 `stream_options.include_usage`README 中有可选 env `ENFORCE_INCLUDE_USAGE` | ❌ 计费不区分缓存命中;缓存 token 按全价输入计 | 第三方调研(逐 commit 源码审查)+ 官方 README | [awesome-ai-gateway virtual-keys-metering](https://github.com/cuihuan/awesome-ai-gateway/blob/main/docs/virtual-keys-metering.zh-CN.md)、[one-api README](https://github.com/songquanpeng/one-api) |
| **new-apiQuantumNous** | ✅ 转发路径基本保留缓存字段OpenAI 渠道流式 `*usage = lastStreamResponse.Usage` 整体拷贝);**但存在多个已证实的 bug**:自定义渠道/火山方舟流式把 `cached_tokens` 打成 0#5672xAI 渠道流式转发对但内部计费 usage 损坏(#6144缓存命中导致输入 token 变负数(#5003/#5005缓存写入 token 未计费(#6353 | OpenAI 风格 `prompt_tokens_details.cached_tokens`;清理/重建 usage 时会注入大量默认字段(`text_tokens/audio_tokens/claude_cache_creation_*` 等) | 多个渠道的流式 usage 处理有 bug见上「透传模式」直连上游→字段原样 | ⚠️ 内部计费有 `CacheRatio` + `CacheCreationRatio`5m/1h 拆分),但多个 bug 导致缓存计费错误甚至倒扣 | 官方源码 + Issue 讨论 + 第三方调研 | new-api#6144、#5672、#5003、#6353源码 `relay/channel/openai/helper.go`awesome-ai-gateway 文档 |
| **one-hubMartialBE** | ✅ 基本透传;**曾被证实 Responses API 的 `cached_tokens``omitempty` 标签被省略**,导致 Codex CLI 报 `missing field 'cached_tokens'`已修复PR #910 | OpenAI 风格 | Responses SSE 的 `input_tokens_details.cached_tokens` 曾缺失(已修复) | v0.14.26 起为 Bedrock 渠道的 Claude 增加 prompt caching 支持;计费沿用 one-api/new-api 体系 | Issue/PR 讨论 + Release 说明 | [one-hub PR #910](https://github.com/MartialBE/one-hub/pull/910)、[Release v0.14.26](https://github.com/MartialBE/one-hub/releases) |
| **国内中转站packycode、灵眸AI 等)** | 参差不齐:宣称「官转」的站会解析并透传 usagepackycode 明说「透传用户的请求…解析 claude 传来的 usage tokens」部分站逆向接口不缓存 | Anthropic 原生格式Claude Code 场景)或 OpenAI 风格 | 实测有的站「完整透传 `cache_creation_input_tokens` / `cache_read_input_tokens`灵眸AI | ⚠️ 中转站按 usage 计费,且**默认按 5m Cache Write 计缓存**packycode缓存命中占比极高用户实测 82.9% cache read | 社区讨论 | LINUX DO 帖、fulitimes 博客,见 §5 |
| **GitHub Copilot** | 终端用户**拿不到 per-request usage**(订阅制)。订阅用量属 token 配额制2026-06 起转 token 计费);企业版 REST metrics API 只给每日聚合 `prompt_tokens_sum/output_tokens_sum`**无缓存拆分**VS Code 的 OTLP 指标不暴露 cached input | 其内部 OpenAI 兼容后端 SSE **会**把 `prompt_tokens_details.cached_tokens` 与 DeepSeek 原生 `prompt_cache_hit_tokens` 透给客户端社区实测free 计划 DeepSeek | 同上(社区实测见原始 SSE | 订阅/token 配额内,无 per-request 缓存折扣展示 | 官方文档 + Issue/社区实测 | GitHub REST Copilot metrics 文档、microsoft/vscode#317837、obsidian-copilot discussion #2380 |
| **Cursor** | 订阅与 BYOK 的用量面板都**展示 Cache Read / Cache Write**官方客服口径usage 报告里显示的是「AI provider 随响应返回的精确 token」BYOK 直连时缓存字段来自 Anthropic/OpenAI | Anthropic/OpenAI 原生 | 多个论坛帖证实 Auto 模式曾路由到不支持缓存的模型导致 cache=0版本问题 | ✅ 面板单列 Cache Read/Write 并计费cache read 价约输入价 10% | 社区讨论(官方客服回复)+ 官方文档未直接确认 | Cursor 论坛帖,见 §6 |
| **Windsurf** | 订阅/credits 制;**计量按 token 且明确区分 cache-read 单价**(如 Sonnetinput 90 credits/M、cache read 9 credits/M、output 450 credits/M说明网关侧跟踪缓存 token | 不暴露原始 usage 给用户,走 credits 换算 | 未找到 per-request usage 暴露证据 | ✅ cache-read 以低价 credit 计费 | 第三方文档 + 官方价格说明 | flexprice.io、Windsurf 官方文档(见 §6 |
| **Augment Code** | token 计费制官方文档明说「自动缓存稳定上下文cached input 按供应商缓存价(约 10%计费」Usage 面板展示 input/output/cache read/cache write 单价 | 不暴露原始 usage 字段 | 未找到 | ✅ 缓存读取按折扣计费 | 官方文档 | [docs.augmentcode.com/models/token-based-pricing](https://docs.augmentcode.com/models/token-based-pricing) |
| **Cloudflare AI Gateway** | 作为透明代理转发(推测透传 usage**官方文档未明确描述缓存 usage 字段的保留/规范化**;其自带「响应缓存」是网关级缓存(`cf-aig-cache-status: HIT/MISS`),与 prompt cache 是两回事;社区实测 `cache_control` 请求体能透传 | 上游协议原样 | 未找到官方文档 | 网关自己的日志/analytics 记录 token usage 供计费统计,不向调用方展示 | 官方文档(缓存功能)+ Issue 讨论cache_control 透传) | Cloudflare AI Gateway docs、openclaw#46709 |
| **Portkey** | ✅ **明确规范化到 OpenAI 格式并保留缓存字段**`prompt_tokens = input + cache_read + cache_creation``cached_tokens` 出现在 `prompt_tokens_details`|Bedrock 场景有明确文档) | Portkey 透传模式下响应按供应商原样;其观测端展示 `cached_tokens` | ✅ 定价公式单独处理 base input / cache read / cache write | 官方文档 | [Portkey Bedrock Prompt Caching](https://docs.portkey.ai/docs/integrations/llms/bedrock/prompt-caching)、[Portkey docs](https://docs.portkey.ai/docs/integrations/llms/openai/prompt-caching-openai) |
| **LiteLLM** | ✅ OpenAI 兼容端点规范化到 OpenAI 风格 `prompt_tokens_details.cached_tokens`,同时在同一 usage 对象中保留 Anthropic 原生 `cache_creation_input_tokens` / `cache_read_input_tokens`**但 Anthropic `/v1/messages` 透传路径不把原生字段映射到 `cached_tokens`,导致指标/计费不识别缓存bug #27763** | 双格式并存OpenAI 风格 + Anthropic 原生) | 流式 usage 合成有历史 bug如 synth chunk 的 `choices` 非空);默认不强制 include_usage | ⚠️ 有独立 cache read/write 单价,但多个计费 bug缓存 token 按全价算(#26807多收 1.67×、cache write 未计入(#33772等 | 官方文档 + Issue 讨论 + 第三方调研 | litellm docs Prompt Caching、#27763、#26807、#33772、awesome-ai-gateway |
| **Vercel AI Gateway顺带** | 面板正确展示 cache read但**缓存 token 按全价输入计费**Kimi 案例 6× 成本) | 上游协议原样 | 未细查 | ⚠️ 计费不应用缓存折扣issue 讨论) | Issue 讨论 | [vercel/ai#13907](https://github.com/vercel/ai/issues/13907) |
---
## 2. OpenRouter重点
**结论先行**OpenRouter 是少数把「缓存命中 token」做成**一等公民**的聚合层——它把各上游Anthropic/OpenAI/Gemini/DeepSeek…的缓存字段**统一规范化**成 OpenAI 风格的 `prompt_tokens_details.cached_tokens`,并增加自有扩展字段 `cache_write_tokens`(缓存写入)与 `cache_discount`(本次缓存折扣金额)。
### 2.1 usage 字段是否原样透传 / 规范化成什么
- 官方 API 参考(`ResponseUsage` 类型):
- `usage.prompt_tokens` / `completion_tokens` / `total_tokens`
- `usage.prompt_tokens_details.cached_tokens`"Tokens cached by the endpoint"+ 可选 `cache_write_tokens`"Tokens written to cache (models with explicit caching)"
- 另有 `completion_tokens_details.reasoning_tokens`、`cost`、`cost_details`(含 `upstream_inference_prompt_cost` 等)、`is_byok`、`server_tool_use_details` 等 OpenRouter 扩展。
- 官方示例:`"usage": { "prompt_tokens": 10339, "completion_tokens": 60, "total_tokens": 10399, "prompt_tokens_details": { "cached_tokens": 10318, "cache_write_tokens": 0 } }`。
- 也就是说:**Anthropic 的 `cache_read_input_tokens` 会被折算进 `cached_tokens`**并参与折扣计费。OpenRouter 官方博客明确说明:缓存读取价格约为正常输入价的 0.1×0.5×Anthropic/DeepSeek/Qwen 0.1×OpenAI 0.25×0.5×……)。
- 没有找到 OpenRouter 会把 Anthropic 原生 `cache_creation_input_tokens` 原样透传的证据——它统一到 OpenAI 风格。OpenRouter 自己的扩展字段就叫 `cache_write_tokens`
### 2.2 流式响应
- 与 OpenAI 相同:需 `stream_options: { include_usage: true }`,最后一个 SSE chunk 带 `usage`(官方博客称**每个响应都包含** `usage.prompt_tokens_details``cached_tokens`/`cache_write_tokens`)。
- 注意OpenRouter 官方「Response caching响应缓存」是**另一回事**——它缓存的是整条响应(`X-OpenRouter-Cache-Status: HIT/MISS` 头);**HIT 时返回的 usage 是 `prompt_tokens: 0, completion_tokens: 0, total_tokens: 0`**官方文档示例。实验时不要把「OpenRouter 响应缓存」当成「prompt cache」。
### 2.3 计费显示
- `usage.cost` 体现缓存折扣后的实际金额;`usage.cost_details` 细分上游各项成本;`cache_discount` 表示本 generation 因缓存省下/付出的金额(写入缓存的那一轮可能为负折扣,因为写缓存更贵)。
- Activity 页面与 `GET /api/v1/generation` 可逐条查看 `cached_tokens` / `cache_write_tokens` / `cache_discount`
- 社区实测2026-07china-llm.comGLM-5 经 OpenRouter 重复调用返回 3200 cached tokens、价格降 75%;同时**同一前缀 DeepSeek 经 OpenRouter 报 0 cached tokens**(原生端点几分钟内有 98% 命中)——**说明 OpenRouter 某些模型/上游不保留缓存,不能一概而论**。Paul's Programming Notes 也实测 Kimi K3 的缓存折扣「过不了 OpenRouter」。
- 第三方安全测评Tarun Chitra 文章)指出:存在供应商「把缓存 token 按全额重新计价」的多收费现象OpenRouter 本身对上游的缓存识别并不总是生效——意味着 **`cached_tokens` 字段是否存在、是否 >0可作为判断上游是否真正给了缓存折扣的观测点**。
**证据等级**:缓存字段设计=官方文档;折扣细节=官方博客;个别模型缓存不过网关=第三方实测上游「repricing」问题=第三方文章。
### 2.4 来源
- https://openrouter.ai/docs/api/api-reference/chat/create-a-chat-completion 官方ResponseUsage 定义/示例)
- https://openrouter.ai/blog/tutorials/prompt-caching-sticky-routing (官方,缓存字段与折扣)
- https://openrouter.ai/docs/guides/features/response-caching (官方,响应缓存 HIT 时 usage 归零)
- https://china-llm.com/blog/openrouter-prompt-caching 第三方实测2026-07-28
- https://www.paulsprogrammingnotes.com/2026/08/kimi-k3-cache-discount-openrouter.html (第三方实测)
---
## 3. opencode / opencode Zen / opencode Go
### 3.1 先说清楚「opencode go」是什么任务要求查清
- `opencode`sst/opencode现仓库 `anomalyco/opencode`,作者 Anomaly前 SST 团队)是**用 Go 写的开源 terminal coding agent**MIT
- **「opencode go」= OpenCode Go**,是 Anomaly 推出的**低价订阅服务**(首月 $5之后 $10/月),提供一批开源/开源权重 coding 模型Kimi、GLM、MiniMax、DeepSeek、Qwen、Grok、GPT-5.6 Luna 等)。**它不是「Go 语言版本的 opencode」而是「一个叫 Go 的订阅套餐」**。它诞生背景是 Anthropic 2026-01 禁止第三方工具使用 Claude 订阅凭据后Anomaly 顺势推出的三个订阅产品之一:**Go$10/月开源模型)**、**Zen按量付费网关**、Black企业网关
- 官方描述Go 是面向国际用户的低成本订阅,通过 OpenAI 兼容 / Anthropic 兼容端点提供Docker 文档确认:`openai_chatcompletions`base URL 为 opencode.ai 的 Go 端点MiniMax/Qwen 等走 Anthropic 客户端)。**订阅制=固定月费+用量限额,不按 token/缓存计费**,因此对「缓存命中计费」不敏感——用户看不到用量明细。
- Zen 才是按量付费:`https://opencode.ai/zen/v1/chat/completions`OpenAI 兼容)、`/v1/messages`Anthropic 兼容)、`/v1/responses`OpenAI Responses、Gemini 风格端点。
### 3.2 Zen 是否保留/计费缓存字段
- **官方价格表opencode.ai/docs/zen对每个模型单独列出 `Cached Read` 和 `Cached Write` 两列单价**(如 MiniMax M3Input $0.30/M、Output $1.20/M、Cached Read $0.06/MClaude SonnetCached Read $0.20/M、Cached Write $2.50/MQwen 3.7 PlusCached Read $0.04、Cached Write $0.50)。**既然按缓存读取/写入单独定价Zen 网关必然解析上游响应里的缓存 usage 字段**——这是「Zen 保留缓存字段」的最强官方证据(间接)。
- 第三方佐证——Bifrost 的 OpenCode provider 文档docs.getbifrost.aiBifrost 用同一套 OpenCode Zen/Go provider 实现):
- OpenCode 返回 `usage.prompt_tokens` / `usage.completion_tokens` / `usage.total_tokens` / **`usage.prompt_tokens_details.cached_tokens`** / `usage.completion_tokens_details.reasoning_tokens`
- 「有些模型上报 `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`Bifrost 会把这些映射成标准 `cached_tokens` 参与定价计算」。
- 「缓存行为取决于底层供应商;有的模型(如 Go 上的 DeepSeek V4 Flash可能根本不缓存」。
- opencode 客户端侧:`packages/llm/src/protocols/openai-chat.ts` 的 `mapUsage` 会映射 `prompt_tokens_details.cached_tokens`issue #33997 里确认);会话级字段 `session.tokens_cache_read`、`info.tokens.cache.read`。**但存在一个已知 bugOpenAI-compatible自定义 baseURL如 LiteLLM 代理)流式路径下 `tokens_cache_read` 恒为 0即使上游 SSE usage chunk 里明明有 `cached_tokens`(实测 5888/6004 ≈98% 命中)**#339972026-06。即**opencode 客户端本身对流式缓存的解析有坑,实验者别只看 opencode 的展示值**。
- opencode TUI 默认不展示缓存明细——有多个第三方插件补足opencode-visual-cache、opencode-cache-hit、oc-plugin-caching说明多模型含 Zen返回的缓存 usage 是**可达**的(插件从 opencode session API 读 `tokens`/`cost`)。
**回答五个问题**
1. 透传?— Zen/Go 后端按协议原生透传(表单即 OpenAI/Anthropic 兼容格式opencode 客户端解析 `cached_tokens`OpenAI 兼容路径有流式 bug
2. 规范化?— 未见 Zen 官方文档说明是否统一改名Bifrost 实现会把 `prompt_cache_hit_tokens` 映射成 `cached_tokens`。未找到 Zen 对 Anthropic 端点的缓存字段改名证据(推测为 Anthropic 原生格式透传)。
3. 流式?— opencode #33997 证实上游流式 chunk 带 `cached_tokens`,但 opencode 展示为 0客户端 bug
4. 计费?— Zen 按 Cached Read/Write 单价收费官方价格表Go 订阅制不按缓存计费。
5. 来源 — 见下。
来源https://opencode.ai/docs/zen、https://opencode.ai/docs/go、https://opencode.ai/zh/go、https://docs.getbifrost.ai/providers/supported-providers/opencode、https://github.com/anomalyco/opencode/issues/33997、#13003、#23109、#34296、https://ai.miraheze.org/wiki/OpenCode_Go第三方介绍、https://thomas-wiegold.com/blog/opencode-go-review第三方评测
---
## 4. 开源自建中转网关one-api / new-api / one-hub
### 4.1 one-apisongquanpeng
- 主干是「OpenAI 兼容格式 `chat/completions` 转发」,上游 OpenAI 协议响应的 usage 基本原样转发;但**计费模型完全没有缓存折扣**:官方 FAQ 的额度公式 = 分组倍率 × 模型倍率 ×(提示 token 数 + 补全 token 数 × 补全倍率)。
- 第三方逐 commit 源码审查cuihuan/awesome-ai-gateway, 2026-07-29结论
- one-api 的 `quota = ceil((promptTokens + completionTokens*completionRatio) * ratio)`**全仓没有 cache read/write 单价**(六网关对比中 one-api/Kong/Higress 是仅有的三家无独立缓存价格的)。
- one-api 最新 commit 停留在 2025-02-21v0.6.10),计量代码比 new-api 旧约 17 个月;流式 usage 缺失时用 tiktoken 兜底重算(但仅对 gpt-3.5/4 前缀建了真编码器Claude/Gemini 流会退回 gpt-3.5-turbo 编码器)。
- 用户视角:**客户端拿到的 usage 里缓存字段大概率保留OpenAI 渠道),但账单不会给缓存折扣**。
- 流式README 有可选环境变量 `ENFORCE_INCLUDE_USAGE`(是否强制在 stream 下返回 usage
- 未找到 one-api 专门讨论 cached_tokens 透传的 issue搜索「one-api cached_tokens」无直接命中其 issue #204 是登录 token 缓存导致额度超额,与 prompt cache 无关,不采用)。
**结论**one-api = usage 大体透传、缓存字段保留与否取决于客户端是否请求 include_usage**计费无缓存折扣**。证据等级:第三方源码审查 + 官方 README。
### 4.2 new-apiQuantumNousone-api 的主要活跃 fork包装「官转」最多的底座
- **转发路径**OpenAI 渠道流式 `handleLastResponse``*usage = lastStreamResponse.Usage`**整体拷贝**`cached_tokens` 保留)——本文直接读取源码 `relay/channel/openai/helper.go` 确认。非流式 `xAIHandler` 直接 `return xaiResponse.Usage, nil`
- **但有一批已证实的 bug全部为 issue 讨论 + 部分有源码定位)**
1. **#6144xAI 渠道)**:流式 handler 双路径分叉——转发给客户端的 usage 是完整的(`cached_tokens=1792` 正确返回),但内部计费用的是手动重建的残缺 usage只拷 3 个标量),`cache_tokens` 记成 0缓存 token 全按全价计费。非流式正常。已提交修复 PR #6145`*usage = *xAIResp.Usage` 整体拷贝)。**「客户端看到的 usage 是对的,网关自己计费是错的」的典型例子**。
2. **#5672(自定义渠道/火山方舟)**:流式模式下 `usage.prompt_tokens_details.cached_tokens` 恒为 0`prompt_tokens` 从 3513 膨胀到 4540+29%`reasoning_tokens` 被清零,并被注入大量默认字段(`text_tokens:0, audio_tokens:0, claude_cache_creation_*:0` 等。非流式正常。已关闭not planned
3. **#5003 / #5005(缓存命中→输入 token 为负数)**:上游按 Anthropic 排除语义返回cache read 已从输入中排除new-api 又减了一次,输入算出 16,638账单反而「倒贴」给用户第三方文档给出可复算算术。重视用户实测「站长亏损」。
4. **#6353Claude 缓存写入 token 未计费)**5m/1h TTL 拆分缺席时级联 bug 把 cache creation 值清零,最贵的写入 token 打了 100% 折。开放中。
5. **#1103Gemini reasoning 未计费,开放 14 个月)**`completion_tokens`124不含 `reasoning_tokens`109790% 输出 token 未计费(属推理字段,非缓存,顺带记录)。
- **透传模式**new-api 的 issue 模板明确写「透传模式会直接转发请求,请自行确认上游行为;开启透传后的转发相关反馈不接受 issue」→ **存在「透传(直连上游)」开关,开启后缓存字段随上游原样返回**;反之普通中继模式会走上面的 usage 规范化逻辑(可能补默认字段、改计数)。
- **计费**`service/text_quota.go`OpenAI 语义)`promptQuota = (PromptTokens - CacheTokens) + CacheTokens * CacheRatio`,并有 `CacheCreationRatio`5m/1h 拆分)——**new-api 是少数原生支持缓存折扣计费的开源网关**,但 bug 多。
**结论**new-api「会」保留缓存字段多个渠道/修复后),但**流式+自定义渠道/部分内置渠道历史上会丢/损坏缓存字段或计费错误**;实验透过 new-api 必须同时看「客户端收到的 usage」与「网关消费日志/账单」两处。证据等级官方源码helper.go + issue 中源码定位)+ issue 讨论 + 第三方调研awesome-ai-gateway
### 4.3 one-hubMartialBEone-api 的另一活跃 fork
- 与 new-api 同源(都 fork 自 one-api能力上对齐 new-api 的缓存计费方向README 称「支持更多模型」)。
- **直接证据PR #9102026-01由 done-hub 转来)——「修复 Responses API cached_tokens 字段缺失问题」**:原代码对 `ResponsesUsageInputTokensDetails.CachedTokens` 用了 `omitempty` 标签,**值为 0 时字段被省略**,导致 Codex CLI 解析 `response.completed` 事件时报 `missing field 'cached_tokens'` 并无限重试。修复=移除 omitempty 保证零值也输出。→ **说明网关在 Responses 路径会把 `cached_tokens` 弄丢(至少历史版本)**
- Release v0.14.26:「为通过 AWS Bedrock 渠道访问的 Claude 模型添加 prompt caching 支持」PR #850)→ one-hub 主动做缓存透传/支持。
- 计费沿用 one-api/new-api 体系new-api 特性 `CacheRatio` 等是否完全同步需逐个版本核对,未找到独立证据)。
**结论**one-hub 基本透传,但历史上有 Responses API 丢 `cached_tokens` 的 bug 并已修复;实验者用 Codex Responses 端点时建议对照上游原始响应。证据等级PR 讨论 + Release 说明。
### 4.4 来源汇总
- https://github.com/songquanpeng/one-api README额度公式、ENFORCE_INCLUDE_USAGE
- https://github.com/QuantumNous/new-api/issues/6144 、#5672 、#5003 、#5005 、#6353 、#1103
- https://raw.githubusercontent.com/QuantumNous/new-api/main/relay/channel/openai/helper.go (源码)
- https://github.com/MartialBE/one-hub/pull/910 、https://github.com/MartialBE/one-hub/releases v0.14.26
- https://github.com/cuihuan/awesome-ai-gateway/blob/main/docs/virtual-keys-metering.zh-CN.md (第三方逐 commit 审查2026-07-29含上述 issue 的状态核实与可复算算术)
---
## 5. 国内常见中转/拼车 API 站packycode、灵眸AI 等)与缓存计费讨论
### 5.1 packycodePackyAPI自称「官转」
- LINUX DO 官方商家帖2025-07「Packycode 的计费保持和官网的 api 计费方式一样」「**我们会透传用户的请求(保护隐私),最后解析 claude 传过来的 usage tokens我们默认使用 5m Cache Writes 做 cache 的计费**」——**明说基于上游 usage 计费、缓存按 5m cache write 计费**。同时有用户问「Claude code 拼车的时候,背后是 Claude code 的池子,不会没有办法命中 cache 吗」——官方回复大意:全局用 Claude Code 的话缓存命中由 Claude Code 自管,实际消耗不大。
- GitHub 宣传页2026PackyAPI 主站按量付费、计费对标 Claude/OpenAI 官网价格Codex 有独立包月站。
- 用户实测(什么值得买/其他帖Claude Code 场景 cache read 占输入大头(另一帖统计 82.9% cache read / 15.6% cache write / 1.5% fresh input**cache 命中基本决定中转实际价格**。
### 5.2 灵眸AI 等(社区实测透传)
- fulitimes 博客2026Claude Code 缓存指南「实测灵眸AI **完整透传** `cache_creation_input_tokens``cache_read_input_tokens` 这两个字段,可在后台账单中查看每次请求的 cache 命中情况」;并警告「**很多便宜平台用逆向接口,不支持 Prompt Caching**——表面价低但无缓存差距」;验证方法=在响应 usage 里查这两个字段是否存在。
- 知乎/博客普遍教程:判断中转是否支持缓存的唯一方法是看响应 usage 里有没有 `cache_creation_input_tokens` / `cache_read_input_tokens`Anthropic 风格)。说明**社区已把「usage 缓存字段是否透传」当作中转站质量的验收标准**。
### 5.3 结论(针对四个问题)
1. 是否透传缓存字段:**参差不齐**。口碑「官转」站大多解析上游 usage 并据此计费packycode 明说灵眸AI 实测透传);逆向/低价接口通常无缓存。**没有统一规范**。
2. 规范化/改名一般保持上游协议原生Claude Code 场景=Anthropic 原生字段OpenAI 兼容场景=OpenAI 风格)。
3. 流式Claude Code 流式 usage 走 Anthropic `message_start`/`message_delta`;有 issue 表明 Claude Code 类客户端对 messageDelta 里的缓存计数有兼容问题cline#4346 讨论 Anthropic API 在 messageDelta 增加累计缓存计数的兼容问题)。
4. 计费显示:中转站按解析后的 usage 计费并**普遍把缓存写入按 5m 档定价**1.25×输入价),缓存读取按 0.1×用户可看到余额消耗部分站如灵眸AI后台可查 cache 命中明细。
- 证据等级:除 GitHub 宣传页外几乎全部为社区讨论/用户实测(无官方文档)。**未找到「中转站统一丢弃缓存字段」的系统性证据**;相反,多个实测表明主流中转会透传。
来源:
- https://linux.do/t/topic/771392 Packycode 计费说明帖)
- https://linux.do/t/topic/1620430 cache read 占比 82.9% 实测)
- https://linux.do/t/topic/2591545 Sub2API 中转 Claude Code 消耗统计)
- https://blog.fulitimes.com/claude-code-cost-optimization 灵眸AI 透传实测、逆向接口无缓存)
- https://github.com/CherryHQ/cherry-studio/discussions/15278 Feiyuan API「原生透传 cache_control」的站长自述Claude 中转缓存讨论)
- https://github.com/cline/cline/issues/4346 Anthropic messageDelta 缓存计数的客户端兼容问题)
---
## 6. 订阅制 coding planGitHub Copilot / Cursor / Windsurf / Augment Code
统一先回答「是否向终端用户暴露 token usage/缓存信息」:**多数不暴露原始 per-request usage但 Cursor/Augment 等会在用量面板里展示缓存拆分明细Copilot/Windsurf 只给聚合/credit 换算后的信息**。
### 6.1 GitHub Copilot
- 经典订阅制token 配额):用户拿不到 per-request usage。2026-06 起逐步转 token 计费Medium/官方博客)。
- 企业版提供 REST Copilot usage metrics APIenterprise/org 级):返回**每日聚合**的 `prompt_tokens_sum`、`output_tokens_sum`、`avg_tokens_per_request` 等,**没有缓存 token 拆分字段**(官方文档示例可见)。→ 官方聚合指标里**看不到 cached tokens**。
- VS Code 内 OTLP 指标microsoft/vscode#317837 确认 **Copilot Chat 的 OTLP metrics 不暴露 cached input token usage**;但 GitHub 定价区分 normal input 与 cached input说明**平台侧在按缓存计费**,只是不暴露给用户)。
- 有趣的实证obsidian-copilot 的讨论(#2380贴出免费 Copilot 计划DeepSeek v4的**原始 SSE**——`usage.prompt_tokens_details.cached_tokens` 和顶层 `prompt_cache_hit_tokens`/`prompt_cache_miss_tokens` **都原样出现在流里**(总计 128 cached。→ **Copilot 的 OpenAI 兼容后端(至少 DeepSeek 路径)会把缓存字段透传给流式客户端**,尽管官方不提供 per-request 文档。该讨论同时指出 DeepSeek 的缓存折扣对 Copilot 免费用户「用不上」(因为系统提示没被缓存)。
- copilot-cli issue #3808:请求 Copilot CLI 对 Claude Sonnet 启用 Anthropic 缓存断点(当前「无可见优化」)——说明 Copilot CLI 订阅路径**目前不刻意利用/暴露 Anthropic prompt cache**。
- **结论**Copilot=订阅+token 配额;缓存字段**不面向终端用户文档化**;企业聚合 API 无缓存拆分;底层 SSE 有透传迹象社区实测。证据等级官方文档metrics API 字段)+ issue 讨论。
来源https://docs.github.com/rest/copilot/copilot-usage-metrics 、https://github.com/microsoft/vscode/issues/317837 、https://github.com/logancyang/obsidian-copilot/discussions/2380 、https://github.com/github/copilot-cli/issues/3808 、https://code.visualstudio.com/blogs/2026/06/17/improving-token-efficiency-in-github-copilot
### 6.2 Cursor
- 论坛官方账号客服口径thread「Why are cache read and write chargeable?」「In all cases we show the precise token consumed in Usage report **as provided by AI provider sent back with AI response**」——**用量面板展示的缓存拆分明细来自上游 API 响应原样**;「有些供应商把 cache write 算进 Input 只单列 cache read有些Anthropic单独分开我们按供应商返回的展示」。
- 订阅Pro与 BYOK 的用量面板都单列 **Cache Read / Cache Write**且按缓存价计费cache read ≈ 输入价 10%。多篇论坛帖用「0 cache read / 0 cache write → usage 暴涨」排查 Auto 模式路由到不支持缓存的模型(版本 2.6.12 → 2.6.18 修复)。
- **注意**:这说明 Cursor 订阅计划**会展示**缓存 token 明细(这是少数订阅制里对用户可见的);但这只是「面板展示」,非公开 API —— Cursor 不提供获取原始 usage 的 API未找到
- 另一个相关实证microsoft/vscode#312939OpenRouter BYOK in Copilot**经 OpenRouter 的 Claude 在 agent 模式里 `cached_tokens` 恒 0**,与原生 Anthropic BYOK 对比 10 倍成本差异——聚合层缓存是否生效对 agent 成本影响极大。
**结论**Cursor=订阅制但用量面板单列 cache read/write透传自上游响应无公开 usage API。证据等级社区讨论官方客服回复+ 论坛实测;官方文档未直接确认面板字段。
来源https://forum.cursor.com/t/someone-please-explain-why-are-cache-read-and-write-chargeable/153538/8 、https://forum.cursor.com/t/auto-mode-not-using-prompt-caching-0-cache-read-write-sudden-usage-spike/154278 、https://forum.cursor.com/t/cache-read-token/153794 、https://github.com/microsoft/vscode/issues/312939
### 6.3 Windsurf
- credits + token 混合计费:外部模型按「模型供应商 API 价 + 20% 加成」换算 credit**明确区分 input / cache-read / output 三种单价**flexprice.io 整理Claude Sonnet 4input 90 credits/M、**cache read 9 credits/M**、output 450 credits/M1 credit=$0.04)。→ Windsurf 计量层**按 cache-read 打折计费**,说明其网关解析并保留了缓存字段。
- 用户侧**看不到原始 usage 字段**,只能看到 credit 消耗与用量面板Tokenminning 的 Windsurf 页提到「Quota & billingdaily/weekly quota, cache reads, enterprise ACUs」→ 官方文档存在 cache reads 相关条目(推测在用量说明中,未逐字核验)。
- **结论**:订阅/credits 制;缓存 token 参与折扣计费(第三方资料);未找到向用户暴露 per-request usage 的证据。证据等级:第三方价格分析 + 官方文档存在性(未逐字核验)。
来源https://flexprice.io/blog/windsurf-ai-pricing-breakdown 、https://tokenminning.ai/ides/windsurf 、Windsurf 官方文档quota & billing未逐字核验
### 6.4 Augment Code
- 官方文档Token-Based Pricing「Augment **自动缓存稳定上下文**repo index、AGENTS.md、最近文件**cached input tokens 按供应商缓存价计费(约输入价 10%**服务费随缩水」「Usage → Models 面板展示每个模型的 input/output/**cache read/cache write** 单价」。
- 定价体系2025-10 起从 message 制改 credit 制token 制文档较新,网页 2026 版本同时提到 token-based pricing 与 credit
- **结论**:订阅/credit 制,官方明确缓存读取按折扣计费并在面板展示缓存单价——但没有公开 API 暴露原始 usage 字段。证据等级:官方文档。
来源https://docs.augmentcode.com/models/token-based-pricing 、https://www.augmentcode.com/blog/augment-codes-pricing-is-changing
---
## 7. Cloudflare AI Gateway / Portkey / LiteLLM及顺带 Vercel AI Gateway
### 7.1 Cloudflare AI Gateway
- 官方「Caching」文档指的是**网关级响应缓存**:按 provider+endpoint+model+auth+body 构造 SHA-256 cache key`cf-aig-cache-status: HIT/MISS` 头标识;**命中时直接返回缓存响应,不再调用上游**——这是「cache 掉整条响应」,不是 prompt cache。命中响应的 usage 含义取决于缓存内容(官方未在此文档中说明 usage 归零;**与 OpenRouter 响应缓存把 usage 清零不同Cloudflare 文档未写明**,实验时注意区分)。
- Anthropic provider 文档:给出把 base URL 指向 AI Gateway 的示例(`/ai/v1/messages`**未提到会规范化/丢弃 Anthropic 的 `cache_read_input_tokens`**。社区openclaw#46709实测请求体的 `cache_control` 能透传到 gatewaybug 是在 openclaw 侧 TTL 设置,不是网关丢弃)。
- **未找到**官方文档明确说明 Cloudflare AI Gateway 对上游 usage 缓存字段的保留/改名策略——按「透明代理」设计推测为原样透传(推测,证据不足)。
- Workers AI非网关文档确认其在 `usage` 对象里返回 cached token 计数——但那是 Cloudflare 自营推理,不是聚合层。
**结论**Cloudflare AI Gateway 未文档化缓存 usage 字段处理;其自带缓存是响应级缓存(有 HIT/MISS 头);请求侧 cache_control 可达。证据等级:官方文档(缓存功能)+ issue 讨论cache_control 透传)+ 推测usage 透传)。
来源https://developers.cloudflare.com/ai-gateway/features/caching 、https://developers.cloudflare.com/ai-gateway/usage/providers/anthropic 、https://github.com/openclaw/openclaw/issues/46709 、https://developers.cloudflare.com/workers-ai/features/prompt-caching
### 7.2 Portkey
- **有明确的规范化文档**Bedrock Prompt Caching 页):
- 「Portkey normalizes responses to the OpenAI format」`prompt_tokens` **包含**缓存 token`prompt_tokens = inputTokens + cache_read_input_tokens + cache_creation_input_tokens`。
- `cached_tokens` 出现在 usage 里OpenAI 风格);定价时先从 prompt_tokens 减去缓存部分,再分别按 base input / cache read折扣价/ cache write 计价。
- 其观测端/Inference API Responses 返回 `usage.input_tokens_details.cached_tokens`(官方 API 参考示例)。
- 自带「响应缓存simple/semantic」与 prompt cache 是两回事Portkey blog 明说两者可叠加)。
- **结论**Portkey 会保留并**主动规范化**缓存字段到 OpenAI 风格(`prompt_tokens_details.cached_tokens`),且计费按缓存分项。证据等级:官方文档。
来源https://docs.portkey.ai/docs/integrations/llms/bedrock/prompt-caching 、https://docs.portkey.ai/docs/integrations/llms/openai/prompt-caching-openai 、https://docs.portkey.ai/docs/api-reference/inference-api/responses/retrieve-response 、https://portkey.ai/blog/openais-prompt-caching-a-deep-dive
### 7.3 LiteLLM
- 官方 Prompt Caching 文档「For the supported providers, **LiteLLM follows the OpenAI prompt caching usage object format**」→ OpenAI 兼容 `completion()` 返回 `usage.prompt_tokens_details.cached_tokens`;同时返回对象里也带 Anthropic 原生 `cache_creation_input_tokens` / `cache_read_input_tokens`(官方示例的 Usage 对象同时含两者)。即**双格式并存**(规范化 + 保留原生)。
- `/v1/messages`Anthropic 兼容端点):按 Anthropic 原生返回 `cache_creation_input_tokens` / `cache_read_input_tokens`(官方 anthropic_unified 文档)。
- **已知 bug #27763**Anthropic `/v1/messages`(含 Vertex/Bedrock 透传路径)**不会把原生 `cache_read_input_tokens` 映射成 `prompt_tokens_details.cached_tokens`**,导致 Prometheus 的 `litellm_cached_tokens_metric_total` 恒为 0、缓存命中看起来像没发生`litellm_spend_metric` 可能把缓存读取按全价算。
- 计费:有 `cache_read_input_token_cost` / `cache_creation_input_token_cost` 单价,但**计费 bug 多**litellm#26807自定义定价路径缓存 token 按全价算,用户多付 1.67×)、#33772OpenAI `cache_write_tokens` 未计入成本,消费远低于厂商账单)、#11364Anthropic 缓存成本算错)、#34875生产流式 80.7% 行成本 $0并发竞态
- 流式:默认不强制 include_usage`always_include_stream_usage` 默认关);合成末端 usage chunk 曾有 `choices` 非空的历史 bug#28735 等)。
- **结论**LiteLLM 意图是「OpenAI 风格规范化 + 保留原生」,但 Anthropic 透传路径的功能与计费都有多个已知坑,实验中应同时对比原生字段与 `cached_tokens`。证据等级:官方文档 + issue 讨论 + 第三方调研。
来源https://docs.litellm.ai/docs/completion/prompt_caching 、https://docs.litellm.ai/docs/anthropic_unified 、https://github.com/BerriAI/litellm/issues/27763 、#26807 、#33772 、#11364 、https://github.com/cuihuan/awesome-ai-gateway/blob/main/docs/virtual-keys-metering.zh-CN.md
### 7.4 Vercel AI Gateway顺带
- vercel/ai#139072026-03经 Vercel AI Gateway 调 `moonshotai/kimi-k2.5`,面板正确显示 Cache Read 5.8M93.5% 命中),但**账单按全价输入计费**——真实成本 $4.00 vs 直连 $1.286×。→ 网关侧「显示缓存但不应用缓存折扣」的实例。证据等级issue 讨论。
- 来源https://github.com/vercel/ai/issues/13907
---
## 8. 实验建议(通过聚合层验证缓存字段时的检查清单与坑)
### 8.1 该检查哪些字段(按入口格式)
- **OpenAI 兼容入口(大多数聚合层采用)**
- `usage.prompt_tokens_details.cached_tokens`(聚合层规范化后应在此)
- 扩展字段OpenRouter `cache_write_tokens`、`cache_discount`、`cost_details`LiteLLM 同对象里还可能带 `cache_creation_input_tokens` / `cache_read_input_tokens`
- Responses API 入口Codex 类客户端):`usage.input_tokens_details.cached_tokens`one-hub 曾因 omitempty 漏掉此字段)
- **Anthropic 兼容入口(`/v1/messages`**`usage.input_tokens`、`cache_creation_input_tokens`、`cache_read_input_tokens`+新格式 `cache_creation.ephemeral_5m/1h_input_tokens`
- **DeepSeek/部分上游**:顶层 `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`(注意有些聚合层会原样透传、有些会映射进 `cached_tokens`——Bifrost 的做法是映射)
- **同一个请求的三种视图都要抓,不能只看一种**
1. 客户端收到的响应 usage
2. 网关消费日志/账单里的 token 拆分new-api#6144 的教训:这两者可能不一致——响应是对的、账单是坏的);
3. 上游(如果可直连对照)原生 usage——用于判断聚合层是「透传」「改名」还是「丢弃」。
### 8.2 已知的坑(汇总自本节调研)
1. **流式 usage 必须有 `stream_options.include_usage=true`**,否则 OpenAI Chat Completions 风格的流根本没有 usage chunkOpenRouter 同规则LiteLLM 有合成兜底但历史上有格式 bug。实验脚本务必显式带该参数并对齐最后一个 chunk。
2. **网关自身另有「响应缓存」result cache**OpenRouter 的 `X-OpenRouter-Cache-Status: HIT` 时 usage 全为 0Cloudflare AI Gateway 有 `cf-aig-cache-status`;此类命中不是 prompt cache别误读为「缓存命中 token=0」。
3. **前缀漂移/路由漂移**:聚合层多供应商路由会让同一 session 落到不同上游导致缓存失效OpenRouter 用 `session_id` 做 sticky routing 以保缓存。实验中固定供应商(`provider` 参数)或固定 session_id 再测。
4. **客户端解析 bug 会掩盖真相**opencode 对 OpenAI-compatible 流式 provider 的 `tokens_cache_read` 恒 0#33997——不要用 opencode 的展示值当结论,要看原始 SSE。
5. **显示 vs 计费分离**new-api xAI 渠道(#6144响应正确但账单按全价Vercel AI Gateway#13907面板显示缓存但账单全价。**验证「缓存字段是否透传」和「缓存是否影响账单」是两件事**,后者在中转站/订阅网关里只能靠站方后台,无法从响应验证。
6. **供应商/模型差异**同一聚合层下DeepSeek 缓存可能不过网关china-llm 实测 OR 上 0 cached而 GLM 正常Kimi K3 缓存折扣不经过 OpenRouter。实验要按模型逐个测不能拿一个模型代表全部。
7. **语义差异**Anthropic 的 `input_tokens` 是「最后一个缓存断点之后的 token」缓存读取已排除OpenAI 的 `prompt_tokens` **包含**缓存读取。字段 `cached_tokens > input 总量` 只有在排除语义下才可能出现new-api#5003 曾因此把输入算成负数)。取值与对账时务必按供应商语义。
8. **订阅制服务Copilot/Windsurf/Augment/Cursor 订阅)没有公开 per-request usage API**无法从响应侧做该实验Cursor 面板展示的 cache read/write 数据点据客服称来自上游响应。若实验目标是「验证缓存命中 token」应优先选按量 APIOpenRouter、Zen、中转站
9. **国内中转站验证**Claude Code 场景看 `cache_creation_input_tokens` / `cache_read_input_tokens` 是否存在且随轮次递增(命中);缺失=该站(逆向/无缓存)不保留缓存字段。社区普遍以「响应 usage 是否带缓存字段」作为中转是否『支持缓存计费』的验收标准。
10. **缓存写入也有计费折扣的镜像**OpenRouter 用 `cache_write_tokens`、Anthropic 用 `cache_creation_input_tokens`5m=1.25×、1h=2× 输入价)。实验前两轮必然出现 cache write>0、cache read=0符合预期别把首轮 cache read=0 当成「网关丢字段」。
### 8.3 建议的最小实验矩阵
| 层 | 建议入口 | 必查字段 | 对照 |
|---|---|---|---|
| 直连官方(对照组) | Anthropic/OpenAI/DeepSeek 原生 | `cache_read_input_tokens` / `cached_tokens` / `prompt_cache_hit_tokens` | — |
| OpenRouter | `chat/completions` + include_usage | `cached_tokens`+`cache_write_tokens`+`cost`/`cache_discount` | 与直连对照;固定 provider+session_id |
| opencode Zen/Go | `v1/chat/completions`/`v1/messages` | 协议原生缓存字段 | 与官方价格表 Cached Read 列对照 |
| new-api/one-hub | chat/completions流式+非流式各一遍) | `cached_tokens`;同时看网关消费日志 | 非流式作为基线(历史上流式丢字段 bug 多) |
| LiteLLM | completion + /v1/messages | `cached_tokens``cache_read_input_tokens` 是否同时出现 | 抓 `litellm_cached_tokens_metric` 是否>0 |
| 国内中转站 | Anthropic 兼容 | `cache_creation/read_input_tokens` | 两轮同前缀请求,命中应>0 |
---
## 9. 一句话总结
- **透传且规范化得最好**OpenRouter统一 OpenAI 风格 `cached_tokens`+扩展、Portkey明确规范化并分项计价、Bifrost`prompt_cache_hit_tokens` 映射为 `cached_tokens`)。
- **意图透传但坑多**new-api / one-hub多个流式/Responses bug、LiteLLMAnthropic 透传路径不映射、计费 bug 多、Cloudflare AI Gateway未文档化推测透传
- **计费不含缓存或订阅不暴露**one-api无缓存单价、Copilot聚合 API 无缓存拆分、Windsurf/Augment按缓存折扣计费但不暴露原始字段、Cursor面板展示缓存明细但没有公开 API
- **核心陷阱**:「客户端收到的 usage」≠「网关账单」≠「上游计费」三者要分开验证流式必须 `include_usage`;注意区分网关的 prompt cacheKV cache 命中与网关的响应缓存result cache可能返回 usage 全 0
---
*报告完。所有引用为调研时2026-08-29可访问的 URL证据等级逐条标注凡「未找到证据」处均已如实说明。*

View File

@ -0,0 +1,304 @@
# 国内官方 LLM API「缓存命中 Token」字段对照报告
> 调研子智能体 #2 · 调研时间2026-08-29
> 调研范围:**国内官方 API**DeepSeek / Moonshot Kimi / 阿里通义千问 / 智谱 GLM / 字节豆包 / MiniMax / 阶跃星辰 Step / 百度文心千帆)
> 数据来源:以**各厂商官方文档**为准(文末附全部 URL个别引用了第三方报道处已单独标注。
> 结论确定性说明:本文所有"字段名 / 官方示例 JSON / 官方计费规则"均直接取自官方文档,可据此设计实验;**本文只做了文档调研,未实际跑请求验证**,实测时字段是否如实返回以实验为准。
---
## 一、总览表(速查)
| # | 厂商 | 官方平台 | 缓存机制 | 命中字段usage 内位置) | 最小触发阈值 | 命中计费折扣(官方口径) |
|---|---|---|---|---|---|---|
| 1 | DeepSeek | api.deepseek.com | **自动**(无需配置) | 顶层 `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`**注意:不在 details 里** | 文档未给出固定值 | deepseek-v4-flash 峰值:缓存命中 $0.014/M vs 未命中 $0.44/M≈3.2%,约 1/31 |
| 2 | Moonshot Kimi | platform.moonshot.cn / platform.kimi.com | 自动(当前推荐);历史上曾有显式 cache API | `usage.cached_tokens`(顶层,官方示例);部分资料为 `usage.prompt_tokens_details.cached_tokens` | 文档未给出固定值(按前缀匹配) | kimi-k3 为 $0.30/M vs $3.00/M = **10%**k2.7-code ≈20%k2.6 ≈16.8%k2.5 ≈16.7% |
| 3 | 通义千问 Qwen | DashScope / 阿里云百炼 | OpenAI 兼容:**隐式自动** + 显式 `cache_control`;原生 DashScope同一套显式机制 | OpenAI 兼容:`usage.prompt_tokens_details.cached_tokens`(命中)、`...cache_creation_input_tokens`创建DashScope`usage.prompt_tokens_details.cached_tokens`(部分海外地域 `usage.cached_tokens` 顶层) | 隐式 ≥256 tokensQwen3.7 系列约 2000显式块 ≥1024 tokens | 隐式命中 20%(阿里部署常规);显式命中 10%、创建 125%qwen3.8-max 例外(以控制台为准) |
| 4 | 智谱 GLM | bigmodel.cn | **自动**(默认开启) | `usage.prompt_tokens_details.cached_tokens` | 智谱部署 GLM 为 512 tokens阿里云文档口径 | 命中按标准价格 **50%**(智谱官方);阿里云转售口径 25% |
| 5 | 字节豆包 Doubao | 火山方舟 volces.com | **仅显式**Context API 待下线Responses API 推荐) | `usage.prompt_tokens_details.cached_tokens` | 无自动缓存;最大缓存长度≈上下文窗口-最大输出 | 缓存输入折扣价(低于新输入)+ 存储费(元/千 token/小时) |
| 6 | MiniMax | platform.minimaxi.com国内/ platform.minimax.io海外 | 自动(被动)+ 显式 Anthropic 兼容 | OpenAI 格式:`usage.prompt_tokens_details.cached_tokens`Anthropic 格式:`usage.cache_read_input_tokens` / `cache_creation_input_tokens` | **≥512 tokens**(自动) | M3命中 $0.12/M vs 输入 $0.60/M = **20%**M2.7$0.06 vs $0.30 = 20%M2.5/M2.1$0.03 vs $0.30 = 10%(显式写入 $0.375/M |
| 7 | 阶跃星辰 Step | platform.stepfun.com | **自动** | 顶层 `usage.cached_tokens`**注意:在顶层,不在 details 里** | **≥256 tokens** | 缓存部分按该模型费用 **20%** 计费 |
| 8 | 百度文心 ERNIE | 千帆 ModelBuilderqianfan.baidubce.com | **自动**(默认开启,无需改代码) | `usage.prompt_tokens_details.cached_tokens` | 文档未给出固定值 | 命中按 prompt 单价 **40%** |
> ⚠️ 最容易踩坑的两点:
> 1. **DeepSeek 和 Step 的字段在 `usage` 顶层**`prompt_cache_hit_tokens` / `cached_tokens`),其余六家在 `usage.prompt_tokens_details` 下——实验代码要同时兼容这两种形态。
> 2. **只有豆包是纯显式缓存**(需要创建缓存或传 `caching` 参数),其余六家自动缓存 + 千问可显式。不传任何参数就指望豆包返回 cached_tokens 是无效的。
---
## 二、逐家明细
### 2.1 DeepSeek深度求索api.deepseek.com
- **官方文档**
- API 参考usage 结构https://api-docs.deepseek.com/api/create-chat-completion
- 定价页缓存命中价https://api-docs.deepseek.com/quick_start/pricing
- **缓存机制**:自动 context cache无需任何配置有"高峰期/非高峰期"分时定价(峰值=非峰值的 2 倍)。
- **usage 字段(官方 API 参考 schema 原文)**DeepSeek 是**独立字段模型**——
```json
"usage": {
"completion_tokens": 10,
"prompt_tokens": 16, // = prompt_cache_hit_tokens + prompt_cache_miss_tokens
"prompt_cache_hit_tokens": 0, // ← 命中缓存的 token 数(顶层!)
"prompt_cache_miss_tokens": 16, // ← 未命中缓存的 token 数(顶层!)
"total_tokens": 26,
"completion_tokens_details": { "reasoning_tokens": 0 }
}
```
- 官方定义原文:"Number of tokens in the prompt that hits the context cache."
- **流式行为**:官方流式示例中,最后一个 chunk`finish_reason=stop`)携带 `usage`;若设置 `stream_options.include_usage=true`,会在 `data: [DONE]` 前再补一个 `choices` 为空的 usage chunk其余 chunk 的 `usage``null`
- **计费折扣(官方定价页,单位 $/1M tokens**
| 模型 | 输入(缓存命中) 峰值/非峰值 | 输入(缓存未命中) 峰值/非峰值 |
|---|---|---|
| deepseek-v4-flash | $0.014 / $0.007 | $0.44 / $0.22 |
| deepseek-v4-pro | $0.044 / $0.022 | $1.32 / $0.66 |
即命中≈未命中的 **1/31≈3.2%**折扣力度为国内最大。峰值时段UTC 周一至五 01:0004:00 与 06:0010:00。
> 第三方报道知乎非官方DeepSeek V4-Pro 人民币口径"缓存命中 0.1 元/百万 vs 未命中 3 元/百万(差 30 倍),促销窗口 0.025 元"。此条为第三方转述,仅作参考。
- **实验要点**:读取顶层 `usage.prompt_cache_hit_tokens`;未命中时该字段为 `0`(官方示例即返回 0不要把它当缺失。
---
### 2.2 Moonshot Kimiplatform.moonshot.cn / platform.kimi.com
- **官方文档**
- API 参考Chathttps://platform.kimi.com/docs/api/chat
- 上下文缓存指南https://platform.kimi.com/docs/guides/context-caching
- 定价页https://platform.kimi.com/docs/pricing/chat
- **缓存机制**:默认会自动缓存(官方指南:"当请求包含相同前缀时自动缓存,无需手动调用;命中后自动续期")。历史上曾公测显式缓存 API`POST /v1/caching`2024 年月之暗面文档),当前 platform.moonshot.cn API 列表已不含该端点,以自动缓存为准。
- **usage 字段(官方 API 参考示例原文)**
```json
"usage": {
"prompt_tokens": 19,
"completion_tokens": 21,
"total_tokens": 40,
"cached_tokens": 10 // ← 命中缓存 token顶层官方示例原文
}
```
官方《上下文缓存指南》PDF中的 usage 示例则为 `usage.prompt_tokens_details.cached_tokens`。**两处官方文档形态不一致**,实验时两个位置都要读。
- **请求参数**:官方请求体字段 `prompt_cache_key`(官方原文)——“用于缓存相似请求的响应以优化缓存命中率。对于 Coding Agent通常是代表单个会话的 session id 或 task id退出并恢复会话时应保持不变。对于 Kimi Code Plan此字段为必填以提高缓存命中率。”不传时按前缀自动匹配。
- **流式行为**:官方指南明确"**流式返回时,最后一个 chunk 会携带 usage含 cached_tokens**"。
- **计费折扣(官方定价,$/1M**
| 模型 | 输入(未命中) | 输入(命中) | 折扣 |
|---|---|---|---|
| kimi-k3 | $3.00 | $0.30 | **10%** |
| kimi-k2.7-code | $0.95 | $0.19 | 20% |
| kimi-k2.6 | $0.95 | $0.16 | ≈16.8% |
| kimi-k2.5 | $0.60 | $0.10 | ≈16.7% |
| moonshot-v1 系列 | — | 无 | 无缓存折扣 |
- **实验要点**:最后一轮流式 chunk 的 usage 是主战场;`cached_tokens` 与 `prompt_tokens_details.cached_tokens` 两个位置都要探测。
---
### 2.3 通义千问 QwenDashScope / 阿里云百炼)
- **官方文档**阿里云百炼《上下文缓存Context Cache》https://help.aliyun.com/zh/model-studio/context-cache
- **缓存机制OpenAI 兼容模式与原生 DashScope 模式已分别核实)**
- **隐式缓存(自动)**对所有支持模型默认开启、不可关闭按前缀匹配。OpenAI 兼容与 DashScope 均可命中。
- **显式缓存(需主动开启)**:在 messages 的 content 中加 `"cache_control": {"type": "ephemeral"}`(仅此一种 type从 messages 开头到标记位置创建缓存块OpenAI 兼容、DashScope、Anthropic 兼容三种协议均支持。单次最多 4 个标记;向后回溯最近 20 个 content 块;最小缓存块 **1024 tokens**;有效期 **5 分钟(命中则重置)**
- **usage 字段**
- OpenAI 兼容 · 隐式命中(官方示例原文):
```json
"usage": {
"prompt_tokens": 3019,
"completion_tokens": 104,
"total_tokens": 3123,
"prompt_tokens_details": { "cached_tokens": 2048 }
}
```
- OpenAI 兼容 / DashScope · 显式缓存:同时上报创建与命中(官方示例原文):
```json
// 第一次请求(创建缓存) // 第二次请求(命中缓存)
"cache_creation_input_tokens": 1605, "cache_creation_input_tokens": 0,
"cached_tokens": 0, "cached_tokens": 1605,
// 均位于 usage.prompt_tokens_details 下
```
- 原生 DashScope · 视觉模型海外地域(新加坡):命中字段一度为顶层 `usage.cached_tokens`(文档注明"后续将升级至 `prompt_tokens_details.cached_tokens`");国内(北京)地域直接在 `usage.prompt_tokens_details.cached_tokens`
- Anthropic 兼容:`usage.cache_read_input_tokens`(命中,**不计入** `input_tokens`)、`usage.cache_creation_input_tokens`(创建)。
- **计费折扣(官方)**
- 隐式:命中 token 按输入标准价 **20%**(阿里百炼部署常规模型;`qwen3.8-max` 例外,以控制台为准)。
- 显式:**创建**缓存 token 按标准输入价 **125%****命中**按 **10%**qwen3.8-max 例外)。
- **触发阈值(官方)**:阿里云百炼部署模型的隐式缓存最少 **256 tokens**Qwen3.7 系列约 **2000 tokens**
- **实验要点**多协议多形态是千问的特色——OpenAI 兼容/DashScope 看 `prompt_tokens_details`Anthropic 兼容看 `cache_read_input_tokens`,部分海外地域 DashScope 看顶层 `cached_tokens`。本任务重点实验是 OpenAI 兼容 + 原生 DashScope 两种。
---
### 2.4 智谱 GLMbigmodel.cn
- **官方文档**《上下文缓存》https://docs.bigmodel.cn/cn/guide/capabilities/cache
- **缓存机制****自动(隐式)缓存**,默认启用,无需手动配置;基于内容相似度/前缀自动触发。
- **usage 字段(官方原文)**"响应字段 `usage.prompt_tokens_details.cached_tokens`"——
```json
"usage": {
"prompt_tokens": …,
"completion_tokens": …,
"total_tokens": …,
"prompt_tokens_details": { "cached_tokens": … } // ← 命中缓存 token
}
```
官方示例代码取值方式:`response.usage.prompt_tokens_details.cached_tokens`(未命中时需判空/缺省为 0
- **计费折扣(官方)**:缓存命中 Token 按优惠价格计费,"**通常为标准价格的 50%**"新内容按标准价、输出按标准价。GLM Coding Plan 套餐内积分抵扣口径官方套餐页GLM-5.3 Input 系数 6.9 / Cached Input 系数 1.7≈24.6%)。
- **有效期(官方)**"缓存有合理的时效性,过期后会重新计算",未公布固定数值。
- **第三方口径**(阿里云百炼文档):智谱部署的 GLM 触发隐式缓存最少 **512 tokens**;阿里云转售 GLMZHIPU/GLM-5.2 等)命中按 25%。
- **实验要点**:普通对话请求即可验证;同一 system 前缀连续请求看 `prompt_tokens_details.cached_tokens` 是否增长。
---
### 2.5 字节豆包 Doubao火山方舟 volces.com
- **官方文档**
- 《上下文缓存Context API待下线》https://www.volcengine.com/docs/82379/1396491
- 《上下文缓存》主文档 https://www.volcengine.com/docs/82379/1398933
- **缓存机制****仅显式缓存,无自动缓存**。两种 API
1. **Context API待下线**:先 `POST /api/v3/context/create` 创建缓存(`mode: "session"` 会话缓存 / `"common_prefix"` 前缀缓存,返回 `ctx-*` ID再调用 `POST /api/v3/context/chat/completions`(请求体带 `context_id`使用。TTL 可配,范围 1 小时7 天([3600,604800] 秒),未使用则过期、使用则重置。
2. **Responses API推荐**:请求体传 `"caching": {"type": "enabled"}`(加 `"prefix": true` 为前缀缓存)创建 Session/前缀缓存,返回缓存 ID后续用 `"previous_response_id": "<ID>"` 复用。过期时刻用 Unix 时间戳配置,最大当前时间 +604800 秒7 天)。支持多模态与工具缓存、可手动删除任意缓存 ID。
- 需在控制台「开通管理」→「推理(缓存)定价」开启缓存。
- **usage 字段官方示例原文Context Chat API 响应)**
```json
"usage": {
"prompt_tokens": 28,
"completion_tokens": 4,
"total_tokens": 32,
"prompt_tokens_details": { "cached_tokens": 18 } // ← 缓存输入 token
}
```
创建缓存接口的响应同样带 `usage.prompt_tokens_details.cached_tokens`(首建时为 0
- **流式行为**:官方 SDK 示例用 `stream_options={"include_usage": True}`chunk 的 `usage` 非空(含 cached_tokens
- **计费(官方)**:四类——新输入(标准价);**缓存输入(折扣价,显著低于新输入)**;输出(标准价);**存储费**(元/千 token/小时,按每自然小时缓存最大 token 量计,直到 TTL 到期或删除)。官方举例存储单价 0.000017 元/千 token/小时示例值Doubao-1.5-pro-32k 示例缓存输入 1.6 元/千万 tokens。实际单价以《模型价格》页为准。
- **实验要点**:不传缓存参数直接调 `/chat/completions` 是**不会**返回缓存字段的(有自动 KV 缓存但不在 usage 中体现);必须走 Context Chat API`context_id`)或 Responses API`caching`/`previous_response_id`)才能看到 `cached_tokens`
---
### 2.6 MiniMaxplatform.minimaxi.com 国内 / platform.minimax.io 海外)
- **官方文档**https://platform.minimax.io/docs/api-reference/text-prompt-caching国内域名为同一套文档platform.minimaxi.com
- **缓存机制**:两套并行——
1. **自动缓存(被动 Prompt Caching**:无需改调用方式。前缀匹配顺序为"工具列表 → 系统提示 → 用户消息"。有效期由系统按负载自动调整,命中则续期。
2. **显式缓存(仅 Anthropic 兼容 API**:在 content 中加 `"cache_control": {"type": "ephemeral"}`**5 分钟 TTL命中自动续期**;首次写入缓存有额外费用。
- **usage 字段**OpenAI 格式官方示例原文):
```json
"usage": {
"prompt_tokens": 1200,
"completion_tokens": 300,
"total_tokens": 1500,
"prompt_tokens_details": { "cached_tokens": 800 } // ← 自动缓存命中
}
```
Anthropic 格式(显式/自动均可出现):
```json
"usage": { "input_tokens": 108, "output_tokens": 91,
"cache_creation_input_tokens": 0, // 创建缓存(显式)
"cache_read_input_tokens": 14813 } // 命中缓存
```
- **触发阈值(官方)**:自动缓存适用于**输入 ≥512 tokens** 的请求。
- **计费折扣(官方 PayGo 示例)**
- MiniMax-M3输入 $0.60/M命中 $0.12/M**20%**
- MiniMax-M2.7:输入 $0.30/M命中 $0.06/M20%),显式写入 $0.375/M
- MiniMax-M2.5 / M2.1:输入 $0.30/M命中 $0.03/M10%),显式写入 $0.375/M。
- **支持模型**自动缓存——M3 / M2.7 / M2.5 / M2.1 系列显式缓存——M2.7 / M2.5 / M2.1 / M2 系列。
- **实验要点**OpenAI 兼容接口看 `prompt_tokens_details.cached_tokens`;如果用 Anthropic 协议则看 `cache_read_input_tokens`。首次请求建立缓存(可能为 0第二次请求读取。
---
### 2.7 阶跃星辰 Stepplatform.stepfun.com
- **官方文档**《Prompt 缓存最佳实践》https://platform.stepfun.com/docs/zh/guides/developer/prompt-cache
- **缓存机制****自动**;请求超过 **256 tokens 时自动启用**,按 Prompt 前缀匹配。缓存淘汰采用 **LRU最近最少使用**,不设固定 TTL高峰期缓存更容易被逐出。
- **usage 字段(官方示例原文)**——**顶层 `cached_tokens`,不在 details 里**
```json
"usage": {
"cached_tokens": 512, // ← 命中缓存 token顶层
"prompt_tokens": 591,
"completion_tokens": 120,
"total_tokens": 711
}
```
官方判定方法原文:"如果 response.usage 存在 cached_tokens 字段则表明该请求命中缓存cached_tokens 的值即为命中的 Token 长度。"
- **流式行为**:官方 Web 搜索示例显示**每个流式 chunk 都带 usage含 cached_tokens**,与 OpenAI 惯例(仅末 chunk不同需多次读取。
- **计费折扣(官方)**:缓存部分 Token 按"**对应模型费用的 20%**"计费。
- **支持模型(官方)**step-3.7-flash、step-3.5-flash、step-3.5-flash-2603、step-1o-turbo-vision 等(文档列出的系列);其他模型暂不支持。
- **实验要点**prompt 要 ≥256 tokens 才有缓存;命中读取顶层 `usage.cached_tokens`(注意与 DeepSeek 顶层字段不同名)。
---
### 2.8 百度文心 ERNIE千帆 ModelBuilder
- **官方文档**《prompt cache 上线公告》https://ai.baidu.com/ai-doc/WENXINWORKSHOP/Rm6uq7jy9
- **缓存机制****自动**,对所有用户默认开启,无需修改代码(官方原文)。
- **usage 字段(官方响应示例原文)**
```json
"usage": {
"prompt_tokens": 159,
"completion_tokens": 89,
"total_tokens": 248,
"prompt_tokens_details": { "cached_tokens": 128 } // ← 命中缓存 token
}
```
官方说明:"当本次请求已命中缓存usage 中返回 cached_tokens 字段……代表命中缓存的 token 数量。"(即未命中时该字段可能缺失。)
- **计费折扣(官方)**:命中缓存的 `cached_tokens``prompt_tokens` 单价的 **40%** 计算。模型示例ERNIE-4.0-Turbo-8K 输入命中0.0012 元/千 tokens vs 输入未命中0.003 元/千 tokens输出 0.009 元/千 tokens。
- **有效期(官方)**"系统将定期清理一段时间没有使用过的缓存";官方同时明确"命中概率并不是 100%,即使上下文完全一致的请求也存在无法命中的概率"。
- **实验要点**:同一 prompt 连续请求(官方示例即为同样长文案换问题),观察 `prompt_tokens_details.cached_tokens`;未命中时字段可能缺失,需容错。
---
## 三、跨厂商对照(实验脚本设计要点)
### 3.1 字段位置差异(最重要)
| 厂商 | 命中字段完整路径 | 未命中时表现 |
|---|---|---|
| DeepSeek | `usage.prompt_cache_hit_tokens`(顶层,另有 `prompt_cache_miss_tokens` | 返回 0 |
| Kimi | `usage.cached_tokens`(顶层)或 `usage.prompt_tokens_details.cached_tokens` | 文档两种示例并存 |
| QwenOpenAI/百炼) | `usage.prompt_tokens_details.cached_tokens`;显式另有 `cache_creation_input_tokens` | 未命中为 0/缺失 |
| QwenDashScope 海外部分模型) | `usage.cached_tokens`(顶层) | — |
| GLM | `usage.prompt_tokens_details.cached_tokens` | 缺失(官方例程判空) |
| 豆包 | `usage.prompt_tokens_details.cached_tokens` | 需要显式缓存才出现 |
| MiniMax | OpenAI 格式:`usage.prompt_tokens_details.cached_tokens`Anthropic 格式:`usage.cache_read_input_tokens` / `cache_creation_input_tokens` | 首次请求命中可能为 0 |
| Step | `usage.cached_tokens`(顶层) | 未命中时无该字段(官方判定) |
| 百度千帆 | `usage.prompt_tokens_details.cached_tokens` | 缺失 |
**兼容读取建议**:统一读取器按以下优先级取值——
```
candidates = [
usage.get("prompt_cache_hit_tokens"), # DeepSeek
usage.get("cached_tokens"), # Kimi / Step / 部分 DashScope
(usage.get("prompt_tokens_details") or {}).get("cached_tokens"), # 其余各家
(usage.get("prompt_tokens_details") or {}).get("cache_read_input_tokens"), # Anthropic 兼容
]
```
### 3.2 流式 usage 位置差异
- **DeepSeek**:末 chunk 或 include_usage 追加 chunk。
- **Kimi**:流式末 chunk 携带 usage。
- **Step**:每个 chunk 都可能带 usage。
- **豆包**SDK 需 `stream_options={"include_usage": True}`
- 其余Qwen/GLM/MiniMax/千帆):按 OpenAI 惯例,`stream_options.include_usage=true` 时末 chunk 带 usage非流式直接看响应 usage。
### 3.3 缓存触发阈值
- Step≥256 tokensMiniMax≥512 tokensQwen 隐式≥256部分模型更高全局建议构造 **≥2048 tokens 的稳定前缀** 再测,避开各家阈值差异。
### 3.4 显式 vs 自动(决定实验脚本形态)
- 只发普通请求即可验证DeepSeek、Kimi、Qwen隐式、GLM、MiniMax、Step、百度千帆。
- 必须额外走显式流程:**豆包**(先建缓存/传 caching 参数Qwen 如需显式命中cache_control ephemeral5 分钟 TTL、1024 tokens 起)也需加标记。
---
## 四、来源清单与确定性分级
| 事实 | 确定性 | 依据 |
|---|---|---|
| DeepSeek usage 顶层 `prompt_cache_hit_tokens/miss_tokens`V4 缓存命中价($0.014 vs $0.44 峰值) | 高(官方 API 参考与定价页原文) | https://api-docs.deepseek.com/api/create-chat-completion · /quick_start/pricing |
| Kimi `usage.cached_tokens`(顶层);`prompt_cache_key`;流式末 chunk 带 usageK3 命中 10%$0.30/$3.00 | 高(官方指南/API/定价页K3 价格亦有第三方 blog 复述一致) | https://platform.kimi.com/docs/api/chat · /docs/guides/context-caching · /docs/pricing/chat |
| Qwen OpenAI 兼容 & DashScope 隐式/显式缓存字段、1024 阈值、5min TTL、20%/10%/125% 计费 | 高(阿里云官方文档原文+示例 JSON | https://help.aliyun.com/zh/model-studio/context-cache |
| GLM 自动缓存、`prompt_tokens_details.cached_tokens`、命中约 50% | 高(智谱官方);智谱部署 512 阈值、25% 折扣为阿里云文档转述(中) | https://docs.bigmodel.cn/cn/guide/capabilities/cache |
| 豆包仅显式缓存、`prompt_tokens_details.cached_tokens`、TTL 1h7d、Responses API caching 参数 | 高(火山方舟官方文档原文) | https://www.volcengine.com/docs/82379/1396491 · 1398933 |
| MiniMax 自动≥512、OpenAI/Anthropic 双字段、M3 命中 20%$0.12/$0.60 | 高官方文档RooCode issue 表格亦一致 | https://platform.minimax.io/docs/api-reference/text-prompt-caching |
| Step 自动≥256、顶层 `cached_tokens`、20% 计费、LRU | 高(官方文档原文) | https://platform.stepfun.com/docs/zh/guides/developer/prompt-cache |
| 百度千帆自动默认开启、`prompt_tokens_details.cached_tokens`、40% 计费 | 高(官方公告响应示例) | https://ai.baidu.com/ai-doc/WENXINWORKSHOP/Rm6uq7jy9 |
| DeepSeek V4-Pro 人民币缓存价0.1 元 vs 3 元/百万) | 低-中(仅第三方知乎转述,非官方) | 第三方报道 |
| Kimi 命中价逐模型数值 | 中(官方定价页为主,第三方 blog 佐证) | platform.kimi.com pricing + 第三方 blog |
**验证状态**:以上均为**官方文档调研结论**,尚未做真实 API 请求实测。建议下一步按第三节要点构造实验脚本逐家验证字段如实返回。

View File

@ -0,0 +1,91 @@
# 国内官方 LLM API 缓存字段速查(官方示例 JSON 摘录)
> 配套报告:同目录 `README.md`。以下 JSON 均为各厂商**官方文档原文示例**摘录,直接复制进实验脚本对照。
## 1. DeepSeek —— usage 顶层命中/未命中
```json
"usage": {
"prompt_tokens": 16,
"completion_tokens": 10,
"total_tokens": 26,
"prompt_cache_hit_tokens": 0,
"prompt_cache_miss_tokens": 16,
"completion_tokens_details": { "reasoning_tokens": 0 }
}
```
## 2. Moonshot Kimi —— usage 顶层 cached_tokens官方示例原文
非流式响应:
```json
"usage": { "prompt_tokens": 19, "completion_tokens": 21, "total_tokens": 40, "cached_tokens": 10 }
```
流式响应(最后一个 chunkfinish_reason=stop 时携带):
```json
"usage": {"prompt_tokens":19,"completion_tokens":13,"total_tokens":32,"cached_tokens":12}
```
请求参数 `prompt_cache_key`(官方原文):“用于缓存相似请求的响应以优化缓存命中率。对于 Coding Agent通常是代表单个会话的 session id 或 task id退出并恢复会话时应保持不变。对于 Kimi Code Plan此字段为必填以提高缓存命中率。”
官方《上下文缓存指南》PDF 示例亦出现 `usage.prompt_tokens_details.cached_tokens`,两处并存,实验需双读。)
## 3. 通义千问 Qwen阿里云百炼OpenAI 兼容)
隐式命中:
```json
"usage": { "prompt_tokens": 3019, "completion_tokens": 104, "total_tokens": 3123,
"prompt_tokens_details": { "cached_tokens": 2048 } }
```
显式cache_control ephemeral
```json
"usage": { "prompt_tokens": 2174, "completion_tokens": 0,
"prompt_tokens_details": { "cache_creation_input_tokens": 2156, "cached_tokens": 0 } }
// 第二次请求命中cache_creation_input_tokens=0, cached_tokens=2156
```
原生 DashScope`usage.prompt_tokens_details['cached_tokens']`(部分海外地域视觉模型为顶层 `usage.cached_tokens`,官方注明后续升级)。
## 4. 智谱 GLM
> 官方文档只给出字段名,未公布具体示例数字,以下为字段结构示意(值用占位符):
```json
"usage": { "prompt_tokens": <int>, "completion_tokens": <int>, "total_tokens": <int>,
"prompt_tokens_details": { "cached_tokens": <int> } }
```
## 5. 字节豆包(火山方舟 Context Chat API
```json
"usage": { "prompt_tokens": 28, "completion_tokens": 4, "total_tokens": 32,
"prompt_tokens_details": { "cached_tokens": 18 } }
```
(需创建 ctx-* 缓存并传 context_id或 Responses API 传 `"caching":{"type":"enabled"}` / `previous_response_id`。)
## 6. MiniMax
OpenAI 兼容格式:
```json
"usage": { "prompt_tokens": 1200, "completion_tokens": 300, "total_tokens": 1500,
"prompt_tokens_details": { "cached_tokens": 800 } }
```
Anthropic/Messages 格式(自动或显式均可出现):
```json
"usage": { "input_tokens": 108, "output_tokens": 91,
"cache_creation_input_tokens": 0, "cache_read_input_tokens": 14813 }
```
## 7. 阶跃星辰 Step —— usage 顶层 cached_tokens
```json
"usage": { "cached_tokens": 512, "prompt_tokens": 591, "completion_tokens": 120, "total_tokens": 711 }
```
## 8. 百度文心(千帆 ModelBuilder
```json
"usage": { "prompt_tokens": 159, "completion_tokens": 89, "total_tokens": 248,
"prompt_tokens_details": { "cached_tokens": 128 } }
```
## 统一读取优先级(实验脚本建议)
```python
usage = resp.get("usage") or {}
pdet = usage.get("prompt_tokens_details") or {}
cached = (
usage.get("prompt_cache_hit_tokens") # DeepSeek
or usage.get("cached_tokens") # Kimi / Step / 部分 DashScope
or pdet.get("cached_tokens") # Qwen/GLM/豆包/MiniMax/千帆
or pdet.get("cache_read_input_tokens") # Anthropic 兼容
or 0
)
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,422 @@
# 海外官方 LLM API「缓存命中 token 字段」调研报告v2 重试版)
> 调研时间2026-08-29
> 调研人:子智能体 #4
> 范围:海外**官方 API**OpenAI / Anthropic / Google Gemini / xAI Grok / Mistral AIAWS Bedrock 与 Azure OpenAI 仅简述透传方式)
> 方法以官方文档为准platform.openai.com / docs.anthropic.com / platform.claude.com / ai.google.dev / docs.x.ai / docs.mistral.ai / learn.microsoft.com / aws.amazon.com 官方博客),社区与第三方内容仅作辅助并标注来源等级。
> 说明官方文档随时间变化2026 年的文档已覆盖 GPT-5.x、Claude Opus/Sonnet 5 等新模型),本报告同时保留「历史经典行为」(如 OpenAI 1024 阈值、Anthropic 1024/2048与「文档当前状态」供实验对照。
---
## 1. 总览对照表
| 提供商 | 缓存类型 / 启用方式 | 命中字段 JSON 路径(非流式) | 写入(创建)字段 | 自动 / 显式 | 最低门槛 | 官方文档链接 |
|---|---|---|---|---|---|---|
| **OpenAI** Chat Completions | prompt cachingKV cache | `usage.prompt_tokens_details.cached_tokens` | `usage.prompt_tokens_details.cache_write_tokens`GPT-5.6+ 上报;老模型无写入字段) | 自动implicitGPT-5.6+ 可选显式 breakpoint | 历史 1024 tokens128 递增当前文档GPT-5.6+ = 1024更早模型 = 2048 | https://platform.openai.com/docs/guides/prompt-caching |
| **OpenAI** Responses API | prompt caching | `usage.input_tokens_details.cached_tokens` | `usage.input_tokens_details.cache_write_tokens` | 自动 / 显式(`prompt_cache_options.mode` + `prompt_cache_breakpoint` | 同上 | https://platform.openai.com/docs/guides/prompt-caching |
| **Anthropic** Claude Messages API | prompt caching前缀缓存 | `usage.cache_read_input_tokens` | `usage.cache_creation_input_tokens`(另有细分对象 `usage.cache_creation.ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens` | 显式:块级 `cache_control``{"type":"ephemeral"}`);也提供顶层 `cache_control` 自动断点 | 因模型而异512 / 1024 / 2048 / 4096 均有(详见 §3 表) | https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching即 platform.claude.com/docs/en/build-with-claude/prompt-caching |
| **Google Gemini** GLMGenerative Language API | 隐式缓存 + 显式 Context Caching`cachedContents` 资源) | `usageMetadata.cachedContentTokenCount`SDK snake_case`usage_metadata.cached_content_token_count`;另有带 modality 细分 `cacheTokensDetails` | 无「usage 内写入字段」;显式缓存通过 `cachedContents.create` 创建资源TTL 计费) | 隐式2.5 及更新模型自动;显式:须先创建 CachedContent 再传 `cachedContent` | 隐式Gemini 2.5 = 2048 tokensGemini 3.x = 4096 tokens历史2.5 Flash 曾 1024 / 2.5 Pro 曾 2048显式缓存资源 ≥1 分钟 TTL按 token·时长计费 | https://ai.google.dev/gemini-api/docs/generate-content/caching |
| **xAI** Grok | prompt cachingmessages 前缀缓存) | Chat Completions`usage.prompt_tokens_details.cached_tokens`Responses API`usage.input_tokens_details.cached_tokens` | 无独立字段(官方仅暴露 `cached_tokens` | 自动;建议设 `x-grok-conv-id` / `prompt_cache_key` 提升命中率 | 官方文档未公布固定 token 门槛(按消息前缀整段匹配) | https://docs.x.ai/developers/advanced-api-usage/prompt-caching |
| **Mistral AI** | prompt caching前缀缓存OpenAI 兼容格式) | `usage.prompt_tokens_details.cached_tokens` | 无独立字段(未命中时该字段为 0 或省略) | 显式:须在请求中传 `prompt_cache_key` 提高命中;命中与否由服务端决定 | 缓存块 = 64 tokens`cached_tokens` 恒为 64 的倍数;<64 token 无命中 | https://docs.mistral.ai/studio/conversations/advanced/prompt-caching |
| **AWS Bedrock** | 透传Claude 系用 `cachePoint`system/tools 内Amazon Nova 自动缓存 | `usage`(原生透传 Anthropic 的 `cacheReadInputTokens`/`cacheCreationInputTokens`converse 返回 `usage.cacheReadInputTokens`SDK 中为 `usage_metadata` | `cacheCreationInputTokens` | 显式cachePoint/ Nova 自动 | Claude 按模型(同 AnthropicNova 最高 20K tokens | https://aws.amazon.com/blogs/machine-learning/effectively-use-prompt-caching-on-amazon-bedrock |
| **Azure OpenAI** | 透传:与 OpenAI 字段一致prompt caching | Chat Completions`usage.prompt_tokens_details.cached_tokens`Responses API`usage.input_tokens_details.cached_tokens` | `usage.prompt_tokens_details.cache_write_tokens`GPT-5.6+ | 自动GPT-5.6+ 支持 breakpoint / `prompt_cache_key` | 最低 1024 tokens前 1024 必须完全一致 | https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/prompt-caching |
**一句话结论**:五家海外官方 API 中OpenAI / xAI / Mistral / Gemini 用「自动或半自动 + `cached_tokens` 类字段」Anthropic 用「显式 cache_control + read/write 双字段」。唯一同时提供「命中 + 写入」双向拆分的官方原生字段是 **Anthropic**`cache_read_input_tokens` / `cache_creation_input_tokens`)与 **OpenAI GPT-5.6+ / Responses API**`cached_tokens` / `cache_write_tokens`)。
---
## 2. 各家详细信息
### 2.1 OpenAIChat Completions API + Responses API
**官方文档**https://platform.openai.com/docs/guides/prompt-caching2026-08 抓取,官方文档原文;历史公告 https://openai.com/index/api-prompt-caching 作辅助)
**1) 是否支持 / 自动或显式**
- 支持,**默认自动启用**implicit caching无需改代码。
- GPT-5.6 及更新模型支持**显式缓存断点**`prompt_cache_options.mode: "explicit"` + 块上 `prompt_cache_breakpoint: {"mode":"explicit"}`)与 `prompt_cache_key`(影响路由、帮助同前缀请求命中同一台机器)。
- 更早模型仅有隐式缓存,断点由 OpenAI 按模型间隔自动放置。
**2) 命中字段完整 JSON 路径**
- Chat Completions API`usage.prompt_tokens_details.cached_tokens`
- Responses API`usage.input_tokens_details.cached_tokens`
- 官方文档定价示例Responses API 用法,摘自官方 docs 原文):
```json
// Responses API官方文档 "Request 1 · Response usage"
{
"usage": {
"input_tokens": 12000,
"input_tokens_details": {
"cached_tokens": 0,
"cache_write_tokens": 12000
}
}
}
// 第二次请求命中:
{
"usage": {
"input_tokens": 15000,
"input_tokens_details": {
"cached_tokens": 12000,
"cache_write_tokens": 3000
}
}
}
```
- Chat Completions经典格式官方社区/公告示例):`usage.prompt_tokens_details.cached_tokens`
```json
{
"usage": {
"prompt_tokens": 1253,
"completion_tokens": 72,
"total_tokens": 1325,
"prompt_tokens_details": {
"cached_tokens": 1024
},
"completion_tokens_details": {
"reasoning_tokens": 0
}
}
}
```
**3) 缓存写入/创建字段**
- 有Responses API `usage.input_tokens_details.cache_write_tokens`Chat Completions `usage.prompt_tokens_details.cache_write_tokens`GPT-5.6+ 上报;更早模型不计写入费、也不上报该字段)。
- 注:官方 docs 的成本计算示例同时读取 `cached_tokens``cache_write_tokens` 计算输入成本。
**4) 流式响应stream=true**
- Chat Completionsusage含 cached_tokens只在**最后一个 chunk** 返回,且必须设置 `stream_options: {"include_usage": true}`,否则流式响应不含 usage。
- Responses API流式下 usage 在 `response.completed` 事件中携带,字段路径不变。
- 字段路径在流式与批式下**完全一致**。
**5) 最低门槛**
- 历史2024-10 公告GPT-4o/o1 时代):**≥1024 tokens** 自动缓存,命中按 **128 tokens 递增**1024/1152/1280/1408…缓存通常 510 分钟无活动后清除、最长 1 小时。
- 当前官方文档2026-08GPT-5.6 及以后 = **1024 visible tokens**GPT-5.5 及更早 = **2048 visible tokens**个别老模型可更短GPT-5.6 不再按 128 取整(精确到缓存断点),旧模型上报时向下取整到 128 倍数。
**6) 计费折扣**
- 历史模型:命中 token 打 5 折50% off
- 当前GPT-5.6+ 缓存读 0.1×、缓存写 1.25×(写一次 + 读一次 = 1.35× vs 不缓存 2×更早模型读价为模型相关折扣、写入不额外计费。
**7) 注意事项**
- 缓存匹配的是「完整渲染前缀」model、tools、parallel_tool_calls、格式参数等任何相关设置变化都可能破坏前缀。
- 命中不保证 100%(路由溢出、机器未持有缓存)。官方建议用 `prompt_cache_key` 提高路由一致性。
- 实验时优先用 Responses API 的 `input_tokens_details`,或 Chat Completions 的 `prompt_tokens_details`;两处都要注意老模型字段可能为 null/缺省(`cached_tokens` 为 0 也算明确返回)。
### 2.2 Anthropic ClaudeMessages API
**官方文档**https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching与 platform.claude.com/docs/en/build-with-claude/prompt-caching 同源2026-08 抓取)
**1) 是否支持 / 自动或显式**
- 支持,**必须显式标记**
- 显式块级:在 `system` / `tools` / `messages.content` 块上加 `"cache_control": {"type": "ephemeral"}`(可加 `"ttl": "1h"` 延长到 1 小时)。
- 自动断点:在请求**顶层**加一个 `cache_control`系统自动把断点放到最后一个可缓存块并随对话增长前移2026 新增的 automatic caching 模式)。
- 断点最多 4 个;缓存前缀顺序 tools → system → messages。
**2) 命中字段完整 JSON 路径**
- `usage.cache_read_input_tokens`(本次请求从缓存读取的 token 数)
- `usage.input_tokens`(未命中、实际处理的 token 数)
- 官方文档示例1 小时 TTL 输出):
```json
{
"usage": {
"input_tokens": 2048,
"cache_read_input_tokens": 1800,
"cache_creation_input_tokens": 248,
"output_tokens": 503,
"cache_creation": {
"ephemeral_5m_input_tokens": 148,
"ephemeral_1h_input_tokens": 100
}
}
}
```
**3) 缓存写入/创建字段**
- 有:`usage.cache_creation_input_tokens`(写入缓存的新 token 数1 小时 TTL 时另有细分对象 `usage.cache_creation.ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens`(二者之和 = cache_creation_input_tokens
**4) 流式响应stream=true**
- usage 在 **`message_start` 事件**的 `message.usage` 中(含 input_tokens / cache_creation_input_tokens / cache_read_input_tokens`output_tokens` 增量在 `message_delta` 事件。字段路径与批式一致。
- 官方原文:"Monitor cache performance using these API response fields, within `usage` in the response (or `message_start` event if streaming)。"
**5) 最低门槛**(当前文档,按模型)
| 模型 | 最低可缓存 token 数 |
|---|---|
| Claude Opus 5 / Fable 5 / Mythos 5 | 512 |
| Claude Mythos Preview / Opus 4.7 | 2,048 |
| Claude Opus 4.6 / 4.5 | 4,096 |
| Claude Opus 4.8 / Sonnet 5 / Sonnet 4.6 / 4.5 / Opus 4.1 / Opus 4 / Sonnet 4 | 1,024 |
| Claude Haiku 4.5 | 4,096 |
| Claude Haiku 3.5 | 2,048 |
- 历史经典值claude-3/3.5 时代Sonnet/Opus = 1024、Haiku = 2048。低于门槛即使打了 cache_control 也不会缓存、不报错,只会在 usage 里两个缓存字段都为 0。
**6) 计费折扣**
- 缓存写5m TTL1.25× 基础输入价缓存写1h TTL2× 基础输入价;**缓存读/刷新0.1× 基础输入价**(约 90% 折扣)。
**7) 注意事项**
- 命中要求前缀 100% 一致(一个字符差异即 miss缓存生命周期 5 分钟1h 可选),从请求开始计时。
- 思考块thinking不能单独打 cache_control但可作为助手轮内容被缓存enabling/disabling web search、citations、effort 等设置会失效部分缓存。
- 可用 `max_tokens: 0` 预热缓存(不产生输出)。
- 官方提供 cache diagnostics 接口用于排查前缀差异。
### 2.3 Google GeminiGenerative Language API / Vertex AI
**官方文档**https://ai.google.dev/gemini-api/docs/generate-content/caching 与 REST 参考 https://ai.google.dev/api/generate-content2026-08 抓取)
**1) 是否支持 / 自动或显式**
- 支持两种:
- **隐式缓存**Gemini 2.5 及更新模型默认自动启用,请求里什么都不用加。
- **显式 Context Caching**:用 `cachedContents.create` 创建 CachedContent 资源(含 model/contents/systemInstruction/ttl然后在 generateContent 请求传 `cachedContent: "<cache 资源名>"` 引用。可用 OpenAI 兼容库时在 `extra_body``cached_content`
- Vertex AI 同样支持context caching字段名一致。
**2) 命中字段完整 JSON 路径**
- REST`GenerateContentResponse.usageMetadata.cachedContentTokenCount`
- SDKsnake_casePython/Node`response.usage_metadata.cached_content_token_count`
- 细分字段:`usageMetadata.cacheTokensDetails[]`(按 modality 的命中 token 明细)。
- 官方 REST 参考中 UsageMetadata JSON节选
```json
{
"promptTokenCount": integer,
"cachedContentTokenCount": integer,
"candidatesTokenCount": integer,
"toolUsePromptTokenCount": integer,
"thoughtsTokenCount": integer,
"totalTokenCount": integer,
"promptTokensDetails": [ { "modality": "...", "tokenCount": integer } ],
"cacheTokensDetails": [ { "modality": "...", "tokenCount": integer } ],
"candidatesTokensDetails": [ { "modality": "...", "tokenCount": integer } ]
}
```
- 社区实测示例Gemini 2.5来源discuss.ai.google.dev等级=辅助):显式缓存命中时 `cached_content_token_count=4115`、`cache_tokens_details=[{modality:'TEXT', token_count:4115}]`。
**3) 缓存写入/创建字段**
- usage 内**没有**缓存写入字段;「写入」体现在显式 CachedContent 资源的计费(按 token 数 × 存储时长 TTL 计费),资源元数据中有 `usageMetadata.totalTokenCount`。隐式缓存的写入由 Google 内部处理,不暴露字段。
**4) 流式响应stream=true / streamGenerateContent**
- `usageMetadata` 在**最后一个 chunk** 返回REST `streamGenerateContent` 的末帧SDK 中亦在流结束的响应对象上)。当前官方文档没有为缓存命中另设流式事件,字段路径不变。
- 社区有多起「流式末尾 usageMetadata 里 cachedContentTokenCount 缺失」的报告discuss.ai.google.dev等级=辅助,未 100% 确认为官方 bug实验时建议同时打印非流式结果对照。
**5) 最低门槛**
- 隐式当前文档表Gemini 2.5 Flash / 2.5 Pro = **2,048 tokens**Gemini 3.x3.1 Pro Preview / 3.5 / 3.6 / 3.7 Flash= **4,096 tokens**
- 历史Google 官方博客 2025-052.5 Flash 曾 1,024、2.5 Pro 曾 2,048——阈值随版本调整以文档当前值为准。
- 显式缓存:无 token 下限但资源有 TTL默认 1h可 300s 起),按 token×时间计费≥1 分钟 TTL。
**6) 计费折扣**
- 隐式缓存命中按缓存价计费Gemini 2.5 起缓存输入约为基础价 10% 档;具体以官方定价页为准)。
- 显式缓存:命中 token 折扣 90%2.5+ 模型)/ 75%2.0 模型),外加缓存存储费($/token·hour
**7) 注意事项**
- 隐式缓存命中率不受控制、非保证显式缓存保证计费折扣但要多维护资源生命周期create/list/update/delete API
- `cachedContentTokenCount` 只统计命中的 token`promptTokenCount` 仍含全部输入;计算未命中部分 = promptTokenCount cachedContentTokenCount。
- 实验时注意 SDK 属性名 snake_case`cached_content_token_count`)与 REST camelCase`cachedContentTokenCount`)的差异。
### 2.4 xAI Grok
**官方文档**https://docs.x.ai/developers/advanced-api-usage/prompt-cachingHow it works / Usage & Pricing / Best Practices & FAQ2026-08 抓取)
**1) 是否支持 / 自动或显式**
- 支持,**完全自动**(按 messages 数组起始匹配前缀);建议设置 HTTP 头 `x-grok-conv-id`(或 Responses API 的 `prompt_cache_key`)提升命中率。
- 无 cache_control 之类显式标记。
**2) 命中字段完整 JSON 路径**
- Chat Completions API`usage.prompt_tokens_details.cached_tokens`
- Responses API`usage.input_tokens_details.cached_tokens`
- 官方示例Chat Completions
```json
{
"usage": {
"prompt_tokens": 125,
"completion_tokens": 48,
"total_tokens": 173,
"prompt_tokens_details": {
"text_tokens": 125,
"audio_tokens": 0,
"image_tokens": 0,
"cached_tokens": 98
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
}
}
```
- 官方示例Responses API`usage.input_tokens_details.cached_tokens`(路径同 OpenAI Responses API
**3) 缓存写入/创建字段**
- 无。官方只暴露 `cached_tokens`≤0 表示 miss等于 prompt_tokens 表示整段命中)。
**4) 流式响应stream=true**
- 官方 FAQ**流式与非流式均支持缓存**;流式下第一个空 token 对应缓存查找与 prefill 阶段。usage 聚合到最后一个 chunk沿用 OpenAI 风格,需 `stream_options.include_usage`)。字段路径不变。
**5) 最低门槛**
- 官方文档未公布固定 token 门槛;机制按「消息前缀精确匹配」整段生效(示例中 3 条消息被整体缓存)。实验时建议让共享前缀 ≥ 数百 token 并保持多轮对话以观察命中增长。
**6) 计费折扣**
- 缓存命中 token 按「cached prompt token 价」计费(低于常规输入价;具体比率见各模型定价页)。
**7) 注意事项**
- 命中无保证(内存压力可驱逐缓存、请求可能路由到别的机器);换 `x-grok-conv-id` 可强制 miss便于对照实验。
- 典型多轮turn1 cached=0建缓存→ turn2 cached=前 50 → turn3 cached=前 120官方示例
### 2.5 Mistral AI
**官方文档**https://docs.mistral.ai/studio/conversations/advanced/prompt-caching 与 API 参考 https://docs.mistral.ai/api/endpoint/chat2026-08 抓取)
**1) 是否支持 / 自动或显式**
- 支持 prompt caching**需在请求中显式传 `prompt_cache_key`**(会话/工作流 ID来提升命中且请求体必须保留共享前缀多轮重发完整历史。命中与否由服务端决定key 不保证命中。
- 接口格式 OpenAI 兼容(/v1/chat/completions
**2) 命中字段完整 JSON 路径**
- `usage.prompt_tokens_details.cached_tokens`
- 官方示例:
```json
{
"id": "a4db7c530548494f8ff9986bcd2a7737",
"created": 1773840064,
"model": "mistral-large-latest",
"usage": {
"prompt_tokens": 1013,
"total_tokens": 1043,
"completion_tokens": 30,
"prompt_tokens_details": {
"cached_tokens": 1008
}
},
"object": "chat.completion"
}
```
- 未命中时 `cached_tokens` 为 0 或字段被省略。
**3) 缓存写入/创建字段**
- 无独立写入字段;计费侧「可收费未缓存输入 = prompt_tokens cached_tokens」。
**4) 流式响应stream=true**
- 官方文档未单列流式差异API 与 OpenAI 兼容,流式下 usage含 cached_tokens在最后一个 chunk需 stream_options.include_usage 等开关)。实验时建议以「最后一个 chunk 的 usage」为准核对并用非流式对照等级基于兼容性推断官方未明示
**5) 最低门槛**
- **缓存块 = 64 tokens**`cached_tokens` 恒为 64 的倍数prompt < 64 tokens 不会命中共享前缀越长可复用越多
**6) 计费折扣**
- 缓存命中 token 按标准输入价 **10%** 计费(官方原文)。
**7) 注意事项**
- `prompt_cache_key` 不应包含密钥/敏感数据;变更 prompt 开头部分会导致 miss命中率可在 Admin Panel Usage 按模型查看。
### 2.6 AWS Bedrock / Azure OpenAI透传简述
**AWS Bedrock**
- 透传方式Claude 系模型在 `converse` / `invoke_model` 请求的 system/tools 内放 `{"cachePoint": {"type": "default"}}` 标记缓存点Claude 平台另有 cache_control 等效写法Amazon Nova 模型则自动缓存(文本 prompt最多 20K tokens
- 响应中透传 Anthropic 原样字段:`usage.cacheReadInputTokens` / `usage.cacheCreationInputTokens`REST/ SDK `usage_metadata``cache_read_input_tokens` / `cache_creation_input_tokens`AWS 官方博客示例:
```json
"usage": {
"input_tokens": 10,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 37209,
"output_tokens": 324
}
```
- 门槛/折扣与 Anthropic 一致ClaudeClaude 之外模型(截至 2026-06 官方/社区口径多数尚未支持缓存AWS re:Post 有用户确认 Nova 支持、Mistral 在 Bedrock 上不支持)。
- 官方https://aws.amazon.com/blogs/machine-learning/effectively-use-prompt-caching-on-amazon-bedrock
**Azure OpenAI**
- 透传方式:与 OpenAI 同名请求结构与字段,模型名改为 Azure 部署名。
- 命中字段Chat Completions `usage.prompt_tokens_details.cached_tokens`Responses API `usage.input_tokens_details.cached_tokens`GPT-5.6+ 另有 `usage.prompt_tokens_details.cache_write_tokens`
- 门槛:**≥1024 tokens** 且前 1024 tokens 完全一致GPT-5.5 及更早按 128 递增取整GPT-5.6+ 不取整默认自动启用GPT-5.6+ 支持 `prompt_cache_key` / `prompt_cache_options.mode` / `prompt_cache_breakpoint`、`prompt_cache_options.ttl="30m"`。
- 官方示例:
```json
{
"usage": {
"prompt_tokens": 1566,
"completion_tokens": 1518,
"total_tokens": 3084,
"prompt_tokens_details": {
"audio_tokens": null,
"cached_tokens": 1408,
"cache_write_tokens": 0
},
"completion_tokens_details": { "audio_tokens": null, "reasoning_tokens": 576 }
}
}
```
- 官方https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/prompt-caching
---
## 3. 实验建议(按供应商)
通用思路:**连续发两条「共享固定前缀 + 不同后缀」的请求**,记录第 1 次(写入/未命中)与第 2 次(命中)的 usage 字段;前缀长度务必超出各家门槛;两次请求间隔须在缓存 TTL 内。
### OpenAI
- 构造:固定 system/developer 指令≥1024 tokens建议 2000+)放最前 + 每次变化的 user 后缀;两次请求只改后缀。
- 读取字段Chat Completions `usage.prompt_tokens_details.cached_tokens`Responses API `usage.input_tokens_details.cached_tokens`GPT-5.6+ 可同时看 `cache_write_tokens`/`cache_read` 0.1× 计费)。
- 断言:第 2 次请求 `cached_tokens > 0`(且应为前缀长度附近,老模型按 128 取整)。
- 流式:必须 `stream_options: {"include_usage": true}`,取最后一个 chunk 的 usage。
- 建议同时打印完整 `usage` 对象,防止 SDK 解包时字段为 None。
### Anthropic Claude
- 构造system 数组放长文本(≥对应模型门槛,如 Sonnet 4.5/5 = 1024Haiku 4.5 = 4096块尾加 `"cache_control": {"type": "ephemeral"}`;连续发两条相同前缀请求。
- 读取字段:`usage.cache_read_input_tokens`(命中)与 `usage.cache_creation_input_tokens`(首次写入);`input_tokens` 为未命中部分。
- 断言:第 1 次 `cache_creation_input_tokens ≈ 前缀长`、第 2 次 `cache_read_input_tokens ≈ 前缀长`;两次都在 5 分钟(默认 TTL内。
- 流式:读 `message_start` 事件的 `message.usage`
- 额外可试:`max_tokens: 0` 预热 + 顶层 `cache_control`automatic caching两种模式各跑一轮。
### Google Gemini
- 构造:
- 隐式:长 system_instruction / 长首条 user 消息≥2048 tokensGemini 2.5≥4096 若用 3.x连续请求、保持前缀不变。
- 显式:先 `cachedContents.create`model + contents + ttl如 300s~1h再 generateContent 传 `cachedContent`,用于对照验证。
- 读取字段:`response.usage_metadata.cached_content_token_count`REST 为 `usageMetadata.cachedContentTokenCount`);明细看 `cache_tokens_details[]`
- 断言:显式缓存第 1 次 cached=0或写资源、之后 cached ≈ 缓存内容 token 数;隐式缓存命中需 ≥ 门槛且不保证,多试几轮或调高峰时段。
- 流式:取流末尾 chunk 的 usageMetadata 核对;若缺失可与非流式对照。
- 注意promptTokenCount 含缓存 token未命中部分 = promptTokenCount cachedContentTokenCount。
### xAI Grok
- 构造:固定 system + 固定历史轮次 + 变化的最新 user 消息;设置 `x-grok-conv-id`Chat Completions`prompt_cache_key`Responses API保持一致。
- 读取字段Chat Completions `usage.prompt_tokens_details.cached_tokens`Responses API `usage.input_tokens_details.cached_tokens`
- 断言:多轮递增 —— turn1 cached=0 → turn2 cached=前几轮总 token → turn3 更大;若一直为 0改用不同/省略 conv-id 强制 miss 对照已验证机制。
- 流式:可观察第一个空 tokenprefillusage 取最后 chunk。
### Mistral AI
- 构造:固定 system + 历史 + 变化的最后 user 消息,**每条请求都传相同 `prompt_cache_key`(如会话 ID并完整重发前缀**;前缀建议 ≥128 tokens至少 2 个 64 块)以便观察倍数。
- 读取字段:`usage.prompt_tokens_details.cached_tokens`。
- 断言:第 2 次 `cached_tokens > 0` 且为 64 的倍数;未命中时为 0 或字段缺失。计费未缓存输入 = prompt_tokens cached_tokens10% 折扣)。
- 流式:取最后一个 chunk 的 usage必要时非流式对照。
### AWS Bedrock / Azure OpenAI顺带
- BedrockClaudesystem 里加 `{"cachePoint": {"type": "default"}}`,看 `usage.cacheReadInputTokens` / `cacheCreationInputTokens` 增减。
- Azure OpenAI与 OpenAI 实验相同≥1024 tokens、前缀不动`usage.prompt_tokens_details.cached_tokens` 与 GPT-5.6+ 的 `cache_write_tokens`
---
## 附:信息来源与等级
| 内容 | 来源 | 等级 |
|---|---|---|
| OpenAI 全部字段/门槛/计费 | platform.openai.com/docs/guides/prompt-caching官方2026-08 抓取) | 官方文档 |
| OpenAI 历史 1024/128 递增/5 折 | openai.com/index/api-prompt-caching官方公告 | 官方发布 |
| Anthropic 全部字段/门槛/计费/流式 | docs.anthropic.com= platform.claude.comprompt-caching官方2026-08 抓取) | 官方文档 |
| Gemini 隐式/显式/门槛 | ai.google.dev/gemini-api/docs/generate-content/caching、ai.google.dev/api/generate-content官方 | 官方文档 |
| Gemini usageMetadata JSON | ai.google.dev/api/generate-content官方 REST 参考) | 官方文档 |
| Gemini 2.5 隐式缓存历史阈值 | developers.googleblog.comGoogle 官方博客) | 官方发布(辅助) |
| Gemini 流式/字段缺失现象 | discuss.ai.google.dev 社区帖 | 第三方(辅助) |
| xAI 全部字段/机制/流式 FAQ | docs.x.ai/developers/advanced-api-usage/prompt-caching官方2026-08 抓取) | 官方文档 |
| Mistral 全部字段/64 块/10% 计费 | docs.mistral.ai/studio/conversations/advanced/prompt-caching、docs.mistral.ai/api/endpoint/chat官方 | 官方文档 |
| Bedrock cachePoint/usage 透传 | aws.amazon.com 官方博客 + AWS re:Post + Portkey 文档 | 官方博客/第三方辅助 |
| Azure OpenAI 字段/门槛 | learn.microsoft.com微软官方 | 官方文档 |
> 提示:以上信息抓取于 2026-08-29部分字段/门槛随时间迭代(如 Gemini 阈值、Anthropic 门槛、OpenAI GPT-5.6 行为改动)。实验前建议按本报告给出的官方链接复核最新值;凡第三方转述均已在文中标注「等级=辅助」。

View File

@ -584,6 +584,8 @@ async def run_deep_compression(
# 关键:重置 current_context_tokens避免自动压缩续接后阈值判断仍读到压缩前的大值而陷入死循环。
# 真实上下文长度会在下一次 API 响应后被重新写入。
# 同时置位 cache_cold_start_pending压缩重写了上下文前缀缓存可能已失效
# 下一次真实调用若未命中缓存其输入会被计入冷启动豁免值cache_exempt_input_tokens
try:
target_manager.update_token_statistics(
conversation_id,
@ -591,6 +593,7 @@ async def run_deep_compression(
output_tokens=0,
total_tokens=0,
current_context_tokens=0,
cache_cold_start_pending=True,
)
except Exception as exc:
_emit(sender, "system_message", {"content": tr("deep_compression.stats_reset_failed", error=exc)})

View File

@ -72,6 +72,8 @@ export const syncMethods = {
this.currentConversationTokens.cumulative_input_tokens = data.cumulative_input_tokens || 0;
this.currentConversationTokens.cumulative_output_tokens = data.cumulative_output_tokens || 0;
this.currentConversationTokens.cumulative_total_tokens = data.cumulative_total_tokens || 0;
this.currentConversationTokens.cumulative_cached_input_tokens = data.cumulative_cached_input_tokens || 0;
this.currentConversationTokens.cache_exempt_input_tokens = data.cache_exempt_input_tokens || 0;
if (typeof data.current_context_tokens === 'number') {
this.resourceSetCurrentContextTokens(data.current_context_tokens);

View File

@ -33,6 +33,16 @@
{{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }}
</div>
</div>
<div class="stat-block">
<div class="stat-label">{{ $t('sidebar.cumulativeCachedInput') }}</div>
<div class="stat-value">
{{ formatTokenCount(currentConversationTokens.cumulative_cached_input_tokens || 0) }}
</div>
</div>
<div class="stat-block">
<div class="stat-label">{{ $t('sidebar.cacheHitRate') }}</div>
<div class="stat-value">{{ cacheHitRateText }}</div>
</div>
</div>
</div>
<div class="usage-cell usage-cell--right usage-cell--performance panel-card">
@ -128,6 +138,8 @@ const props = defineProps<{
currentConversationTokens: {
cumulative_input_tokens?: number;
cumulative_output_tokens?: number;
cumulative_cached_input_tokens?: number;
cache_exempt_input_tokens?: number;
};
currentContextTokens: number;
containerStatus: any;
@ -152,6 +164,19 @@ const quotaTiers = computed(() => [
{ key: 'search', label: 'sidebar.quotaTierSearch', value: props.usageQuota.search }
]);
// = / ( - )
//
const cacheHitRateText = computed(() => {
const totalInput = props.currentConversationTokens.cumulative_input_tokens || 0;
const exemptInput = props.currentConversationTokens.cache_exempt_input_tokens || 0;
const effectiveInput = totalInput - exemptInput;
if (effectiveInput <= 0) {
return '--';
}
const cached = props.currentConversationTokens.cumulative_cached_input_tokens || 0;
return `${((cached / effectiveInput) * 100).toFixed(1)}%`;
});
const hasContainerStats = computed(() => {
const status = props.containerStatus;
if (!status || !status.stats) {

View File

@ -676,6 +676,8 @@ export async function initializeLegacySocket(ctx: any) {
ctx.currentConversationTokens.cumulative_input_tokens = data.cumulative_input_tokens || 0;
ctx.currentConversationTokens.cumulative_output_tokens = data.cumulative_output_tokens || 0;
ctx.currentConversationTokens.cumulative_total_tokens = data.cumulative_total_tokens || 0;
ctx.currentConversationTokens.cumulative_cached_input_tokens = data.cumulative_cached_input_tokens || 0;
ctx.currentConversationTokens.cache_exempt_input_tokens = data.cache_exempt_input_tokens || 0;
socketLog(
`Cumulative token stats updated: input=${data.cumulative_input_tokens}, output=${data.cumulative_output_tokens}, total=${data.cumulative_total_tokens}`

View File

@ -55,6 +55,8 @@ export default {
currentContext: 'Current context',
cumulativeInput: 'Total input',
cumulativeOutput: 'Total output',
cumulativeCachedInput: 'Cached input',
cacheHitRate: 'Cache hit rate',
performanceStats: 'Performance',
memory: 'Memory',
containerMetricsPending: 'Container is running, waiting for metrics...',

View File

@ -59,6 +59,8 @@ export default {
currentContext: '当前上下文',
cumulativeInput: '累计输入',
cumulativeOutput: '累计输出',
cumulativeCachedInput: '累积缓存输入',
cacheHitRate: '缓存命中率',
performanceStats: '性能统计',
memory: '内存',
containerMetricsPending: '容器已运行,等待采集指标...',

View File

@ -5,6 +5,8 @@ interface ConversationTokens {
cumulative_input_tokens: number;
cumulative_output_tokens: number;
cumulative_total_tokens: number;
cumulative_cached_input_tokens: number;
cache_exempt_input_tokens: number;
}
interface ProjectStorage {
@ -65,7 +67,9 @@ export const useResourceStore = defineStore('resource', {
currentConversationTokens: {
cumulative_input_tokens: 0,
cumulative_output_tokens: 0,
cumulative_total_tokens: 0
cumulative_total_tokens: 0,
cumulative_cached_input_tokens: 0,
cache_exempt_input_tokens: 0
} as ConversationTokens,
projectStorage: {
used_bytes: 0,
@ -97,7 +101,9 @@ export const useResourceStore = defineStore('resource', {
this.currentConversationTokens = {
cumulative_input_tokens: 0,
cumulative_output_tokens: 0,
cumulative_total_tokens: 0
cumulative_total_tokens: 0,
cumulative_cached_input_tokens: 0,
cache_exempt_input_tokens: 0
};
},
setCurrentContextTokens(value: number) {
@ -143,6 +149,10 @@ export const useResourceStore = defineStore('resource', {
this.currentConversationTokens.cumulative_output_tokens =
data.data.total_output_tokens || 0;
this.currentConversationTokens.cumulative_total_tokens = data.data.total_tokens || 0;
this.currentConversationTokens.cumulative_cached_input_tokens =
data.data.total_cached_input_tokens || 0;
this.currentConversationTokens.cache_exempt_input_tokens =
data.data.cache_exempt_input_tokens || 0;
if (typeof data.data.current_context_tokens === 'number') {
this.currentContextTokens = data.data.current_context_tokens;
}

View File

@ -132,6 +132,14 @@
font-size: 20px;
}
// 深色主题下 --accent 为灰色#606060作为大字号统计数字辨识度不足
// 当前上下文数字改用 --text-primarydark 下为白色保证可读性
body[data-theme='dark'] {
.stat-value--accent {
color: var(--text-primary);
}
}
.stat-value--success {
color: var(--state-success);
}

View File

@ -100,6 +100,7 @@ class TokenMixin:
"input_tokens": int(payload.get("input_tokens") or payload.get("total_input_tokens") or 0),
"output_tokens": int(payload.get("output_tokens") or payload.get("total_output_tokens") or 0),
"total_tokens": int(payload.get("total_tokens") or 0),
"cached_input_tokens": int(payload.get("cached_input_tokens") or payload.get("total_cached_input_tokens") or 0),
"updated_at": payload.get("updated_at"),
}
except (OSError, json.JSONDecodeError, ValueError) as exc:
@ -117,13 +118,14 @@ class TokenMixin:
with open(path, 'w', encoding='utf-8') as fh:
json.dump(data, fh, ensure_ascii=False, indent=2)
def _increment_workspace_token_totals(self, input_tokens: int, output_tokens: int, total_tokens: int):
def _increment_workspace_token_totals(self, input_tokens: int, output_tokens: int, total_tokens: int, cached_input_tokens: int = 0):
if input_tokens <= 0 and output_tokens <= 0 and total_tokens <= 0:
return
snapshot = self._load_token_totals()
snapshot["input_tokens"] = snapshot.get("input_tokens", 0) + max(0, int(input_tokens))
snapshot["output_tokens"] = snapshot.get("output_tokens", 0) + max(0, int(output_tokens))
snapshot["total_tokens"] = snapshot.get("total_tokens", 0) + max(0, int(total_tokens))
snapshot["cached_input_tokens"] = snapshot.get("cached_input_tokens", 0) + max(0, int(cached_input_tokens))
snapshot["updated_at"] = datetime.now().isoformat()
self._save_token_totals(snapshot)
@ -137,9 +139,11 @@ class TokenMixin:
total_tokens = int(normalized_usage.get("total_tokens") or (prompt_tokens + completion_tokens))
# 当前上下文长度优先取专用字段;缺失时回退到 prompt_tokens
current_context_tokens = int(normalized_usage.get("current_context_tokens") or prompt_tokens)
# 本次请求命中缓存的输入 token全厂商字段已在 normalize 中归一化)
cached_input_tokens = int(normalized_usage.get("cached_input_tokens") or 0)
try:
self._increment_workspace_token_totals(prompt_tokens, completion_tokens, total_tokens)
self._increment_workspace_token_totals(prompt_tokens, completion_tokens, total_tokens, cached_input_tokens)
except Exception as exc:
print(f"[TokenStats] 无法写入累计Token: {exc}")
@ -160,6 +164,7 @@ class TokenMixin:
completion_tokens,
total_tokens,
current_context_tokens=current_context_tokens,
cached_input_tokens=cached_input_tokens,
)
if success:
@ -221,6 +226,8 @@ class TokenMixin:
'cumulative_input_tokens': cumulative_stats.get("total_input_tokens", 0) if cumulative_stats else 0,
'cumulative_output_tokens': cumulative_stats.get("total_output_tokens", 0) if cumulative_stats else 0,
'cumulative_total_tokens': cumulative_stats.get("total_tokens", 0) if cumulative_stats else 0,
'cumulative_cached_input_tokens': cumulative_stats.get("total_cached_input_tokens", 0) if cumulative_stats else 0,
'cache_exempt_input_tokens': cumulative_stats.get("cache_exempt_input_tokens", 0) if cumulative_stats else 0,
'current_context_tokens': cumulative_stats.get("current_context_tokens", 0) if cumulative_stats else 0,
'updated_at': datetime.now().isoformat()
}

View File

@ -147,6 +147,11 @@ class MetadataMixin:
"total_input_tokens": 0,
"total_output_tokens": 0,
"total_tokens": 0,
"total_cached_input_tokens": 0,
# 豁免出命中率分母的冷启动输入累计(首轮未命中 + 压缩后首轮未命中的输入)
"cache_exempt_input_tokens": 0,
# 深度压缩后待判定标记:下一次真实调用若无缓存命中,其输入累加进豁免值
"cache_cold_start_pending": False,
"current_context_tokens": 0,
"updated_at": now
}
@ -161,12 +166,15 @@ class MetadataMixin:
if key not in token_stats:
token_stats[key] = default_value
# 确保数值类型正确
# 确保数值类型正确cache_cold_start_pending 为布尔,不在此转换)
try:
token_stats["total_input_tokens"] = int(token_stats.get("total_input_tokens", 0))
token_stats["total_output_tokens"] = int(token_stats.get("total_output_tokens", 0))
token_stats["total_tokens"] = int(token_stats.get("total_tokens", 0))
token_stats["total_cached_input_tokens"] = int(token_stats.get("total_cached_input_tokens", 0))
token_stats["cache_exempt_input_tokens"] = int(token_stats.get("cache_exempt_input_tokens", 0))
token_stats["current_context_tokens"] = int(token_stats.get("current_context_tokens", 0))
token_stats["cache_cold_start_pending"] = bool(token_stats.get("cache_cold_start_pending", False))
except (ValueError, TypeError):
print("⚠️ Token统计数据损坏重置为0")
token_stats = defaults

View File

@ -48,6 +48,8 @@ class TokenMixin:
output_tokens: int,
total_tokens: int,
current_context_tokens: Optional[int] = None,
cached_input_tokens: int = 0,
cache_cold_start_pending: bool = False,
) -> bool:
"""
更新对话的Token统计
@ -58,6 +60,8 @@ class TokenMixin:
output_tokens: 输出Token数量
total_tokens: 本次请求的总Token数量prompt+completion
current_context_tokens: 当前上下文长度用于压缩阈值判断
cached_input_tokens: 本次请求命中缓存的输入Token数量
cache_cold_start_pending: 置位压缩后待判定标记深度压缩后调用时传入
Returns:
bool: 更新是否成功
@ -74,9 +78,28 @@ class TokenMixin:
# 更新统计数据
token_stats = conversation_data["token_statistics"]
# ── 冷启动豁免:首轮/压缩后首轮若未命中缓存,其输入属于“建立缓存”成本,
# 豁免出命中率分母cache_exempt_input_tokens若命中则说明缓存延续正常处理。
# 判断需在累加之前进行(首轮判定依赖累加前的 total_input_tokens 为 0
if input_tokens > 0:
is_first_call = token_stats.get("total_input_tokens", 0) == 0
cold_start_pending = bool(token_stats.get("cache_cold_start_pending", False))
if is_first_call or cold_start_pending:
if cached_input_tokens <= 0:
token_stats["cache_exempt_input_tokens"] = (
token_stats.get("cache_exempt_input_tokens", 0) + int(input_tokens)
)
# 压缩后首轮消费标记(首轮不涉及该标记,置 False 无副作用)
token_stats["cache_cold_start_pending"] = False
token_stats["total_input_tokens"] = token_stats.get("total_input_tokens", 0) + input_tokens
token_stats["total_output_tokens"] = token_stats.get("total_output_tokens", 0) + output_tokens
token_stats["total_tokens"] = token_stats.get("total_tokens", 0) + total_tokens
token_stats["total_cached_input_tokens"] = token_stats.get("total_cached_input_tokens", 0) + max(0, int(cached_input_tokens or 0))
# 置位压缩后待判定标记(深度压缩重置统计时传入)
if cache_cold_start_pending:
token_stats["cache_cold_start_pending"] = True
if current_context_tokens is None:
# 兼容旧调用:未显式传入时,默认以输入 token 作为当前上下文长度
current_context_tokens = input_tokens
@ -116,6 +139,8 @@ class TokenMixin:
"total_input_tokens": token_stats.get("total_input_tokens", 0),
"total_output_tokens": token_stats.get("total_output_tokens", 0),
"total_tokens": token_stats.get("total_tokens", 0),
"total_cached_input_tokens": token_stats.get("total_cached_input_tokens", 0),
"cache_exempt_input_tokens": token_stats.get("cache_exempt_input_tokens", 0),
"current_context_tokens": token_stats.get("current_context_tokens", 0),
"updated_at": token_stats.get("updated_at"),
"conversation_id": conversation_id

View File

@ -16,6 +16,7 @@ INPUT_TOKEN_KEYS = (
"inputTokens",
"promptTokens",
"prefill_tokens",
"promptTokenCount",
)
OUTPUT_TOKEN_KEYS = (
"completion_tokens",
@ -24,6 +25,7 @@ OUTPUT_TOKEN_KEYS = (
"completionTokens",
"generated_tokens",
"generatedTokens",
"candidatesTokenCount",
)
TOTAL_TOKEN_KEYS = (
"total_tokens",
@ -31,6 +33,45 @@ TOTAL_TOKEN_KEYS = (
"total_token_count",
"totalTokenCount",
)
# 缓存命中 token 数的所有已知字段位置2026-08 调研,见 cache_research/SUMMARY.md
# - OpenAI 系/Qwen/GLM/MiniMax/xAI/Mistral/千帆/OpenRouter: usage.prompt_tokens_details.cached_tokens
# Responses API 为 usage.input_tokens_details.cached_tokens
# - DeepSeek: usage.prompt_cache_hit_tokens顶层
# - Kimi / 阶跃Step / 部分 DashScope 地域: usage.cached_tokens顶层
# - Anthropic / Bedrock / MiniMax-Anthropic 模式/中转站: usage.cache_read_input_tokens顶层
# - Gemini: usageMetadata.cachedContentTokenCount
CACHED_INPUT_TOKEN_KEYS = (
"cached_input_tokens", # normalize 输出自身的字段名(保证二次归一化幂等)
"cached_tokens",
"cachedTokens",
"prompt_cache_hit_tokens",
"promptCacheHitTokens",
"cache_read_input_tokens",
"cacheReadInputTokens",
"cached_content_token_count",
"cachedContentTokenCount",
)
CACHE_WRITE_TOKEN_KEYS = (
"cache_creation_input_tokens",
"cacheCreationInputTokens",
"cache_write_tokens",
"cacheWriteTokens",
)
# 缓存详情可能出现的嵌套容器OpenAI 风格 details 对象)
PROMPT_DETAILS_KEYS = (
"prompt_tokens_details",
"input_tokens_details",
"promptTokensDetails",
"inputTokensDetails",
)
# Anthropic 语义的输入键与顶层缓存字段组合:仅当【输入命中 input_tokens 类键】
# 且【顶层存在 cache_read_input_tokens / cache_creation_input_tokens】时才判定为
# Anthropic 语义input_tokens 不含缓存部分),需要把缓存部分加回总输入。
# 注意OpenAI Responses API 也用 input_tokens 键但其缓存字段在 input_tokens_details 里
# input_tokens 本身含缓存),因此不能用键名单独判断,必须同时要求顶层 Anthropic 字段存在。
ANTHROPIC_STYLE_INPUT_KEYS = {"input_tokens", "inputTokens"}
ANTHROPIC_CACHE_READ_KEYS = ("cache_read_input_tokens", "cacheReadInputTokens")
ANTHROPIC_CACHE_WRITE_KEYS = ("cache_creation_input_tokens", "cacheCreationInputTokens")
CURRENT_CONTEXT_KEYS = (
"current_context_tokens",
"currentContextTokens",
@ -60,28 +101,49 @@ def _to_int(value: Any) -> Optional[int]:
def _first_int(payload: Dict[str, Any], keys: Iterable[str]) -> Optional[int]:
_, value = _first_int_with_key(payload, keys)
return value
def _first_int_with_key(payload: Dict[str, Any], keys: Iterable[str]) -> tuple:
"""返回 (命中键名, 值);未命中返回 (None, None)。"""
for key in keys:
if key in payload:
value = _to_int(payload.get(key))
if value is not None:
return value
return None
return key, value
return None, None
def normalize_usage_payload(raw: Any) -> Optional[Dict[str, int]]:
if not isinstance(raw, dict):
return None
prompt_tokens = _first_int(raw, INPUT_TOKEN_KEYS)
prompt_key, prompt_tokens = _first_int_with_key(raw, INPUT_TOKEN_KEYS)
completion_tokens = _first_int(raw, OUTPUT_TOKEN_KEYS)
total_tokens = _first_int(raw, TOTAL_TOKEN_KEYS)
current_context_tokens = _first_int(raw, CURRENT_CONTEXT_KEYS)
prompt_details = raw.get("prompt_tokens_details") or raw.get("input_tokens_details")
if isinstance(prompt_details, dict):
cached = _first_int(prompt_details, ("cached_tokens", "cachedTokens"))
# cached tokens are still part of prompt tokens in most APIs. Keep the
# detail accessible for callers that need it, but do not add it again.
# 缓存命中先查顶层字段DeepSeek/Kimi/Step/Anthropic/Gemini再查 details 容器OpenAI 系)
cached_input_tokens = _first_int(raw, CACHED_INPUT_TOKEN_KEYS)
cache_write_tokens = _first_int(raw, CACHE_WRITE_TOKEN_KEYS)
for details_key in PROMPT_DETAILS_KEYS:
prompt_details = raw.get(details_key)
if not isinstance(prompt_details, dict):
continue
if cached_input_tokens is None:
cached_input_tokens = _first_int(prompt_details, CACHED_INPUT_TOKEN_KEYS)
if cache_write_tokens is None:
cache_write_tokens = _first_int(prompt_details, CACHE_WRITE_TOKEN_KEYS)
# Anthropic 语义校准:顶层出现 cache_read/cache_creation 字段且输入键为 input_tokens 时,
# input_tokens 不含缓存读取/写入部分,加回以统一“总输入”口径;
# OpenAI 系prompt_tokens 或 details 内 cached_tokens本身含缓存部分不校准。
anthropic_read = _first_int(raw, ANTHROPIC_CACHE_READ_KEYS)
anthropic_write = _first_int(raw, ANTHROPIC_CACHE_WRITE_KEYS)
if prompt_key in ANTHROPIC_STYLE_INPUT_KEYS and (anthropic_read or anthropic_write):
prompt_tokens = (prompt_tokens or 0) + (anthropic_read or 0) + (anthropic_write or 0)
completion_details = raw.get("completion_tokens_details") or raw.get("output_tokens_details")
if isinstance(completion_details, dict):
reasoning = _first_int(completion_details, ("reasoning_tokens", "reasoningTokens"))
@ -105,6 +167,7 @@ def normalize_usage_payload(raw: Any) -> Optional[Dict[str, int]]:
"completion_tokens": int(completion_tokens),
"total_tokens": int(total_tokens),
"current_context_tokens": int(current_context_tokens),
"cached_input_tokens": int(cached_input_tokens or 0),
}