同一个 API Key 接多个模型,真正难的往往不是“转发请求”,而是计费。
文本模型通常按输入/输出 Token 分别计价;视频生成则可能按秒、分辨率或任务规格结算。若只在前端展示一个“余额”,很容易出现:用户不知道为什么扣钱、运营方也无法核对成本。
这篇记录我在一个多模型 API 网关中处理计费的最小思路:请求前预估、请求后按实际结算、失败自动释放预留,并把每笔数据做成可追踪记录。
一、先把计费单位拆开
不要把所有模型都强行折算成“Token”。
| 能力 | 常见单位 | 计费时需要记录的字段 |
|---|---|---|
| 文本 / 推理 | 输入 Token + 输出 Token | input_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 即可在开发者中心核对记录。对接多上游时,这比在日志里猜“到底走了哪条线路”可靠得多。
六、三个容易踩坑的地方
- 只按总 Token 计价。 输入和输出价格通常不同,合并后账单会失真。
- 任务提交即扣最终费用。 视频、图片等异步任务必须等待最终状态。
- 失败后忘记释放预留。 这会让用户余额被无故冻结,也会增加客服成本。
结语
多模型网关的计费不是“加一个余额字段”就结束了。要做到可信,至少要让每次调用有实际模型、有实际用量、有最终状态,并且能回查。
项目代码与调用示例: github.com/eugeneliuhz…
如果你在做 AI API 网关,欢迎交流你处理异步任务、预留与退款的方式。