假设一次模型调用用了 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 + 输出200

800 个缓存命中 Token 已经包含在 1000 个输入里,不能再加一次。计算费用时,把输入拆成“命中”和“未命中”,再加上输出,刚好对应三个计价类别。

缓存复用的是符合条件的输入前缀。命中的内容仍属于本次上下文,也仍然收费,只是使用缓存命中单价。重复发送历史不保证一定命中,计算时应使用接口实际返回的数量。缓存机制说明介绍了哪些输入可以复用之前的计算。

total_tokens 只表示总量,没有区分不同单价,不能拿它直接计算整次费用。字段定义见 DeepSeek 接口文档。接入其他服务时,也需要先确认缓存字段是否包含在输入总量中,再整理成对应的计价分类。

DeepSeek Flash 价格与算例

分清数量之后,再看每一类按什么价格收费。以下使用 DeepSeek 在 2026 年 10 月 21 日的官方价目表,Flash 的价格单位为元/百万 Token:

类别高峰空闲
输入,缓存命中0.040.02
输入,缓存未命中21
输出84

高峰时段为北京时间周一至周五 9:00—12:00、14:00—18:00,不含中国法定节假日,其余时间使用空闲价格。

请求中的模型名使用 deepseek-flash。旧名称 deepseek-v4-flash 虽然仍可调用,但实际由 DeepSeek-V4.1-Flash-1017 提供服务,并按 Flash 价格计费,所以不能看到旧名称就继续套用历史价格。价格、模型映射和时段规则以官方页面为准。

表中的单价对应一百万个 Token。计算某一类的费用时,先用数量乘单价,再除以一百万,最后把三类费用加起来:

费用 =(命中量 × 命中单价
        + 未命中量 × 未命中单价
        + 输出量 × 输出单价)÷ 1,000,000

按高峰价格计算,明细如下:

类别数量单价费用/元
命中输入8000.040.000032
未命中输入20020.000400
输出20080.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.1000000000000000055511151231257827021181583404541015625

0.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。把输入、输出总量固定,只改变缓存命中量,高峰费用会得到下面的结果:

命中量未命中量输入费用/元输出费用/元总费用/元
010000.0020000.0016000.003600
8002000.0004320.0016000.002032
100000.0000400.0016000.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 费用。独立工具服务、套餐优惠等项目有各自的计费规则,需要另外核算。