计费与倍率

专栏:new-api 源码拆解 · 第 8 / 12 篇
new-api计费倍率

:::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.75relay/helper/price.go:39,官方定价的精确换算);ratio 非零时结果最小为 1(防”算出 0 而免费”);分组倍率先查 GroupGroupRatio(用户组×请求组嵌套)再落 GroupRatio(第 4 篇)。

独立的按次计价路径:quota = modelPrice × QuotaPerUnit × groupRatioQuotaPerUnit = 500000,即 1 美元)。还有 tiered 阶梯计费表达式引擎(pkg/billingexpr):用表达式声明分段价格(系数为 $/1M tokens),引擎执行后按 rawCost / 1_000_000 × QuotaPerUnit × groupRatio 换算成 quota。

三段式:预扣 → 结算 → 退款

图表(newapi-billing-ratios.md)

逐段拆解设计意图:

预扣估算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 秒聚合。

常见踩坑

  1. 新模型忘了配倍率——非自用模式直接报「模型价格未配置」(自用模式 fallback 倍率 37.5,防自用被堵);
  2. 并发预扣竞态——自研最容易资损的点;new-api 用 Redis Lua 原子 HINCRBY + schema 版本校验,降级 DB 时用 WHERE quota >= ? 条件更新;
  3. 退款不幂等——网络重试导致重复退款;照抄 Refund 的标记位写法;
  4. 只结算不记日志——RecordConsumeLog 同时供对账与看板,拆开实现迟早不同步;
  5. Claude 缓存语义混用——新旧 usage 语义下缓存 tokens 是否从 prompt 中扣除不同,公式分了分支,自研时极易算错。

随堂练习(带验收标准)

  1. 手算:modelRatio=2、completionRatio=3、groupRatio=0.8,prompt 1000(无缓存)+ 补全 500 tokens。验收:先手算(基础 1000 + 500×3 = 2500 有效 token × 2 × 0.8 = 4000 quota),再与消费日志比对一致;
  2. 在管理台配置一个按次计价模型(0.5 美元/次),验证日志 quota = 0.5 × 500000 × 组倍率;
  3. 退款实验:给某渠道配错误 key 并开启 RetryTimes,观察全部失败后用户额度被完整返还(对比请求前后余额)。验收:余额零变化;
  4. service/billing_session.gopreConsume,画出预扣失败时令牌额度回滚的完整调用链。

← 返回文章列表