Skip to content
上新Nano Banana 2.1 已上线:Google 新一代生图模型,单张低至 $0.0336,原生 4K,约上一代半价查看模型
HopBase
← 返回博客

多租户大模型用量按客户计费:一租户一 Key 与真实扣费

先说结论:多租户 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:租户请求了套餐外的模型。可以把它映射成升级提示。

上线前检查清单

  1. 每个租户 Key 都设了额度和并发上限。
  2. 账本写入的是返回的扣费,绝不是本地价格表。
  3. 流式请求和中途断开的请求,用 CSV 导出来结算。
  4. 异步任务的扣费在任务进入终态时写入,并按各租户存下的任务 ID 归属。
  5. 每月对账时,按 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。