自建OpenAI兼容网关接入6家中国大模型:快乐路径之外的五个坑

自托管 API 网关 He-API 统一六家中国模型,真正难点在 token 计费与分发。
He-API 是一个自托管的 OpenAI 兼容 API 网关,把 DeepSeek、Kimi、Qwen、Doubao、ERNIE、GLM 六家中国模型提供商统一到一个端点和一把密钥后面,动机是中国模型成本比西方模型低 80%–90%,但接入体验割裂。作者用两个月全职时间搭建了 Go 网关、Redis 限流、Kafka + ClickHouse 用量管道和 PostgreSQL 元数据存储。让项目差点翻车的并非架构,而是 token 计费:金额不能用浮点数、上游用量上报口径不一、对账必须可回溯。此外还有流式与错误格式的“兼容”陷阱、热重载、双向限流和 base_url 拼接等问题。项目已端到端验证,但零用户零收入,作者强调构建与分发是两种不同技能。
一个自托管的 API 网关,把 DeepSeek、Kimi、Qwen、Doubao、ERNIE、GLM 六家中国模型提供商统一收敛到一个 OpenAI 兼容端点后面——听起来是个下午就能搭完的活。作者花了两个月全职时间,其中差点让项目翻车的,既不是路由层,也不是 Kafka 管道,而是 token 计费。
这个项目叫 He-API,动机朴素得近乎无聊:中国模型在多数工作负载上比 GPT-4/Claude 级别的西方模型便宜 80%–90%,但对身处中国之外的开发者而言,实际接入相当痛苦——中文文档、无法国际通用的支付方式、各家不一致的鉴权、没有统一格式。于是,一个端点、一把密钥、一套请求结构,成了这个项目的基本命题。

架构长什么样
整体链路并不复杂:客户端(任意 OpenAI SDK)→ Nginx + Let's Encrypt → Go 编写的 api-gateway,网关内部依次经过 Redis 支撑的权限解析器、Redis + Lua 实现的限流器、模型路由器、各提供商适配层(kimi / deepseek广告 / qwen / doubao / ernie / glm),最终打到上游提供商。用量事件经 Kafka 写入 ClickHouse,账单、模型与提供商元数据落在 PostgreSQL。
15 个服务跑在 systemd 上,除一个 Next.js 管理控制台外全部是 Go;中间件包括 Postgres、Kafka、Redis、ClickHouse,22 个迁移脚本全部幂等。作者的原话是:"这是所有人都会画的那张图。下面才是真正咬人的地方。"
坑一:"OpenAI 兼容"九成是边缘情况
一个非流式 completion 的快乐路径,作者一个下午就搞定了。之后的全部时间都花在长尾上。
流式输出是第一道坎。各家提供商对 chunk 结构的理解并不一致:有的会发 data: [DONE],有的直接关连接;有的在最后一个 chunk 里带 usage 块,有的在流式场景下干脆不报用量,只对非流式调用报告。对网关开发者而言,"兼容"的真实含义是"对你恰好测过的那几条路径兼容"。
错误结构差异同样棘手。某家的 400 是带 error.message 字段的 JSON,另一家是一个裸字符串,第三家则返回 200 但把错误藏在 body 里。网关必须把这一切归一化成不会破坏客户端既有错误处理逻辑的形式——否则"零代码改动"这句话,在第一次某家上游打喷嚏时就会变成谎言。
坑二:用量记账是 schema 问题,不是代码问题
这是差点让项目翻车的一环。
按 token 计费意味着 token 数量就是钱,而钱永远不该用浮点数表示。单价大约在每百万 token 0.1 到 2 元人民币之间,单次请求金额极小;如果用 float64 做算术,误差会累积,并在数百万行数据上复利式放大。作者的做法是:Postgres 用 NUMERIC,热路径用整数微单位,并显式决定舍入发生在哪一层。
更麻烦的是对账。由于各家上报用量的方式不一致(见上一条),不能盲目信任上游数字。最终方案是在网关侧解析用量,并为从不报告用量的流式请求保留本地估算兜底,同时保留逐请求的审计明细,让任何一张账单都能回溯到原始事件。
作者给出的教训相当直白:在写下第一行计费代码之前就定好金额的表示方式,而不是在发现一个解释不清的 0.3% 差异之后。
为什么金额不能用浮点数:IEEE 754 双精度浮点数(float64)只能精确表示分母为 2 的幂的分数,0.1、0.2 这类十进制小数在二进制下是无限循环小数,存储时必然被截断。单次误差可能在 10⁻¹⁶ 量级,看似无关紧要;但当单价低至每百万 token 0.1 元、单次请求金额本身就是极小值时,误差相对于被计量数值的比例会被显著放大。再乘以数百万次请求并叠加月度结算,就会产生对不上账的差异。NUMERIC 是 PostgreSQL 提供的任意精度十进制类型,按十进制字面量精确存储,适合做金额字段;所谓“整数微单位”则是把 1 元表示为 1,000,000 个最小单位,用 BIGINT 存储和运算,二者都能保证加减法的精确性。关键不只是选类型,而是显式决定舍入发生在哪一层——是在每次请求计费时、日汇总时,还是出账时,因为不同的舍入点会得到不同的总额。
ClickHouse 与 Kafka 在这条链路里的角色:Kafka 作为用量事件的缓冲队列,保证上游网关的写路径不被下游分析存储的抖动阻塞;ClickHouse 是列式 OLAP 数据库,擅长对海量事件做聚合扫描(如“某账号某月总 token 数”),但不擅长高频单行更新与事务。因此原始用量事件进 ClickHouse 做分析与审计,而账单、模型与提供商这类需要事务一致性的元数据放 PostgreSQL。二者的写入延迟与一致性语义不同,这也解释了为什么作者说“自托管 ClickHouse 对两人团队是凌晨两点多照看的一个东西”——它需要独立的容量规划与运维。
坑三:热重载比重新部署更重要
提供商密钥最早需要重启才能生效,这意味着每次密钥轮换都要重新部署,也就意味着一个请求必然失败的窗口期。
修复方式毫不光鲜,却消除了一整类运维痛苦:网关每 30 秒轮询一个内部的 active providers 端点,原子性地替换内存中的提供商集合。密钥以 AES-256-GCM 静态加密,只在内存中解密。轮换一把密钥现在就是"在管理控制台里保存,等 30 秒,完事"。作者的建议是:如果你在做任何对接多个第三方 API 的东西,在你需要热重载路径之前就把它建好——运维痛苦的增长速度比功能需求快。
AES-256-GCM 与静态加密:AES-256 指密钥长度 256 位的对称加密算法,在可预见的算力下暴力破解不现实;GCM(Galois/Counter Mode)是一种带关联数据的认证加密模式,除机密性外还提供完整性校验,能检测密文是否被篡改。所谓“静态加密”(encryption at rest)指密钥不落盘明文,数据库中存的是密文,网关启动时用主密钥解密到内存,只在内存中以明文形式短暂存在。这种做法把泄露面从“数据库被读”收窄到“进程内存被读”,是密钥管理的常见起步姿势。
需要注意的是,主密钥本身仍需保护(环境变量、KMS 或 systemd 的凭据机制),否则静态加密只是把问题往后挪了一层。热重载路径本身也要考虑失效语义:轮询间隔(此处 30 秒)决定了密钥轮换的生效延迟,若某把密钥被紧急吊销,仍需一个更快的主动失效通道。
坑四:限流需要两个维度,不是一个
作者最初的限流器只有一个按 key 的 QPS 数字。它以一种不易察觉的方式失效:内部子请求和面向用户的请求消耗同一份预算,于是一个 QPS=1 的免费层账号把自己限流了。
能用的版本有两个维度——稳定的按 key 速率,加上一个突发桶,桶的大小按账号年龄和消费额缩放。新账号只给很小的突发额度,有历史记录的账号则给得宽松。这种做法粗糙,但它几乎杀掉了全部自动化滥用,同时不碰真正的重度用户。计数器的操作本身也要小心:应用层的读后写在负载下会产生竞态,检查并自增应当放进一段针对 Redis 的原子 Lua 脚本里。
令牌桶与原子操作:令牌桶(token bucket)是限流的经典模型,桶以恒定速率补充令牌,每个请求消耗若干令牌,桶空则拒绝。相比固定窗口计数器,它能允许短时突发流量,同时约束长期平均速率——这正是作者所说“按 key 的稳定速率 + 按账号年龄和消费额缩放突发桶”的理论基础。
为什么读后写会产生竞态:限流的标准逻辑是“读当前计数 → 判断是否超限 → 自增计数”。在并发环境下,两个请求可能同时读到同一个旧值,双双判定为未超限,然后各自自增,导致实际放行量超过阈值。这类问题的通用解法是把“检查并自增”做成一个不可分割的原子操作。Redis 的 Lua 脚本在执行期间是单线程、不可被打断的,因此把判断逻辑和数据修改一起写进脚本,就能在单次脚本调用内完成整套动作,避免应用层往返带来的窗口期。同理,Redis 的 INCR、SET NX 等原生命令也是常用的原子构件。这也呼应了作者“内部子请求与用户请求共享同一预算”的教训——限流维度设计错了,再原子也救不了。
坑五:base_url 陷阱
小、尴尬,但值得写下来。多数 OpenAI SDK 会把路径拼接到你的 base_url 上,如果你文档里写 https://api.example.com,而 SDK 追加了 /v1/chat/completions,就会 404。有些 SDK 想要主机名,有些想要带 /v1 的版本。这里搞错,你的第一次集成就失败了,而失败原因跟你的实际产品毫无关系。
作者会做得不一样的地方
- 先写计费正确性测试。 不是因为计费有趣,而是因为其他一切改动成本都很低,唯独它不是。
- 假定每一个上游字段都是可选的。 按最差的上报者设计归一化层,再为表现好的做优化。
- 这个规模的项目不要自托管 ClickHouse。 它很优秀,纸面上也是正确选择;但对一个两人的团队来说,它只是凌晨两点又要多照看的一个东西。
- 第一天就问分发问题。 见下文。
现状:零用户,零收入
作者给的状态很诚实:它能工作,已端到端验证(真实上游调用 → 真实模型响应 → HTTP 200,在仓库里有记录),但零用户、零收入。
"我构建了它。我没有分发它。这是两种不同的技能,而我只擅长其中一种"——这恰恰是没人会在你听到"只要做出来,用户自然会来"时告诉你的那件事。
代码公开在 GitHub 的 page6668/He-API 仓库。作者尤其愿意在评论里回答用量记账和热重载这两块的问题,因为那是他第二次做会换个做法的地方。此外,如果有人在东南亚或中东,想基于这个成本优势做 API 转售生意,源码、线上环境、域名、运维脚本和交接文档整体可谈。
对多数开发者而言,这篇文章真正值钱的不是架构图,而是那五个坑的排序:计费正确性排在最前面,而分发问题排在最容易被忽略的最后。
背景补充
关于“OpenAI 兼容”的行业背景:OpenAI 的 Chat Completions API 事实上已成为行业事实标准,大量 SDK、框架与工具默认以此接口为交互约定(如 base_url、/v1/chat/completions 路径、choices[].delta 流式增量、usage 字段结构)。因此新入场的模型提供商普遍选择“兼容 OpenAI”而非自创协议,以复用整个生态的客户端。但由于官方并未提供形式化的兼容性测试套件,各家实现在边缘路径上分歧明显:流式结束信号、错误体结构、usage 在不同调用模式下的出现与否、finish_reason 取值等。这正是原文“兼容的真实含义是对你恰好测过的那几条路径兼容”背后的结构性原因——兼容是契约的近似,而非契约本身。对网关开发者而言,可行的做法是把归一化层当作独立组件,按“最差上报者”设计字段的可选性,并为每个提供商维护一组契约测试。
相关推荐

Claude Pro不含API权限?Max与Team计划新增API额度详解
Claude Pro订阅不含API访问权限,API需单独计费。但Max和Team计划现已包含每月API额度:Max 5x为100美元,Max 20x为200美元,Team每席位最高合计500美元。本文详解各档位额度、领取方式及对开发者的影响。

隔离环境实测35万星REA:4秒装完,但两个坑要当心
隔离实验室实测35万星开源逆向工程工具REA:4秒安装、15秒列出截图工具全部外联地址、梳理565个通信通道,同时暴露写入用户级设置与31个高风险工具权限两个坑,附三条安全使用规则。
逆向工程+AI Agent:几周克隆Adobe应用的技术真相
逆向工程+AI Agent:几周克隆Adobe应用的技术真相
一位开发者因Adobe提前取消费转向逆向工程,用AI Coding Agent几周内做出7款开源替代品。本文解析逆向工程原理、Reverse Engineer Anything这个MCP Server六天破10万星的原因,以及AI克隆应用在性能、专有数据和法律层面的真实边界。