多模型 API 网关怎么做精确计费:输入、输出 Token 与视频按秒结算

0 阅读3分钟

同一个 API Key 接多个模型,真正难的往往不是“转发请求”,而是计费。

文本模型通常按输入/输出 Token 分别计价;视频生成则可能按秒、分辨率或任务规格结算。若只在前端展示一个“余额”,很容易出现:用户不知道为什么扣钱、运营方也无法核对成本。

这篇记录我在一个多模型 API 网关中处理计费的最小思路:请求前预估、请求后按实际结算、失败自动释放预留,并把每笔数据做成可追踪记录。

一、先把计费单位拆开

不要把所有模型都强行折算成“Token”。

能力常见单位计费时需要记录的字段
文本 / 推理输入 Token + 输出 Tokeninput_tokens、output_tokens
图片张数 / 尺寸images、size
视频秒数 / 分辨率 / 任务规格seconds、size、task_id
语音时长 / 字符数duration 或 characters

这里有一个原则:客户账单单位必须和上游成本单位一致或可明确换算。

例如文本模型分别保存输入、输出售价;视频模型直接保存“每秒售价”。不要为了界面统一而把视频的实际秒数伪装成 Token。

二、请求前:先计算最大可能费用

以文本为例,如果调用方传入 max_tokens,可以先按: 输入预估费用 + max_tokens × 输出单价 计算最大扣费预留。

伪代码:

const inputCost = inputTokens * inputUnitPrice / 1_000_000;
const maxOutputCost = maxTokens * outputUnitPrice / 1_000_000;
const reserve = inputCost + maxOutputCost;

if (wallet < reserve) {
  throw new Error("余额不足,本次不会调用上游");
}

视频的预留更直接:

const reserve = seconds * pricePerSecond;

这一步的意义是:避免上游已经生成成功,但客户余额不足,最后成本落在平台身上。

三、请求后:必须按实际用量结算

预留不是最终扣费。

文本接口成功后,应读取上游返回的真实 usage

{
  "prompt_tokens": 90,
  "completion_tokens": 16
}

最终费用应按真实输入、输出 Token 算:

const finalCost =
  usage.prompt_tokens * inputUnitPrice / 1_000_000 +
  usage.completion_tokens * outputUnitPrice / 1_000_000;

视频任务则在状态变为 succeeded 后按实际完成秒数结算。提交成功不等于最终生成成功;任务失败、取消或被上游拒绝时,应释放之前的预留。

四、把“请求记录”和“扣费记录”合并

用户不需要看到两套重复的列表。

我建议一条调用记录至少包含:

  • 本地时间与请求 ID
  • 请求模型与实际模型
  • 状态:已完成 / 任务处理中 / 失败
  • 用量:Token 或实际秒数
  • 本次实际扣费
  • 失败原因(若有)

这样用户只要看一行,就能知道“这次调了什么、花了多少、为什么”。

五、一个真实文本调用的返回结构

{
  "routing": {
    "requested_model": "deepseek-v4-pro",
    "actual_model": "deepseek-v4-pro"
  },
  "usage": {
    "prompt_tokens": 90,
    "completion_tokens": 16
  },
  "billing": {
    "charged_cny": 0.00207
  }
}

客户端保存请求 ID 即可在开发者中心核对记录。对接多上游时,这比在日志里猜“到底走了哪条线路”可靠得多。

六、三个容易踩坑的地方

  1. 只按总 Token 计价。 输入和输出价格通常不同,合并后账单会失真。
  2. 任务提交即扣最终费用。 视频、图片等异步任务必须等待最终状态。
  3. 失败后忘记释放预留。 这会让用户余额被无故冻结,也会增加客服成本。

结语

多模型网关的计费不是“加一个余额字段”就结束了。要做到可信,至少要让每次调用有实际模型、有实际用量、有最终状态,并且能回查。

项目代码与调用示例: github.com/eugeneliuhz…

如果你在做 AI API 网关,欢迎交流你处理异步任务、预留与退款的方式。