开源 Codex 代理中转站:一键部署 Codex API 网站,支持 API Key、Token 记录和套餐管理

发布时间:2026/9/29 3:34:46
开源 Codex 代理中转站:一键部署 Codex API 网站,支持 API Key、Token 记录和套餐管理 1. 为什么我要自己搭一个 Codex API 代理中转站如果你手上有多个脚本、多个客户端都想调用 Codex 这类模型直接把上游 Key 散落在每台机器上很快就会遇到三个问题Key 泄露了不知道谁在用、每个调用方用量对不上账、想给不同人分配不同额度却只能靠手动改配置。Codex API 代理中转站要解决的就是这件事——对外只暴露一个统一地址和一组自建 API Key对内把请求转发到上游同时把 Token 用量、套餐额度、Key 状态全部记下来。这个场景适合三类人个人开发者想给自己几个项目做统一入口小团队需要给成员发不同 Key 并限制额度运营者想做一个带注册、套餐、兑换码的 API 管理站点。技术选型上Spring Cloud Gateway 是天然合适的——它本身就是响应式网关路由、过滤器、限流、鉴权都有现成抽象配合 Spring WebFlux 做非阻塞转发单机扛住日常调用量没什么压力。我这次落地的路径是用 Spring Cloud Gateway 做转发骨架在全局过滤器里做 API Key 校验和 Token 计量套餐管理落到数据库上游统一走 TaoToken 的 API 通道。下面按“能直接抄”的粒度给出配置和验证步骤。2. TaoToken 前置统一 Key 与 API 通道在写网关代码之前先把上游通道确定下来。TaoToken 提供统一的 API 入口你只需要一个平台 Key就能在网关里作为上游凭证使用不用把多个厂商的 Key 分散配置。官网地址https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基地址https://taotoken.net/api你需要先在控制台创建一个 API Key这个 Key 会写进网关的上游转发配置里。创建入口在 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite如果你还想先确认模型对话链路是否通可以先用模型对话页面手动发一条请求https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite接入文档在这里路由路径和鉴权头的细节以文档为准https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite注意平台 Key 只放在网关服务端绝对不要下发到客户端。客户端拿到的应该是你在网关里生成的、带额度限制的自建 Key。3. 可复制配置Spring Cloud Gateway 路由与过滤器骨架3.1 依赖与基础配置项目基于 Spring Boot Spring WebFlux Spring Cloud GatewayJava 8 及以上都能跑。核心依赖dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis-reactive/artifactId /dependencyapplication.yml里配置路由和上游地址。这里把/v1/**转发到 TaoToken 的/api/v1/**/api/**转发到上游/api/**server: port: 8080 spring: cloud: gateway: routes: - id: codex_v1 uri: https://taotoken.net predicates: - Path/v1/** filters: - StripPrefix0 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 20 redis-rate-limiter.burstCapacity: 40 key-resolver: #{apiKeyResolver} - id: codex_api uri: https://taotoken.net predicates: - Path/api/** - id: dashboard uri: http://localhost:8081 predicates: - Path/dashboard/** redis: host: 127.0.0.1 port: 6379 taotoken: upstream-key: sk-你的平台Key upstream-base: https://taotoken.net/apikey-resolver用请求头里的 API Key 做限流维度这样每个自建 Key 独立计数Configuration public class RateLimitConfig { Bean KeyResolver apiKeyResolver() { return exchange - Mono.justOrEmpty( exchange.getRequest().getHeaders().getFirst(Authorization) ).defaultIfEmpty(anonymous); } }3.2 API Key 校验过滤器全局过滤器做三件事解析客户端 Key、查库校验状态和额度、把上游 Key 替换进去。校验不通过直接返回 401 或 429不往后转发。Component public class ApiKeyAuthFilter implements GlobalFilter, Ordered { Autowired private ApiKeyService apiKeyService; Value(${taotoken.upstream-key}) private String upstreamKey; Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path exchange.getRequest().getURI().getPath(); if (path.startsWith(/dashboard) || path.startsWith(/auth)) { return chain.filter(exchange); } String raw exchange.getRequest().getHeaders().getFirst(Authorization); if (raw null || !raw.startsWith(Bearer )) { return reject(exchange, HttpStatus.UNAUTHORIZED, missing api key); } String clientKey raw.substring(7); return apiKeyService.findByKey(clientKey) .flatMap(record - { if (!record.isActive()) { return reject(exchange, HttpStatus.FORBIDDEN, key disabled); } if (record.getQuotaUsed() record.getQuotaLimit()) { return reject(exchange, HttpStatus.TOO_MANY_REQUESTS, quota exceeded); } ServerHttpRequest mutated exchange.getRequest().mutate() .header(Authorization, Bearer upstreamKey) .header(X-Client-Key, clientKey) .build(); return chain.filter(exchange.mutate().request(mutated).build()); }) .switchIfEmpty(reject(exchange, HttpStatus.UNAUTHORIZED, invalid api key)); } private MonoVoid reject(ServerWebExchange exchange, HttpStatus status, String msg) { exchange.getResponse().setStatusCode(status); exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); byte[] body ({\error\:\ msg \}).getBytes(StandardCharsets.UTF_8); return exchange.getResponse().writeWith( Mono.just(exchange.getResponse().bufferFactory().wrap(body))); } Override public int getOrder() { return -100; } }3.3 Token 用量记录过滤器计量放在响应阶段从上游返回的 JSON 里取usage.total_tokens累加到对应 Key 的用量上。用ServerHttpResponseDecorator拦截响应体Component public class TokenUsageFilter implements GlobalFilter, Ordered { Autowired private UsageService usageService; Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String clientKey exchange.getRequest().getHeaders().getFirst(X-Client-Key); if (clientKey null) { return chain.filter(exchange); } ServerHttpResponse original exchange.getResponse(); DataBufferFactory factory original.bufferFactory(); ServerHttpResponseDecorator decorator new ServerHttpResponseDecorator(original) { Override public MonoVoid writeWith(Publisher? extends DataBuffer body) { return super.writeWith(DataBufferUtils.join(body).map(buffer - { byte[] bytes new byte[buffer.readableByteCount()]; buffer.read(bytes); DataBufferUtils.release(buffer); String json new String(bytes, StandardCharsets.UTF_8); int tokens JsonParser.extractTotalTokens(json); if (tokens 0) { usageService.record(clientKey, tokens); } return factory.wrap(bytes); })); } }; return chain.filter(exchange.mutate().response(decorator).build()); } Override public int getOrder() { return -50; } }usageService.record里做两件事往token_usage表插一条明细同时UPDATE api_key SET quota_used quota_used ?。套餐管理就是在这张表上做聚合——按 Key 查当日、当月用量和套餐里的额度上限比对。3.4 套餐与额度表结构套餐管理不需要复杂设计三张表够用表名关键字段作用api_keyid, key_value, user_id, status, quota_limit, quota_used自建 Key 与额度token_usageid, key_value, tokens, model, created_at每次调用的用量明细planid, name, quota, price, duration_days套餐模板用户购买或兑换套餐时把plan.quota累加到api_key.quota_limit兑换码表记录核销状态即可。计费倍率可以放在plan表里加一个rate字段计量时tokens * rate再累加。4. 验证请求curl 跑通鉴权与计量链路启动服务后先用一个自建 Key 发一条对话请求确认转发和计量都生效。curl -X POST http://localhost:8080/v1/chat/completions \ -H Authorization: Bearer sk-你的自建Key \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [{role: user, content: say hi}] }预期返回正常的choices结构。如果返回 401说明 Key 校验没过返回 429说明额度用尽。接着验证计量是否落库。查一下这个 Key 的用量curl http://localhost:8080/dashboard/api/usage?keysk-你的自建Key返回里应该能看到total_tokens大于 0且和刚才那次请求的usage.total_tokens一致。再查一次 Key 状态curl http://localhost:8080/dashboard/api/key/status?keysk-你的自建Key确认quota_used已经增加。如果用量没变多半是响应体被DataBufferUtils.join消费后没有正确回写检查writeWith里是否把bytes重新包成了DataBuffer。再测一下禁用逻辑把 Key 的status改成 0重新发请求应该返回 403。这一步能确认过滤器里的状态判断生效。5. 本篇常见错排查转发 404路径对不上。最常见的是StripPrefix用错。如果你配了Path/v1/**且StripPrefix1转发到上游就变成/chat/completions少了/v1。TaoToken 的路径是/api/v1/**所以要么不 Strip要么在uri里补全前缀。建议先用curl -v看实际转发的 URL。响应体被消费两次导致客户端收不到内容。ServerHttpResponseDecorator里DataBufferUtils.join会把流读完如果忘记把字节重新写回客户端拿到空响应。正确做法是读完、解析、再用factory.wrap(bytes)返回一个新的DataBuffer。Redis 没起导致限流报错。RequestRateLimiter依赖 Redis本地没启动 Redis 时网关启动会失败或请求直接 500。要么先起 Redis要么临时把限流过滤器去掉再调通主链路。上游 Key 没替换成功。检查mutate().header(Authorization, ...)是否在chain.filter之前执行。如果先 filter 再 mutate上游收到的还是客户端 Key会鉴权失败。计量数字对不上。流式响应stream: true的 usage 在最后一个 chunk 里DataBufferUtils.join能拿到完整流但解析时要遍历所有data:行找usage字段。非流式响应直接取根节点的usage.total_tokens即可。额度判断有并发问题。高并发下quota_used quota_limit的判断和后续累加不是原子的可能超发。生产环境建议用 Redis 的INCR做预扣或者数据库行锁。6. 后续接入与长期使用建议网关跑通之后客户端接入只需要把 base_url 指向你的网关地址Key 换成自建 Key。如果你要长期跑编码类 Agent 或高频调用建议直接看 Coding Plan额度和计费方式更适合持续使用https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite控制台里可以管理用户、Key 和套餐https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite如果你用 Claude Code 这类客户端接入配置参考https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite我自己的做法是网关只做鉴权、转发、计量三件事套餐和用户体系单独一个服务通过内部接口调用。这样网关的过滤器链路足够短出问题时排查范围小。另外平台 Key 定期轮换轮换时只改网关配置客户端无感知。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询