OpenAI API token 怎么计算?第三方指南与实操整理
简答:OpenAI API token 是模型分词后统计的输入和输出单位,不能用字符数固定换算。请求完成后读取 usage;费用计算时,缓存 token 是输入 token 的子集,推理 token 是输出 token 的子集,不能重复相加。
定义:Token 是模型处理输入、生成输出以及部分消息结构时使用的计量单位,API 通常在 usage 中分别返回输入、输出、缓存和推理等统计字段。
OpenAI API token 到底按什么计算
OpenAI API token 不是“一个汉字等于一个 token”,也不是字符数除以固定数字。模型会对文字、空格、标点、消息角色、工具定义、图片和文件等内容进行分词或结构化处理,最终数量取决于具体模型和请求格式。
实际使用时可以分成两个问题:第一,请求已经完成后有多少 token,应读取响应里的 usage;第二,请求发送前预计有多少输入 token,可以使用官方 input token counting 接口。OpenAI 的 Token Counting 指南说明,该接口会按照模型实际接收的请求结构统计输入,并覆盖消息结构、工具、图片和文件等本地 tokenizer 不容易准确处理的内容。OpenAI Token Counting 官方指南
使用 Chat Completions 时,常见顶层字段是 prompt_tokens、completion_tokens 和 total_tokens;使用 Responses API 时,常见字段是 input_tokens、output_tokens 和 total_tokens。不同接口和模型的详细字段可能不同,应以实际响应及对应 API Reference 为准。OpenAI Chat Completions 创建接口OpenAI Responses API Reference
输入、输出、缓存和推理 token 的关系
| 字段或概念 | 含义 | 计算关系 |
|---|---|---|
prompt_tokens 或 input_tokens | 请求发送给模型的输入 token,可能包含消息、历史记录、工具定义、图片或文件等 | 缓存 token 和缓存写入 token属于输入统计的细分部分,不应在此基础上重复相加 |
cached_tokens | 输入中从提示缓存复用的 token 数 | 是输入 token 的子集,不是额外增加的一组 token |
cache_write_tokens | 本次请求写入提示缓存的输入 token 数,是否返回取决于接口和模型 | 如果价格页单独列出缓存写入价格,应从普通输入部分中分离计算 |
completion_tokens 或 output_tokens | 模型生成的输出 token 总数 | 推理 token、工具调用参数及部分不可见结构可能包含在其中 |
reasoning_tokens | 推理模型用于内部推理的输出 token 数 | 是输出 token 的子集,不能在输出 token之外再次加一遍 |
total_tokens | 请求的总 token 统计 | 通常是输入 token 加输出 token,而不是再把缓存或推理字段另算一次 |
Chat Completions 的详细统计路径通常是 usage.prompt_tokens_details.cached_tokens、usage.prompt_tokens_details.cache_write_tokens 和 usage.completion_tokens_details.reasoning_tokens。官方接口说明将 cached_tokens 定义为提示中的缓存 token,将 reasoning_tokens 定义为模型生成的推理 token。OpenAI Chat Completion usage 字段说明
因此,如果用户看到的回答很短,但 completion_tokens 或 output_tokens 较高,可能是输出统计包含了不可见的格式或结构 token,也可能包含推理模型使用的 reasoning_tokens。推理 token 仍然属于输出统计,不能因为用户看不到就从实际用量中删除。OpenAI Token Counting 官方指南
缓存 token 和推理 token 为什么不能重复计算
这是计算费用时最容易出错的地方。假设一次 Chat Completions 响应返回:
prompt_tokens = 1200prompt_tokens_details.cached_tokens = 800completion_tokens = 300completion_tokens_details.reasoning_tokens = 100
这里不是把 1200、800、300、100 全部相加。正确理解是:1200 是全部输入 token,其中 800 个属于缓存输入;300 是全部输出 token,其中 100 个属于推理 token。缓存 token和推理 token分别是输入、输出的子集。
如果没有缓存写入费用,通用费用公式可以写成:
总费用 =(输入 token − 缓存 token)÷ 1,000,000 × 普通输入单价 + 缓存 token ÷ 1,000,000 × 缓存输入单价 + 输出 token ÷ 1,000,000 × 输出单价
如果接口返回了 cache_write_tokens,并且当前模型价格页单独列出缓存写入价格,则应进一步拆分:
普通输入 token = 输入 token − 缓存 token − 缓存写入 token
总费用 = 普通输入 token × 普通输入单价 + 缓存 token × 缓存输入单价 + 缓存写入 token × 缓存写入单价 + 输出 token × 输出单价
上述各项在公式中都要再除以 1,000,000。OpenAI 的提示缓存文档明确说明,缓存写入不是在普通输入费用上额外叠加,而是根据输入 token 属于普通输入、缓存输入或缓存写入类别分别计价;具体单价应以当前模型价格页为准。OpenAI Prompt Caching 官方指南OpenAI API 官方价格页
最小可运行示例:读取输入、缓存、输出和推理 token
下面示例使用 Python 3 标准库调用 Chat Completions,并打印常见 usage 字段。它不写死接口域名或模型名:BASE_URL 应设置为已经包含版本路径的基础地址,代码不会再次拼接 /v1;KEY 只从环境变量读取;MODEL_ID 应填写所用平台公开模型列表中的实际 ID。
依赖工具:Python 3,无需安装第三方 SDK。
export BASE_URL="https://api.openai.com/v1"
export KEY="your-api-key"
export MODEL_ID="以所用平台官方模型列表为准"如果项目使用 .env 文件,可以写成:
BASE_URL=https://api.openai.com/v1
KEY=your-api-key
MODEL_ID=以所用平台官方模型列表为准不要把密钥提交到代码仓库,在 .gitignore 中加入:
.env保存为 read_usage.py 后运行:
import json
import os
import urllib.request
base_url = os.environ["BASE_URL"].rstrip("/")
api_key = os.environ["KEY"]
model_id = os.environ["MODEL_ID"]
payload = {
"model": model_id,
"messages": [
{"role": "user", "content": "用一句话解释 token。"}
],
"max_completion_tokens": 80,
}
request = urllib.request.Request(
f"{base_url}/chat/completions",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request, timeout=60) as response:
result = json.load(response)
usage = result.get("usage", {})
prompt_details = usage.get("prompt_tokens_details") or {}
completion_details = usage.get("completion_tokens_details") or {}
print("prompt_tokens:", usage.get("prompt_tokens"))
print("cached_tokens:", prompt_details.get("cached_tokens"))
print("cache_write_tokens:", prompt_details.get("cache_write_tokens"))
print("completion_tokens:", usage.get("completion_tokens"))
print("reasoning_tokens:", completion_details.get("reasoning_tokens"))
print("total_tokens:", usage.get("total_tokens"))
print("answer:", result["choices"][0]["message"]["content"])成功响应通常应包含 choices 和 usage。某些兼容接口不返回缓存或推理细分字段,程序会打印 None,这不代表一定没有对应统计,只表示当前响应没有提供该字段。若接口或旧模型不接受 max_completion_tokens,可能返回参数错误;这时应查看该接口文档,确认是否需要改用 max_tokens,不要同时发送两个参数。OpenAI 官方 Chat Completions 示例展示了 prompt_tokens_details 和 completion_tokens_details 的返回结构。OpenAI Chat Completions 创建接口
发送前怎样预估输入 token
如果只是比较几段纯文本的长度,可以使用本地 tokenizer,例如 tiktoken;但它不一定能完整模拟消息包装、工具 schema、图片、文件和模型特定行为。OpenAI 官方建议在需要准确预估时,使用与正式请求相同的输入调用 input token counting 接口。OpenAI Token Counting 官方指南
官方接口是 POST /v1/responses/input_tokens,返回对象中的 input_tokens 是模型将接收的输入 token 数。OpenAI Get input token counts API Reference
下面是一个不依赖第三方 SDK 的最小 Python 示例:
import json
import os
import urllib.request
base_url = os.environ["BASE_URL"].rstrip("/")
api_key = os.environ["KEY"]
model_id = os.environ["MODEL_ID"]
payload = {
"model": model_id,
"input": [
{
"role": "user",
"content": [
{"type": "input_text", "text": "用一句话解释 token。"}
],
}
],
}
request = urllib.request.Request(
f"{base_url}/responses/input_tokens",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request, timeout=60) as response:
result = json.load(response)
print("object:", result.get("object"))
print("input_tokens:", result["input_tokens"])预估流程可以按四步执行:
- 确定正式请求使用的模型 ID。
- 把正式请求中的系统指令、历史消息、工具定义、图片或文件输入完整准备好。
- 调用
/responses/input_tokens,记录返回的input_tokens。 - 正式请求完成后,再把响应里的实际 usage 写入日志,用实际结果校正预算。
如果使用 OpenAI 兼容接口,应先确认该接口是否实现了 Responses API 的 input token counting 路径。没有实现时,以该接口公开文档和实际返回字段为准,不要默认所有兼容接口都支持同一组端点。
Token、上下文长度和限速有什么区别
Token 用量描述一次请求实际处理了多少输入和输出;上下文长度描述一次请求能够容纳的输入、输出及模型相关内容范围;限速描述一段时间内允许发送多少请求或 token。三者相关,但不是同一个指标。
OpenAI 的限速文档列出了 RPM、RPD、TPM、TPD 等指标。一个请求可能因为请求数达到 RPM 而触发限制,也可能因为 token 数达到 TPM 而触发限制。因此,不能把一次请求的 token 数直接当成每分钟额度,也不能把价格和限速混为一谈。OpenAI Rate Limits 官方指南
在工程日志中,建议至少记录模型 ID、输入 token、输出 token、缓存 token、推理 token、请求 ID、响应时间和错误状态。这样可以区分“单次请求变长”“缓存命中变化”“推理消耗增加”和“每分钟额度不足”等不同问题。
对于多轮对话,历史消息通常会继续成为后续请求的输入;对于工具调用,工具名称、描述和参数 schema 也可能占用输入上下文。发送前若要判断是否接近上下文限制,应使用最终请求结构进行计数,而不是只统计用户可见文字。
降低 token 消耗的实用方法
- 减少重复历史:只保留当前任务需要的对话内容,过长历史可以先摘要。
- 缩短系统指令:合并重复规则,删除不会影响结果的背景说明。
- 控制输出上限:根据任务设置合理的
max_output_tokens或max_completion_tokens,但要确认所用接口支持哪个参数。 - 按需发送工具:只提供当前任务需要的工具和 schema,避免把无关函数定义全部放进请求。
- 保持可缓存前缀稳定:将不会频繁变化的指令、工具定义和公共上下文放在前面,再放用户动态内容。
- 观察真实字段:同时记录普通输入、缓存输入、缓存写入、输出和推理字段,不要只保存
total_tokens。
提示缓存可以降低重复前缀的输入成本,但缓存 token 仍然会计入相关 token 限速;是否命中缓存也不能只凭请求文本相似来判断,应以响应中的 usage 统计为准。OpenAI Prompt Caching 官方指南
常见问题
中文一个字等于一个 token 吗?
不一定。Token 是模型分词或结构化处理后的片段,中文、英文、数字、空格和标点可能采用不同切分方式。准确数量应读取响应中的 usage,或使用对应模型的 input token counting 接口。
cached_tokens 是不是要在输入 token之外再加一次?
不是。cached_tokens 是输入 token 的子集。计算缓存输入费用时,应把输入 token拆成普通输入、缓存输入以及必要时的缓存写入部分,不能把全部输入和缓存部分重复相加。
reasoning_tokens 是不是额外的输出费用?
通常不是单独加在 output_tokens 或 completion_tokens之外的另一组输出。reasoning_tokens 是输出统计中的细分字段,具体计费类别应以当前模型价格页为准。
为什么可见回答很短,但 completion_tokens 很多?
输出统计可能包含用户不可见的结构或格式 token;对于推理模型,还可能包含 reasoning_tokens。最终应以响应中的 completion_tokens_details 或 output_tokens_details 以及顶层输出字段为准。
max_completion_tokens 是实际消耗的 token 数吗?
不是。它是生成上限,不是实际用量。实际消耗应读取响应中的 completion_tokens 或 output_tokens;同时要确认所用模型和兼容接口是否支持该参数。
OpenAI 兼容接口的 token 规则完全一样吗?
不能默认完全一样。应分别核对 Base URL、模型 ID、usage 字段、Responses 或 Chat Completions 支持情况,以及公开计费说明。没有明确说明的字段和价格,以对应接口官方文档为准。