Compare commits
2 Commits
69c5953fe7
...
640274b7c7
| Author | SHA1 | Date | |
|---|---|---|---|
| 640274b7c7 | |||
| 3a4ea67e26 |
84
cache_research/SUMMARY.md
Normal file
84
cache_research/SUMMARY.md
Normal 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+ 可显式断点) | 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 |
|
||||
297
cache_research/aggregators/report.md
Normal file
297
cache_research/aggregators/report.md
Normal file
@ -0,0 +1,297 @@
|
||||
# 聚合层调研报告:聚合 API / 中转服务 / coding plan 的「缓存命中 token」字段透传情况
|
||||
|
||||
- 撰写时间:2026-08-29
|
||||
- 调研人:子智能体 #3(聚焦聚合层)
|
||||
- 配套调研(其他子智能体负责):官方海外 API(OpenAI/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` | 已知 bug:OpenAI-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 Zen(PAUG 网关)** | 见下;同时提供 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-api(songquanpeng)** | 大体透传上游 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-api(QuantumNous)** | ✅ 转发路径基本保留缓存字段(OpenAI 渠道流式 `*usage = lastStreamResponse.Usage` 整体拷贝);**但存在多个已证实的 bug**:自定义渠道/火山方舟流式把 `cached_tokens` 打成 0(#5672);xAI 渠道流式转发对但内部计费 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-hub(MartialBE)** | ✅ 基本透传;**曾被证实 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 等)** | 参差不齐:宣称「官转」的站会解析并透传 usage(packycode 明说「透传用户的请求…解析 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 单价**(如 Sonnet:input 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-07,china-llm.com):GLM-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 M3:Input $0.30/M、Output $1.20/M、Cached Read $0.06/M;Claude Sonnet:Cached Read $0.20/M、Cached Write $2.50/M;Qwen 3.7 Plus:Cached Read $0.04、Cached Write $0.50)。**既然按缓存读取/写入单独定价,Zen 网关必然解析上游响应里的缓存 usage 字段**——这是「Zen 保留缓存字段」的最强官方证据(间接)。
|
||||
- 第三方佐证——Bifrost 的 OpenCode provider 文档(docs.getbifrost.ai,Bifrost 用同一套 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`。**但存在一个已知 bug:OpenAI-compatible(自定义 baseURL,如 LiteLLM 代理)流式路径下 `tokens_cache_read` 恒为 0,即使上游 SSE usage chunk 里明明有 `cached_tokens`(实测 5888/6004 ≈98% 命中)**(#33997,2026-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-api(songquanpeng)
|
||||
- 主干是「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-21(v0.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-api(QuantumNous,one-api 的主要活跃 fork,包装「官转」最多的底座)
|
||||
- **转发路径**:OpenAI 渠道流式 `handleLastResponse` 里 `*usage = lastStreamResponse.Usage`(**整体拷贝**,`cached_tokens` 保留)——本文直接读取源码 `relay/channel/openai/helper.go` 确认。非流式 `xAIHandler` 直接 `return xaiResponse.Usage, nil`。
|
||||
- **但有一批已证实的 bug(全部为 issue 讨论 + 部分有源码定位)**:
|
||||
1. **#6144(xAI 渠道)**:流式 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. **#6353(Claude 缓存写入 token 未计费)**:5m/1h TTL 拆分缺席时级联 bug 把 cache creation 值清零,最贵的写入 token 打了 100% 折。开放中。
|
||||
5. **#1103(Gemini reasoning 未计费,开放 14 个月)**:`completion_tokens`(124)不含 `reasoning_tokens`(1097),90% 输出 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-hub(MartialBE,one-api 的另一活跃 fork)
|
||||
- 与 new-api 同源(都 fork 自 one-api);能力上对齐 new-api 的缓存计费方向(README 称「支持更多模型」)。
|
||||
- **直接证据:PR #910(2026-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 packycode(PackyAPI,自称「官转」)
|
||||
- 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 宣传页(2026):PackyAPI 主站按量付费、计费对标 Claude/OpenAI 官网价格;Codex 有独立包月站。
|
||||
- 用户实测(什么值得买/其他帖):Claude Code 场景 cache read 占输入大头(另一帖统计 82.9% cache read / 15.6% cache write / 1.5% fresh input);**cache 命中基本决定中转实际价格**。
|
||||
|
||||
### 5.2 灵眸AI 等(社区实测透传)
|
||||
- fulitimes 博客(2026,Claude 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 plan:GitHub 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 API(enterprise/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#312939,OpenRouter 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 4:input 90 credits/M、**cache read 9 credits/M**、output 450 credits/M;1 credit=$0.04)。→ Windsurf 计量层**按 cache-read 打折计费**,说明其网关解析并保留了缓存字段。
|
||||
- 用户侧**看不到原始 usage 字段**,只能看到 credit 消耗与用量面板;Tokenminning 的 Windsurf 页提到「Quota & billing(daily/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` 能透传到 gateway(bug 是在 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×)、#33772(OpenAI `cache_write_tokens` 未计入成本,消费远低于厂商账单)、#11364(Anthropic 缓存成本算错)、#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#13907(2026-03):经 Vercel AI Gateway 调 `moonshotai/kimi-k2.5`,面板正确显示 Cache Read 5.8M(93.5% 命中),但**账单按全价输入计费**——真实成本 $4.00 vs 直连 $1.28(6×)。→ 网关侧「显示缓存但不应用缓存折扣」的实例。证据等级: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 chunk(OpenRouter 同规则;LiteLLM 有合成兜底但历史上有格式 bug)。实验脚本务必显式带该参数并对齐最后一个 chunk。
|
||||
2. **网关自身另有「响应缓存」(result cache)**:OpenRouter 的 `X-OpenRouter-Cache-Status: HIT` 时 usage 全为 0;Cloudflare 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」,应优先选按量 API(OpenRouter、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)、LiteLLM(Anthropic 透传路径不映射、计费 bug 多)、Cloudflare AI Gateway(未文档化,推测透传)。
|
||||
- **计费不含缓存或订阅不暴露**:one-api(无缓存单价)、Copilot(聚合 API 无缓存拆分)、Windsurf/Augment(按缓存折扣计费但不暴露原始字段)、Cursor(面板展示缓存明细但没有公开 API)。
|
||||
- **核心陷阱**:「客户端收到的 usage」≠「网关账单」≠「上游计费」,三者要分开验证;流式必须 `include_usage`;注意区分网关的 prompt cache(KV cache 命中)与网关的响应缓存(result cache,可能返回 usage 全 0)。
|
||||
|
||||
---
|
||||
*报告完。所有引用为调研时(2026-08-29)可访问的 URL;证据等级逐条标注;凡「未找到证据」处均已如实说明。*
|
||||
304
cache_research/official_china/README.md
Normal file
304
cache_research/official_china/README.md
Normal 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 tokens(Qwen3.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 | 千帆 ModelBuilder(qianfan.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:00–04:00 与 06:00–10:00。
|
||||
> 第三方报道(知乎,非官方):DeepSeek V4-Pro 人民币口径"缓存命中 0.1 元/百万 vs 未命中 3 元/百万(差 30 倍),促销窗口 0.025 元"。此条为第三方转述,仅作参考。
|
||||
- **实验要点**:读取顶层 `usage.prompt_cache_hit_tokens`;未命中时该字段为 `0`(官方示例即返回 0),不要把它当缺失。
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Moonshot Kimi(platform.moonshot.cn / platform.kimi.com)
|
||||
|
||||
- **官方文档**
|
||||
- API 参考(Chat):https://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 通义千问 Qwen(DashScope / 阿里云百炼)
|
||||
|
||||
- **官方文档**:阿里云百炼《上下文缓存(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 智谱 GLM(bigmodel.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**;阿里云转售 GLM(ZHIPU/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 MiniMax(platform.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/M(20%),显式写入 $0.375/M;
|
||||
- MiniMax-M2.5 / M2.1:输入 $0.30/M,命中 $0.03/M(10%),显式写入 $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 阶跃星辰 Step(platform.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` | 文档两种示例并存 |
|
||||
| Qwen(OpenAI/百炼) | `usage.prompt_tokens_details.cached_tokens`;显式另有 `cache_creation_input_tokens` | 未命中为 0/缺失 |
|
||||
| Qwen(DashScope 海外部分模型) | `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 tokens;MiniMax:≥512 tokens;Qwen 隐式:≥256(部分模型更高);全局建议:构造 **≥2048 tokens 的稳定前缀** 再测,避开各家阈值差异。
|
||||
|
||||
### 3.4 显式 vs 自动(决定实验脚本形态)
|
||||
- 只发普通请求即可验证:DeepSeek、Kimi、Qwen(隐式)、GLM、MiniMax、Step、百度千帆。
|
||||
- 必须额外走显式流程:**豆包**(先建缓存/传 caching 参数);Qwen 如需显式命中(cache_control ephemeral,5 分钟 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 带 usage;K3 命中 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 1h–7d、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 请求实测。建议下一步按第三节要点构造实验脚本逐家验证字段如实返回。
|
||||
91
cache_research/official_china/usage_fields_reference.md
Normal file
91
cache_research/official_china/usage_fields_reference.md
Normal 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 }
|
||||
```
|
||||
流式响应(最后一个 chunk,finish_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
422
cache_research/official_overseas_v2/report.md
Normal file
422
cache_research/official_overseas_v2/report.md
Normal file
@ -0,0 +1,422 @@
|
||||
# 海外官方 LLM API「缓存命中 token 字段」调研报告(v2 重试版)
|
||||
|
||||
> 调研时间:2026-08-29
|
||||
> 调研人:子智能体 #4
|
||||
> 范围:海外**官方 API**(OpenAI / Anthropic / Google Gemini / xAI Grok / Mistral AI;AWS 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 caching(KV cache) | `usage.prompt_tokens_details.cached_tokens` | `usage.prompt_tokens_details.cache_write_tokens`(GPT-5.6+ 上报;老模型无写入字段) | 自动(implicit);GPT-5.6+ 可选显式 breakpoint | 历史 1024 tokens(128 递增);当前文档: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** GLM(Generative 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 tokens,Gemini 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 caching(messages 前缀缓存) | 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 按模型(同 Anthropic);Nova 最高 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 OpenAI(Chat Completions API + Responses API)
|
||||
|
||||
**官方文档**:https://platform.openai.com/docs/guides/prompt-caching(2026-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 Completions:usage(含 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…),缓存通常 5–10 分钟无活动后清除、最长 1 小时。
|
||||
- 当前官方文档(2026-08):GPT-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 Claude(Messages 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 TTL):1.25× 基础输入价;缓存写(1h TTL):2× 基础输入价;**缓存读/刷新: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 Gemini(Generative Language API / Vertex AI)
|
||||
|
||||
**官方文档**:https://ai.google.dev/gemini-api/docs/generate-content/caching 与 REST 参考 https://ai.google.dev/api/generate-content(2026-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`
|
||||
- SDK(snake_case,Python/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.x(3.1 Pro Preview / 3.5 / 3.6 / 3.7 Flash)= **4,096 tokens**。
|
||||
- 历史(Google 官方博客 2025-05):2.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-caching(How it works / Usage & Pricing / Best Practices & FAQ,2026-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/chat(2026-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 一致(Claude);Claude 之外模型(截至 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 = 1024,Haiku 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 tokens,Gemini 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 对照已验证机制。
|
||||
- 流式:可观察第一个空 token(prefill),usage 取最后 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_tokens(10% 折扣)。
|
||||
- 流式:取最后一个 chunk 的 usage;必要时非流式对照。
|
||||
|
||||
### AWS Bedrock / Azure OpenAI(顺带)
|
||||
- Bedrock(Claude):system 里加 `{"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.com)prompt-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.com(Google 官方博客) | 官方发布(辅助) |
|
||||
| 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 行为改动)。实验前建议按本报告给出的官方链接复核最新值;凡第三方转述均已在文中标注「等级=辅助」。
|
||||
@ -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)})
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1751,12 +1751,12 @@ const permissionSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
const lockedByPlan = props.currentWorkMode === 'plan';
|
||||
return options.map((opt) => ({
|
||||
id: `perm:${opt.value}`,
|
||||
label: opt.label,
|
||||
label: t(opt.labelKey),
|
||||
description: lockedByPlan
|
||||
? t('input.permissionLockedReadonlyDesc')
|
||||
: opt.value === current
|
||||
? t('input.optionWithCurrent', { desc: opt.description || '' })
|
||||
: opt.description || '',
|
||||
? t('input.optionWithCurrent', { desc: t(opt.descriptionKey) })
|
||||
: t(opt.descriptionKey),
|
||||
disabled: lockedByPlan,
|
||||
action: () => emit('change-permission-mode', opt.value)
|
||||
}));
|
||||
@ -1769,12 +1769,12 @@ const executionSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
const lockedByPlan = props.currentWorkMode === 'plan';
|
||||
return options.map((opt) => ({
|
||||
id: `exec:${opt.value}`,
|
||||
label: opt.label,
|
||||
label: t(opt.labelKey),
|
||||
description: lockedByPlan
|
||||
? t('input.executionLockedSandboxDesc')
|
||||
: opt.value === current
|
||||
? t('input.optionWithCurrent', { desc: opt.description || '' })
|
||||
: opt.description || '',
|
||||
? t('input.optionWithCurrent', { desc: t(opt.descriptionKey) })
|
||||
: t(opt.descriptionKey),
|
||||
disabled: lockedByPlan,
|
||||
action: () => emit('change-execution-mode', opt.value)
|
||||
}));
|
||||
@ -1817,11 +1817,11 @@ const networkSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
const current = String(props.currentNetworkPermission || '');
|
||||
return options.map((opt) => ({
|
||||
id: `network:${opt.value}`,
|
||||
label: opt.label,
|
||||
label: t(opt.labelKey),
|
||||
description:
|
||||
opt.value === current
|
||||
? t('input.optionWithCurrent', { desc: opt.description || '' })
|
||||
: opt.description || '',
|
||||
? t('input.optionWithCurrent', { desc: t(opt.descriptionKey) })
|
||||
: t(opt.descriptionKey),
|
||||
action: () => emit('change-network-permission', opt.value)
|
||||
}));
|
||||
});
|
||||
@ -1832,11 +1832,11 @@ const workModeSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
const current = String(props.currentWorkMode || '');
|
||||
return options.map((opt) => ({
|
||||
id: `workmode:${opt.value}`,
|
||||
label: opt.label,
|
||||
label: t(opt.labelKey),
|
||||
description:
|
||||
opt.value === current
|
||||
? t('input.optionWithCurrent', { desc: opt.description || '' })
|
||||
: opt.description || '',
|
||||
? t('input.optionWithCurrent', { desc: t(opt.descriptionKey) })
|
||||
: t(opt.descriptionKey),
|
||||
action: () => emit('change-work-mode', opt.value)
|
||||
}));
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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}`
|
||||
|
||||
@ -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...',
|
||||
|
||||
@ -59,6 +59,8 @@ export default {
|
||||
currentContext: '当前上下文',
|
||||
cumulativeInput: '累计输入',
|
||||
cumulativeOutput: '累计输出',
|
||||
cumulativeCachedInput: '累积缓存输入',
|
||||
cacheHitRate: '缓存命中率',
|
||||
performanceStats: '性能统计',
|
||||
memory: '内存',
|
||||
containerMetricsPending: '容器已运行,等待采集指标...',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -132,6 +132,14 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
// 深色主题下 --accent 为灰色(#606060),作为大字号统计数字辨识度不足,
|
||||
// 「当前上下文」数字改用 --text-primary(dark 下为白色)保证可读性。
|
||||
body[data-theme='dark'] {
|
||||
.stat-value--accent {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-value--success {
|
||||
color: var(--state-success);
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user