费用计算方式
分别填写输入 token、输出 token,以及各自每百万 token 的美元单价。费用等于输入数量除以一百万后乘输入单价,加上输出数量除以一百万后乘输出单价。结果以美元显示,并保留四位小数。
计算示例
假设输入 100000 token、输出 20000 token,填写的输入单价为 1 美元/百万、输出单价为 2 美元/百万,估算结果为 0.14 美元。这是演示用单价;工具根据表单数值计算,不自动获取模型价格,也没有计算缓存、批处理或其他费用项。
微额请求累计:为什么每笔都显示 0.0000,整批却有费用
上一节解决“费用分成哪些类别”,这里处理另一个容易漏掉的问题:同一批请求,先把每笔金额显示成四位小数再求和,可能与保留原始精度后求和差很多。页面显示是阅读格式;把显示值复制进另一个表格,并不自动得到可用于汇总的原始金额。
设想要在离线验收里估算一批相同的短请求。本例固定 250 次,每次输入 13 tokens、输出 3 tokens,输入单价 1.25 USD/M、输出单价 8 USD/M,整批教学预算 0.0100 USD。这些都是虚构的练习参数,不对应任何模型或服务商报价。只研究两类 token 的乘加;不增加缓存、工具、税费等项目,不把结果解释为真实应付金额。
每次输入费用 = 13 × 1.25 / 1000000 = 0.00001625 USD 每次输出费用 = 3 × 8 / 1000000 = 0.00002400 USD 每次合计 = 0.00004025 USD 250 次原值合计 = 0.01006250 USD 合计后显示四位 = 0.0101 USD 每笔先显示四位再相加 = 250 × 0.0000 = 0.0000 USD
这里不是二进制浮点误差:即使用精确十进制计算,提前丢弃小数位仍会丢掉信息。原值合计已经大于 0.0100;错误的逐笔显示合计却会判定没有超出预算。两者相差 0.01006250 USD,原因是 250 次同方向的舍入损失累积。
手算还提供一项独立验收答案:单次金额等于 161/4,000,000 美元,250 次等于 161/16,000 美元。代码用 Fraction 的这个固定分数核对 Decimal 计算结果,不从被测函数生成“期望值”。在相同用量与价格假设下,248 次为 0.00998200,249 次为 0.01002225,因此最多 248 次满足这份教学预算。这个数量不是未来实际请求成本的保证。
可运行实验:保留原值,只在显示层舍入
把下面完整代码保存为 rounding_lab.py,用 Python 3.12 或更新版本运行。它只用标准库,不联网、不读取密钥或账单,也没有改写本页计算器。输入在函数参数中明确给出;需要换练习数据时可调用 batch_cost,并保留原来的固定答案测试。
- 输入、输出 tokens 只接受真正的非负 int,各自最多 10¹²;次数最多 10⁶。布尔值虽然与整数有关,仍被明确拒绝。
- 单价单位为美元/百万 tokens,只收普通十进制字符串,最多六位小数、最大 1,000,000。预算也收十进制字符串,最多十二位小数、最大 2×10¹⁸ 美元。两个限制都是实验合同,不是服务商限额。float、负数、科学计数、空白、NaN、Infinity 和超界值均拒绝,不做悄悄截断。
- 每个计算函数使用独立的 50 位 Context,隔离调用方的精度、指数范围及陷阱。两项 token×单价相加最多需要 25 位有效数字,乘最多 10⁶ 次后不超过 31 位;分母是 10⁶,仍为有限十进制。50 位覆盖这个合同,并用 Inexact 陷阱防止无声损失。扩大范围或增加除法时必须重新证明。
- 显示函数使用单独上下文,明确采用 ROUND_HALF_UP 保留四位。0.00005 在 HALF_UP 下成为 0.0001,在 HALF_EVEN 下成为 0.0000;这说明需要写明策略,不能默认所有系统一致。
- 预算通过条件为未舍入金额 ≤ 预算。这里没有真实请求拦截、并发预算锁或账单核验,也不提供财务建议。
"""Synthetic USD/million-token arithmetic. Python 3.12+, standard library only."""
import re
import sys
from decimal import (Context, Decimal, Inexact, ROUND_HALF_UP,
ROUND_HALF_EVEN, localcontext)
from fractions import Fraction
MILLION = Decimal(1_000_000)
DISPLAY = Decimal('0.0001')
# Never inherit the caller's precision, exponent bounds, rounding or traps.
EXACT = Context(prec=50, rounding=ROUND_HALF_UP, Emin=-999999, Emax=999999)
EXACT.traps[Inexact] = True
VIEW = Context(prec=50, rounding=ROUND_HALF_UP, Emin=-999999, Emax=999999)
def integer(value, name, maximum):
if type(value) is not int or not 0 <= value <= maximum:
raise ValueError(name + ': expected bounded nonnegative integer')
return value
def decimal_text(value, name, places, maximum):
if (type(value) is not str or len(value) > 32
or not re.fullmatch(r'(?:0|[1-9][0-9]*)(?:\.[0-9]{1,'
+ str(places) + r'})?', value)):
raise ValueError(name + ': invalid decimal text')
result = Decimal(value)
if not result.is_finite() or result > maximum:
raise ValueError(name + ': exceeds lab bound')
return result
def price(value):
return decimal_text(value, 'price', 6, Decimal('1000000'))
def budget_amount(value):
return decimal_text(value, 'budget', 12, Decimal('2000000000000000000'))
def unit_cost(input_tokens, output_tokens, input_price, output_price):
i = integer(input_tokens, 'input', 10**12)
o = integer(output_tokens, 'output', 10**12)
pi, po = price(input_price), price(output_price)
with localcontext(EXACT):
return (Decimal(i) * pi + Decimal(o) * po) / MILLION
def batch_cost(input_tokens, output_tokens, input_price, output_price, requests):
n = integer(requests, 'requests', 10**6)
with localcontext(EXACT):
return unit_cost(input_tokens, output_tokens, input_price, output_price) * n
def display(value):
if (type(value) is not Decimal or not value.is_finite()
or value < 0 or value > Decimal('2000000000000000000')):
raise ValueError('display: expected bounded finite Decimal')
with localcontext(VIEW):
return value.quantize(DISPLAY, rounding=ROUND_HALF_UP)
def equal(actual, expected, name):
# Explicit checks remain active under python -O.
if actual != expected:
raise AssertionError(f'{name}: {actual!r} != {expected!r}')
def rejects(fn):
try:
fn()
except ValueError:
return
raise AssertionError('accepted invalid input')
def main():
modes = ('--mutate-early-round', '--mutate-unit', '--mutate-tie')
if len(sys.argv) > 2 or (len(sys.argv) == 2 and sys.argv[1] not in modes):
raise SystemExit('usage: python rounding_lab.py [' + '|'.join(modes) + ']')
mode = sys.argv[1] if len(sys.argv) == 2 else ''
with localcontext(EXACT):
unit = unit_cost(13, 3, '1.25', '8')
total = batch_cost(13, 3, '1.25', '8', 250)
if mode == '--mutate-early-round':
total = display(unit) * 250
if mode == '--mutate-unit':
total *= 1000 # Wrong price unit: thousands instead of millions.
equal(Fraction(total), Fraction(161, 16000), 'exact batch oracle')
equal(unit, Decimal('0.00004025'), 'unit oracle')
equal(display(total), Decimal('0.0101'), 'display oracle')
early = display(unit) * 250
equal(early, Decimal('0.0000'), 'early rounding counterexample')
equal(unit_cost(3250, 750, '1.25', '8'), total, 'same-price regrouping')
budget = budget_amount('0.0100')
equal(total > budget, True, 'exact budget decision')
equal(early > budget, False, 'rounded budget counterexample')
c248 = batch_cost(13, 3, '1.25', '8', 248)
c249 = batch_cost(13, 3, '1.25', '8', 249)
equal(c248, Decimal('0.00998200'), '248 oracle')
equal(c249, Decimal('0.01002225'), '249 oracle')
equal(c248 <= budget < c249, True, 'adjacent budget boundary')
equal(c248 <= budget_amount('0.00998200'), True, 'inclusive equality')
equal(batch_cost(0, 0, '0', '0', 0), Decimal(0), 'zero')
tie = Decimal('0.00005')
with localcontext(VIEW):
up = display(tie)
even = tie.quantize(DISPLAY, rounding=ROUND_HALF_EVEN)
if mode == '--mutate-tie':
up = even
equal(up, Decimal('0.0001'), 'half-up tie oracle')
equal(even, Decimal('0.0000'), 'half-even tie oracle')
# Independent integer-ratio oracle at the input limits, under hostile caller context.
with localcontext() as hostile:
hostile.prec = 3
hostile.Emax = 9
hostile.Emin = -9
high = batch_cost(10**12 - 1, 10**12 - 1,
'999999.999999', '999999.999999', 10**6)
fixed_high = Fraction(999999999998000000000001, 500000)
equal(Fraction(high), fixed_high, 'high-digit oracle')
equal(display(high), Decimal('1999999999996000000.0000'), 'high display')
equal(batch_cost(10**12, 10**12, '1000000', '1000000', 10**6),
Decimal('2000000000000000000'), 'maximum batch')
checks = 0
for position, bound in [('input', 10**12), ('output', 10**12), ('requests', 10**6)]:
for bad in [-1, True, 1.5, '13', bound + 1, None]:
args = dict(input_tokens=13, output_tokens=3, input_price='1.25',
output_price='8', requests=250)
args[{'input': 'input_tokens', 'output': 'output_tokens',
'requests': 'requests'}[position]] = bad
rejects(lambda args=args: batch_cost(**args))
checks += 1
for parser in [price, budget_amount]:
for bad in ['NaN', 'Infinity', '-1', '1e3', '', ' 1', '01', 1.25, True, None,
'1.0000000000001', '999999999999999999999999999999999']:
rejects(lambda bad=bad, parser=parser: parser(bad))
checks += 1
for bad in ['1.0000001', '1000001']:
rejects(lambda bad=bad: price(bad))
checks += 1
for bad in [Decimal('NaN'), Decimal('Infinity'), Decimal('-1'), 1.25]:
rejects(lambda bad=bad: display(bad))
checks += 1
equal(checks, 48, 'rejection count')
equal(batch_cost(13, 3, '1.25', '8', 250), Decimal('0.01006250'), 'recovery')
print(f'unit_exact_usd={unit:f}')
print(f'batch_exact_usd={total:f}')
print(f'batch_display_usd={display(total):f}')
print(f'sum_of_row_displays_usd={early:f}')
print(f'budget_usd={budget:f}; over_budget={total > budget}')
print(f'requests_248_usd={c248:f}; requests_249_usd={c249:f}')
print('max_identical_requests_within_budget=248')
print('PASS: fixed oracles, regrouping, budget, ties, high digits, 48 rejections, recovery')
if __name__ == '__main__':
main()
固定答案、负对照与恢复:让提前舍入真的失败
以下输出来自 Python 3.12.14 的本地离线运行。正常模式与 python -O 优化模式均退出 0;所有检查使用显式异常,不会因为 -O 移除 assert 而失效。
unit_exact_usd=0.00004025 batch_exact_usd=0.01006250 batch_display_usd=0.0101 sum_of_row_displays_usd=0.0000 budget_usd=0.0100; over_budget=True requests_248_usd=0.00998200; requests_249_usd=0.01002225 max_identical_requests_within_budget=248 PASS: fixed oracles, regrouping, budget, ties, high digits, 48 rejections, recovery
除了手算的 161/16,000 美元总额,脚本分别核对 248 次和 249 次的固定小数答案,并测试刚好等于预算时允许通过。上界案例在调用方精度仅 3 位、指数范围被缩小的条件下仍得到固定分数答案;另测所有参数取上限时的总额 2×10¹⁸。这些检查覆盖的是受限输入合同,并不证明任意大的 Decimal 运算都精确。
48 次输入拒绝覆盖两类 tokens、次数、价格、预算和显示输入;拒绝后重新传入有效参数,必须恢复到 0.01006250。测试结果不是对网页输入控件的测试。
用三个负对照主动破坏实际计算路径。下面后三条各应退出 1;第一、二个被总额固定分数抓住,第三个被 HALF_UP 的固定边界答案抓住。它们分别代表提前舍入、把百万误作千的单位错误,以及偷换舍入策略。
python rounding_lab.py python rounding_lab.py --mutate-early-round python rounding_lab.py --mutate-unit python rounding_lab.py --mutate-tie python -O rounding_lab.py python -O rounding_lab.py --mutate-early-round python -O rounding_lab.py --mutate-unit python -O rounding_lab.py --mutate-tie python rounding_lab.py
提前舍入变体的关键失败行如下。去掉参数后重新运行正常版本,应再次退出 0;无需修改源文件。
AssertionError: exact batch oracle: Fraction(0, 1) != Fraction(161, 16000)
这里是主动注入的离线错误,不是线上工具故障。如果自己的汇总管道有类似差异,应回到 token 原量、适用单价和原值金额重新计算,不要给已经舍入的显示列乘“修正系数”。同价条件下,3,250 个输入 tokens 与 750 个输出 tokens 一次计算,应等于 250 次原值求和;如果模型、单价、生效期或费用类别发生变化,应分别计算再汇总。
把离线结果用于自己的估算前,检查这五件事
- 记录用途。估算教学预算、展示费用、核对服务商账单是不同任务;先说清哪一种结果需要保留哪些精度。
- 检查原数据。只有“0.0000”这样的展示文本时,无法反推出原始 token 数和未舍入金额;应回到用量来源,而不是把零当成免费请求。
- 把舍入规则写成显式配置。四位显示、分币显示、逐行舍入、分组舍入、发票舍入未必相同;本例不推测真实服务商的结算规则。
- 比较预算时注明边界。本例以原值 ≤ 预算为通过,并明确验证了 248/249 的相邻边界。真实调用的输出长度、重试和其他费用不确定时,还需要自己的预留策略,不能照搬 248 次。
- 给复现材料留证据。记录输入、币种、价格单位、价格来源与适用时间、计算方法和输出;本例仅验证虚构固定单价,未测试页面新输入、浏览器舍入、实际 API 用量或账单。
资料核对日期:2026-10-14。Python Decimal 官方文档解释 Context、有限十进制、Inexact 与 quantize;Python Fraction 官方文档说明有理数及 Decimal 转换。这两个当前文档入口已解析为 Python 3.15 文档,而实验解释器为 3.12.14;本文使用的接口在实测版本可用。本文只使用技术文档核对数值接口,没有引用任何模型报价,也没有推断任何服务商实际结算规则。
常见问题
这个工具会测量文本的 token 数吗?
不会。你需要填写已有的 token 数量;工具负责按数量和单价计算费用。