先说结论:多租户 AI 产品要按客户结算模型用量,最简单可靠的做法是给每个租户(或每个租户档位)一个独立的 API Key。在这个 Key 上设置美元消费上限和并发上限,并记录网关为每次请求返回的实际扣费,而不是拿 token 数自己估算。在 HopBase 上,每个计费的非流式响应都带 X-HopBase-Cost-USD 和 usage.cost_usd;每个完成的视频或图片任务都返回 usage.cost_usd;GET /v1/usage 返回某个 Key 还能花多少。某个租户的额度用完后,它的请求收到 402 insufficient_quota,其他租户照常使用。Key 超出自身并发上限时收到 429 apikey_concurrency_limit,一个吵闹的客户耗不光你的容量。流式响应没有扣费响应头,用按 Key 导出的 CSV 对账即可,其中每一行都能复算。
问题:一张账单,很多客户
如果你做的是 AI 智能体平台、带 AI 功能的垂直 SaaS,或者白标 AI 服务,模型服务商只给你开一张账单,而你的每个客户都期待拿到自己的那一张。两者之间,几乎每个团队最后都要回答这些问题:
- 租户 A 这个月花了我们多少钱,按模型拆分是多少?
- 怎么防止某个租户失控的智能体循环把所有人的预算花光?
- 怎么在我们自己的后台给客户显示剩余额度?
- 财务问起来时,能不能证明发票上的每一行?
光靠 token 数回答不了这些。不同模型价格不同,缓存输入更便宜,有的模型把推理 token 按输出计费,有的超过某个上下文长度就涨价,视频则按秒或按视频 token 计费。如果你自己拿 token 数乘价格表,每次有新模型上线,这张表就会过时一次。
三种架构对比
| 做法 | 租户如何隔离 | 费用数字从哪来 | 由谁执行限制 |
|---|---|---|---|
| 请求打标签,自己记账 | 应用里的租户 ID;共用一个服务商 Key | 自己维护的价格表 × token 数 | 你的代码,在每次调用前 |
| 自建代理 | 代理里的虚拟 Key 或客户 ID 字段 | 代理的价格表 | 代理 |
| 在会返回实际扣费的网关上,每个租户一个 Key | 每个租户或档位一个网关 Key | 网关对每次请求的扣费 | 网关:按 Key 的消费上限和并发上限 |
三种都行得通。本文接下来讲第三种,因为它省掉了最难的两件事:维护一张准确的价格表,以及在请求发出前执行限额。
第一步:每个租户一个 Key,并设上限
在 HopBase 控制台打开 API 密钥,为每个租户新建一个 Key。Key 在控制台里创建和修改,没有公开的 API 可用,所以请把建 Key 作为租户开通流程中的一步。每个 Key 上有三项设置最关键:
- 分组决定这个 Key 能调哪些模型。一个 Key 可以跨家族挂 1 到 10 个分组,所以「基础版」租户可以只给 DeepSeek 和千问,「专业版」租户再加上 Claude 和视频。调用 Key 分组之外的模型会返回 404
model_not_found。 - 额度是这个 Key 的美元消费上限。用完后,该 Key 的请求返回 402
insufficient_quota,提示「API key quota exhausted」。账户下的其他 Key 不受影响。 - 并发是这个 Key 上同时在途的请求数。超出上限的请求收到 429
apikey_concurrency_limit,并带Retry-After: 1。留空表示不设 Key 级上限。账户级上限仍然叠加生效:默认每个账户同时在途 5 个请求,可申请提高。
把 Key 加密存进租户表,绝不要暴露到租户的浏览器里。由你的后端代表租户调用模型。
第二步:每次请求都记下实际扣费
同步调用(对话、生图、语音)在计费后的响应里返回从你余额扣掉的金额:
- 响应头
X-HopBase-Cost-USD,例如0.012。 - 非流式 JSON 里的
usage.cost_usd,值相同,和普通 token 计数放在一起。
写一个薄封装,把它变成一条账本记录:
import os, sqlite3
from openai import OpenAI
db = sqlite3.connect("ledger.db")
db.execute("""CREATE TABLE IF NOT EXISTS usage_ledger (
ts TEXT DEFAULT CURRENT_TIMESTAMP, tenant_id TEXT, model TEXT,
prompt_tokens INT, completion_tokens INT, cost_usd REAL, request_id TEXT)""")
def client_for(tenant):
# tenant["hopbase_key"] 是该租户自己的 Key,在服务端解密
return OpenAI(base_url="https://api.hop-base.com/v1", api_key=tenant["hopbase_key"])
def chat(tenant, model, messages, **kw):
raw = client_for(tenant).chat.completions.with_raw_response.create(
model=model, messages=messages, **kw)
resp = raw.parse()
cost = float(raw.headers.get("X-HopBase-Cost-USD") or 0)
db.execute("INSERT INTO usage_ledger (tenant_id, model, prompt_tokens, completion_tokens, cost_usd, request_id) VALUES (?,?,?,?,?,?)",
(tenant["id"], model, resp.usage.prompt_tokens,
resp.usage.completion_tokens, cost, resp.id))
db.commit()
return resp
关于这个数字,有几条对计费很重要的规则:
- 没有产生输出的失败请求不计费,也不带扣费响应头。
- 如果网关在内部重试了某个请求,只有最终完成的那次尝试计费,返回的就是那次的金额。挂了多个分组的 Key,按实际服务该请求的分组计费。
- 如果你的客户端提前断开,已生成的输出照常计费。即使你没看到响应头,这笔扣费也会出现在控制台的使用记录里。
- 流式(SSE)响应不带扣费响应头,因为响应头在生成开始前就已发出;流的最后一个 usage 事件只有 token 数,没有金额。把 token 数(发送
stream_options: {"include_usage": true})记为一条待结算记录,再用 CSV 导出(第四步)补上金额。
第三步:异步视频与图片任务
视频任务用 POST /v1/video/generate 提交,用 GET /v1/video/tasks/{task_id} 轮询。任务进入终态后,单个任务查询的响应顶层会带一个 usage 对象:
{
"id": "vt-1",
"status": "completed",
"usage": { "cost_usd": 0.5 }
}
任务结束时把 usage.cost_usd 写进账本。未扣费的失败任务返回 0。查询单个任务时,只有创建它的那个 Key 能看到这个字段。任务列表(不带 ID 的 GET /v1/video/tasks)覆盖整个账户,包括其他 Key 创建的任务,所以请把每个任务 ID 和对应租户一起存下来,不要把原始列表直接转给租户。视频提交还会预占预估费用,所以接近上限的租户会在提交时就收到 402,而不是跑到一半才失败。
第四步:对账与开票
到了结算周期末,从控制台使用记录页导出 CSV,可按 Key 和日期范围筛选(每个文件最多 50,000 行、400 天)。每一行都有时间、模型、用量、官方牌价币种、官方费用、折扣、汇率、实扣金额和备注,所以每一行都能复算:
官方费用 × 折扣 ÷ 汇率 = 实扣金额
失败请求会标注为未扣费,中断的请求会标注为已按产生的输出计费。按 Key 汇总实扣金额,和你的账本比对,再把第二步里的流式待结算记录补齐。控制台还按 Key 提供消费和请求趋势图。如果你通过 MCP 客户端操作,只读的 get_usage 工具会按模型返回当前 Key 的请求数、token 数和费用合计,默认最近 7 天,最长 92 天。
向租户收多少钱由你决定。常见做法是成本加一定利润,或者按高于预期成本的价格出售额度包。既然账本里记的是真实扣费,哪种方式都只是一次乘法。
第五步:给租户显示剩余额度
想在你的后台显示实时的「剩余额度」,用租户的 Key 调 GET /v1/usage:
curl https://api.hop-base.com/v1/usage \
-H "Authorization: Bearer sk-tenant-key"
一个额度 $10、已用 $2 的 Key,balance 为 8,quota.total 为 10,quota.used 为 2,单位都是美元。如果 Key 没有单独设置额度,balance 就是账户余额,所以打算展示这个数字的话,请给每个租户 Key 都设上额度。即使 Key 还有剩余额度,账户余额先用完时也会返回 402。这个接口本身不计费。
尊重租户边界的错误处理
- 402
insufficient_quota:该租户的额度已用完,或者你的账户余额为零。用GET /v1/usage区分是哪一种,然后只对该租户显示「额度已用完」。 - 429
apikey_concurrency_limit:该租户超出了自己的并发上限。只对这个租户排队或退避,不影响其他租户。 - 429
user_concurrency_limit:你整个账户到了上限。这需要谈容量,而不是针对某个租户修。 - 404
model_not_found:租户请求了套餐外的模型。可以把它映射成升级提示。
上线前检查清单
- 每个租户 Key 都设了额度和并发上限。
- 账本写入的是返回的扣费,绝不是本地价格表。
- 流式请求和中途断开的请求,用 CSV 导出来结算。
- 异步任务的扣费在任务进入终态时写入,并按各租户存下的任务 ID 归属。
- 每月对账时,按 Key 比对账本合计和导出文件。
常见问题
不维护价格表,怎么按客户追踪大模型成本?
读取网关返回的扣费。在 HopBase 上,同步调用看 X-HopBase-Cost-USD 响应头或 usage.cost_usd,异步任务完成后看 usage.cost_usd。它们都是该请求实际扣掉的美元金额。
能限制某个客户最多花多少吗?
能。给这个客户的 API Key 设置额度。用完后,该 Key 的请求返回 402 insufficient_quota,其他 Key 照常工作。
为什么流式响应没有扣费响应头?
响应头在生成开始前就已发出,那时最终扣费还不知道。把流最后一个 usage 事件里的 token 数记为待结算记录,再按控制台使用记录或 CSV 导出补上金额。
不同客户能用不同的模型吗?
能。每个 Key 只挂你选的分组(1 到 10 个),套餐由 Key 本身来执行。调用 Key 分组之外的模型会返回 404。
详细说明在哪里?
扣费字段和 /v1/usage 见账户与目录 API,上限见并发、超时与计费,CSV 对账方法见我们的用量对账指南。牌价见 hop-base.com/pricing。