API 计费不对怎么查:token 计数口径、重复扣费与逐条核对方法
觉得 API 计费不对,常见的情况不是平台算错了,而是你估算用的口径和平台计费的口径不一样:输入、输出、缓存、推理 token 单价不同,不同模型的 tokenizer 切分方式不同,客户端还可能在你不知情的情况下重试。要核对账单,最可靠的办法是把每次请求响应里的 usage 字段记下来,按同一套单价重算一遍,再和平台的明细对比。
为什么我自己估的 token 和账单对不上?
最常见的原因是把所有 token 当成一个价。实际上一次请求至少有四类 token,各自计价:
| 类别 | 是什么 | 常见误区 |
|---|---|---|
| 输入 token | system prompt、历史消息、工具定义、你本次发的内容 | 只算了用户这句话,忘了历史对话和工具 schema 每轮都会重发 |
| 输出 token | 模型生成的可见内容 | 用字数粗估,中英文、代码的 token 密度差很多 |
| 推理 token | 推理类模型在给出答案前的内部思考 | 响应里看不到正文,但按输出计费 |
| 缓存 token | 命中提示缓存的输入部分,或写入缓存的部分 | 读缓存通常比普通输入便宜,写缓存可能更贵,具体倍率以官方价格页为准 |
输出比输入贵得多,这一点会把误差放大。按 cdnping 在 2026-10-17 从 OpenRouter 公开模型目录抓取的 16 个主流模型价格,输出单价普遍是输入单价的 3 到 5 倍,其中一个模型约为 39 倍。所以同样“多算了 1 万 token”,落在输入还是输出上,金额可能差一个数量级。这些价格会变,想跟踪变化可以看 /models 的调价记录和 /changes 对官方价格页的逐行监控,做法在大模型 API 价格变了怎么第一时间知道里有详细说明。
Agent 类工具为什么特别费 token?
Claude Code、Codex 这类工具每一轮都会带上完整的上下文:系统指令、工具定义、已读过的文件、之前每一步的工具调用结果。假设一次会话跑了 30 轮,每轮上下文平均 4 万 token,仅输入就是 120 万 token,而你看到的对话可能只有几屏。这部分如果没有命中缓存,就全部按普通输入计费。
不同 tokenizer 会让同一段文字算出不同的 token 数吗?
会。每家模型用自己的分词器,同一段中文在不同模型上的 token 数可能相差明显,代码和 JSON 也一样。用某一家的 tokenizer 去估另一家的账单,本身就会有偏差。
更稳妥的做法:
- OpenAI 系模型可以用
tiktoken本地计数,但要选对编码,并且记住消息格式本身也会占少量 token。 - Anthropic 提供
/v1/messages/count_tokens接口,按实际请求体返回输入 token 数,比本地估算准。 - 其他模型以响应里的
usage为准,不要拿通用分词器去推算。
如果你用的是中转站或聚合网关,还要确认它转发到的是不是你以为的那个模型。模型换了,tokenizer 和单价都会跟着变,怎么确认 API 给你的是哪个模型里有可以动手做的一致性检查。
流式输出和重试会不会重复扣费?
账单“凭空多出来”的部分,常见的有三种来源:
- 客户端自动重试:很多 SDK 默认对超时、429、5xx 自动重试若干次。如果第一次请求其实已经在服务端生成了一部分甚至全部内容,只是响应没回到你这里,这次调用可能已经计费,重试又算一次。
- 流式中途断开:流式请求在中途断开时,已经生成的 token 通常仍会计费。客户端随后重新发起整轮请求,输入 token 也会再算一遍。
- 上层工具的重放:Agent 工具在出错后经常把整轮上下文重发,单次失败的代价是“整段上下文 + 已生成的输出”。
失败和重试往往集中出现在服务不稳定的时段。以 cdnping 镜像的 AGI Plan 公开请求数据为例(AGI Plan 是 cdnping 的商业合作方;窗口为北京时间 2026-10-15 19:05 至 2026-10-17 18:05,逐小时计数),2026-10-16 14:05 和 15:05 两个小时分别有 722 次和 877 次失败,都超过了同小时的成功次数(664 和 455),其余大部分小时的失败数在两位数以内。对账时如果某几个小时的消耗明显偏高,先去看那段时间服务端有没有故障,再看客户端日志里同一个任务是不是被发了多次。可用率怎么从这种计数里算出来,可以参考自己测 AI API 的可用率。
怎么用 usage 字段逐条核对?
第一步:确认每次请求都拿到了 usage
非流式请求的响应里直接带 usage。流式请求要注意:
- OpenAI 兼容接口需要在请求里加
"stream_options": {"include_usage": true},最后一个数据块才会带上usage,否则流式请求可能拿不到任何用量信息。 - Anthropic 接口的
message_start事件里有输入用量,message_delta事件里有累计的输出 token,要以最后一个message_delta为准,不要把每个事件里的数字相加。
第二步:认清字段
| 含义 | OpenAI 兼容格式 | Anthropic 格式 |
|---|---|---|
| 输入 token | usage.prompt_tokens(含缓存命中部分) | usage.input_tokens(不含缓存部分) |
| 缓存读取 | usage.prompt_tokens_details.cached_tokens | usage.cache_read_input_tokens |
| 缓存写入 | 无单独字段 | usage.cache_creation_input_tokens |
| 输出 token | usage.completion_tokens(含推理) | usage.output_tokens |
| 推理 token | usage.completion_tokens_details.reasoning_tokens | 计入 output_tokens |
注意两种格式在“输入是否包含缓存”上口径相反。把 OpenAI 的 prompt_tokens 再加上 cached_tokens,或者把 Anthropic 的 input_tokens 当成全部输入,都会算错。
第三步:在自己这边记账
在调用层加一层日志,每次请求记一行。下面是一个最小例子,把 OpenAI 兼容接口的用量按单价重算:
import json, time
# 单价按你实际使用的渠道填写,单位:每百万 token
PRICE = {"input": 0.0, "cached": 0.0, "output": 0.0}
def log_usage(resp_json, request_id, path="usage.jsonl"):
u = resp_json.get("usage") or {}
cached = (u.get("prompt_tokens_details") or {}).get("cached_tokens", 0)
prompt = u.get("prompt_tokens", 0)
output = u.get("completion_tokens", 0)
cost = ((prompt - cached) * PRICE["input"]
+ cached * PRICE["cached"]
+ output * PRICE["output"]) / 1_000_000
row = {"ts": time.time(), "id": request_id, "model": resp_json.get("model"),
"prompt": prompt, "cached": cached, "output": output, "cost": cost}
with open(path, "a") as f:
f.write(json.dumps(row) + "\n")
request_id 用响应里的 id 字段,或你自己生成后放进请求头的 ID。重试时要复用同一个业务 ID、另记一个尝试次数,这样才能在日志里看出哪些是重复请求。
第四步:和平台明细对账
拿到平台导出的明细后,按下面的顺序比:
- 总请求数:平台条数比你日志多,说明有你没记到的调用:SDK 自动重试、另一个进程在用同一个密钥,或密钥泄漏。
- 按小时聚合的 token 量:差额集中在少数几个小时,回头看这几个小时有没有故障或断流。
- 单条比对:挑差额最大的几条,用请求 ID 对上,比较输入、缓存、输出三项。输入对得上、输出差很多,多半是推理 token;输入差很多,多半是缓存口径或上下文膨胀。
- 单价:数量都对上了金额还对不上,检查渠道单价是否在这段时间调整过。
中转站和聚合网关的扣费要额外注意什么?
用中转站或聚合网关时,你看到的账单经过了一层换算,需要额外确认:
- 计量单位:是直接按 token、按次,还是按自定义单位。
- 换算规则是否公开:输入、输出、缓存是否分别换算,不同模型的系数是多少。
- 失败请求怎么算:5xx、超时、客户端主动断开分别是否扣费。
- 能否导出逐条明细:只有总额、没有逐条记录的话,基本无法自己核对。
这几项在AI API 中转站怎么选的 8 项检查里都有展开。
以 CU 为例:统一计量单位怎么换算?
前面提到的 AGI Plan(是 cdnping 的商业合作方)用的是 CU 这种统一计量单位:1 CU 等于基准模型 1,000 个输出 token;按量付费时 1 元 = 500 CU。按这个定义,在基准模型上生成 10 万个输出 token 消耗 100 CU,折合按量价 0.2 元。输入 token、缓存以及其他能力组的折算系数,以 agiplan.dev/pricing 公布的为准,这里不替它推算。
统一单位的好处是一份额度可以在多个能力组之间切换,代价是你要多做一步换算才能和官方 token 价格对比。核对时还是同样的方法:每条请求记下 usage,按公布的系数折成 CU,再和后台扣减对比。如果你在 Claude Code、Codex 里改了接口地址,记得确认请求确实走到了你以为的那个地址,配置方法见Claude Code 自定义 API 与 Codex、OpenCode 接口地址配置。AGI Plan 各能力组的公开可用率可以在 /status/agiplan 查看,对账时遇到失败集中的时段可以对照。
一份排查清单
账单对不上时,按这个顺序过一遍:
- 流式请求是否开启了用量回传(
include_usage或读取message_delta) - 输入 token 是否混淆了“含缓存”和“不含缓存”两种口径
- 推理 token 是否被算进了输出
- SDK 的自动重试次数是多少,日志里能否区分重试
- 差额是否集中在某几个小时,那段时间有无服务故障
- 平台记录的请求数是否多于自己的日志,密钥是否只在预期的地方使用
- 统计期间单价或换算系数是否调整过
- 中转站或网关是否提供逐条明细,失败请求是否计费
如果查下来发现主要成本来自上下文膨胀,而不是计费错误,那就该从用法上省了:把补全、格式化这类简单任务分到便宜的模型上,思路见多模型路由怎么做。