缓存计费的坑:三笔账必须互斥
模型网关的缓存计费有三笔账:普通输入、缓存读取、缓存写入。三笔必须互斥——同一 token 只能进一个桶。漏减其中一项,就是重复收费。这次是一个选择性升级移植漏掉了一行减法,写入 token 同时按普通输入价和缓存写入价收了两次。
2.25x 是怎么来的
官方定价里,缓存写入按普通输入价的 1.25 倍收费,写入量报告在 cache_write_tokens,读取量在 cached_tokens,而 input_tokens 是包含明细的总输入。正确的分桶公式:
uncached_input = input_tokens - cached_tokens - cache_write_tokens
cost = uncached_input × 普通输入价
+ cached_tokens × 缓存读取价
+ cache_write_tokens × 缓存写入价
+ output_tokens × 输出价
出问题的分支只算了 input_tokens - cache_read_tokens,随后又把缓存写入单独送进写入计费。当上游返回非零写入量时,写入 token 既留在普通输入桶里,又进了一次写入桶。官方写入价是 1.25x,叠加之后实际收费变成 2.25x——比官方价高出 80%,再叠服务等级、长上下文和用户倍率。
同一个遗漏还有连带伤害:计费器用 input_tokens + cache_write_tokens 判断总上下文,可能把没超 272k 的请求提前判进长上下文档位。
生产数据:缺陷真实,但没实际多扣
有意思的是,这个缺陷在生产里一次也没触发。核查时该模型历史 3952 条请求里,cache_creation_tokens > 0 的是 0 条,缓存读取倒是有 3900 条、累计约 251 美元。写入量恒为 0,所以生产数据里没有多扣的证据,也就谈不上补扣或补偿。
但这不改变缺陷的性质:只要上游某天开始返回非零写入量,重复计费立刻生效。拿"还没触发"当"没问题",是另一种侥幸。
测试为什么没抓住
分层测试全过了:解析测试验证"能读出写入量",定价测试验证"能按写入价算钱"。问题出在纵向闭环没人覆盖——从原始 usage 进入分桶、再到费用明细和统计,整条链没人走一遍。
更隐蔽的是,测试 helper 自己用的就是旧公式 input - cache_read,实现和测试保持一致地错误。加上专项测试文件带了构建标签,普通全量测试根本不跑它。测试和实现用同一份错的假设互相印证,门禁就形同虚设。
选择性升级的教训
这次用的是"选择性升级":不上游全量合并,而是按主题手工移植。上游同一个补丁里其实有四件事:写入量别名解析、价格处理、分桶互斥、端到端回归测试。本地移植了前两件,漏了后两件。
流程层的根因是缺少一张覆盖矩阵:上游每个语义 hunk 落到本地哪里、对应哪条端到端测试。v0.1.150 的台账把该主题标成"完整吸收",后续版本只审计增量,默认信任了错误的基线,缺陷就一路保留下来。
修复本身不复杂:分桶补减写入量,补上缺失的回归测试,顺带把协议转换里的写入量字段一并带上。全量测试通过后,这个分支才算真正吸收完。
小结
计费代码的正确性不看单价算得对不对,看每一分钱有没有且仅有一个归属。三笔账互斥是条不证自明的规则,但规则不会自己 enforce—— enforcing 它的是纵向闭环测试,和"每个上游语义改动都有本地落点"的移植纪律。