chore(doc):移除无用文档

This commit is contained in:
chengqian committed 2026-08-18 15:11:04 +08:00
1 parent 0bac1668bb
commit 9161ddb5c8
7 files changed
+462 -3322

No files matched your search

File diff suppressed because it is too large. Load diff
-210
View File
@@ -1,210 +0,0 @@
# Relaykit 迁移方案执行摘要
> **文档版本**:v1.0
> **生成日期**:2026-07-28
> **完整方案**:[relaykit-migration-plan.md](./relaykit-migration-plan.md)
>
> **状态(2026-07-29 更新)**:✅ 代码完成 + 切换已落地(阶段 0–7)。特性开关已移除、relaykit 常开、旧转换器作容错回退保留;DTO 去重完成。本文下方「建议批准执行 / 75 天计划」为立项时历史内容,实际执行状态以 `relaykit-migration-plan.md` 阶段 7「最终执行状态」为准。
---
## 一、项目背景
### 当前问题
team-api 的 `relay/` 模块(26,138 行代码)存在以下痛点:
1. **转换逻辑分散**:24 个供应商的协议转换代码散落在各自的 `channel/` 目录,大量重复代码
2. **难以测试**:转换函数缺乏独立测试,依赖完整的 HTTP 链路
3. **维护成本高**:新增供应商需要 5 天开发时间,协议变更适配需要 2-3 天
4. **无法复用**:转换逻辑与 GoFrame/Gin 框架耦合,无法在其他项目中使用
### 解决方案
参考 new-api 项目的 **relaykit 模块化架构**,将协议转换逻辑提取为独立的 Go 模块:
- **零依赖**:不依赖任何框架,纯粹的协议转换库
- **高可测试**:Golden 测试保证字节级精确性
- **易扩展**:注册表模式,新增供应商只需实现接口
- **可复用**:可以开源或在其他项目中使用
---
## 二、核心架构
### Relaykit 模块结构
```
relaykit/
├── types/ # 核心类型定义(RelayFormat、EndpointType)
├── relayconvert/ # 协议转换核心
│ ├── convmeta/ # Meta 接口(宿主信息契约)
│ ├── internal/ # 转换器实现
│ │ ├── oai_chat/ # OpenAI Chat Completions 转换器
│ │ ├── claude_messages/ # Claude Messages API 转换器
│ │ ├── gemini_chat/ # Gemini generateContent 转换器
│ │ └── oai_responses/ # OpenAI Responses API 转换器
│ ├── request_registry.go # 请求转换器注册表
│ ├── response_registry.go # 响应转换器注册表
│ └── text_converter_registry.go # 文本转换器注册表(统一接口)
└── dto/ # 数据传输对象(兼容层)
```
### 关键设计
1. **Meta 接口**:宿主应用(team-api)实现该接口,转换器通过它访问渠道信息、模型名称等
2. **Options 快照**:请求级配置快照,避免并发读取配置时的竞态条件
3. **注册表模式**:转换器通过注册表管理,支持运行时查找和多跳转换
4. **StreamState 接口**:有状态的流式转换(如 Gemini → OpenAI 需要累积 tool_use)
5. **Golden 测试**:每个转换器都有 testdata/ 目录存储预期输出,测试时逐字节对比
---
## 三、迁移策略
### 核心原则
- **零停机**:双轨并行,功能开关控制,随时可回滚
- **降低风险**:分 7 个阶段渐进式迁移,每个阶段独立验收
- **质量保证**:测试覆盖率 ≥ 85%,Golden 测试通过率 100%
### 7 个阶段
| 阶段 | 工作内容 | 预计时间 | 交付物 |
|------|---------|---------|--------|
| 0 | 准备工作 | 3 天 | 功能开关 + 监控配置 + 测试基线 |
| 1 | relaykit 模块骨架 | 5 天 | 类型定义 + Meta 接口 + 注册表 |
| 2 | DTO 迁移 | 5 天 | 类型别名(向后兼容) |
| 3 | 核心转换器实现 | 10 天 | OpenAI ↔ Claude ↔ Gemini + Golden 测试 |
| 4 | 集成到主项目 | 7 天 | RelayInfo 实现 Meta + Handler 调用 relaykit |
| 5 | 迁移其他供应商 | 24 天 | 24 个供应商全部迁移 |
| 6 | 测试与验证 | 14 天 | 单元测试 + 集成测试 + 压力测试 + 灰度验证 |
| 7 | 清理旧代码 | 7 天 | 删除旧转换函数 + 移除功能开关 + 文档更新 |
**总计**:**75 天**(11 周,约 2.5 个月)
---
## 四、关键里程碑
| 里程碑 | 日期 | 验收标准 |
|--------|------|---------|
| M1: relaykit 模块可用 | Day 13 | DTO + 核心转换器实现完成,测试通过 |
| M2: 首个供应商集成 | Day 30 | Anthropic 在生产环境使用 relaykit,成功率 ≥ 99.5% |
| M3: 核心供应商迁移完成 | Day 44 | Anthropic + OpenAI + Gemini + Qwen 迁移完成 |
| M4: 全部供应商迁移完成 | Day 54 | 24 个供应商全部使用 relaykit |
| M5: 测试验证完成 | Day 68 | 单元 + 集成 + 压力测试全部通过 |
| M6: 迁移完成 | Day 75 | 旧代码清理完毕,文档更新,项目交付 |
---
## 五、风险与回滚
### 高风险项
| 风险 | 缓解措施 |
|------|---------|
| 转换逻辑错误导致请求失败 | Golden 测试 + 灰度验证 + 监控告警 + 自动回滚 |
| 性能回退导致服务不可用 | 基准测试 + 压力测试 + 流量控制 + P95 延迟阈值告警 |
| 边界情况未覆盖 | 多样化测试用例 + 生产流量回放 + 灰度分批放量 |
### 当前回滚方式\r\n\r\n阶段 7 已移除运行时功能开关。relaykit 在已覆盖方向上始终优先,转换器未匹配、解析失败或转换失败时由现有 bridge 回退到旧实现。若需要整体停用 relaykit,必须 revert 阶段 7 cutover 提交并重新构建部署,不能再通过修改配置并重启完成。
---
## 六、成功指标(KPI)
### 质量指标
- ✅ 单元测试覆盖率 ≥ 85%
- ✅ Golden 测试通过率 = 100%
- ✅ 转换成功率 ≥ 99.5%(生产环境 30 天滚动)
### 性能指标
- ✅ P50 转换延迟 < 0.5ms
- ✅ P95 转换延迟 < 1ms
- ✅ P99 端到端延迟增加 < 20ms
### 可维护性指标
- ✅ 代码行数减少 ≥ 20%(长期)
- ✅ 新增供应商开发时间 < 3 天(原 5 天)
- ✅ 协议变更适配时间 < 1 天(原 2-3 天)
---
## 七、资源需求
### 人力投入
| 角色 | 人数 | 投入时间 |
|------|------|---------|
| 后端开发(Go) | 2 人 | 全职 11 周 |
| 测试工程师 | 1 人 | 全职 4 周 |
| 架构师/Tech Lead | 1 人 | 兼职 20%(Code Review + 架构审查) |
| 运维工程师 | 1 人 | 兼职 10%(监控 + 灰度发布) |
### 基础设施
- 测试环境:与现有环境共用
- 监控系统:扩展现有 Prometheus/Grafana
- CI/CD:扩展现有 Jenkins/GitLab CI
---
## 八、预期收益
### 短期收益(迁移完成后 3 个月内)
1. **代码量减少 20%**:从 26,138 行降至约 21,000 行
2. **测试覆盖率提升**:从 < 50% 提升至 85%+
3. **转换逻辑集中**:24 个供应商的转换代码统一管理
### 中期收益(6 个月内)
1. **开发效率提升 60%**:新增供应商从 5 天降至 2 天
2. **维护成本降低 80%**:协议变更适配从 2-3 天降至 < 1 天
3. **质量稳定性提升**:Golden 测试防止转换回归
### 长期收益(1 年内)
1. **可复用性**:relaykit 可开源或在其他项目中使用
2. **社区贡献**:接受社区贡献新的供应商适配器
3. **技术影响力**:作为 AI 协议转换标准库推广
---
## 九、决策建议
### 建议批准执行
**理由**:
1. ✅ **风险可控**:分阶段迁移 + 双轨并行 + 功能开关 + < 5 分钟回滚
2. ✅ **收益显著**:开发效率提升 60%,维护成本降低 80%
3. ✅ **方案完善**:7 个阶段 + 75 天时间线 + 详细回滚方案 + KPI 指标
4. ✅ **长期价值**:零依赖独立模块,可开源复用
### 关键成功因素
1. **严格执行 Golden 测试**:每个转换器必须有 3+ 个测试用例
2. **监控先行**:灰度验证前必须配置好成功率和延迟监控
3. **及时回滚决策**:成功率 < 99.5% 或 P95 延迟增加 > 50ms 立即回滚
4. **充分的测试时间**:阶段 6 预留 14 天进行全面测试,不压缩
---
## 十、附录
### 相关文档
- [完整迁移方案](./relaykit-migration-plan.md)(2645 行,93 个主章节)
### 参考项目
- **new-api/relaykit**:D:\github\new-api\relaykit\(11,264 行,架构参考)
- **team-api/relay**:D:\github\team-api\relay\(26,138 行,迁移对象)
@@ -0,0 +1,462 @@
# 大模型 Usage 字段与缓存 Token 语义规范
> **文档用途**:四大主流 AI 协议(OpenAI Chat/Responses、Claude、Gemini)的 usage 字段规格、缓存 token 语义、计费换算公式,以及项目代码适配指南。
>
> **调研日期**:2026-08-16 ~ 2026-08-17(基于官方文档联网核验)
>
> **覆盖范围**:OpenAI Chat Completions、OpenAI Responses、Claude Messages API、Gemini GenerateContent API
---
## 一、核心结论:缓存 Token 语义差异
四家协议中**三家是「包含」口径,Claude 是唯一的「排除」口径**:
| 协议 | 顶层输入字段 | 缓存 token 是否含在内 | 未缓存输入怎么得到 |
|---|---|---|---|
| OpenAI Chat Completions | `prompt_tokens` | ✅ **包含**,`cached_tokens` 是它的子集 | `prompt_tokens - cached_tokens - cache_write_tokens` |
| OpenAI Responses | `input_tokens` | ✅ **包含**,`cached_tokens` 是它的子集 | `input_tokens - cached_tokens - cache_write_tokens` |
| **Claude Messages** | `input_tokens` | ❌ **排除**,缓存是独立并列字段 | `input_tokens` 本身就是未缓存部分 |
| Gemini GenerateContent | `promptTokenCount` | ✅ **包含**,`cachedContentTokenCount` 是它的子集 | `promptTokenCount - cachedContentTokenCount` |
**Claude 总输入的计算方式(与其他三家不同)**:
```text
Claude 总输入 token = input_tokens + cache_read_input_tokens + cache_creation_input_tokens
```
官方原文(Anthropic):
> "Total input tokens in a request is the summation of `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`."
> "The `input_tokens` field represents only the tokens that come after the last cache breakpoint in your request - not all the input tokens you sent."
---
## 二、各协议详细字段规格
### 2.1 OpenAI Chat Completions (`/v1/chat/completions`)
**顶层字段**:
| 字段 | 说明 |
|---|---|
| `prompt_tokens` | 总输入 token,**包含**缓存命中部分 |
| `completion_tokens` | 总输出 token,**包含** reasoning tokens |
| `total_tokens` | = prompt_tokens + completion_tokens |
**输入明细(`prompt_tokens_details`)**:
| 字段 | 说明 |
|---|---|
| `cached_tokens` | 缓存命中读到的输入 token(`prompt_tokens` 的子集) |
| `cache_write_tokens` | 本次写入缓存的 token(`prompt_tokens` 的子集) |
| `audio_tokens` / `text_tokens` / `image_tokens` | 按模态细分(`prompt_tokens` 的子集) |
**输出明细(`completion_tokens_details`)**:
| 字段 | 说明 |
|---|---|
| `reasoning_tokens` | 推理 token(`completion_tokens` 的子集) |
| `audio_tokens` / `text_tokens` | 按模态细分(`completion_tokens` 的子集) |
| `accepted_prediction_tokens` / `rejected_prediction_tokens` | Predicted Outputs 功能相关 |
**缓存计费**:
- 命中部分按折扣价计费(gpt-4o 为 0.5×)
- 可缓存的最小前缀为 1024 token,按 128 token 增量命中
**官方示例**(prompt caching guide):
```json
{
"usage": {
"prompt_tokens": 2006,
"prompt_tokens_details": {
"cached_tokens": 1920
}
}
}
```
解读:1920 是 2006 的一部分,剩余 86 个未命中。
---
### 2.2 OpenAI Responses API (`/v1/responses`)
**顶层字段**:
| 字段 | 说明 |
|---|---|
| `input_tokens` | 总输入,**包含**缓存命中和缓存写入 |
| `output_tokens` | 总输出,**包含** reasoning_tokens |
| `total_tokens` | = input_tokens + output_tokens |
**输入明细(`input_tokens_details`)**:
| 字段 | 说明 |
|---|---|
| `cached_tokens` | 缓存命中(子集) |
| `cache_write_tokens` | 本次写入缓存(子集,Responses 特有字段) |
**输出明细(`output_tokens_details`)**:
| 字段 | 说明 |
|---|---|
| `reasoning_tokens` | 推理 token(子集) |
**注意事项**:
- 没有模态细分、没有预测 token 字段(较 Chat Completions 精简)
- 复用 `previous_response_id` 时每次都为整个已见对话历史计费,`input_tokens` 会随轮次增长
**官方示例**:
```json
{
"usage": {
"input_tokens": 2600,
"input_tokens_details": {
"cached_tokens": 2000,
"cache_write_tokens": 400
},
"output_tokens": 300,
"output_tokens_details": {
"reasoning_tokens": 120
},
"total_tokens": 2900
}
}
```
解读:2600 = 2000 cached + 400 write + 200 新输入。
---
### 2.3 Claude Messages API (`/v1/messages`)
**输入侧字段**:
| 字段 | 说明 |
|---|---|
| `input_tokens` | **仅计最后一个 cache 断点之后的输入**(未走缓存的常规输入) |
| `cache_read_input_tokens` | 从缓存读到的 |
| `cache_creation_input_tokens` | 本次写入缓存的 |
| `cache_creation.ephemeral_5m_input_tokens` | 创建 5 分钟缓存条目的 token |
| `cache_creation.ephemeral_1h_input_tokens` | 创建 1 小时缓存条目的 token |
**输出侧字段**:
| 字段 | 说明 |
|---|---|
| `output_tokens` | 总输出(含 thinking),**是计费权威值** |
| `output_tokens_details.thinking_tokens` | 思考 token(`output_tokens` 的子集,只读观测用) |
**非 token 元数据**:
| 字段 | 说明 |
|---|---|
| `server_tool_use.web_search_requests` / `web_fetch_requests` | 服务端工具调用次数(按次计费) |
| `service_tier` | standard / priority / batch |
| `inference_geo` | 推理所在地理区域 |
**计费倍率**(官方 prompt caching 文档):
| Token 类型 | 倍率(相对基础输入价) |
|---|---|
| 常规输入 | 1.0× |
| 5 分钟缓存写入(ephemeral_5m) | 1.25× |
| 1 小时缓存写入(ephemeral_1h) | 2× |
| 缓存读取 | 0.1× |
**注意事项**:
- Claude 没有原生 `total_tokens` 字段
- `cache_creation.ephemeral_5m_input_tokens + ephemeral_1h_input_tokens` 等于 `cache_creation_input_tokens`,它们是 TTL 细分,不能重复相加
- 老响应没有 `cache_creation` 细分时,`cache_creation_input_tokens` 默认按 5m(1.25×)计
**官方完整示例**:
```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
},
"output_tokens_details": { "thinking_tokens": 0 },
"server_tool_use": { "web_search_requests": 0, "web_fetch_requests": 2 },
"service_tier": "standard",
"inference_geo": "global"
}
}
```
---
### 2.4 Gemini GenerateContent API
**注意**:Gemini 的用量不在 `usage` 字段里,而是响应顶层的 `usageMetadata`,且为 camelCase。
**字段规格**:
| 字段 | 说明 |
|---|---|
| `promptTokenCount` | 总输入,**包含**缓存部分 |
| `cachedContentTokenCount` | 其中命中缓存的(显式 context caching;2.5 系列隐式缓存命中也体现在此字段) |
| `candidatesTokenCount` | 输出 token(不含思考) |
| `thoughtsTokenCount` | 思考 token,**输出侧**,按输出价计费 |
| `toolUsePromptTokenCount` | 工具使用 prompt 的 token |
| `totalTokenCount` | = promptTokenCount + thoughtsTokenCount + candidatesTokenCount |
| `promptTokensDetails[]` / `cacheTokensDetails[]` / `candidatesTokensDetails[]` / `toolUsePromptTokensDetails[]` | 按模态(TEXT/IMAGE/AUDIO/VIDEO)细分的列表 |
| `serviceTier` | 服务层级 |
**官方原文**(关键):
> "When `cachedContent` is set, this is still the total effective prompt size meaning this includes the number of tokens in the cached content."
> "Total token count for the generation request (prompt + thoughts + response candidates)."
**官方示例**:
```json
{
"usageMetadata": {
"promptTokenCount": 1050,
"cachedContentTokenCount": 1000,
"candidatesTokenCount": 200,
"thoughtsTokenCount": 150,
"toolUsePromptTokenCount": 0,
"totalTokenCount": 1400,
"promptTokensDetails": [{ "modality": "TEXT", "tokenCount": 1050 }],
"cacheTokensDetails": [{ "modality": "TEXT", "tokenCount": 1000 }],
"candidatesTokensDetails": [{ "modality": "TEXT", "tokenCount": 200 }]
}
}
```
---
## 三、计费换算公式
```text
【OpenAI 系(Chat Completions / Responses 同构)】
输入成本 = (input_tokens − cached_tokens − cache_write_tokens) × P_in
+ cached_tokens × P_cached
+ cache_write_tokens × P_cache_write
输出成本 = output_tokens × P_out
# reasoning 已含在 output 里,不另计
【Claude】
输入成本 = input_tokens × P_in
+ cache_creation_5m × P_in × 1.25
+ cache_creation_1h × P_in × 2
+ cache_read × P_in × 0.1
输出成本 = output_tokens × P_out
# thinking 已含在 output 里
# 总输入量展示用 = 三项相加,切勿把 input_tokens 当总输入
【Gemini】
输入成本 = (promptTokenCount − cachedContentTokenCount) × P_in
+ cachedContentTokenCount × P_cached
输出成本 = (candidatesTokenCount + thoughtsTokenCount) × P_out
```
---
## 四、协议互转的坑
### 4.1 最容易踩的坑:Claude ↔ 其他三家
**问题**:把 Claude 的 `input_tokens` 直接映射成 OpenAI 的 `prompt_tokens`,会导致缓存场景下输入量少算(Claude 侧)或多算(反向)。
**正确做法**:
```text
Claude → OpenAI / Gemini(加法):
顶层输入 = input_tokens + cache_read_input_tokens + cache_creation_input_tokens
缓存明细 = cache_read_input_tokens(映射为 cached_tokens)
OpenAI / Gemini → Claude(减法):
input_tokens = 顶层输入 − 缓存命中 − 缓存写入
cache_read_input_tokens = 缓存命中
cache_creation_input_tokens = 缓存写入
```
**注意**:OpenAI 的 `cached_tokens` 是「命中读取」语义,没有 Claude 的「写入 1.25×/2×」概念;反向转换时 Claude 的写入 token 一般并入常规输入或缓存读取,需按计费策略明确选一边。
---
## 五、流式响应中 usage 的位置
| 协议 | 流式 usage 出现位置 |
|---|---|
| OpenAI Chat | 需设 `stream_options: {"include_usage": true}`,最后一个 chunk 带 usage |
| OpenAI Responses | `response.completed` 事件的 `response.usage` |
| Claude | `message_start` 带输入侧 usage,`message_delta` 累计 `output_tokens`<br>**已知问题**:部分流式场景 `message_delta` 只带 `output_tokens`,输入侧字段缺失,需保留 `message_start` 的值合并 |
| Gemini | `streamGenerateContent` 最后一个 chunk 的 `usageMetadata` |
---
## 六、推荐的内部统一模型
**不建议只保存通用的 `input_tokens` 和 `output_tokens`**。为了准确计费和协议转换,建议至少保存:
```text
input_total # 总输入(含缓存)
input_ordinary # 普通输入(不含缓存)
cache_read # 缓存读取
cache_write # 缓存写入
output_total # 总输出
visible_output # 可见输出
reasoning_or_thinking # 推理/思考 token
provider_total # 供应商返回的 total_tokens
```
**推荐映射**:
| 协议 | `input_total` | `input_ordinary` | `cache_read` | `cache_write` |
|---|---|---|---|---|
| OpenAI Chat | `prompt_tokens` | `prompt_tokens - cached_tokens - cache_write_tokens` | `cached_tokens` | `cache_write_tokens` |
| OpenAI Responses | `input_tokens` | `input_tokens - cached_tokens - cache_write_tokens` | `cached_tokens` | `cache_write_tokens` |
| Claude | 三个输入桶之和 | `input_tokens` | `cache_read_input_tokens` | `cache_creation_input_tokens` |
| Gemini | `promptTokenCount` | `promptTokenCount - cachedContentTokenCount` | `cachedContentTokenCount` | 无对应生成响应字段 |
**输出侧注意事项**:
- OpenAI 的 `completion_tokens` / `output_tokens` 已包含 `reasoning_tokens`
- Claude 的 `output_tokens` 已包含 `thinking_tokens`
- Gemini 的 `candidatesTokenCount` 不包含 `thoughtsTokenCount`;其完整生成侧数量应依据 `totalTokenCount - promptTokenCount` 或直接分别保留 candidates 与 thoughts
- **所有减法都应做非负保护**,以防第三方兼容服务返回不一致数据
---
## 七、项目代码需要改进的地方
### 7.1 已正确处理的部分
✅ OpenAI Responses 已设置 `CacheIncludedInPrompt=true`,符合 OpenAI 顶层输入包含缓存的语义
✅ Gemini 已设置 `CacheIncludedInPrompt=true`,符合 `promptTokenCount` 包含 `cachedContentTokenCount` 的语义
✅ Claude 将缓存读取和缓存创建保留为独立桶,符合 Claude 语义
### 7.2 需要更新或明确标注的部分
#### 问题 1:OpenAI Chat 通用明细缺少官方 `cache_write_tokens`
**位置**:`relaykit/dto/usage.go`
**现状**:目前有项目自定义的 `cached_creation_tokens`,但它不是 OpenAI 官方 JSON 字段 `cache_write_tokens`。
**建议**:增加官方 `cache_write_tokens` 字段,不要与自定义字段混用。
---
#### 问题 2:OpenAI Responses 输入明细缺少 `cache_write_tokens`
**位置**:`relay/dto/openai_responses.go`
**现状**:`InputTokenDetails` 当前只有 `cached_tokens` 及模态扩展字段。
**建议**:增加官方 `cache_write_tokens` 字段。
---
#### 问题 3:Responses DTO 中存在非当前官方 schema 的扩展字段
**位置**:`relay/dto/openai_responses.go`
**现状**:`input_tokens_details.text_tokens/audio_tokens/image_tokens` 以及输出侧的文本、音频、预测 token 字段不在官方 Responses schema 中。
**建议**:如果为了兼容第三方上游而保留,应明确标记为兼容扩展,不能当作 OpenAI 官方字段。
---
#### 问题 4:Claude DTO 缺少最新 Usage 字段
**位置**:`relaykit/dto/claude.go`
**现状**:当前缺少:
- `output_tokens_details.thinking_tokens`
- `inference_geo`
- `server_tool_use.web_fetch_requests`
**建议**:补充上述官方字段。
---
#### 问题 5:Gemini DTO 缺少 `usageMetadata.serviceTier`
**位置**:`relaykit/dto/gemini.go`
**现状**:已覆盖主要 token 字段和模态明细,但未覆盖当前官方 `serviceTier`。
**建议**:补充该字段。
---
#### 问题 6:Claude 转 OpenAI 的 usage 口径不符合 OpenAI 子集语义 ⚠️
**位置**:`relaykit/relayconvert/internal/oai_chat/claude_to_openai_response.go`
**现状**:当前将 Claude `input_tokens` 直接映射为 OpenAI `prompt_tokens`,同时把 `cache_read_input_tokens` 放入 `cached_tokens`。这样 `cached_tokens` 并未包含在 `prompt_tokens` 中,与 OpenAI 官方语义不一致。
**正确的协议映射应为**:
```go
prompt_tokens = input_tokens + cache_read_input_tokens + cache_creation_input_tokens
prompt_tokens_details.cached_tokens = cache_read_input_tokens
prompt_tokens_details.cache_write_tokens = cache_creation_input_tokens
total_tokens = prompt_tokens + completion_tokens
```
---
#### 问题 7:Claude 内部 `TotalTokens` 不是跨协议总量
**位置**:Claude 路径
**现状**:当前使用 `input_tokens + output_tokens` 生成内部 `TotalTokens`,缓存 token 另存。
**说明**:这可以作为内部计费表示,但不能把该值解释为 Claude 请求实际处理的全部 token,也不能直接映射为 OpenAI `total_tokens`。
---
## 八、参考来源
以下页面均在 2026-08-16 ~ 2026-08-17 实际联网获取并读取正文:
### OpenAI
- [Chat Completions API Reference](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create)
- [Responses API Reference](https://developers.openai.com/api/reference/resources/responses/methods/create)
- [Prompt Caching Guide](https://developers.openai.com/api/docs/guides/prompt-caching)
- [openai-python — completion_usage.py(PromptTokensDetails/CompletionTokensDetails 权威 schema)](https://github.com/openai/openai-python/blob/main/src/openai/types/completion_usage.py)
- [openai-python — response_usage.py(Responses usage 权威 schema)](https://github.com/openai/openai-python/blob/main/src/openai/types/responses/response_usage.py)
### Anthropic
- [Messages API Reference](https://platform.claude.com/docs/en/api/messages)
- [Prompt Caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)
### Google Gemini
- [GenerateContent API Reference — UsageMetadata](https://ai.google.dev/api/generate-content#UsageMetadata)
- [Gemini API — Generating content](https://ai.google.dev/api/generate-content)
---
## 九、总结
对于缓存输入,四种协议应按两类处理:
```text
包含型:OpenAI Chat、OpenAI Responses、Gemini
独立桶型:Claude
```
最新版 OpenAI 又进一步把包含型输入拆成"缓存读取、缓存写入、普通输入"三个计费类别。因此,项目后续不应只维护 `CacheIncludedInPrompt` 一个布尔值,还应显式支持 `cache_write_tokens`,并在协议转换时分别处理缓存读、缓存写和普通输入。
**核心规则**:
- OpenAI/Gemini → 顶层输入包含缓存(减法得普通输入)
- Claude → 三个独立桶相加才是总输入(加法)
- 协议转换时必须显式处理缓存读/写/普通输入的拆分与合并
- 所有减法操作必须做非负保护
@@ -1,331 +0,0 @@
# 大模型协议 Usage 与缓存 Token 语义调研(官方联网核验版)
## 文档信息
- 核验日期:2026-08-16
- 核验方式:联网读取 OpenAI、Anthropic、Google 官方 API Reference 与 Prompt Caching 文档正文。
- 覆盖范围:OpenAI Chat Completions、OpenAI Responses、Claude Messages API、Gemini GenerateContent API。
- 不包含:Azure OpenAI、Amazon Bedrock、Vertex AI 等托管平台对字段的二次封装,也不把第三方兼容网关的扩展字段视为官方字段。
## 核心结论
| 协议 | 顶层输入字段 | 缓存读取字段 | 缓存写入字段 | 顶层输入是否包含缓存 |
| --- | --- | --- | --- | --- |
| OpenAI Chat Completions | `prompt_tokens` | `prompt_tokens_details.cached_tokens` | `prompt_tokens_details.cache_write_tokens` | **包含缓存读取和缓存写入** |
| OpenAI Responses | `input_tokens` | `input_tokens_details.cached_tokens` | `input_tokens_details.cache_write_tokens` | **包含缓存读取和缓存写入** |
| Claude Messages | `input_tokens` | `cache_read_input_tokens` | `cache_creation_input_tokens` | **不包含,三个字段是独立桶** |
| Gemini GenerateContent | `promptTokenCount` | `cachedContentTokenCount` | GenerateContent usage 无对应字段 | **包含缓存读取** |
不能把四种协议的“输入 token”直接按同一口径使用:
```text
OpenAI / Responses:
普通输入 = 顶层输入 - cached_tokens - cache_write_tokens
Claude:
总输入 = input_tokens + cache_read_input_tokens + cache_creation_input_tokens
Gemini:
普通输入 = promptTokenCount - cachedContentTokenCount
```
其中 `cache_write_tokens` 是 OpenAI 官方文档中的新字段。官方 Prompt Caching 文档说明:GPT-5.6 及后续模型系列会报告缓存写入 token;早期模型没有该字段或不产生对应的额外缓存写入计费。
## OpenAI Chat Completions
### 官方 Usage 结构
顶层字段:
| 字段 | 官方语义 |
| --- | --- |
| `prompt_tokens` | prompt 使用的 token 数 |
| `completion_tokens` | 生成 completion 使用的 token 数 |
| `total_tokens` | `prompt_tokens + completion_tokens` |
| `prompt_tokens_details` | 输入 token 明细 |
| `completion_tokens_details` | 输出 token 明细 |
截至核验日期,`prompt_tokens_details` 的官方字段为:
| 字段 | 官方语义 |
| --- | --- |
| `audio_tokens` | prompt 中的音频输入 token |
| `cache_write_tokens` | 写入缓存的 prompt token 原始数量 |
| `cached_tokens` | prompt 中命中缓存的 token |
| `image_tokens` | prompt 中的图像输入 token |
| `text_tokens` | prompt 中的文本输入 token |
`completion_tokens_details` 的官方字段为:
| 字段 | 官方语义 |
| --- | --- |
| `accepted_prediction_tokens` | Predicted Outputs 中被实际采用的预测 token |
| `audio_tokens` | 模型生成的音频 token |
| `reasoning_tokens` | 模型生成的推理 token |
| `rejected_prediction_tokens` | Predicted Outputs 中未被采用的预测 token;官方明确说明仍计入 completion token、计费及上下文限制 |
| `text_tokens` | 模型生成的文本 token |
### 缓存口径
OpenAI Prompt Caching 官方示例表明,顶层输入包含缓存读取、缓存写入和普通输入。例如:
```json
{
"usage": {
"input_tokens": 2600,
"input_tokens_details": {
"cached_tokens": 2000,
"cache_write_tokens": 400
}
}
}
```
该示例的官方解释是:2000 个 token 从缓存读取,400 个 token 写入缓存,剩余 200 个既未读取也未写入。因此 Chat Completions 对应公式为:
```text
普通输入 token =
prompt_tokens
- prompt_tokens_details.cached_tokens
- prompt_tokens_details.cache_write_tokens
```
`text_tokens`、`image_tokens`、`audio_tokens` 是按模态的拆分;`cached_tokens`、`cache_write_tokens` 是按缓存状态的拆分。两组维度可能重叠,不能把所有 details 字段直接求和。
## OpenAI Responses
### 官方 Usage 结构
```json
{
"usage": {
"input_tokens": 2600,
"output_tokens": 120,
"total_tokens": 2720,
"input_tokens_details": {
"cached_tokens": 2000,
"cache_write_tokens": 400
},
"output_tokens_details": {
"reasoning_tokens": 50
}
}
}
```
截至核验日期,Responses 官方 API Reference 中的字段为:
| 位置 | 字段 | 官方语义 |
| --- | --- | --- |
| `usage` | `input_tokens` | 输入 token 数 |
| `usage` | `output_tokens` | 输出 token 数 |
| `usage` | `total_tokens` | 使用的总 token 数 |
| `input_tokens_details` | `cached_tokens` | 从缓存取回的 token 数 |
| `input_tokens_details` | `cache_write_tokens` | 写入缓存的输入 token 数 |
| `output_tokens_details` | `reasoning_tokens` | 推理 token 数 |
缓存公式为:
```text
普通输入 token =
input_tokens
- input_tokens_details.cached_tokens
- input_tokens_details.cache_write_tokens
```
Responses 当前官方 schema 没有在 `input_tokens_details` 中声明 `text_tokens`、`image_tokens`、`audio_tokens`,也没有在 `output_tokens_details` 中声明预测或模态明细。兼容服务可能返回扩展字段,但不能将它们标注成 OpenAI Responses 官方字段。
## Claude Messages API
### 官方 Usage 结构
Claude 官方 API Reference 当前声明的 token 字段包括:
| 字段 | 官方语义 |
| --- | --- |
| `input_tokens` | 未从缓存读取、也未用于创建缓存的输入 token;Prompt Caching 文档进一步说明,它通常对应最后一个缓存断点之后的 token |
| `cache_creation_input_tokens` | 用于创建缓存条目的输入 token |
| `cache_read_input_tokens` | 从缓存读取的输入 token |
| `output_tokens` | 输出 token 的权威总数,包含 thinking token |
| `cache_creation.ephemeral_5m_input_tokens` | 创建 5 分钟缓存条目的 token |
| `cache_creation.ephemeral_1h_input_tokens` | 创建 1 小时缓存条目的 token |
| `output_tokens_details.thinking_tokens` | 内部推理 token 明细,是 `output_tokens` 的子集 |
Usage 中还包含以下非 token 元数据:
- `inference_geo`
- `server_tool_use.web_fetch_requests`
- `server_tool_use.web_search_requests`
- `service_tier`,可能为 `standard`、`priority`、`batch` 或 `null`
### 缓存口径
Anthropic 官方 API Reference 和 Prompt Caching 文档均明确给出:
```text
总输入 token =
input_tokens
+ cache_creation_input_tokens
+ cache_read_input_tokens
```
因此,Claude 的 `input_tokens` **不包含**缓存读取和缓存创建 token。它与 OpenAI、Gemini 的顶层输入字段语义不同。
另外:
- Claude 没有原生 `total_tokens` 字段。
- `cache_creation.ephemeral_5m_input_tokens + ephemeral_1h_input_tokens` 等于 `cache_creation_input_tokens`,它们是 TTL 细分,不能重复相加。
- `output_tokens_details.thinking_tokens <= output_tokens`;`output_tokens` 仍是计费使用的权威输出总量。
- Amazon Bedrock 等托管平台可能使用不同字段名和缓存规则,应单独核验对应平台文档。
## Gemini GenerateContent API
### 官方 UsageMetadata 结构
截至核验日期,Gemini 官方 `UsageMetadata` 字段为:
| 字段 | 官方语义 |
| --- | --- |
| `promptTokenCount` | prompt 的总有效 token 数;官方明确说明设置 `cachedContent` 时仍包含缓存内容 token |
| `cachedContentTokenCount` | prompt 中缓存内容的 token 数 |
| `candidatesTokenCount` | 所有生成候选响应的 token 总数 |
| `toolUsePromptTokenCount` | tool-use prompt 中的 token 数 |
| `thoughtsTokenCount` | thinking 模型的思考 token 数 |
| `totalTokenCount` | 生成请求的总 token 数,官方定义为 prompt + thoughts + response candidates |
| `promptTokensDetails[]` | 请求输入按模态的 token 明细 |
| `cacheTokensDetails[]` | 缓存输入按模态的 token 明细 |
| `candidatesTokensDetails[]` | 响应候选按模态的 token 明细 |
| `toolUsePromptTokensDetails[]` | tool-use 请求输入按模态的 token 明细 |
| `serviceTier` | 请求使用的服务层级,非 token 字段 |
### 缓存口径
Google 官方 API Reference 的原意非常明确:`promptTokenCount` 在设置 `cachedContent` 后仍表示总有效 prompt 大小,包含缓存内容。因此:
```text
普通输入 token = promptTokenCount - cachedContentTokenCount
```
Gemini 的 `thoughtsTokenCount` 不是 `candidatesTokenCount` 的子集。官方给出的总量定义是:
```text
totalTokenCount = promptTokenCount + thoughtsTokenCount + candidatesTokenCount
```
所以有 thinking 时,不能使用 `promptTokenCount + candidatesTokenCount` 代替 `totalTokenCount`。程序应优先采用上游返回的 `totalTokenCount`。
## 推荐的内部统一模型
不建议只保存通用的 `input_tokens` 和 `output_tokens`。为了准确计费和协议转换,建议至少保存:
```text
input_total
input_ordinary
cache_read
cache_write
output_total
visible_output
reasoning_or_thinking
provider_total
```
推荐映射:
| 协议 | `input_total` | `input_ordinary` | `cache_read` | `cache_write` |
| --- | --- | --- | --- | --- |
| OpenAI Chat | `prompt_tokens` | `prompt_tokens - cached_tokens - cache_write_tokens` | `cached_tokens` | `cache_write_tokens` |
| OpenAI Responses | `input_tokens` | `input_tokens - cached_tokens - cache_write_tokens` | `cached_tokens` | `cache_write_tokens` |
| Claude | 三个输入桶之和 | `input_tokens` | `cache_read_input_tokens` | `cache_creation_input_tokens` |
| Gemini | `promptTokenCount` | `promptTokenCount - cachedContentTokenCount` | `cachedContentTokenCount` | 无对应生成响应字段 |
输出侧需要额外注意:
- OpenAI 的 `completion_tokens` / `output_tokens` 已包含 `reasoning_tokens`。
- Claude 的 `output_tokens` 已包含 `thinking_tokens`。
- Gemini 的 `candidatesTokenCount` 不包含 `thoughtsTokenCount`;其完整生成侧数量应依据 `totalTokenCount - promptTokenCount` 或直接分别保留 candidates 与 thoughts。
- 所有减法都应做非负保护,以防第三方兼容服务返回不一致数据。
## 本仓库与最新官方协议的差异
### 已正确处理的部分
- OpenAI Responses 已设置 `CacheIncludedInPrompt=true`,符合 OpenAI 顶层输入包含缓存的语义:[`relay/channel/openai/responses.go`](relay/channel/openai/responses.go)。
- Gemini 已设置 `CacheIncludedInPrompt=true`,符合 `promptTokenCount` 包含 `cachedContentTokenCount` 的语义:[`relay/channel/gemini/response.go`](relay/channel/gemini/response.go)。
- Claude 将缓存读取和缓存创建保留为独立桶,符合 Claude 语义:[`relay/channel/claude/response.go`](relay/channel/claude/response.go)。
### 需要更新或明确标注的部分
1. OpenAI Chat 通用明细缺少官方 `cache_write_tokens`。
[`relaykit/dto/usage.go`](relaykit/dto/usage.go) 目前有项目自定义的 `cached_creation_tokens`,但它不是 OpenAI 官方 JSON 字段 `cache_write_tokens`。两者不应使用同一个 JSON 名称或隐式混用。
2. OpenAI Responses 输入明细缺少 `cache_write_tokens`。
[`relay/dto/openai_responses.go`](relay/dto/openai_responses.go) 的 `InputTokenDetails` 当前只有 `cached_tokens` 及模态扩展字段,应增加官方 `cache_write_tokens`。
3. Responses DTO 中存在非当前官方 schema 的扩展字段。
`input_tokens_details.text_tokens/audio_tokens/image_tokens` 以及输出侧的文本、音频、预测 token 字段不在本次联网核验到的 OpenAI Responses 官方 schema 中。如果为了兼容第三方上游而保留,应明确标记为兼容扩展,不能当作 OpenAI 官方字段。
4. Claude DTO 缺少最新 Usage 字段。
[`relaykit/dto/claude.go`](relaykit/dto/claude.go) 当前缺少:
- `output_tokens_details.thinking_tokens`
- `inference_geo`
- `server_tool_use.web_fetch_requests`
5. Gemini DTO 缺少 `usageMetadata.serviceTier`。
[`relaykit/dto/gemini.go`](relaykit/dto/gemini.go) 已覆盖主要 token 字段和模态明细,但未覆盖当前官方 `serviceTier`。
6. Claude 转 OpenAI 的 usage 口径不符合 OpenAI 子集语义。
[`relaykit/relayconvert/internal/oai_chat/claude_to_openai_response.go`](relaykit/relayconvert/internal/oai_chat/claude_to_openai_response.go) 当前将 Claude `input_tokens` 直接映射为 OpenAI `prompt_tokens`,同时把 `cache_read_input_tokens` 放入 `cached_tokens`。这样 `cached_tokens` 并未包含在 `prompt_tokens` 中,与 OpenAI 官方语义不一致。
严格的协议映射应为:
```text
prompt_tokens =
input_tokens
+ cache_read_input_tokens
+ cache_creation_input_tokens
prompt_tokens_details.cached_tokens = cache_read_input_tokens
prompt_tokens_details.cache_write_tokens = cache_creation_input_tokens
total_tokens = prompt_tokens + completion_tokens
```
7. Claude 内部 `TotalTokens` 不是跨协议总量。
当前 Claude 路径使用 `input_tokens + output_tokens` 生成内部 `TotalTokens`,缓存 token 另存。这可以作为内部计费表示,但不能把该值解释为 Claude 请求实际处理的全部 token,也不能直接映射为 OpenAI `total_tokens`。
## 官方来源
以下页面均在 2026-08-16 实际联网获取并读取正文:
### OpenAI
- [Chat Completions API Reference](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create)
- [Responses API Reference](https://developers.openai.com/api/reference/resources/responses/methods/create)
- [Prompt Caching Guide](https://developers.openai.com/api/docs/guides/prompt-caching)
### Anthropic
- [Messages API Reference](https://platform.claude.com/docs/en/api/messages)
- [Prompt Caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)
### Google Gemini
- [GenerateContent API Reference - UsageMetadata](https://ai.google.dev/api/generate-content#UsageMetadata)
## 结论
对于缓存输入,四种协议应按两类处理:
```text
包含型:OpenAI Chat、OpenAI Responses、Gemini
独立桶型:Claude
```
最新版 OpenAI 又进一步把包含型输入拆成“缓存读取、缓存写入、普通输入”三个计费类别。因此,项目后续不应只维护 `CacheIncludedInPrompt` 一个布尔值,还应显式支持 `cache_write_tokens`,并在协议转换时分别处理缓存读、缓存写和普通输入。
-219
View File
@@ -1,219 +0,0 @@
# 大模型协议 Usage 字段调研
> 调研对象:OpenAI Chat Completions、OpenAI Responses、Claude Messages、Gemini generateContent 四个协议响应中的 token 用量(usage)结构。
> 核心关注点:**存在缓存时,输入 token 是否包含缓存 token**。
> 调研日期:2026-08-17(基于各官方文档当时的版本)
## 核心结论
四家协议里三家是「包含」口径,**Claude 是唯一的「排除」口径**:
| 协议 | 输入字段 | 缓存 token 是否含在内 | 未缓存输入怎么得到 |
|---|---|---|---|
| OpenAI Chat Completions | `prompt_tokens` | ✅ 包含,`cached_tokens` 是它的子集 | `prompt_tokens - cached_tokens` |
| OpenAI Responses | `input_tokens` | ✅ 包含,`cached_tokens` 是它的子集 | `input_tokens - cached_tokens - cache_write_tokens` |
| **Claude Messages** | `input_tokens` | ❌ **排除**,缓存是独立并列字段 | `input_tokens` 本身就是未缓存部分;总输入 = 三个字段**相加** |
| Gemini | `promptTokenCount` | ✅ 包含,`cachedContentTokenCount` 是它的子集 | `promptTokenCount - cachedContentTokenCount` |
## 1. OpenAI Chat Completions(`/v1/chat/completions`)
官方文档示例(chat/object):
```json
"usage": {
"prompt_tokens": 1117,
"completion_tokens": 46,
"total_tokens": 1163,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
}
```
字段全集(据官方 SDK `completion_usage.py` 的 schema):
| 字段 | 说明 |
|---|---|
| `prompt_tokens` | 总输入 token,**包含**缓存命中部分 |
| `completion_tokens` | 总输出 token,**包含** reasoning tokens |
| `total_tokens` | = prompt_tokens + completion_tokens |
| `prompt_tokens_details.cached_tokens` | 缓存命中读到的输入 token(prompt_tokens 的子集) |
| `prompt_tokens_details.cache_write_tokens` | 本次写入缓存的 token(较新字段,prompt_tokens 的子集) |
| `prompt_tokens_details.audio_tokens / text_tokens / image_tokens` | 按模态细分(prompt_tokens 的子集) |
| `completion_tokens_details.reasoning_tokens` | 推理 token(completion_tokens 的子集) |
| `completion_tokens_details.audio_tokens / text_tokens` | 按模态细分(completion_tokens 的子集) |
| `completion_tokens_details.accepted_prediction_tokens / rejected_prediction_tokens` | Predicted Outputs 功能相关 |
包含关系的官方例证(prompt caching guide):`prompt_tokens: 2006, cached_tokens: 1920` —— 1920 是 2006 的一部分,剩 86 个未命中。
缓存计费:命中部分按折扣价计费(guide 现行口径 0.1×,实际各模型定价表不同,如 gpt-4o 为 0.5×)。可缓存的最小前缀为 1024 token,按 128 token 增量命中。
## 2. OpenAI Responses API(`/v1/responses`)
usage 结构明显比 Chat Completions 精简(据官方 SDK `response_usage.py`):
```json
"usage": {
"input_tokens": 2600,
"input_tokens_details": {
"cached_tokens": 2000,
"cache_write_tokens": 400
},
"output_tokens": 300,
"output_tokens_details": {
"reasoning_tokens": 120
},
"total_tokens": 2900
}
```
| 字段 | 说明 |
|---|---|
| `input_tokens` | 总输入,**包含**缓存命中和缓存写入(示例:2600 = 2000 cached + 400 write + 200 新输入) |
| `input_tokens_details.cached_tokens` | 缓存命中(子集) |
| `input_tokens_details.cache_write_tokens` | 本次写入缓存(子集)——Responses 特有字段,Chat Completions 侧较新才补上 |
| `output_tokens` | 总输出,**包含** reasoning_tokens(子集) |
| `output_tokens_details.reasoning_tokens` | 推理 token |
| `total_tokens` | = input_tokens + output_tokens |
注意:
- **没有**模态细分、没有预测 token 字段。
- 复用 `previous_response_id` 时每次都为整个已见对话历史计费,`input_tokens` 会随轮次增长,`cached_tokens` 会很大。
## 3. Claude Messages API(`/v1/messages`)
官方 API reference 的 usage 字段全集:
```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
},
"output_tokens_details": { "thinking_tokens": 0 },
"server_tool_use": { "web_search_requests": 0, "web_fetch_requests": 2 },
"service_tier": "standard",
"inference_geo": "global"
}
```
| 字段 | 说明 |
|---|---|
| `input_tokens` | **仅计最后一个 cache 断点之后的输入**(未走缓存的常规输入) |
| `cache_read_input_tokens` | 从缓存读到的 |
| `cache_creation_input_tokens` | 本次写入缓存的 |
| `cache_creation.ephemeral_5m_input_tokens / ephemeral_1h_input_tokens` | 按 TTL 细分的缓存写入,两者之和 = cache_creation_input_tokens |
| `output_tokens` | 总输出(含 thinking),是计费权威值 |
| `output_tokens_details.thinking_tokens` | 思考 token(output_tokens 的子集,只读观测用) |
| `server_tool_use.web_search_requests / web_fetch_requests` | 服务端工具调用次数(按次计费) |
| `service_tier` | standard / priority / batch |
| `inference_geo` | 推理所在地理区域 |
官方原文(关键):
> "Total input tokens in a request is the summation of `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`."
> "The `input_tokens` field represents only the tokens that come after the last cache breakpoint in your request - not all the input tokens you sent."
计费倍率(官方 prompt caching 文档):
| Token 类型 | 倍率(相对基础输入价) |
|---|---|
| 常规输入 | 1.0× |
| 5 分钟缓存写入(ephemeral_5m) | 1.25× |
| 1 小时缓存写入(ephemeral_1h) | 2× |
| 缓存读取 | 0.1× |
老响应没有 `cache_creation` 细分时,`cache_creation_input_tokens` 默认按 5m(1.25×)计。
## 4. Gemini(`generateContent` 的 `usageMetadata`)
注意:Gemini 的用量不在 `usage` 字段里,而是响应顶层的 `usageMetadata`,且为 camelCase:
```json
"usageMetadata": {
"promptTokenCount": 1050,
"cachedContentTokenCount": 1000,
"candidatesTokenCount": 200,
"thoughtsTokenCount": 150,
"toolUsePromptTokenCount": 0,
"totalTokenCount": 1400,
"promptTokensDetails": [{ "modality": "TEXT", "tokenCount": 1050 }],
"cacheTokensDetails": [{ "modality": "TEXT", "tokenCount": 1000 }],
"candidatesTokensDetails": [{ "modality": "TEXT", "tokenCount": 200 }]
}
```
| 字段 | 说明 |
|---|---|
| `promptTokenCount` | 总输入,**包含**缓存部分 |
| `cachedContentTokenCount` | 其中命中缓存的(显式 context caching;2.5 系列隐式缓存命中也体现在此字段) |
| `candidatesTokenCount` | 输出 token(不含思考) |
| `thoughtsTokenCount` | 思考 token,**输出侧**,按输出价计费 |
| `toolUsePromptTokenCount` | 工具使用 prompt 的 token |
| `totalTokenCount` | = promptTokenCount + thoughtsTokenCount + candidatesTokenCount |
| `promptTokensDetails[] / cacheTokensDetails[] / candidatesTokensDetails[] / toolUsePromptTokensDetails[]` | 按模态(TEXT/IMAGE/AUDIO/VIDEO)细分的列表 |
| `serviceTier` | 服务层级 |
官方原文(关键):
> "When `cachedContent` is set, this is still the total effective prompt size meaning this includes the number of tokens in the cached content."
> "Total token count for the generation request (prompt + thoughts + response candidates)."
## 计费换算公式
```
OpenAI 系(两协议同构):
输入成本 = (input_tokens − cached_tokens) × P_in + cached_tokens × P_cached
输出成本 = output_tokens × P_out # reasoning 已含在 output 里,不另计
Claude:
输入成本 = input_tokens × P_in
+ cache_creation_5m × P_in × 1.25 + cache_creation_1h × P_in × 2
+ cache_read × P_in × 0.1
输出成本 = output_tokens × P_out # thinking 已含在 output 里
(总输入量展示用 = 三项相加,切勿把 input_tokens 当总输入)
Gemini:
输入成本 = (promptTokenCount − cachedContentTokenCount) × P_in + cachedContentTokenCount × P_cached
输出成本 = (candidatesTokenCount + thoughtsTokenCount) × P_out
```
## 协议互转的坑
最容易踩的坑是 **Claude ↔ 其他三家**:
- 把 Claude 的 `input_tokens` 直接映射成 OpenAI 的 `prompt_tokens`,会导致缓存场景下输入量少算(Claude 侧)或多算(反向)。
- 正确做法:Claude → 其他,做**加法**(`input + cache_read + cache_creation` → 对方口径的「总输入」);其他 → Claude,做**减法**(总输入 − 缓存命中 → `input_tokens`,缓存命中 → `cache_read_input_tokens`)。
- 注意 OpenAI 的 `cached_tokens` 是「命中读取」语义,没有 Claude 的「写入 1.25×/2×」概念;反向转换时 Claude 的写入 token 一般并入常规输入或缓存读取,需按计费策略明确选一边。
## 流式响应中 usage 的位置
| 协议 | 流式 usage 出现位置 |
|---|---|
| OpenAI chat | 需设 `stream_options: {"include_usage": true}`,最后一个 chunk 带 usage |
| Responses | `response.completed` 事件的 `response.usage` |
| Claude | `message_start` 带输入侧 usage,`message_delta` 累计 `output_tokens`(已知问题:部分流式场景 `message_delta` 只带 `output_tokens`,输入侧字段缺失,需保留 `message_start` 的值合并) |
| Gemini | `streamGenerateContent` 最后一个 chunk 的 `usageMetadata` |
## 参考来源
- [Gemini API — Generating content(usageMetadata 完整规格)](https://ai.google.dev/api/generate-content)
- [OpenAI — Chat Completions API Reference(chat object)](https://platform.openai.com/docs/api-reference/chat/object)
- [OpenAI — Prompt caching guide](https://developers.openai.com/api/docs/guides/prompt-caching)
- [openai-python — completion_usage.py(PromptTokensDetails/CompletionTokensDetails 权威 schema)](https://github.com/openai/openai-python/blob/main/src/openai/types/completion_usage.py)
- [openai-python — response_usage.py(Responses usage 权威 schema)](https://github.com/openai/openai-python/blob/main/src/openai/types/responses/response_usage.py)
- [Anthropic — Messages API Reference(usage object)](https://platform.claude.com/docs/en/api/messages)
- [Anthropic — Prompt caching(计费倍率与 token 拆解)](https://platform.claude.com/docs/en/docs/build-with-claude/prompt-caching)
-77
View File
@@ -1,77 +0,0 @@
# feat/responses 分支待办事项
> 来源:2026-08-17 对照《docs/大模型协议Usage字段调研.md》的 token 处理审计。
> 已完成:P0(Gemini thoughts 计费)、P1(Claude→OpenAI 加法口径 + bridge 联动)、
> P3(input_tokens 口径统一 + 000017 回填迁移)、P4(cache_write_tokens 解析与可见)、
> P5(插件 TokenUsage 扩展)。以下为**未处理**事项。
---
## A. 已确认未修的代码问题
### 1. Claude 5m/1h 缓存写入未分别计价(原 P2,收入项)⚠️ 高
- **位置**:`internal/logic/billing/pricing.go` `computeCost` / `resolveTokenCounts`
- **问题**:全部 `cacheCreationTokens` 按单一 `CacheCreationPrice` 计价;`CachedCreation5mTokens/1hTokens` 仅入库不参与计价。官方倍率:5m=1.25×、1h=2×,老响应无 TTL 细分时默认按 5m。
- **影响**:`CacheCreationPrice` 按 5m 配置则 1h 写入低估约 37.5%;反之 5m 高估。1h 缓存用量越大偏差越大。
- **修复思路**:
1. `PricingResult` 新增 1h 价格字段(或 1h 倍率),需管理后台配置入口
2. `resolveTokenCounts` 分桶返回 5m/1h
3. `computeCost` 分别计价;无 TTL 细分时全部按 5m 兜底
- **前置**:需先决策定价配置方案(独立字段 vs 倍率)
### 2. OpenAI→Gemini 反向转换 reasoning 双计(小修)✅ 建议顺手修
- **位置**:`relay/channel/openai/gemini_response.go:177,225`
- **问题**:`CandidatesTokenCount = usage.CompletionTokens`——OpenAI 的 completion **含** reasoning,而 Gemini 语义 candidates **不含**思考,且同处又单独设了 `ThoughtsTokenCount`。Gemini 客户端按 `total = prompt + candidates + thoughts` 自行汇总时 reasoning 被双计。
- **修复**:`CandidatesTokenCount: completion - reasoning`(判空 details)
- **影响**:仅 Gemini 格式客户端展示,不影响计费
### 3. Claude 服务端工具调用未计费(收入项)⚠️ 高
- **位置**:`relaykit/dto/claude.go` `ClaudeServerToolUsage` 已解析,计费链路(`internal/logic/billing/`)完全忽略
- **问题**:Claude `usage.server_tool_use.web_search_requests / web_fetch_requests` 按次计费(如 web search $10/1k 次),当前漏收。
- **修复思路**:`TokenDetails` 增加按次计数字段 → usage log 落库 → `computeCost` 增加按次项;需模型定价新增服务端工具单价配置
### 4. 流中断请求的用量报表缺口
- **位置**:`relay/handler/relay_handler.go` 流中断路径(`SettleStreamInterrupted` → `recordFailedUsage`)
- **问题**:计费按 `ApplyInterruptedUsageFallback` 估算实际收了钱,但 `recordFailedUsage` 写的 usage log **token 列全为 0**——计费与报表脱节,token 统计系统性偏低。
- **修复思路**:中断路径将估算 usage 传入 `recordFailedUsage`(或改用带 token 的记录路径)
- **建议**:与 #1 一起做,同动结算/报表链路
---
## B. 部署顺序注意事项(非代码 bug)
### 5. 000017 迁移执行时序 ⚠️ 必须遵守
回填 UPDATE 会给所有 `channel_type=2 AND cache>0` 的行加缓存。若新代码先上线、迁移后执行,窗口期新写入的行(已含缓存口径)会被**二次加 cache 双计**。
正确顺序:**停旧服务 → `goose up`(000017)→ `team-api aggregate-usage --from <最早用量日期> --to <今天>` → 启新服务**
### 6. 部署当天 Redis 热桶混合口径
模型性能页"今天"数据(热桶)当天混合新旧口径,次日 01:00 聚合进 `bil_usage_daily` 后自动消解,无需处理。
---
## C. 待确认 / 未审计项
| # | 项 | 说明 |
|---|---|---|
| 7 | Gemini `toolUsePromptTokenCount` | 官方口径模糊(是否含于 promptTokenCount 不明),当前忽略,需查证后决定是否计费 |
| 8 | Gemini 渠道 → Claude 格式客户端 | relaykit 注册了 `gemini→claude` 转换器但 bridge 未接该方向;该路径是否可达、usage 口径是否正确未验证 |
| 9 | Vertex Code Assist 聚合流 | `handleCodeAssistAggregatedStream` 的 usage 处理未验证 |
| 10 | 音频 token 计费 | `audio_input/output_tokens` 已入库,是否有独立单价、计费是否正确未审计 |
| 11 | 插件 `RelayInterceptor` 接线 | P5 只扩展了 `TokenUsage` 结构;填充点(`RelayResponse.Usage` 构造)待插件系统接入 relay 流程时实现 |
| 12 | playground `calculateCost.ts` 无缓存价 | P1 后 prompt 已含缓存,按全价估算偏高;仅影响演示页展示 |
---
## 建议处理顺序
1. **#2**(一处减法,最小成本)
2. **#1 + #4**(收入项 + 同链路报表,需先定 #1 定价配置方案)
3. **#3**(收入项,需新增定价配置)
4. C 组按需排查