Token 怎么算账
4 种统计方式
一、原理:每个 provider 的 usage 字段差异
我们在 2.3 章讲过 token 经济学——cost 跟 token 紧密相关。但实际计算 cost 比你想象的复杂——因为每家 provider 的 usage 字段不一样。
我们看 4 家主流 provider 的 usage 结构。
Anthropic 的 usage:
{
"usage": {
"input_tokens": 1234,
"output_tokens": 567,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0
}
}
4 个字段:
input_tokens—— 这次 input 的 token 数(不包含缓存命中的 token)output_tokens—— 这次 output 的 token 数cache_creation_input_tokens—— 这次 input 中写入缓存的 token 数(cache_write,付 1.25x normal price)cache_read_input_tokens—— 这次 input 中从缓存读取的 token 数(cache_read,付 0.1x normal price)
要算总 input cost:
total_input_cost =
input_tokens × input_price
+ cache_creation_input_tokens × (input_price × 1.25)
+ cache_read_input_tokens × (input_price × 0.1)
总 input token 数:
total_input_tokens =
input_tokens
+ cache_creation_input_tokens
+ cache_read_input_tokens
注意——input_tokens 是非缓存部分。如果你想知道总 input——要加另外两个。
这种分离让你能清楚看到缓存效果——cache_read 占 input 90% 意味着你的 caching 命中率高。
OpenAI 的 usage:
{
"usage": {
"prompt_tokens": 1801,
"completion_tokens": 567,
"total_tokens": 2368,
"prompt_tokens_details": {
"cached_tokens": 1200,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0
}
}
}
更复杂的结构:
prompt_tokens—— input 总数(包含缓存命中的)completion_tokens—— output 总数total_tokens—— 总数(prompt + completion)prompt_tokens_details.cached_tokens—— 其中缓存命中的(折扣价)completion_tokens_details.reasoning_tokens—— o1 模型的 reasoning 消耗(计费但不可见)
要算总 cost:
non_cached_input = prompt_tokens - cached_tokens
total_input_cost =
non_cached_input × input_price
+ cached_tokens × (input_price × 0.5) // OpenAI 缓存 50% 折扣
total_output_cost = completion_tokens × output_price
注意几个差别:
- prompt_tokens 包含缓存(需要减出来才知道非缓存)
- 缓存折扣 50%(不是 Anthropic 的 90%)
- reasoning_tokens 在 completion 里——但用户看不到内容,只收费
Google Gemini 的 usage:
{
"usageMetadata": {
"promptTokenCount": 1801,
"candidatesTokenCount": 567,
"totalTokenCount": 2368,
"cachedContentTokenCount": 1200
}
}
promptTokenCount—— 类似 OpenAI 的 prompt_tokenscandidatesTokenCount—— output(Gemini 叫 candidates)cachedContentTokenCount—— 缓存命中(Gemini 的 caching 用 separate API)
注意:Gemini 用 camelCase(其他 provider 用 snake_case)——adapter 要处理大小写转换。
Bedrock 的 usage:
{
"usage": {
"inputTokens": 1801,
"outputTokens": 567,
"totalTokens": 2368
}
}
最简单——3 个字段、camelCase、没有 caching 信息。
因为 Bedrock 不太支持 prompt caching(至少在某些 model 上)——usage 里没这个数据。
把这 4 个 provider 的 usage 合起来——你看到的差异:
- 字段名(snake_case vs camelCase)
- 字段语义(prompt_tokens 包不包含缓存)
- 缓存信息(有 vs 无、折扣率不同)
- 特殊 token(reasoning vs thinking)
这些差异每个都要 adapter 处理——内部统一表示、外部按 provider 解析。
二、案例:opencode 怎么 normalize 4 种 token 统计
R6-04 阶段的研究告诉我们——opencode 在 packages/opencode/src/session/session.ts 有个 getUsage() 函数——把每个 provider 的 usage 翻译成统一格式。
简化版逻辑:
interface NormalizedUsage {
input_tokens: number // 总 input(包含缓存)
output_tokens: number // 总 output
cache_read_tokens: number // 缓存读
cache_write_tokens: number // 缓存写
reasoning_tokens: number // o1 reasoning
total_tokens: number // 总和
}
function normalizeAnthropicUsage(raw): NormalizedUsage {
return {
input_tokens: raw.input_tokens + raw.cache_read_input_tokens + raw.cache_creation_input_tokens,
output_tokens: raw.output_tokens,
cache_read_tokens: raw.cache_read_input_tokens,
cache_write_tokens: raw.cache_creation_input_tokens,
reasoning_tokens: 0, // Anthropic thinking 算 output
total_tokens: 0 // 算
}
}
function normalizeOpenAIUsage(raw): NormalizedUsage {
const cached = raw.prompt_tokens_details?.cached_tokens || 0
const reasoning = raw.completion_tokens_details?.reasoning_tokens || 0
return {
input_tokens: raw.prompt_tokens, // 已经包含缓存
output_tokens: raw.completion_tokens,
cache_read_tokens: cached,
cache_write_tokens: 0, // OpenAI 不暴露 write 数
reasoning_tokens: reasoning,
total_tokens: raw.total_tokens
}
}
function normalizeGeminiUsage(raw): NormalizedUsage {
return {
input_tokens: raw.promptTokenCount,
output_tokens: raw.candidatesTokenCount,
cache_read_tokens: raw.cachedContentTokenCount || 0,
cache_write_tokens: 0,
reasoning_tokens: 0,
total_tokens: raw.totalTokenCount
}
}
function normalizeBedrockUsage(raw): NormalizedUsage {
return {
input_tokens: raw.inputTokens,
output_tokens: raw.outputTokens,
cache_read_tokens: 0, // 不暴露
cache_write_tokens: 0,
reasoning_tokens: 0,
total_tokens: raw.totalTokens
}
}
每个 adapter 把自家 usage 翻译成 NormalizedUsage。上层代码用统一的字段算 cost:
function calculateCost(usage: NormalizedUsage, model: ModelConfig): number {
const non_cached_input = usage.input_tokens - usage.cache_read_tokens - usage.cache_write_tokens
return (
non_cached_input * model.input_price +
usage.cache_read_tokens * model.cache_read_price +
usage.cache_write_tokens * model.cache_write_price +
usage.output_tokens * model.output_price
) / 1_000_000 // 价格通常按"百万 token"算
}
这种"normalize 后统一算"让上层代码不感知 provider 差异。
但 normalize 有几个坑:
坑 1:Anthropic 的 input_tokens 不包含缓存
如果你以为 Anthropic 的 input_tokens 是总数——会少算 cache 部分的 token。
input_tokens + cache_creation + cache_read 才是总数。
坑 2:OpenAI 的 prompt_tokens 包含缓存
如果你以为 OpenAI 的 prompt_tokens 不包含缓存——会重复加。
prompt_tokens 已经是总数——直接用。
两家相反——容易搞混。
坑 3:reasoning_tokens 计费但不可见
OpenAI o1 的 completion_tokens_details.reasoning_tokens 是隐藏的 reasoning 消耗——用户付钱但看不到内容。
你的 UI 上显示"output: X tokens" 要不要包含 reasoning?
opencode 的选择是——算在 output 里——因为它就是按 output_price 计费。用户看 output 数字时知道"这是包含 reasoning 的总 output"。
坑 4:Gemini 的 caching 是 separate API
Gemini 的 prompt caching 不是 inline 标记(像 Anthropic)——是单独的 cachedContent API:你先调一次 API 创建缓存、得到 cache ID、后续请求引用 cache ID。
usage 里的 cachedContentTokenCount 只是"这次请求从那个缓存读了多少"——不包含缓存的创建成本(那个是单独 API 的成本)。
如果你的 cost 计算只看 usage——会漏算 cache 创建成本。
需要单独追踪。
坑 5:Bedrock 没有 caching 数据
Bedrock 即使支持 caching(某些 model)——usage 里也不暴露。你不知道 cache 命中率、不知道省了多少。
这是 Bedrock 的透明度缺陷——你的 cost 计算只能"按 input/output 算"——caching 折扣没法 surface 给用户。
这些坑都是 opencode 在维护过程中踩出来的——经过反复 debug 和测试形成 normalize 逻辑。
三、设计启示:cost 计算的真实复杂度
这一章的核心论点:Cost 计算不是"input × price + output × price" 这么简单——每个 provider 的 usage 语义不同,要 normalize。
设计 AI 产品时关于 cost 计算,下面几条原则有用:
1. 定义统一的 NormalizedUsage
不要直接用各 provider 的原始 usage——定义一个内部统一结构。
每个 adapter 翻译进内部结构——cost 计算用内部结构。
2. 区分 "non_cached_input" 和 "input_tokens"
input_tokens 是 LLM 看到的总输入(包含缓存)——用于 token budget 计算。
non_cached_input 是非缓存部分——用于 cost 计算。
两者用途不同——分别保存。
3. 缓存折扣率每个 provider 不同
- Anthropic:cache_read 0.1x normal、cache_write 1.25x normal
- OpenAI:cache_read 0.5x normal、cache_write 是 normal(隐含)
- Gemini:单独 API、单独定价
不要假设"所有 provider 缓存都 90% 折扣"——按 provider 配置。
4. reasoning_tokens 单独追踪
OpenAI o1 的 reasoning 占 output token——但用户看不到内容。
UI 上可以分开显示: "Output: 500 tokens (含 reasoning: 300, 可见 text: 200)"
让用户看到 reasoning 消耗。
5. 价格表数据驱动
把每个 model 的价格放配置(如 models.dev、price.json)——不要 hardcode 在代码里。
provider 改价格时——更新配置即可。
opencode 用 models.dev——可以参考。
6. 监控 token 估算 vs 实际
你自己用 4 字符近似估算 input tokens(2.3 章)——实际从 usage 拿真实值。两者差距:
- 估算误差 5-10% 算正常
- 误差 > 20% 说明估算算法需要调
监控差距——校准估算。
7. cost 显示要透明
UI 上展示成本——分项展示比总和好:
Input: $0.012 (1,234 tokens × $10/M)
Cache read: $0.001 (1,200 tokens × $1/M)
Output: $0.085 (567 tokens × $150/M)
Total: $0.098
让用户看清"钱花在哪里"——也能审计你的算法对不对。
8. 测试每个 provider 的 cost 准确性
跑一次小 session,自己算 cost、对比 provider 的账单——两者应该差异 < 1%。
如果差距大——你的 normalize 有 bug。
最后一个观察。Cost 计算这件事——看起来工程——实际是产品 UX 的关键。
用户用 AI 产品最焦虑的是"我花了多少钱、为什么花这么多"。如果你的 cost 数字不准、不透明、不细致——用户失去信任、慢慢流失。
opencode 在 cost 计算上的投入(每个 provider 准确 normalize、缓存分项、reasoning 单独追踪)——是产品 UX 的"隐性投资"。
你做 AI 产品也应该投资这块——花时间把 cost 计算做准、做透明。这是用户信任的来源。
下一章 6.8 是第 6 篇的收尾——我们看 Prompt Caching 这个 cost 优化机制的三种姿态。