从 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 的区别。


  1. 请求进入时,两者怎么估算 prompt token

    共性

    在 controller/relay.go:125-152:

  2. 先从 request 提取 TokenCountMeta

  3. 调 service.EstimateRequestToken(…)

  4. 把结果存到 relayInfo.SetEstimatePromptTokens(tokens)

  5. 再交给 relay/helper/price.go:48 用于预扣费

    也就是说,OpenAI 和 Anthropic 在入口层都先做一次“本地预估”,用于风控/预扣费。


  1. 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 估算明显更细。


  1. 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


  1. 多模态计算差异

    图片

    在 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 这里差异不大,主要差在图片。


  1. 响应返回后,实际 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 语义,项目里专门做了兼容转换。


  1. 一句话概括两者差异

    OpenAI

  • 请求阶段:尽量用真实 tokenizer

  • 还会加上 OpenAI chat 消息格式固定开销

  • 图片 token 按模型规则精算

  • 响应阶段:优先信 upstream usage,缺失再本地补算

    Anthropic

  • 请求阶段:不用 tokenizer,按 Claude 专属权重估算

  • 不加 OpenAI chat 的固定包装 token

  • 图片 token 多数情况下用固定近似值 520

  • 响应阶段:优先信 Anthropic 原生 usage

  • 且会额外处理 cache read / cache creation,再按需要映射成 OpenAI 风格 usage


  1. 你可以重点记住的“本质区别”

    最核心就三条:

  2. OpenAI 文本 token 是“真 tokenizer”计算;Anthropic 文本 token 是“权重估算”

    • service/token_counter.go:404-410
  3. OpenAI 会加 chat 协议格式开销;Anthropic 不会

    • service/token_counter.go:230-235
  4. Anthropic 的实际 usage 里更强调 cache token 语义

    • relay/claude_handler.go:572-585

如果你要,我也可以下一条继续给你整理成一张“对比表”,按:

  • 请求体提取字段

  • 预估算法

  • 图片处理

  • 上游 usage 兜底

  • 缓存 token 处理

    做成更直观的表格版。