从 controller/relay.go 的 Relay 入口往下串了一遍,核心链路是:
controller/relay.go:67 Relay
dto/openai_request.go:119 GeneralOpenAIRequest.GetTokenCountMeta
dto/claude.go:234 ClaudeRequest.GetTokenCountMeta
service/token_counter.go:183 EstimateRequestToken
service/token_counter.go:399 CountTextToken
service/token_estimator.go:68 EstimateToken
relay/helper/price.go:48 ModelPriceHelper
返回阶段:
- OpenAI: relay/channel/openai/relay-openai.go:106, relay/channel/openai/relay-openai.go:195
- Anthropic: relay/channel/claude/relay-claude.go:24, relay/claude_handler.go:815, relay/claude_handler.go:882
下面按“请求预估”和“响应实际 usage”两部分总结 OpenAI 和 Anthropic 的区别。
请求进入时,两者怎么估算 prompt token
共性
在 controller/relay.go:125-152:
先从 request 提取 TokenCountMeta
调 service.EstimateRequestToken(…)
把结果存到 relayInfo.SetEstimatePromptTokens(tokens)
再交给 relay/helper/price.go:48 用于预扣费
也就是说,OpenAI 和 Anthropic 在入口层都先做一次“本地预估”,用于风控/预扣费。
OpenAI 的 token 计算方式
2.1 文本部分:尽量用真正 tokenizer
在 service/token_counter.go:399:
如果是 OpenAI 文本模型,common.IsOpenAITextModel(model) 为真
就走 getTokenEncoder(model) + getTokenNum(…)
也就是真正按 tokenizer 算
代码位置:
service/token_counter.go:404-406
这点是 OpenAI 和 Anthropic 最大区别之一。
2.2 额外加上 OpenAI chat message 格式开销
在 service/token_counter.go:230-235:
如果 info.RelayFormat == types.RelayFormatOpenAI,还会额外加:
meta.ToolsCount * 8
meta.MessagesCount * 3
meta.NameCount * 3
最后再 +3
这明显是在模拟 OpenAI chat/completions 的消息包装成本。
代码位置:
service/token_counter.go:230-235
2.3 OpenAI 请求元数据提取更偏向 Chat 格式
在 dto/openai_request.go:119-224,GetTokenCountMeta() 会提取:
messages 中的 role
name
文本内容
tools 的 name/description/parameters
图片/音频/文件/视频等多模态内容
MaxTokens / MaxCompletionTokens
并且会统计:
MessagesCount
NameCount
ToolsCount
这些字段后面会直接参与 OpenAI 的额外 token 开销计算。
2.4 多模态图片对 OpenAI 是特殊精算
在 service/token_counter.go:22 的 getImageToken(…):
对 OpenAI 系列模型(如 4o/4.1/o1/o3/gpt-5 等)按模型规则精算图片 token
包括 patch-based / tile-based 两套算法
不是简单固定值
而在主流程里:
service/token_counter.go:279-285
只有 common.IsOpenAITextModel(model) 才走 getImageToken(…)
也就是说,OpenAI 图片 token 估算明显更细。
Anthropic 的 token 计算方式
3.1 文本部分:不走 tokenizer,走估算器
在 service/token_counter.go:404-410:
非 OpenAI 模型不会走 tokenizer
直接走 EstimateTokenByModel(model, text)
如果模型名包含 claude,则走:
service/token_estimator.go:225-226
最终使用的是:
service/token_estimator.go:68 EstimateToken(Claude, text)
这是一套基于字符类别权重的估算算法,不是 Claude 官方 tokenizer。
3.2 Claude 有自己一套估算权重
在 service/token_estimator.go:40-42,Claude 的权重是:
Word: 1.13
Number: 1.63
CJK: 1.21
Symbol: 0.4
MathSymbol: 4.52
URLDelim: 1.26
AtSign: 2.82
Emoji: 2.6
Newline: 0.89
Space: 0.39
对比 OpenAI:
OpenAI 的 CJK 更低:0.85
Claude 的 CJK 更高:1.21
Claude 的 Emoji / MathSymbol / @ 更高
OpenAI 的换行更低:0.5
所以从代码设计看,项目认为 Claude 对中文、emoji、数学符号、@ 这类内容更“吃 token”。
3.3 Anthropic 请求没有加 OpenAI 那套 message 包装常数
service/token_counter.go:230-235 那段只对 RelayFormatOpenAI 生效。
所以 Claude 请求不会额外加:
每条消息 +3
每个 name +3
工具 *8
收尾 +3
也就是说,Anthropic 预估主要靠拼出来的 CombineText 本体,不加 OpenAI chat 协议的固定格式开销。
3.4 Claude 的 meta 提取更贴近 Anthropic 消息结构
在 dto/claude.go:234-360:
会提取:
system
- 如果是 string,直接取
- 如果是多段 media,也会取 text/image
messages
- role
- text
- image
- tool_use
- tool_result
tools
- 普通 tool 的 name/description/input_schema
- web search tool 的 name/user_location
特点是:
Claude 把 system 也明确算进去了
tool_use / tool_result 的内容也被拼进文本估算
结构更符合 Anthropic messages API
多模态计算差异
图片
在 service/token_counter.go:276-297:
OpenAI 模型图片:走 getImageToken(…) 精算
非 OpenAI 模型图片:直接 +520
所以 Claude 图片 token 在请求预估阶段是:
固定近似值 520
不是 Claude 专属图片 tokenizer
这点很重要:Claude 的图片 token 预估明显比 OpenAI 粗糙。
音频 / 视频 / 文件
统一是粗略固定值:
audio: +256
video: +4096 * 2
file: +4096
OpenAI 和 Anthropic 这里差异不大,主要差在图片。
响应返回后,实际 usage 的来源区别
这部分比请求预估更关键,因为最终结算更依赖实际 usage。
5.1 OpenAI:优先信上游 usage,缺失时本地补算
OpenAI 处理在:
流式:relay/channel/openai/relay-openai.go:106
非流式:relay/channel/openai/relay-openai.go:195
非流式
在 relay/channel/openai/relay-openai.go:243-258:
如果上游返回的 usage.prompt_tokens == 0,就兜底:
prompt = info.GetEstimatePromptTokens()
completion = 从响应文本再数一次
total = 两者相加
即:
OpenAI 正常情况以 upstream usage 为准;没有 usage 才 fallback 本地估算。流式
在 relay/channel/openai/relay-openai.go:183-186:
如果流里没带 usage,就:
service.ResponseText2Usage(…)
prompt 用 info.GetEstimatePromptTokens()
completion 用输出文本再数
所以 OpenAI 的策略是:
优先使用上游 usage
缺了再用本地 tokenizer / 文本补算
5.2 Anthropic:usage 语义是 Anthropic 原生,再按需要映射成 OpenAI 风格
Anthropic 处理在:
流式:relay/claude_handler.go:815
非流式:relay/claude_handler.go:882
Claude 原生 usage 提取
在 relay/claude_handler.go:672-681 和 691-715:
Claude usage 字段来自上游:
InputTokens
CacheReadInputTokens
CacheCreationInputTokens
OutputTokens
并写入本地 dto.Usage:
PromptTokens = InputTokens
CompletionTokens = OutputTokens
PromptTokensDetails.CachedTokens = CacheReadInputTokens
PromptTokensDetails.CachedCreationTokens = CacheCreationInputTokens
同时标记:
UsageSemantic = “anthropic”
流中如果没有完整 usage
在 relay/claude_handler.go:778-795:
如果流结束时:
PromptTokens == 0,先用入口预估 estimatePromptTokens 兜底
CompletionTokens == 0 或流未正常完成,再用 service.ResponseText2Usage(…) 按文本补算
所以 Anthropic 也是:
优先用上游 usage
缺失时用本地估算兜底
但 Anthropic 有缓存 token 的专门处理
这是 OpenAI 处理里没有这么重视的一点。
在:
relay/claude_handler.go:558-585
relay/claude_handler.go:572-584
有专门逻辑把 Anthropic cache token 映射成 OpenAI 风格 usage:
cache_read_input_tokens
cache_creation_input_tokens
claude_cache_creation_5_m_tokens
claude_cache_creation_1_h_tokens
并在映射时重新计算:
PromptTokens
InputTokens
TotalTokens
也就是:
Anthropic usage 不只是 input/output token,还包含 cache creation / cache read 语义,项目里专门做了兼容转换。
一句话概括两者差异
OpenAI
请求阶段:尽量用真实 tokenizer
还会加上 OpenAI chat 消息格式固定开销
图片 token 按模型规则精算
响应阶段:优先信 upstream usage,缺失再本地补算
Anthropic
请求阶段:不用 tokenizer,按 Claude 专属权重估算
不加 OpenAI chat 的固定包装 token
图片 token 多数情况下用固定近似值 520
响应阶段:优先信 Anthropic 原生 usage
且会额外处理 cache read / cache creation,再按需要映射成 OpenAI 风格 usage
你可以重点记住的“本质区别”
最核心就三条:
OpenAI 文本 token 是“真 tokenizer”计算;Anthropic 文本 token 是“权重估算”
- service/token_counter.go:404-410
OpenAI 会加 chat 协议格式开销;Anthropic 不会
- service/token_counter.go:230-235
Anthropic 的实际 usage 里更强调 cache token 语义
- relay/claude_handler.go:572-585
如果你要,我也可以下一条继续给你整理成一张“对比表”,按:
请求体提取字段
预估算法
图片处理
上游 usage 兜底
缓存 token 处理
做成更直观的表格版。