假设一次模型调用用了 1000 个输入 Token,其中 800 个命中缓存,又生成了 200 个输出 Token。总共用了 1200 个 Token,但计算费用时,不能把这个总数直接乘一个单价。
输入、输出和缓存命中部分可能按不同价格收费。我们需要先从用量中分清这几部分,再找到这次调用适用的价格,分别计算后相加。金额很小时,还要注意计算和保存过程中有没有丢掉小数。
下面用这组用量计算 DeepSeek Flash 的 Token 费用,再用 Python 的 Decimal 实现相同计算。用量是算例,不涉及真实付费调用。
输入、输出与缓存用量
DeepSeek Chat Completions 返回的 usage 中,有输入、输出和缓存用量。上面的例子可以写成:
{
"prompt_tokens": 1000,
"prompt_cache_hit_tokens": 800,
"prompt_cache_miss_tokens": 200,
"completion_tokens": 200,
"total_tokens": 1200
}prompt_tokens 是输入总量,其中一部分命中缓存,另一部分没有命中。completion_tokens 是生成的输出,两者相加得到 total_tokens:
输入:1000 = 命中800 + 未命中200
总量:1200 = 输入1000 + 输出200800 个缓存命中 Token 已经包含在 1000 个输入里,不能再加一次。计算费用时,把输入拆成“命中”和“未命中”,再加上输出,刚好对应三个计价类别。
缓存复用的是符合条件的输入前缀。命中的内容仍属于本次上下文,也仍然收费,只是使用缓存命中单价。重复发送历史不保证一定命中,计算时应使用接口实际返回的数量。缓存机制说明介绍了哪些输入可以复用之前的计算。
total_tokens 只表示总量,没有区分不同单价,不能拿它直接计算整次费用。字段定义见 DeepSeek 接口文档。接入其他服务时,也需要先确认缓存字段是否包含在输入总量中,再整理成对应的计价分类。
DeepSeek Flash 价格与算例
分清数量之后,再看每一类按什么价格收费。以下使用 DeepSeek 在 2026 年 10 月 21 日的官方价目表,Flash 的价格单位为元/百万 Token:
| 类别 | 高峰 | 空闲 |
|---|---|---|
| 输入,缓存命中 | 0.04 | 0.02 |
| 输入,缓存未命中 | 2 | 1 |
| 输出 | 8 | 4 |
高峰时段为北京时间周一至周五 9:00—12:00、14:00—18:00,不含中国法定节假日,其余时间使用空闲价格。
请求中的模型名使用 deepseek-flash。旧名称 deepseek-v4-flash 虽然仍可调用,但实际由 DeepSeek-V4.1-Flash-1017 提供服务,并按 Flash 价格计费,所以不能看到旧名称就继续套用历史价格。价格、模型映射和时段规则以官方页面为准。
表中的单价对应一百万个 Token。计算某一类的费用时,先用数量乘单价,再除以一百万,最后把三类费用加起来:
费用 =(命中量 × 命中单价
+ 未命中量 × 未命中单价
+ 输出量 × 输出单价)÷ 1,000,000按高峰价格计算,明细如下:
| 类别 | 数量 | 单价 | 费用/元 |
|---|---|---|---|
| 命中输入 | 800 | 0.04 | 0.000032 |
| 未命中输入 | 200 | 2 | 0.000400 |
| 输出 | 200 | 8 | 0.001600 |
| 合计 | 0.002032 |
总费用是 0.002032 元。其中,输入费用由 800 个命中 Token 和 200 个未命中 Token 分别计算。若先把全部 1000 个输入按未命中价格收费,再加上 800 个命中的费用,就会重复计算命中部分,得到错误结果 0.003632 元。
空闲单价是高峰的一半,同一组用量算出来就是 0.001016 元。这里固定数量,只比较价格变化;真实调用还需要使用各自返回的用量。
Decimal 与金额精度
上面的费用有六位小数,后面还可能继续汇总。用 Python 计算这类金额时,可以通过 Decimal 按十进制进行运算。
构造 Decimal 时,价格最好写成字符串。下面三行看起来相近,结果却不同:
from decimal import Decimal
print(0.1 + 0.2)
print(Decimal("0.1") + Decimal("0.2"))
print(Decimal(0.1))输出:
0.30000000000000004
0.3
0.10000000000000000555111512312578270211815834045410156250.1 先成为二进制浮点数时,保存的值就已经与十进制的 0.1 有偏差。再调用 Decimal(0.1),转换的是这个浮点数的精确值,偏差也会一起保留。
Decimal("0.1") 则直接从十进制字符串构造数值。因此,价格配置可以保存成 "0.04" 这样的字符串,取出来后再转成 Decimal,避免中间先经过 float。
Decimal 的运算仍然受上下文精度和舍入方式限制,并不是无限精度。本文的数量、价格和百万单位可以在默认精度下计算;如果业务中的数量范围更大,就需要相应设置精度。计算过程中也不应每一步都截成两位小数,小额费用可能就此丢失。Python Decimal 文档介绍了构造方式、运算精度和舍入规则。
费用计算实现
把前面的计算写成函数,需要用到两份数据:接口返回的用量,以及这次调用适用的价格。
示例把高峰和空闲价格放在 PRICES 中,由调用方选择档位。这样,补算一笔历史请求时,也可以明确传入当时的价格,而不会因为现在是空闲时段,就把过去的高峰请求按空闲价格计算。
代码只使用标准库,Python 3.10 及以上可以运行。保存为 token_cost.py,在文件所在目录执行 python3 token_cost.py:
from decimal import Decimal
# CNY / 百万 Token;实际调用应保存当时适用的价格快照。
PRICES = {
"peak": {"cache_hit": "0.04", "cache_miss": "2", "output": "8"},
"off_peak": {"cache_hit": "0.02", "cache_miss": "1", "output": "4"},
}
def calculate_cost(usage: dict, period: str) -> dict[str, Decimal]:
if period not in PRICES:
raise ValueError("period 必须是 peak 或 off_peak")
fields = (
"prompt_tokens",
"prompt_cache_hit_tokens",
"prompt_cache_miss_tokens",
"completion_tokens",
)
for field in fields:
value = usage[field]
if type(value) is not int or value < 0:
raise ValueError(f"{field} 必须是非负整数")
hit = usage["prompt_cache_hit_tokens"]
miss = usage["prompt_cache_miss_tokens"]
if hit + miss != usage["prompt_tokens"]:
raise ValueError("命中量与未命中量之和必须等于输入总量")
rates = {key: Decimal(value) for key, value in PRICES[period].items()}
unit = Decimal("1000000")
costs = {
"cache_hit": hit * rates["cache_hit"] / unit,
"cache_miss": miss * rates["cache_miss"] / unit,
"output": usage["completion_tokens"] * rates["output"] / unit,
}
costs["total"] = sum(costs.values(), Decimal("0"))
return costs
if __name__ == "__main__":
usage = {
"prompt_tokens": 1000,
"prompt_cache_hit_tokens": 800,
"prompt_cache_miss_tokens": 200,
"completion_tokens": 200,
"total_tokens": 1200,
}
for period in ("peak", "off_peak"):
print(period)
for name, amount in calculate_cost(usage, period).items():
print(f" {name}: {amount:.8f} CNY")输出:
peak
cache_hit: 0.00003200 CNY
cache_miss: 0.00040000 CNY
output: 0.00160000 CNY
total: 0.00203200 CNY
off_peak
cache_hit: 0.00001600 CNY
cache_miss: 0.00020000 CNY
output: 0.00080000 CNY
total: 0.00101600 CNY函数先检查用量,再把字符串价格转成 Decimal,分别计算命中输入、未命中输入和输出费用,最后求和。返回的三项明细与前面的手算结果对应。
数量需要是非负整数,命中量与未命中量之和需要等于输入总量;缺少字段会抛出 KeyError,不会补成零。这里的价格由代码内的常量提供,如果改成从外部配置读取,还需要检查币种、计价单位,以及价格是否为有限非负数。
打印时的 .8f 只决定显示八位小数,函数返回的金额仍然是 Decimal。把输入、输出总量固定,只改变缓存命中量,高峰费用会得到下面的结果:
| 命中量 | 未命中量 | 输入费用/元 | 输出费用/元 | 总费用/元 |
|---|---|---|---|---|
| 0 | 1000 | 0.002000 | 0.001600 | 0.003600 |
| 800 | 200 | 0.000432 | 0.001600 | 0.002032 |
| 1000 | 0 | 0.000040 | 0.001600 | 0.001640 |
明细保存与展示舍入
一次调用只花了 0.002032 元,如果按两位小数保存,就变成了 0.00。每笔都这样处理,100 次调用加起来仍然是零,而原始金额相加应为 0.2032 元。
问题出在舍入发生得太早。下面比较“逐笔舍入后相加”和“先相加再舍入”,代码可单独保存为 rounding_demo.py,运行 python3 rounding_demo.py:
from decimal import Decimal, ROUND_HALF_UP
cost = Decimal("0.002032")
cent = Decimal("0.01")
print(cost.quantize(cent, rounding=ROUND_HALF_UP) * 100)
print((cost * 100).quantize(cent, rounding=ROUND_HALF_UP))输出:
0.00
0.20第一行先把单笔费用舍入到分,再乘 100,结果是零;第二行保留单笔金额,先算总额,再舍入到分,得到 0.20。
因此,保存明细和汇总时要保留所需的小数精度,展示时再按规则处理。quantize(Decimal("0.01")) 用来把金额量化到分,这里对非负金额采用 ROUND_HALF_UP。实际与供应商对账时,仍要使用供应商约定的舍入方式,不能把示例规则直接当成结算规则。quantize() 文档介绍了这个操作。
精度也需要在传输和存储时保留下来。JSON 可以把金额写成十进制字符串;数据库则使用能容纳相应金额范围和小数位的精确数值类型。否则,前面算得再准确,保存时仍可能丢掉小额费用。相关问题见 JSON数字精度与小数格式丢失问题。
用量完整性与价格快照
前面的函数要求用量字段完整,是因为“缓存命中为零”和“没有收到缓存命中量”会带来不同结果。收到零,可以继续按未命中价格计算;字段缺失,则还不知道应该把多少输入分给哪一档价格。
如果用 usage.get(field, 0) 自动补零,程序虽然能算出一个金额,但使用的数量可能就错了。缺失信息需要保留下来,而不是用默认值掩盖。
流式调用也需要等整次用量返回后再结算。DeepSeek Chat Completions 在 [DONE] 之前的最后一个内容块返回 usage,其中记录的是整次请求的用量。它不是文本片段的增量,不能把各块出现的累计用量反复相加。流式返回规则说明了 usage 怎样随响应返回。
如果中途断流,没有取得最终用量,就还缺少完整计量依据。界面已经显示了多少字,不能用来反推这次调用完整的输入、输出和缓存用量。
保存计算结果时,把原始 usage、模型身份和调用时间一起记录,同时保留币种、计价单位与适用费率快照。以后即使当前价目表改了,也能知道这笔费用当时是怎样算出来的。历史记录不应随着当前价格变化被重新计算;需要补算或对账时,应保留相应依据。
用量或适用价格缺失时,可以把费用记为 null,同时记录原因。只有数量和价格都已确认、计算结果为零,才是零费用。
这里计算的是模型 Token 费用。独立工具服务、套餐优惠等项目有各自的计费规则,需要另外核算。