1. 中转站不只是转发请求
最简单的代理接口只做三件事:接收请求、替换上游密钥、把响应返回客户端。这种代码可以用于本地验证,却不能直接经营一个按量计费的中转站。
只要平台开始向多个用户发放 API Key,就必须同时回答几个问题:这个用户能使用哪些模型,当前余额是否足够,这次请求应该选择哪个上游账号,流式响应中途断开怎样收费,失败重试是否会产生两笔账,以及管理员能否还原某次扣费。
Sub2API 的定位正是 AI API 网关。它把鉴权、Token 计费、账号调度、并发限制和请求转发放在同一条调用链中。我们会参考它的职责拆分,但用 Hono 和 TypeScript 重新实现适合当前课程的版本。
2. 完整请求链路
一笔请求先通过控制规则,再进入上游调用,最后根据实际用量结算。下面这张图先把完整顺序固定下来:
顺序不能随意调整。例如 RPM 计数应当放在余额和权限检查之后,否则一个已经被禁用的 API Key 仍会消耗限流计数。价格解析要在转发前完成,否则模型回答结束后发现价格不存在,系统既不能向用户收费,也无法撤销已经产生的上游成本。
请求上下文还需要携带几组稳定标识:
01export interface BillingContext {02requestId: string03userId: string04apiKeyId: string05groupId: string06requestedModel: string07billingModel: string08pricingVersionId: string09traceId: string10startedAt: Date11}
这些字段不能在异步结算时重新猜测。模型路由可能已经切换,上游账号也可能更新,必须把请求开始时确定的计费上下文作为快照传给结算任务。
3. 控制面与数据面
中转站可以分成控制面和数据面。控制面负责低频配置,例如模型价格、用户分组、渠道倍率、上游账号、套餐和限额。数据面处理每一笔模型请求,对延迟和稳定性更敏感。
控制面修改价格时,不应该直接覆盖正在执行请求所使用的数据。更稳妥的做法是创建新价格版本,并设置生效时间。数据面在请求开始时解析一次价格,把 pricingVersionId 固定下来,后续流式响应持续几分钟也不会中途变价。
数据面通常包含以下服务:
| 服务 | 负责内容 |
|---|---|
| Auth Service | 校验平台 API Key,得到用户与分组 |
| Eligibility Service | 检查余额、套餐、配额和限流 |
| Pricing Service | 解析模型、价格版本和倍率 |
| Scheduler | 选择可用的上游账号或渠道 |
| Proxy Service | 转发请求并处理 SSE |
| Usage Adapter | 把厂商 usage 转为统一格式 |
| Billing Service | 预占、计算、结算和释放 |
| Ledger Service | 原子写入账本和余额快照 |
服务是职责边界,不代表必须部署成八个微服务。课程实现会先放在一个 Hono 应用中,通过模块和接口隔离;只有流量、团队和故障边界真正需要时,才考虑拆分进程。
4. 中转站的核心要求
设计评审时,可以从六类要求检查系统是否完整。
准确性要求每类 Token 使用正确单价,倍率和舍入规则可复算。一致性要求账本、余额和用量不能出现一部分成功、一部分失败。幂等性要求客户端、网关和队列重复提交都不会重复扣费。
可用性要求 Redis 或价格源短时故障时有明确策略,不能靠捕获异常后继续免费转发。可追溯性要求账单能够关联用户、API Key、模型、上游账号、请求和价格版本。可控制性则包括余额下限、套餐额度、RPM、TPM、并发数和单次请求预算。
这些要求之间会发生冲突。限流缓存故障时,可以选择临时放行以保住可用性;余额查询故障时继续放行却可能造成直接损失。因此系统不能只有一个笼统的“降级模式”,而要为每种依赖定义故障策略。
5. 哪些数据可以相信
客户端提交的 model、max_tokens 和业务标签可以参与路由,但客户端提交的 Token 数量不能作为收费依据。否则用户只要把 usage.input_tokens 改成零,就能绕过计费。
正式账单的证据优先级应当是:上游响应中的 usage、上游账单或用量查询接口、平台 tokenizer 估算。前两项是实际执行结果,估算只能用于缺失 usage 时的应急处理。
LangChain callback 和 LangGraph state 可以告诉我们某次 Agent 运行调用了哪些模型节点,适合做费用归因和预算统计。但最终扣费仍由网关完成,因为只有网关掌握真实上游响应、路由结果和平台 API Key。
6. 故障时应该放行还是拒绝
Sub2API 为计费缓存设置了熔断器,连续失败后进入 open 状态,暂时拒绝新的计费请求。这属于 fail-closed:无法确认是否有资格付费时,不把请求送到上游。
课程实现会采用下面的默认策略:
| 故障 | 默认处理 | 原因 |
|---|---|---|
| 余额或套餐状态不可读 | 拒绝并返回 503 | 无法确认支付能力 |
| 模型价格不存在 | 转发前拒绝 | 不能产生无法定价的成本 |
| RPM 缓存短时不可用 | 可按配置临时放行 | 影响容量控制,不直接改变账务 |
| 用量异步队列已满 | 同步结算或拒绝 | 不能丢失扣费任务 |
| 审计日志写入失败 | 账务成功后进入补写队列 | 不应回滚已经完成的上游调用 |
这里还需要区分“请求已经转发”和“尚未转发”。转发前可以安全拒绝;流式内容已经发给客户端后,再返回错误也不能抹去上游费用,此时必须进入待对账状态。
7. 总结
AI 中转站的核心不是反向代理,而是在一次上游调用周围建立鉴权、准入、定价、调度、计量和结算边界。任何一个环节缺失,问题最终都会落到错收、漏收或无法解释的账单上。
下一篇会把这些职责落实为数据表。我们会重点区分用量事件、账本和余额快照,因为三者看起来都在记录金额,实际承担的责任完全不同。