---
title: "API 计费不对怎么查：token 计数口径、重复扣费与逐条核对方法"
description: "账单比自己估的多，通常出在缓存与推理 token、tokenizer 差异、流式中断和自动重试上。本文给出 usage 字段对照表、一段记账脚本和排查顺序，并说明 CU 这类统一计量单位怎么换算。"
url: https://cdnping.org/blog/relay-billing-token-mismatch
language: zh-CN
published: 2026-10-18
category: "开发工具"
tags: ["API 计费","token","中转站","账单核对","开发工具"]
source: cdnping.org
license: Free to quote and summarize with a link to the url above.
---
# 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](/models) 的调价记录和 [/changes](/changes) 对官方价格页的逐行监控，做法在[大模型 API 价格变了怎么第一时间知道](/blog/track-llm-price-and-model-changes)里有详细说明。

### 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 给你的是哪个模型](/blog/verify-model-authenticity-relay)里有可以动手做的一致性检查。

## 流式输出和重试会不会重复扣费？

账单“凭空多出来”的部分，常见的有三种来源：

1. 客户端自动重试：很多 SDK 默认对超时、429、5xx 自动重试若干次。如果第一次请求其实已经在服务端生成了一部分甚至全部内容，只是响应没回到你这里，这次调用可能已经计费，重试又算一次。
2. 流式中途断开：流式请求在中途断开时，已经生成的 token 通常仍会计费。客户端随后重新发起整轮请求，输入 token 也会再算一遍。
3. 上层工具的重放：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 的可用率](/blog/measure-ai-api-uptime-yourself)。

## 怎么用 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 兼容接口的用量按单价重算：

```python
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、另记一个尝试次数，这样才能在日志里看出哪些是重复请求。

### 第四步：和平台明细对账

拿到平台导出的明细后，按下面的顺序比：

1. 总请求数：平台条数比你日志多，说明有你没记到的调用：SDK 自动重试、另一个进程在用同一个密钥，或密钥泄漏。
2. 按小时聚合的 token 量：差额集中在少数几个小时，回头看这几个小时有没有故障或断流。
3. 单条比对：挑差额最大的几条，用请求 ID 对上，比较输入、缓存、输出三项。输入对得上、输出差很多，多半是推理 token；输入差很多，多半是缓存口径或上下文膨胀。
4. 单价：数量都对上了金额还对不上，检查渠道单价是否在这段时间调整过。

## 中转站和聚合网关的扣费要额外注意什么？

用中转站或聚合网关时，你看到的账单经过了一层换算，需要额外确认：

- 计量单位：是直接按 token、按次，还是按自定义单位。
- 换算规则是否公开：输入、输出、缓存是否分别换算，不同模型的系数是多少。
- 失败请求怎么算：5xx、超时、客户端主动断开分别是否扣费。
- 能否导出逐条明细：只有总额、没有逐条记录的话，基本无法自己核对。

这几项在[AI API 中转站怎么选](/blog/api-relay-how-to-evaluate)的 8 项检查里都有展开。

### 以 CU 为例：统一计量单位怎么换算？

前面提到的 AGI Plan（是 cdnping 的商业合作方）用的是 CU 这种统一计量单位：1 CU 等于基准模型 1,000 个输出 token；按量付费时 1 元 = 500 CU。按这个定义，在基准模型上生成 10 万个输出 token 消耗 100 CU，折合按量价 0.2 元。输入 token、缓存以及其他能力组的折算系数，以 [agiplan.dev/pricing](https://agiplan.dev/pricing) 公布的为准，这里不替它推算。

统一单位的好处是一份额度可以在多个能力组之间切换，代价是你要多做一步换算才能和官方 token 价格对比。核对时还是同样的方法：每条请求记下 `usage`，按公布的系数折成 CU，再和后台扣减对比。如果你在 Claude Code、Codex 里改了接口地址，记得确认请求确实走到了你以为的那个地址，配置方法见[Claude Code 自定义 API 与 Codex、OpenCode 接口地址配置](/blog/configure-custom-api-endpoint-claude-code-codex)。AGI Plan 各能力组的公开可用率可以在 [/status/agiplan](/status/agiplan) 查看，对账时遇到失败集中的时段可以对照。

## 一份排查清单

账单对不上时，按这个顺序过一遍：

- [ ] 流式请求是否开启了用量回传（`include_usage` 或读取 `message_delta`）
- [ ] 输入 token 是否混淆了“含缓存”和“不含缓存”两种口径
- [ ] 推理 token 是否被算进了输出
- [ ] SDK 的自动重试次数是多少，日志里能否区分重试
- [ ] 差额是否集中在某几个小时，那段时间有无服务故障
- [ ] 平台记录的请求数是否多于自己的日志，密钥是否只在预期的地方使用
- [ ] 统计期间单价或换算系数是否调整过
- [ ] 中转站或网关是否提供逐条明细，失败请求是否计费

如果查下来发现主要成本来自上下文膨胀，而不是计费错误，那就该从用法上省了：把补全、格式化这类简单任务分到便宜的模型上，思路见[多模型路由怎么做](/blog/multi-model-routing-save-cost)。
