token 估算器:在计费之前猜对 token 数
专栏:new-api 源码拆解 · 第 12 / 12 篇:::info 学习目标 完成本篇后你能够:解释网关为什么需要 token 估算器及其三个消费场景;读懂字符级状态机估算器的权重表与规则;手算一段中英混合文本的估算值;描述图像 token 的 tile/patch 两种公式。 前置:第 8 篇(计费)完成。预计时长:60 分钟。 :::
:::note 本章术语速查(新手建议先读)
- Tokenizer(分词器):把文本切成 token 的程序。各家模型用自己的分词器,同一段文字切出来的 token 数不同。
- tiktoken:OpenAI 官方的分词器库(BPE 算法)——对 OpenAI 模型精确,对别家只是数字游戏。
- BPE(字节对编码):常见的分词算法,高频组合成一个 token、生僻内容拆成多块——“abc123xyz” 会被切开的原因。
- 状态机(State Machine):程序按当前”状态”决定如何处理下一个输入。估算器用它记住”现在是否在单词中间”。
- 权重表(multipliers):每种字符类的经验 token 成本(如中文字 0.68~1.21、Emoji 2+)。
- tile / patch:图片计 token 的两种官方公式——按 512px 瓦片数计费,或按 32px 小块数计费。 :::
第 8 篇的计费三段式有个隐含前提:预扣费发生在上游响应之前——那时谁也不知道真实的 token 数。这篇的主角就是填补这个空白的模块:token 估算器(service/token_estimator.go + service/token_counter.go)。
它在哪被消费:三个场景
三个场景的精度要求不同:预扣费只要”大致够”(结算时多退少补);兜底场景则是最终账单(上游不给 usage,本地估算就是唯一依据);计数端点必须尽量准(用户直接看着这个数字)。
分层设计:估算器 vs 完整计数器
| 文件 | 职责 | 何时用 |
|---|---|---|
token_estimator.go(231 行) | 字符级状态机启发式估算,按厂商分权重 | 非 OpenAI 模型(注释原话:“非openai模型,使用tiktoken-go计算没有意义,使用估算节省资源”) |
token_counter.go(419 行) | 完整请求计数:文本 + 消息格式开销 + 图片/音频/视频/文件 | 预扣费估算与 count_tokens 端点 |
usage_helpr.go(33 行) | 上游缺 usage 时的兜底组装 | 渠道适配器调用 |
核心 1:字符级状态机估算器
没有 tokenizer 怎么估算 token?new-api 的答案是一个按字符分类的状态机:不同字符类有不同权重,同类连续字符只在”进词”时计一次。先看权重表(token_estimator.go:36-46):
// vendor 参考值:每厂商每字符类的经验权重
multipliersMap = map[Provider]multipliers{
Gemini: { Word: 1.15, Number: 2.8, CJK: 0.68, Symbol: 0.38,
MathSymbol: 1.05, URLDelim: 1.2, AtSign: 2.5,
Emoji: 1.08, Newline: 1.15, Space: 0.2, BasePad: 0 },
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, BasePad: 0 },
OpenAI: { Word: 1.02, Number: 1.55, CJK: 0.85, Symbol: 0.4,
MathSymbol: 2.68, URLDelim: 1.0, AtSign: 2.0,
Emoji: 2.12, Newline: 0.5, Space: 0.42, BasePad: 0 },
}
这张表浓缩了大量 tokenizer 的经验规律:中日韩字符在 Gemini 里便宜(0.68/字)、在 Claude 里贵(1.21/字);Emoji 与数学符号是所有 tokenizer 的贵客(Claude 的数学符号高达 4.52);@ 会把单词切碎(2.02.82);URL 分隔符反而被 tokenizer 优化得很好(1.01.26)。权重带读写锁(multipliersLock),支持运行时调整。
状态机的核心循环(简化为伪码,原码见 EstimateToken,token_estimator.go
对 text 的每个字符 r:
空白 → currentWordType 归位;\n/\t 用 Newline 权重,空格用 Space
中日韩 → 归位;count += CJK
Emoji → 归位;count += Emoji
字母/数字 → 若之前不在词中,或从字母切到数字(BPE 会拆 "abc123")
→ 计一次 Word 或 Number;词中字符免费
其他 → 归位;按数学符号/@/URL 分隔符/普通标点分别计
最后:ceil(count) + BasePad
手算检查点:EstimateToken(Claude, "你好 world 3.14")——你好 = 2×1.21;空格 = 0.39;world 进词 1×1.13(4 个字母免费)+ 空格 0.39;数字 “3.14”:3 进词 1.63,”.” 0.4,“14” 换类型 1.63 → 合计 ≈ 2.42+0.39+1.13+0.39+1.63+0.4+1.63 ≈ 7.99 → ceil = 8。估算器对中英混合文本的误差通常在 ±20% 内——对”预扣费”和”兜底计费”两个用途足够。
核心 2:OpenAI 用真 tokenizer,其余用估算
// service/token_counter.go(节选)
func CountTextToken(text string, model string) int {
if common.IsOpenAITextModel(model) {
tokenEncoder := getTokenEncoder(model)
return getTokenNum(tokenEncoder, text) // tiktoken-go:精确
}
// 非openai模型,使用tiktoken-go计算没有意义,使用估算节省资源
return EstimateTokenByModel(model, text)
}
tiktoken 是 OpenAI 的 BPE 编码器——对 OpenAI 模型精确,对别家模型毫无意义。这个”该精确时精确、该省时省”的判断,配合 EstimateTokenByModel 的模型名路由(含 gemini→Gemini、claude→Claude、其余→OpenAI),就是整个估算层的调度逻辑。
核心 3:图片 token——两种官方公式
图片 token 不是”估”出来的,是按各厂商公开的计费公式逐字实现的(getImageToken):
tile 公式(4o/4.1/4.5/o1/o3 系):① 先把图缩进 2048×2048 的方框 → ② 再把短边缩放到 768 → ③ 数 512×512 的瓦片数 → 瓦片数 × tileTokens + baseTokens。每档模型的 base/tile 常数不同(gpt-4o 是 85/170,o1/o3 是 75/150,gpt-4o-mini 高达 2833/5667)。
patch 公式(o4-mini/gpt-4.1-mini/5-mini/nano 系):32×32 像素一块,总 patch 数封顶 1536(超了按面积等比缩小再对齐),每模型乘一个经验系数(1.62~2.46)。
特例也有趣:glm-4 直接返回常数 1047(实测值);detail: "low" 只收 baseTokens;运算符开关(GetMediaToken/GetMediaTokenNotStream)可以让管理员关闭图片/非流式场景的本地计数。
核心 4:音频与文件——按时长与固定值
- 音频转写:按时长计费——
ceil(时长秒) / 60 × 1000token/分钟(与”按分钟定价”对齐);负时长先钳到 0(源码注释:时长来自用户可伪造的元数据,负值会让预扣费变负); - 音频输出(TTS/Realtime):
时长 / 60 × 200 / 0.24换算成 quota(对齐 $0.24/分钟); - 文件类:音频 256、视频 8192(4096×2)、未知/文件 4096——固定估算值,宁高勿低。
组装:CountRequestToken 的完整账单
CountRequestToken(token_counter.go
TokenTypeTextNumber 用于按字符计价的模型)+ 消息格式开销(OpenAI 格式:每条消息 +3、每个工具 +8、每个 name +3、整体 +3——对话模板本身也占 token)+ 各文件的图片/音频/视频 token。结果写入上下文键 ContextKeyPromptTokens,预扣费(第 8 篇)直接取用。
与计费链路的联系
- 预扣费(第 8 篇):
EstimateRequestToken受CountToken开关控制——管理员可关闭中继的 token 估算; - count_tokens 端点:
CountRequestToken不受该开关控制(源码注释:Claude 的 messages/count_tokens 这类工具端点必须始终可用); - 兜底计费:
ResponseText2Usage被 10+ 个渠道适配器调用(palm/cloudflare/tencent/xai/dify…),并写入ContextKeyLocalCountTokens——下游知道这笔账是估的,审计时能区分”上游实测”与”本地估算”。
常见踩坑
- 以为估算等于真实——估算只服务预扣与兜底;有真实 usage 时永远以结算为准。两个数字的差异可从日志的
Other字段观察; - 非 OpenAI 模型跑 tiktoken——数字”精确”但语义错误;这正是估算器存在的原因;
- 用户可控的时长/文件大小不做钳制——负数与天文数字都会击穿计费(源码里的负时长钳位注释就是为此);
- 忘了消息格式开销——只数文本会系统性低估(OpenAI 每条消息 3 token 的模板成本)。
随堂练习(带验收标准)
- 手算
EstimateToken(OpenAI, "user@example.com")——注意 @ 的 2.0 权重与单词切分。验收:与go test或日志中的值一致; - 用管理台发起一次带图请求(4o 模型,已知分辨率的图),手算 tile 公式并与消费日志对比。验收:误差 ≤ 2 token;
- 找一个”上游不返回 usage”的渠道(如部分 cloudflare/dify 配置),确认日志里该笔消费来自本地估算(
ContextKeyLocalCountTokens); - 进阶:给权重表加一个新 Provider(如 DeepSeek),用真实响应的 usage 反推各字符类权重——这就是当年这张表的经验来源。