大模型API计费系统搭建:从配置到扣费验证
给大模型API加计费,核心就两件事:一是在请求转发前校验并预扣额度,二是在响应返回后按实际token用量结算。
比较轻量的做法是用Nginx配合Lua脚本,在网关层拦截请求,用Redis存余额和扣费记录,不需要改动后端模型服务。
下面按从零搭建的顺序讲清楚每一步。
先明确计费链路和准备条件
这套方案适用于自建或代理大模型API、需要按调用量或token数向用户收费的场景。
请求链路是:客户端带API Key请求Nginx,Lua脚本校验Key并预扣额度,放行到后端模型接口,响应返回后Lua读取usage字段完成结算。
需要准备:一台Linux服务器(以Ubuntu 22.04为例)、已安装Nginx、Redis、以及带lua-nginx-module的Nginx(OpenResty自带)。
如果不想编译,直接装OpenResty最省事:
apt install -y curl gnupg2 ca-certificates
# 添加OpenResty官方源并安装,具体源地址以官网为准
apt install -y openresty
装完后执行 openresty -V 确认包含 --with-lua-nginx-module,
再启动Redis:systemctl enable --now redis-server。
配置Key校验和预扣额度
在OpenResty的 nginx.conf 的 http 块内引入计费脚本。
核心逻辑:从请求头取 Authorization,用Key去Redis查用户ID和余额,余额不足直接返回402,余额充足则按预估token预扣。
location /v1/chat/completions {
access_by_lua_file /etc/nginx/lua/billing.lua;
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
}
billing.lua 里做三件事:
- 用
ngx.req.get_headers()["authorization"]取Key,查RedisHGET user:key:,查不到返回401。uid - 用
HGET user:balance:读余额,低于单次最低预扣值(比如0.001元)返回402并提示充值。balance - 预扣用
HINCRBYFLOAT user:balance:,同时把本次请求ID和预扣金额写入balance -0.001 HSET pending:,方便后续结算或回滚。
注意预扣值只是占位,最终以响应里的 usage.total_tokens 为准。
响应后按实际token结算
在 location 中加 body_filter_by_lua_file,读取上游返回的JSON,解析 usage.total_tokens,按单价计算实际费用,与预扣值做差额补扣或退还。
流式响应(SSE)要特殊处理:累计拼接chunk,在最后一个chunk里找usage,找不到就按预扣值结算并记录日志,避免漏扣。
结算代码片段示意:
local body = ngx.arg[1]
if body and body:find('"usage"') then
local total = tonumber(body:match('"total_tokens"%s*:%s*(%d+)'))
if total then
local cost = total * unit_price
redis:hincrbyfloat('user:balance:'..uid, 'balance', pre_deduct - cost)
redis:hdel('pending:'..req_id)
end
end
单价建议放在Redis配置里,方便调价不用重启。
避坑指南
- 流式响应漏扣:SSE的usage可能只在最后一个data块出现,body_filter会被多次调用,务必用缓冲区拼接后再解析,否则大量请求按预扣值结算,长期亏钱。
- 并发超扣:预扣和结算之间不是原子操作,高并发下可能余额扣成负数。对同一用户加Redis分布式锁,或在预扣时用Lua脚本保证查余额和扣减原子执行。
- Key泄露:不要把Redis密码和单价硬编码在lua文件里,用环境变量或独立配置文件,文件权限设600。
- 结算失败回滚:请求超时或上游5xx时,预扣额度要退还,建议用定时任务扫描
pending:*超过5分钟的记录做补偿。
验证扣费是否生效
用一个测试Key发两次请求,第一次正常调用,第二次把余额改成0再调,预期分别返回200和402。
然后查Redis:
redis-cli HGET user:balance: balance
redis-cli KEYS 'pending:*'
余额应等于初始值减去实际费用,pending 里不应残留当前请求ID。
再对比后端日志里的token数和扣费金额,确认单价计算一致。
流式接口单独测一次,确认最后一个chunk结算后余额正确变动。
如果余额对不上,先看 error.log 里lua是否报解析错误,再检查单价配置是否和代码里读取的key一致。
常见疑问
Q:必须用OpenResty吗?
普通Nginx需要自行编译lua-nginx-module,步骤多且容易版本不匹配;OpenResty已集成,新手优先选它。
Q:token数从哪来?
取决于后端模型接口是否返回usage字段。如果后端不返回,只能在网关层估算,误差较大,建议优先选带usage的接口。
Q:能按请求次数而不是token计费吗?
可以,把结算逻辑改成每次固定扣费即可,但预扣和结算仍要保持原子性,避免并发问题。