计费与倍率
专栏:new-api 源码拆解 · 第 8 / 12 篇:::info 学习目标 完成本篇后你能够:手算一次请求的 quota 消耗;解释预扣费-结算-退款三段式为什么必要、各环节如何防资损;说出阶梯计费与订阅/钱包双资金来源的设计。 前置:第 4 篇完成。预计时长:75 分钟。 :::
:::note 本章术语速查(新手建议先读)
- Quota(配额):new-api 的内部货币。1 美元 = 500000 quota,一切消费按它记账。
- 倍率(Ratio):价格乘数——模型倍率(模型贵不贵)、补全倍率(回复比提问贵几倍)、分组倍率(这个用户组打几折)。
- 预扣费(Pre-consume):请求前先冻结一笔钱,防止用户欠费;结算后多退少补。
- 结算(Settle):按实际 token 用量算清最终费用。
- 退款(Refund):请求失败时把预扣的钱退回,必须”幂等”(重复退也不会多退)。
- decimal:高精度十进制计算,避免浮点数算钱出现 0.1+0.2≠0.3 的问题。 :::
计费是网关商业化的心脏,也是最容易出资损事故的模块:预扣不够会透支、结算错了用户投诉、退款不幂等会重复返钱。new-api 的答案是三段式架构——请求前预扣费、响应后按实际用量结算、失败时幂等退款。
倍率体系:一问一答值多少钱
按量计费的核心公式(service/text_quota.go:300-365,全程 decimal 高精度):
quota = ( 基础提示 tokens
+ 缓存命中 tokens × cacheRatio
+ 缓存写入 tokens × createCacheRatio ← 5 分钟 / 1 小时分开计
+ 图片 tokens × imageRatio
+ 补全 tokens × completionRatio )
× modelRatio × groupRatio
+ 音频附加 + 工具调用附加
// service/text_quota.go:300-325(节选)
ratio := dModelRatio.Mul(dGroupRatio)
...
if !dCacheTokens.IsZero() {
if !summary.IsClaudeUsageSemantic && !legacyClaudeDerived {
baseTokens = baseTokens.Sub(dCacheTokens) // 缓存命中从基础提示中扣出
}
cachedTokensWithRatio = dCacheTokens.Mul(dCacheRatio)
}
...
promptQuota := baseTokens.Add(cachedTokensWithRatio).Add(imageTokensWithRatio).
Add(cachedCreationTokensWithRatio)
completionQuota := dCompletionTokens.Mul(dCompletionRatio)
quotaCalculateDecimal := promptQuota.Add(completionQuota).Mul(ratio)
四个必须注意的细节:缓存命中 tokens 从基础提示中扣出单独乘低倍率(不重复计费),且 Claude 的 usage 语义有新旧两套(IsClaudeUsageSemantic 分支);Claude 1 小时缓存写入倍率 = 5 分钟档 × 6/3.75(relay/helper/price.go:39,官方定价的精确换算);ratio 非零时结果最小为 1(防”算出 0 而免费”);分组倍率先查 GroupGroupRatio(用户组×请求组嵌套)再落 GroupRatio(第 4 篇)。
独立的按次计价路径:quota = modelPrice × QuotaPerUnit × groupRatio(QuotaPerUnit = 500000,即 1 美元)。还有 tiered 阶梯计费表达式引擎(pkg/billingexpr):用表达式声明分段价格(系数为 $/1M tokens),引擎执行后按 rawCost / 1_000_000 × QuotaPerUnit × groupRatio 换算成 quota。
三段式:预扣 → 结算 → 退款
逐段拆解设计意图:
预扣估算(relay/helper/price.go):preConsumedTokens = max(promptTokens, PreConsumedQuota默认500) + maxTokens,再乘 modelRatio × groupRatio——按最坏情况估,防”实际爆量、预扣不够”的透支。
信任旁路(service/billing_session.go:187-241):
// service/billing_session.go:187-241(节选)
if s.shouldTrust(c) {
s.trusted = true
effectiveQuota = 0 // 余额充足:跳过预扣,省一次写
...
} else if effectiveQuota > 0 {
// 1) 预扣令牌额度
if err := PreConsumeTokenQuota(s.relayInfo, effectiveQuota); err != nil {
return types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed,
http.StatusForbidden, ...)
}
// 2) 预扣资金来源(钱包/订阅)
if err := s.funding.PreConsume(effectiveQuota); err != nil {
// 预扣失败 → 回滚令牌额度
if s.tokenConsumed > 0 && !s.relayInfo.IsPlayground {
... model.IncreaseTokenQuota(...)
}
结算(service/billing.go:51-95):delta = actualQuota - preConsumed——delta>0 补扣、delta<0 返还,日志明确打印三个数字;无 BillingSession 时回退旧路径。
退款(service/billing_session.go:83-124):settled/refunded 互斥标记 + 锁保证幂等,gopool 异步执行”退资金来源 + 退令牌额度”两步——网络重放不会重复退款。
与其他模块的联系
- ← relay 循环(第 2、7 篇):预扣在重试循环之前(一次任务只预扣一次,换渠道不重复扣);
defer里挂 Refund——任何失败路径统一退款; - ← 分组(第 4 篇):groupRatio 是公式乘数;倍率配置经 Option 热更新即时生效;
- → 日志(第 9 篇):
RecordConsumeLog同时投喂消费日志表与数据看板缓存; - → 缓存(第 10 篇):预扣的原子性靠 Redis Lua(带 schema 版本校验),批量落库让结算写库延迟 5 秒聚合。
常见踩坑
- 新模型忘了配倍率——非自用模式直接报「模型价格未配置」(自用模式 fallback 倍率 37.5,防自用被堵);
- 并发预扣竞态——自研最容易资损的点;new-api 用 Redis Lua 原子 HINCRBY + schema 版本校验,降级 DB 时用
WHERE quota >= ?条件更新; - 退款不幂等——网络重试导致重复退款;照抄
Refund的标记位写法; - 只结算不记日志——
RecordConsumeLog同时供对账与看板,拆开实现迟早不同步; - Claude 缓存语义混用——新旧 usage 语义下缓存 tokens 是否从 prompt 中扣除不同,公式分了分支,自研时极易算错。
随堂练习(带验收标准)
- 手算:modelRatio=2、completionRatio=3、groupRatio=0.8,prompt 1000(无缓存)+ 补全 500 tokens。验收:先手算(基础 1000 + 500×3 = 2500 有效 token × 2 × 0.8 = 4000 quota),再与消费日志比对一致;
- 在管理台配置一个按次计价模型(0.5 美元/次),验证日志 quota = 0.5 × 500000 × 组倍率;
- 退款实验:给某渠道配错误 key 并开启 RetryTimes,观察全部失败后用户额度被完整返还(对比请求前后余额)。验收:余额零变化;
- 读
service/billing_session.go的preConsume,画出预扣失败时令牌额度回滚的完整调用链。