input_tokens * price算出一个数,不代表费用就算对了:单价可能按百万Token;输入总量可能已经包含缓存;长上下文档位可能对整次请求生效;打印成两位小数还会掩盖很小的非零金额。
本篇交付一个标准库离线估算器,输入明确的价格卡和已知用量,输出分类明细、未舍入金额与单独的显示金额。不连接任何模型,不读取用户账单;所有示例价格均为虚构。
1. 范围先固定:这不是通用账单引擎
环境:Python 3.11及以上,仅标准库,无需pip安装。
本例只支持CNY或USD标签、每千或每百万Token、普通输入/缓存读取/输出三个互斥类别,以及“按输入总量选择整次请求档位”的虚构规则。输入总量包含缓存读取部分,这是本例显式前提。缓存写入、图片、音频、视频、工具费用、税费、最低计费、账户折扣、历史价查询和币种换算均不实现。
2026年10月27日Anthropic的GSA公告区分了政府适用对象和采购渠道的生效时间。它说明价格条件要有来源;本文没有把政府折扣当作一般API零售价,而是采用独立的假设价格卡。官方公告
2. 定义输入契约
| 字段 | 本程序的要求 |
|---|---|
| model_id | 与价格卡精确一致,不猜别名 |
| currency | 明确标注CNY或USD,不自动转换 |
| unit_tokens | 整数1000或1000000,拒绝字符串与bool |
| version/source_ref/effective_date | 保留版本、来源说明和有效日期;有字段不等于已验证来源真实性 |
| 三类Token计数 | 已知的非负整数,上限10的12次方;None、bool、小数和字符串都拒绝 |
| 价格 | 非负普通十进制字符串,最多9位整数和8位小数 |
| 档位 | 从0开始、下界严格递增,达到下界即选择该档,作用于整次请求 |
限制数字长度是为了给本地精度和资源消耗划定范围,不是厂商限制。使用其他币种或规则,应新增有依据的显式实现,不能悄悄回退。
3. 完整文件offline_cost.py
"""Teaching estimator only. All demo prices and usage are fictional."""
from dataclasses import dataclass
from datetime import date
from decimal import Decimal, localcontext, ROUND_HALF_UP
import json
import re
def amount(value: str) -> Decimal:
# Bound digits/scale so precision=50 is sufficient for this demo's counts.
if type(value) is not str or re.fullmatch(r"(?:0|[1-9][0-9]{0,8})(?:\.[0-9]{1,8})?", value) is None:
raise ValueError("rate must be a non-negative decimal string, at most 9+8 digits")
return Decimal(value)
def tokens(value: int, name: str) -> int:
if type(value) is not int or not 0 <= value <= 10**12:
raise ValueError(f"{name}: known integer count in 0..10**12 required")
return value
@dataclass(frozen=True)
class Tier:
min_input_tokens: int
input_rate: str
cache_read_rate: str
output_rate: str
@dataclass(frozen=True)
class PriceCard:
model_id: str
currency: str
unit_tokens: int
version: str
effective_date: str
source_ref: str
tiers: tuple[Tier, ...]
def estimate(card: PriceCard, *, model_id: str, input_tokens: int,
output_tokens: int, cached_input_tokens: int) -> dict:
if model_id != card.model_id or not model_id:
raise ValueError("exact model_id mismatch")
if card.currency not in ("CNY", "USD"):
raise ValueError("only explicitly labelled CNY or USD supported")
if type(card.unit_tokens) is not int or card.unit_tokens not in (1000, 1_000_000):
raise ValueError("unit_tokens must explicitly be 1000 or 1000000")
if not card.version or not card.source_ref:
raise ValueError("price version and source_ref required")
try:
date.fromisoformat(card.effective_date)
except (ValueError, TypeError):
raise ValueError("valid price effective_date required") from None
n_in = tokens(input_tokens, "input_tokens")
n_out = tokens(output_tokens, "output_tokens")
cached = tokens(cached_input_tokens, "cached_input_tokens")
if cached > n_in:
raise ValueError("cached input must be a subset of total input")
if type(card.tiers) is not tuple or not card.tiers:
raise ValueError("nonempty immutable tier tuple required")
previous = -1
for tier in card.tiers:
lower = tokens(tier.min_input_tokens, "tier minimum")
if lower <= previous or (previous == -1 and lower != 0):
raise ValueError("tier minima must start at zero and strictly increase")
for rate in (tier.input_rate, tier.cache_read_rate, tier.output_rate):
amount(rate)
previous = lower
# Entire-request tier selection, never progressive/marginal pricing.
chosen = max((t for t in card.tiers if n_in >= t.min_input_tokens),
key=lambda t: t.min_input_tokens)
categories = (("uncached_input", n_in - cached, chosen.input_rate),
("cached_input", cached, chosen.cache_read_rate),
("output", n_out, chosen.output_rate))
with localcontext() as context:
context.prec = 50
context.rounding = ROUND_HALF_UP
lines = []
total = Decimal("0")
for label, count, rate in categories:
cost = Decimal(count) * amount(rate) / Decimal(card.unit_tokens)
total += cost
lines.append({"category": label, "tokens": count, "rate": rate,
"unrounded_cost": format(cost, "f")})
display = total.quantize(Decimal("0.000001"), rounding=ROUND_HALF_UP)
return {"model_id": card.model_id, "currency": card.currency,
"price_version": card.version, "effective_date": card.effective_date,
"source_ref": card.source_ref, "unit_tokens": card.unit_tokens,
"tier_min_input_tokens": chosen.min_input_tokens, "lines": lines,
"unrounded_total": format(total, "f"), "display_total": format(display, "f"),
"rounding_policy": "sum first; display 6dp HALF_UP; not a settlement rule",
"is_invoice": False}
DEMO_CARD = PriceCard(
model_id="demo-text-v1", currency="CNY", unit_tokens=1_000_000,
version="fictional-lesson-6", effective_date="2026-10-28",
source_ref="fictional:classroom-only",
tiers=(Tier(0, "2.00", "0.50", "8.00"),
Tier(200_000, "4.00", "1.00", "12.00")))
if __name__ == "__main__":
print(json.dumps(estimate(DEMO_CARD, model_id="demo-text-v1",
input_tokens=10_000, output_tokens=2_000,
cached_input_tokens=4_000), ensure_ascii=False, indent=2))
本例用Decimal字符串和局部上下文执行运算。计数与价格的位数有界,单位又是10的整数次幂,因此示例范围内的分类乘除可保留精确十进制结果。若扩展到任意分母或无限精度数据,要重新审查舍入与精度,不能直接复用这个结论。Python 3.11 Decimal官方文档
4. 演示输入、输出与计算依据
python offline_cost.py
DEMO_CARD与固定演示用量在文件末尾,都是人工假设。本次实际本地输出为:
{
"model_id": "demo-text-v1",
"currency": "CNY",
"price_version": "fictional-lesson-6",
"effective_date": "2026-10-28",
"source_ref": "fictional:classroom-only",
"unit_tokens": 1000000,
"tier_min_input_tokens": 0,
"lines": [
{
"category": "uncached_input",
"tokens": 6000,
"rate": "2.00",
"unrounded_cost": "0.012"
},
{
"category": "cached_input",
"tokens": 4000,
"rate": "0.50",
"unrounded_cost": "0.002"
},
{
"category": "output",
"tokens": 2000,
"rate": "8.00",
"unrounded_cost": "0.016"
}
],
"unrounded_total": "0.030",
"display_total": "0.030000",
"rounding_policy": "sum first; display 6dp HALF_UP; not a settlement rule",
"is_invoice": false
}
输入总量10000,其中缓存4000,所以普通输入是6000。三类金额分别为0.012、0.002、0.016,合计0.030元。若直接把10000全部按普通输入计价,再加4000缓存费用,就重复计算了缓存部分。
unrounded_total用于保留数学结果,display_total按本例约定保留6位小数。这个约定只服务于教学显示,不是任何平台的扣费舍入规则。返回值里is_invoice=false刻意保留边界。
effective_date与source_ref是由调用方传入的证据索引,本程序只做结构检查,不会联网确认价格,也不会自动判断历史请求是否匹配价格时段。示例从固定价格卡开始,真实对接还需单独选择并验证价格版本。
5. 单独观察舍入顺序
将下列文件保存为rounding_audit.py:
"""Fictional arithmetic evidence; neither rounding method is a vendor promise."""
from decimal import Decimal, localcontext, ROUND_HALF_UP
import json
def compare_rounding(values: list[str], currency: str) -> dict:
if currency not in ("CNY", "USD") or not values or len(values) > 1000:
raise ValueError("one labelled currency and 1..1000 fictional costs required")
if any(type(value) is not str for value in values):
raise ValueError("decimal strings required; do not pass floats")
numbers = [Decimal(value) for value in values]
if any(not n.is_finite() or n < 0 or n > 1000000 or n.as_tuple().exponent < -8
or len(n.as_tuple().digits) > 16 for n in numbers):
raise ValueError("finite non-negative costs with bounded precision required")
with localcontext() as context:
context.prec = 40
cents = Decimal("0.01")
each = sum((n.quantize(cents, rounding=ROUND_HALF_UP) for n in numbers), Decimal("0"))
raw = sum(numbers, Decimal("0"))
aggregate = raw.quantize(cents, rounding=ROUND_HALF_UP)
return {"currency": currency, "sum_raw": format(raw, "f"),
"round_each_then_sum": format(each, ".2f"),
"sum_then_round": format(aggregate, ".2f"),
"vendor_settlement_rule_verified": False}
if __name__ == "__main__":
print(json.dumps(compare_rounding(["0.0049", "0.0049"], "CNY"), indent=2))
运行python rounding_audit.py,输出:
{
"currency": "CNY",
"sum_raw": "0.0098",
"round_each_then_sum": "0.00",
"sum_then_round": "0.01",
"vendor_settlement_rule_verified": false
}
各0.0049的两笔假数据,逐笔按分舍入与汇总后按分舍入产生不同显示值。真实系统可以有不同合同规则,不能因为这次演示就判断哪种一定正确。
6. 完整测试文件
将以下内容保存为同目录的test_offline_cost.py。三个文件放在同一个新目录即可运行。
from dataclasses import replace
from decimal import Decimal, localcontext
import unittest
from offline_cost import DEMO_CARD, Tier, estimate, amount
from rounding_audit import compare_rounding
class CostTests(unittest.TestCase):
def call(self, card=DEMO_CARD, **changes):
args = dict(model_id="demo-text-v1", input_tokens=10000,
output_tokens=2000, cached_input_tokens=4000)
args.update(changes)
return estimate(card, **args)
def test_partition_without_double_counting(self):
result = self.call()
self.assertEqual([x["tokens"] for x in result["lines"]], [6000, 4000, 2000])
self.assertEqual(Decimal(result["unrounded_total"]), Decimal("0.030"))
self.assertEqual(result["display_total"], "0.030000")
def test_exact_tier_boundary(self):
low = self.call(input_tokens=199999, cached_input_tokens=0)
high = self.call(input_tokens=200000, cached_input_tokens=0)
self.assertEqual(low["tier_min_input_tokens"], 0)
self.assertEqual(high["tier_min_input_tokens"], 200000)
self.assertEqual(high["lines"][0]["rate"], "4.00")
def test_units_equivalent_when_rates_converted(self):
small = replace(DEMO_CARD, unit_tokens=1000,
tiers=(Tier(0, "0.002", "0.0005", "0.008"),))
self.assertEqual(Decimal(self.call(small)["unrounded_total"]),
Decimal(self.call()["unrounded_total"]))
def test_missing_not_zero(self):
for key in ("input_tokens", "output_tokens", "cached_input_tokens"):
with self.subTest(key=key), self.assertRaises(ValueError):
self.call(**{key: None})
def test_known_zero(self):
result = self.call(input_tokens=0, output_tokens=0, cached_input_tokens=0)
self.assertEqual(result["display_total"], "0.000000")
def test_bad_counts(self):
for value in (True, -1, 1.2, "10", 10**12 + 1):
with self.subTest(value=value), self.assertRaises(ValueError):
self.call(input_tokens=value)
def test_cache_subset(self):
with self.assertRaises(ValueError):
self.call(cached_input_tokens=10001)
def test_rates_are_bounded_strings(self):
for value in (0.1, "NaN", "Infinity", "-1", "1e-3", "1.123456789", " 2.0"):
with self.subTest(value=value), self.assertRaises(ValueError):
amount(value)
def test_model_and_unit_rejected(self):
with self.assertRaises(ValueError):
self.call(model_id="another-model")
for unit in (True, 100, "1000000"):
with self.assertRaises(ValueError):
self.call(replace(DEMO_CARD, unit_tokens=unit))
def test_invalid_card_evidence(self):
for key, value in (("currency", "unknown"), ("version", ""),
("source_ref", ""), ("effective_date", "not-a-date")):
with self.subTest(key=key), self.assertRaises(ValueError):
self.call(replace(DEMO_CARD, **{key: value}))
def test_malformed_tiers(self):
for tiers in ((), (Tier(10, "2", "1", "3"),),
(Tier(0, "2", "1", "3"), Tier(0, "4", "2", "6"))):
with self.assertRaises(ValueError):
self.call(replace(DEMO_CARD, tiers=tiers))
def test_local_context_and_immutable_card(self):
with localcontext() as c:
c.prec = 3
result = self.call()
self.assertEqual(c.prec, 3)
self.assertEqual(result["display_total"], "0.030000")
result["lines"][0]["tokens"] = 999
self.assertEqual(self.call()["lines"][0]["tokens"], 6000)
class RoundingTests(unittest.TestCase):
def test_rounding_order_changes_display(self):
r = compare_rounding(["0.0049", "0.0049"], "CNY")
self.assertEqual(r["round_each_then_sum"], "0.00")
self.assertEqual(r["sum_then_round"], "0.01")
self.assertFalse(r["vendor_settlement_rule_verified"])
def test_invalid_amounts(self):
for value in ("NaN", "Infinity", "-0.1", 0.1):
with self.subTest(value=value), self.assertRaises((ValueError, TypeError)):
compare_rounding([value], "CNY")
def test_currency_must_be_labelled(self):
with self.assertRaises(ValueError):
compare_rounding(["1"], "")
if __name__ == "__main__":
unittest.main()
执行:
python -m unittest -v test_offline_cost.py
本次Python3.11.9执行15项测试通过。测试覆盖缓存互斥分配、精确阈值、千/百万单位换算、未知与零、非法计数、缓存超范围、非法价格、模型不匹配、来源字段、档位顺序、局部精度及显示舍入。没有真实模型调用,也没有访问真实费用记录。
7. 常见异常怎样整改
known integer count ... required:先看是不是None。上一课保留的“未知”不能到这里自动填0;未拿到输出或缓存用量时,阻止完整估算并保留待核对状态。
rate must be ... decimal string:不要把float转换成字符串当作通用修复。应从原始价格文本、受控配置或可信十进制记录建立价格;确认单位后再输入。NaN、Infinity和科学计数法在本例中明确不接受。
cached input must be a subset ...:检查字段含义或请求归属。本例要求输入总量包含缓存读取,目标服务若不同,应换适配器,不能用min(cache,input)把错误掩盖过去。
阈值附近费用突然变化:本例是达到200000后整次请求采用B档,不是把超出的1个Token按B档收费。先核对真实规则是哪一种,不凭函数名推断。
显示0.000000:检查unrounded_total,它可能仍是非零。显示精度与结算精度不同,不能把界面0当免费承诺。
计算结果与真实账单不同:本篇没有实现税费、工具、最低计费、折扣或特殊用量分类。先核对范围和价格版本,不改动原始证据来凑金额。
8. 小练习与下期
复制虚构价格卡,把单位改为1000,同时把三项单价除以1000,验证结果不变。再把缓存计数改为None,检查它能否阻止完整金额输出。最后为“边界前一项、边界值、边界后一项”各写一个断言,练习说明整次选档与累进算法的差别。
下一篇给日志脱敏写恶意输入样本:如何保留排错证据,却不泄露密钥、请求头或用户原文。
欢迎在评论区描述一个不含账户、支付和用户信息的虚构边界用例;先列出公式的前提,再讨论实现。
作者说明:本文经AI辅助整理与校对。程序、价格卡、用量和结果全部为本地教学样本,不是报价工具或实际账单。
本文基于Python标准库Decimal实现大模型API调用费用的离线精确估算,支持缓存拆分、整次请求档位选择及多单位(千/百万Token)价格计算。强调输入契约明确性、舍入顺序敏感性与精度可控性,覆盖15项边界与异常测试,不依赖外部服务或真实账单数据,适用于教学与本地验证场景。
365

被折叠的 条评论
为什么被折叠?



