ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Codex CLI接入自建OpenAI兼容中转网关:模型映射与流式转发实战

Codex CLI接入自建OpenAI兼容中转网关:模型映射与流式转发实战 简介Codex中转API部署项目代码包提供了在Mac系统上快速接入Codex CLI与中转API的完整实施方案面向需要绕开官方限制、通过自定义中转地址调用代码生成能力的开发者解决模块安装、全局配置文件准备、环境变量设置到功能验证的全流程落地问题新手也能按包内指引逐步完成基础搭建。压缩包体积仅8KB共含3个文件分别以html、inscode、gitignore类型呈现html存放部署步骤与配置说明inscode作为可执行的部署/启动脚本gitignore用于规范项目版本控制范围三者共同构成一份轻量但可实操的代码包。资源已有3898人学习适合具备基础终端操作能力、希望提升代码生成效率的中高级软件工程师。通过该代码包读者可对照完成Codex CLI安装与全局配置明确API Key和中转地址的写入位置并掌握401 Unauthorized错误从检查认证信息到修正配置的完整排查思路从而缩短环境对接时间避开常见网络转发与鉴权陷阱。1. Codex 部署第一件事把官方端点换成自己的中转 APICodex 是 OpenAI 的命令行编程智能体装好之后能在终端里直接读仓库、改代码、跑测试。但真正用起来第一步往往卡在端点配置上默认配置去连官方端点Key 散落在各台机器想换模型还要改一堆环境变量。下面这套组合是我一直在用的落地方式自己起一个 OpenAI 兼容的中转 API 网关把 Codex CLI 的请求全部收口到这个网关由网关统一鉴权、改写模型名、记录日志再转发给真正提供模型能力的上游服务。适合手里有多份模型 Key、需要在团队共用、或者想把 Codex 接到 DeepSeek 这类非默认模型的人。读完你可以照着代码从零搭出网关把 Codex 接进去跑通并且知道哪些日志字段是排查问题时最该盯的。2. 自建 OpenAI 兼容中转网关Key 收口、模型映射与转发代码2.1 为什么不用“官方直连”而要自建中转Codex 默认读取环境变量或 auth.json 里的登录态去连官方端点机器少的时候够用人一多问题就来了。第一是 Key 散落每台开发机都要放一份真实 Key换人、离职、轮换的时候根本不知道谁手里还留着旧的第二是想换模型很别扭Codex 的模型名跟上游模型名往往不是一回事官方配置只让你选模型名没法做“把 Codex 的请求改写成一个完全不同的上游模型”这种事第三是没法记账官方控制台能看到账号维度的用量但看不到项目维度、用户维度的请求量和 token 消耗。自建中转网关就是冲着这三个问题来的真实 Key 只存在服务器上开发机拿到的只是一个随时可以吊销的随机串模型映射由网关统一控制Codex 侧不用感知上游变化日志按用户、按项目、按模型分别落盘月底对账有的放矢。网关本身不生产模型能力它只做协议统一、鉴权、转发、记账这四件事。常见做法有两种走向。一种是用 one-api、new-api 这类现成的开源网关界面里配渠道和令牌就能用功能全、有对账缺点是概念多新手要在一堆字段里分清楚什么是渠道、什么是令牌、什么是模型映射绕起来挺晕。另一种是自己写一个几十行的转发层把 Codex 发来的 OpenAI 兼容请求原样转发到上游。这篇文章采用后者核心原因是你能看到每个字段是怎么流转的出了问题不至于面对一个黑匣子。等你把这条路径跑熟了再换回 one-api 这类项目理解成本会低很多。2.2 极简转发网关的 Python 实现FastAPI httpx下面这段代码是我自己常用的最小可用版本。它监听 8000 端口实现了 Codex 会碰到的两个端点/v1/responses 给新版 Codex 用/v1/chat/completions 给旧版或兼容模式用。请求进来后先校验 Authorization 头是否等于预设的网关 Key再把 body 里的 model 字段按映射表改写最后转发给上游并且用 StreamingResponse 把上游的流式响应逐字节透传回去。# relay.py —— Codex 中转网关最小实现 import os import json import logging from fastapi import FastAPI, Request, Response from fastapi.responses import StreamingResponse import httpx app FastAPI() # 网关自己的 KeyCodex 客户端配置时用这个 GATEWAY_KEY os.environ.get(RELAY_KEY, sk-relay-demo) # 上游模型服务的 /v1 基地址指向真正提供模型能力的地方 UPSTREAM_URL os.environ.get(UPSTREAM_URL, https://api.example.com/v1) # 上游服务的真实 Key只存在服务器环境变量里不下发到客户端 UPSTREAM_KEY os.environ.get(UPSTREAM_KEY, ) # 模型映射Codex 请求的模型名 - 上游模型名 MODEL_MAP json.loads(os.environ.get( MODEL_MAP, {gpt-5: deepseek-chat} )) logging.basicConfig(levellogging.INFO) async def relay(request: Request, upstream_path: str): # 1. 鉴权Codex 发来的 Key 必须等于网关 Key auth request.headers.get(authorization, ) if auth ! fBearer {GATEWAY_KEY}: return Response(status_code401, contentinvalid gateway key) body await request.body() payload json.loads(body) if body else {} # 2. 模型映射只改 model 字段其余参数原样保留 if payload.get(model) in MODEL_MAP: payload[model] MODEL_MAP[payload[model]] # 3. 转发到上游关键必须用 stream 方式不能用普通 post headers { Authorization: fBearer {UPSTREAM_KEY}, Content-Type: application/json, } async with httpx.AsyncClient(timeout600) as client: async with client.stream( POST, f{UPSTREAM_URL}/{upstream_path}, jsonpayload, headersheaders, ) as upstream: response_headers { k: v for k, v in upstream.headers.items() if k.lower() in {content-type, x-request-id} } return StreamingResponse( upstream.aiter_raw(), status_codeupstream.status_code, headersresponse_headers, ) app.post(/v1/responses) async def responses(request: Request): return await relay(request, responses) app.post(/v1/chat/completions) async def chat_completions(request: Request): return await relay(request, chat/completions)逻辑说明relay() 是核心函数Codex 无论走哪个端点都会进到这里。第一步做基础鉴权等于在网关前面加了一道自己的门禁第二步做模型名改写把你 Codex 里的模型名换成上游真正认识的模型名第三步用 httpx 的 stream 方式把请求转发出去并返回 StreamingResponse。这里特意用了 aiter_raw() 而不是把上游响应全部读完再返回目的就是让 SSE 流式响应能一个字节一个字节地透传。Codex 是交互式工具用户看的是流式输出如果网关先攒完整包再吐出来Codex 的界面会长时间停在那转圈体感上跟卡死没区别。参数说明三个环境变量分别控制网关 Key、上游地址、模型映射。RELAY_KEY 是给 Codex 用的可以随时换换了不影响上游UPSTREAM_URL 必须指到上游的 /v1 目录如果指到域名根路径后面的拼接就会变成 /v1/v1/responses肯定 404MODEL_MAP 是 JSON 字符串key 是 Codex 里配置的模型名value 是上游真正认的模型名示例里把 gpt-5 映射到了 deepseek-chat就是常说的 Codex 接入 DeepSeek 的玩法。timeout600 是因为 Codex 跑长任务时单次响应可能持续好几分钟默认 5 秒超时必然翻车。2.3 模型映射、鉴权与日志三个必须写对的配置第一个容易错的是鉴权比较方式。上面代码用的是字符串相等意味着 RELAY_KEY 里不能有多余空格。实际部署时我习惯用 openssl rand -hex 32 生成一段随机串而不是自己编一个好记的单词随机串的好处是别人猜不到换掉也容易。第二个是模型映射。Codex 的默认模型名跟上游模型名往往不是一回事比如 Codex 界面里显示 gpt-5上游如果只部署了 DeepSeek不映射就一定报 model not found。映射表的 value 要跟上游渠道里实际配置的模型标识完全一致大小写都不能错填错的话状态码 400 但错误信息不一定说是模型名的问题。第三个是日志。上面代码为了篇幅只留了 INFO 级日志实际运行我会在转发前加一行import time logging.info(json.dumps({ path: upstream_path, model: payload.get(model), user: request.headers.get(x-user, unknown), ts: time.time(), }))加了这行之后网关日志能看到谁在什么时间请求了哪个路径、原始模型名是什么、映射后变成什么。Codex 客户端给的报错往往很收敛经常就一句「上游返回错误」而网关是唯一能看到双向流量的位置日志不打全后面所有排查都是玄学。我的做法是请求进来先打原始模型名转发前再打一次映射后模型名这样两个字段一对就能确认映射表到底有没有生效。环境变量作用配置建议RELAY_KEY发给 Codex 的网关 Key用 openssl rand -hex 32 生成UPSTREAM_URL上游模型服务的 /v1 基地址必须带 /v1不能指到根路径UPSTREAM_KEY上游服务的真实 Key只放服务器 .env不进 GitMODEL_MAP模型名映射 JSON每次新增模型先确认上游标识什么时候应该从这套极简网关迁移到 one-api 这类项目当你要管理多个用户、多套上游 Key、还要图形化对账时自写网关的鉴权和记账就不够用了。我的建议是先用自写网关验证 Codex 走中转这条路是通的再决定要不要在前面套一层 one-api。迁移时只需要把 RELAY_KEY 换成 one-api 的令牌把 UPSTREAM_URL 换成 one-api 的地址把 MODEL_MAP 换成 one-api 的模型映射Codex 侧一行都不用改。这套极简网关真正的价值不在功能而在于把 Codex 和上游之间的每一个字段都摊开给你看等你看明白了再决定要不要换更重的方案。3. 把 Codex CLI 接到中转端点config.toml 与环境变量二选一3.1 Codex 配置结构model、model_provider 与 env_key 的关系Codex CLI 的配置集中在用户目录下的 .codex/config.toml。第一次接触这个文件的同事几乎都会问同一个问题我已经设置了 OPENAI_API_KEY为什么 Codex 还在要求登录原因是 Codex 的配置有三层容易搞混。第一层 model只决定交互界面上显示哪个模型名第二层 model_provider决定这一组配置走哪套端点第三层 env_key决定从哪个环境变量里读取 Key。默认情况下 Codex 内置了官方 provider你没显式配置时它走官方端点并且优先用 auth.json 里的登录态而不是 API Key。所以只要你没有自定义 provider光设环境变量没用它还是会去找登录令牌。这里有一个很多教程没讲清楚的判断标准base_url 要写到哪一级。Codex 的常见做法是写到 /v1但也有版本要求写到根路径写错了请求路径会变成 /v1/v1/responses 或直接 404。我一般先把 base_url 写成带 /v1 的形式然后跑一次命令去看网关日志里收到的路径。日志是最诚实的Codex 界面上看不出来路径拼错日志一看就知道。Codex 的默认端点也不是一成不变的。新版 Codex CLI 默认走 OpenAI 的 Responses API也就是 POST /v1/responses老版本走 /v1/chat/completions。这个差异直接影响中转网关要支持哪些路由我见过不少同事拿着只实现了 chat/completions 的网关去接新版 Codex结果一发送就报错折腾半天不知道是端点问题。后面第 5 章会专门讲这个坑。另一个容易被忽略的是 auth.json 残留。如果 config.toml 里同时存在官方登录态和自定义 providerCodex 读取优先级会以 model_provider 字段为准但如果 auth.json 里有过期 token某些版本还是会先校验登录态。我处理这种情况的办法很粗暴把 auth.json 备份后移走让 Codex 没有旧登录态可读然后重新用 API key 方式验证。删之前记得备份这是后悔药因为你想切回官方登录态时还得恢复它。3.2 用 config.toml 指定中转网关下面是我在一台开发机上跑通的配置模板。假设中转网关部署在本机 8000 端口。# ~/.codex/config.toml model gpt-5 model_provider myrelay [model_providers.myrelay] name myrelay base_url http://127.0.0.1:8000/v1 env_key RELAY_KEY配置说明model 写 gpt-5 只是会话界面里的显示名这里的 gpt-5 是占位以你实际用的 Codex 版本为准真正的模型改写发生在网关的 MODEL_MAP所以这里不必写成上游真实模型名。model_provider 指向 myrelay 这个自定义小节告诉 Codex 不要用官方默认节点。env_key 告诉 Codex 去读名为 RELAY_KEY 的环境变量注意是环境变量不是 config.toml 里的明文 key。跟官方默认 provider 相比自定义 provider 的好处是不依赖 auth.jsonCodex 启动时不会去检查账号登录态也就不会遇到浏览器登录和验证码环节。配置完之后执行export RELAY_KEYsk-relay-demo codex如果一切正常Codex 进入交互界面同时网关日志出现一条 POST /v1/responses 记录。如果日志里出现 401先确认 RELAY_KEY 是否真在当前 shell 里我经常犯的错是把这个 export 写进了 .bashrc 但还没 source如果是 404回到 3.1 里说的 base_url 问题看路径是 /v1/v1/responses 还是 /responses据此调整末尾的 /v1。3.3 用环境变量覆盖默认端点的适用场景config.toml 是持久化配置适合长期在一台机器上用。但有的场景我不想把网关信息写进配置文件比如临时验证一个新网关、给别人做演示、或者在 CI 里跑一次性任务。这时可以试环境变量覆盖的方式OPENAI_BASE_URLhttp://127.0.0.1:8000/v1 \ OPENAI_API_KEYsk-relay-demo \ codex注意一点环境变量覆盖的是默认 provider不是自定义 provider。如果你的 config.toml 里已经写了 model_provider myrelayCodex 仍然会走 myrelay 的 base_url环境变量根本不会生效。我在这上面吃过一次亏改完 OPENAI_BASE_URL 以为切过去了结果网关日志里一条新请求都没有Codex 还在用旧配置。要让环境变量生效要么不写 model_provider 字段要么临时把它注释掉。我的使用习惯是这样本机长期开发用 config.toml因为要记住网关地址和模型映射服务器上跑批量任务或者临时验证时用环境变量因为不想把个人配置带进共享环境。还有一个常见做法是把网关接到本地模型服务上比如 Ollama 这类支持 OpenAI 兼容端点的本地服务只需要把 UPSTREAM_URL 改成 http://127.0.0.1:11434/v1Codex 侧完全不用再改。还有一个切换技巧Codex 读配置目录是可以通过环境变量覆盖的你可以准备两套配置目录一套连远程网关一套连本地模型。切换时只改环境变量指向的目录比反复改 config.toml 干净得多适合在远程模型和本地模型之间反复横跳的场景。最后多说一句无论用哪种方式验证成功的标准不是 Codex 界面起来了而是网关日志里出现了请求记录界面起来但日志没有说明配置根本没走到网关。4. 部署到服务器并常驻运行Docker Compose 与 systemd 双方案4.1 Docker Compose 启动网关自写网关在本地跑通后下一步是部署到一台所有开发机都能访问的服务器。常见做法是 Docker Compose因为网关依赖很少一个 Python 进程打包进镜像就完事不用在服务器上折腾 Python 版本和依赖冲突。目录结构建议保持简单relay.py、Dockerfile、docker-compose.yml 放同一目录环境变量不直接写进 compose 文件而是通过 env_file 引用 .env。这样换 Key、换上游时只改一个文件。# docker-compose.yml services: relay: build: . ports: - 8000:8000 env_file: - .env restart: unless-stopped# Dockerfile FROM python:3.11-slim WORKDIR /app RUN pip install fastapi uvicorn httpx COPY relay.py . CMD [uvicorn, relay:app, --host, 0.0.0.0, --port, 8000]启动命令和存活验证cp .env.example .env # 首次部署先拷贝模板再改 docker compose up -d curl -X POST http://127.0.0.1:8000/v1/responses \ -H Authorization: Bearer sk-relay-demo \ -H Content-Type: application/json \ -d {model:gpt-5,input:ping} \ -o /dev/null -w %{http_code}\n逻辑说明Dockerfile 里显式安装了 fastapi、uvicorn、httpx 三个依赖没有用 requirements.txt纯粹是为了这个部署场景少维护一个文件生产环境还是建议把三个依赖的版本号固定下来。restart: unless-stopped 保证服务器重启后网关跟着起来。curl 那条命令不是真的调模型而是验证网关本身活着返回 200 说明鉴权和路由都正常返回 401 说明 RELAY_KEY 对不上返回 502 说明 UPSTREAM_URL 填错了或者上游服务不可达这时候去查 .env 里的地址。参数说明端口映射 8000:8000 表示宿主机 8000 对应容器 8000如果服务器上已经有别的服务占用 8000可以改成高位端口比如 18000:8000Codex 侧的 base_url 同步改端口就行。env_file 里至少要配置四个变量RELAY_KEY、UPSTREAM_URL、UPSTREAM_KEY、MODEL_MAP。还有一个经常被忽略的点.env 文件一定不要提交进 Git否则等于把 Key 公开了我在 .gitignore 里固定写一行 .env。4.2 systemd 托管自写网关进程如果你的服务器没有 Docker或者团队规定不允许用容器systemd 是更直接的方式。把 relay.py 放到固定目录建一个 Python 虚拟环境并安装依赖然后写 service 文件# /etc/systemd/system/codex-relay.service [Unit] DescriptionCodex Relay Gateway Afternetwork-online.target [Service] WorkingDirectory/opt/codex-relay EnvironmentFile/opt/codex-relay/.env ExecStart/opt/codex-relay/venv/bin/uvicorn relay:app --host 0.0.0.0 --port 8000 Restartalways RestartSec3 [Install] WantedBymulti-user.target启动命令sudo systemctl daemon-reload sudo systemctl enable --now codex-relay systemctl status codex-relay --no-pager说明EnvironmentFile 指定 .env 路径systemd 会把里面的变量注入到进程环境里这是最简单可靠的传参方式ExecStart 用虚拟环境里 uvicorn 的绝对路径避免 PATH 里多个 Python 版本互相干扰Restartalways 意思是一旦进程退出就拉起RestartSec3 是失败后等 3 秒再拉防止疯狂重启。Docker 和 systemd 怎么选我的判断依据很简单服务器上已经有容器编排体系就选 Docker没有就选 systemd两套方案都能让网关常驻。唯一的差别是日志获取方式Docker 用 docker compose logs -f relaysystemd 用 journalctl -u codex-relay -f。后面要排查时先把日志拉起来再看 Codex 表现会省很多冤枉时间。4.3 连通性验证用 curl 模拟 Codex 的请求路径部署完网关后不要急着打开 Codex先用 curl 把 Codex 的请求路径完整模拟一遍。前面 4.1 的 curl 只验证了网关存活这一条才是端到端验证它会真的走到上游模型服务curl -N -X POST http://127.0.0.1:8000/v1/responses \ -H Authorization: Bearer sk-relay-demo \ -H Content-Type: application/json \ -d {model:gpt-5,input:say ok} \ --max-time 30-N 参数是关闭 curl 自身的缓冲让 SSE 流式输出像 Codex 那样实时打到终端这一步能提前发现第 5 章要说的缓冲问题。观察三个点第一状态码是 200 而不是 400/401/502第二输出内容里有没有响应文本第三网关日志里有没有打印模型映射前后的状态。如果返回 200 但内容为空多半是上游模型对 input 结构有要求这个要在网关日志里翻转发出去的请求体看跟上游文档要求的差异。还有一个细节网关从本机挪到服务器后Codex 的 base_url 里的 127.0.0.1 必须改成服务器地址。经常有人本机验证通过后把网关部署到服务器忘了改 Codex 配置结果 Codex 还在连本机端口日志当然看不着。这个地址应该填开发机能访问到的服务器 IP而不是部署机器上的 localhost。Codex 真实请求和这条 curl 的差异在于 Codex 会带 stream 参数和工具定义但基本协议是一致的curl 通了 Codex 大概率也通不通则要看 Codex 特有参数。5. Codex 中转 API 的 5 个高频踩坑点与排查顺序5.1 “auth token is unavailable”但 key 明明是对的现象执行 codex 后提示 auth token is unavailable要求重新登录你检查了 config.tomlRELAY_KEY 写了环境变量也导了但 Codex 还是跟你较劲。原因Codex CLI 对自定义 provider 的识别是有条件的。如果 model_provider 字段没有正确指向你的自定义小节或者 env_key 没有设置Codex 就会回落到官方 provider 的登录态检查。此时 auth.json 不存在或令牌过期就会报这个错。还有一个常见诱因config.toml 里同时存在旧的官方认证残留Codex 优先校验登录态而不是 API Key。解决先确认 model_provider 和 env_key 两个字段都在且节点的名字拼写一致再确认环境变量真的存在用 printf %s\n ${RELAY_KEY} 打印出来看不要依赖 export 没生效的 shell。如果还不行把 ~/.codex/auth.json 备份后移走让 Codex 没有旧登录态可读再重启 Codex。另外如果不想走官方账号体系就用 API key 方式绕开登录验证这也是自建中转网关的主要优点之一有些同事卡在官方登录页打不开或者手机号验证过不去换成 API key 方式后这些问题都不存在了。5.2 切到本地中转后报 /responses 端点失败现象Codex 能启动一发送消息就报错错误信息里能看到 /responses 字样类似「failed while handling codex endpoint /responses」。原因新版 Codex CLI 默认走 OpenAI 的 Responses API也就是 POST /v1/responses。而很多中转网关只实现了 /v1/chat/completions或者上游模型服务根本不认识 responses 协议。Codex 把请求打到你的网关网关没有对应的路由就会 404或者在网关内部抛异常再包装成一条让人看不懂的报错。这个问题的隐蔽之处在于Codex 界面完全不会告诉你它试图访问哪个路径你不看网关日志就不知道是端点不匹配。解决确保中转网关同时实现了 /v1/responses 和 /v1/chat/completions 两个路由第 2 章的代码已经做了。如果你用自己的老转发层先补 /v1/responses 并把它转发到上游的 responses 路径如果上游只有 chat/completions 协议可以在网关里把 /v1/responses 的请求体做一次转换再打到上游这是更麻烦的路子不建议新手直接写。排查顺序很固定先看网关日志有没有收到 POST /v1/responses没收到说明请求没到网关问题在 base_url收到了但报错再往上游查协议兼容性。日志里能看到真实路径这一步别靠猜。5.3 上游报 model not foundCodex 却显示已发送现象网关日志显示请求转发成功上游返回 400错误体里写着 model not foundCodex 客户端只显示「上游返回错误」完全看不出来是模型名问题。原因Codex 配置里的模型名跟上游模型标识不一致。比如 Codex 里写的是 gpt-5上游服务只有 deepseek-chat网关的 MODEL_MAP 没覆盖这个键请求就会原样带着 gpt-5 打给上游上游当然不认识。还有一种情况是映射表配了但 value 里多了个空格或者大小写不对这类低级错误最容易让人绕圈子。解决在 MODEL_MAP 里把 Codex 的模型名映射成上游模型名改完重启网关再试。这里有个血泪经验映射表最好设计成「未命中的模型直接返回 400」而不是原样转发。原样转发会让错误暴露在上游信息绕一圈再回来排查成本高得多在网关层直接拦下来日志里立刻能看到是哪一步没映射。还有一点模型名的大小写是敏感的deepseek-chat 和 DeepSeek-Chat 在部分上游服务里是两个模型配置前到上游控制台确认一遍。5.4 流式响应转圈SSE 被网关缓冲了现象Codex 界面进入等待状态一个字都出不来偶尔等到超时后又一次性吐出一大段体感非常怪。原因网关转发时把上游响应整体读完了才返回给 Codex。SSE 流式协议要求中间层逐字节透传一旦网关用了普通 POST 等待完整响应或者后端 Nginx 之类的入口开启了响应缓冲Codex 就拿不到增量片段自然表现成卡住。这个症状很容易被误判成网络慢或者模型慢实际问题出在中间层。解决检查网关代码里用的是 client.stream() 还是 client.post()用 post 一定卡改回 stream 并返回 StreamingResponse。Docker 部署时如果前面还有一层 Nginx要把它的响应缓冲关掉这一步和代码无关但症状完全一样很容易误判成网关问题。我的排查习惯是先发一条带 -N 的 curl 命令看终端会不会一个字节一个字节地吐如果 curl 也卡问题一定在网关或入口层不在 Codex。5.5 并发任务一多就 429限流与排队现象团队里几个人同时用 Codex或者一个人开了多个会话网关日志里开始出现 429看上游控制台配额还有但请求就是打不进去。原因上游模型服务的配额是按账号维度算的中转网关不加任何限流时多个 Codex 会话的并发会直接顶到上游额度上限429 是上游主动返回的保护状态不是网络故障。如果 429 集中在同一秒出现就是瞬时并发如果从某个时间点后持续出现就是上游配额被长期占满。解决给网关加一个信号量限流。一个简单做法是在代码里加全局 Semaphoreimport asyncio SEMAPHORE asyncio.Semaphore(5) async def relay(request: Request, upstream_path: str): async with SEMAPHORE: # 原有转发逻辑逻辑说明Semaphore(5) 限制同一时刻最多有 5 个转发请求在途超过的请求在网关排队而不是全部砸向上游。数值要根据上游配额调上游并发是 10这里设 5 比较合适上游并发只有 2这里就得设 1 或 2。这个问题在自写网关上尤其值得提前处理因为 one-api 这类开源网关自带令牌限流自写网关不写这些代码就没有任何保护。6. 用中转网关的日志逆向检查 Codex 的一次完整会话6.1 结构化日志字段设计与 jq 查询网关跑起来后最容易被忽视的就是日志。Codex 的交互是长会话一次任务可能产生几十个请求用自然语言日志一条条翻根本不现实。我习惯把日志输出成 JSON Lines一行一个请求固定字段时间、来源 IP、请求路径、原始模型名、映射后模型名、上游状态码、耗时。这样排错和统计都能用 jq 直接查。# 查看今天所有失败请求 cat relay.log | jq select(.status 400) # 按映射后模型统计请求次数 cat relay.log | jq -r .mapped_model | sort | uniq -c # 查看一次 Codex 会话里 responses 端点的请求序列 cat relay.log | jq select(.path /v1/responses) | {time, status, duration_ms}日志字段要和写在代码里的字段完全对应。常见做法是在转发后用 time.perf_counter() 记录耗时再把所有字段打包成一行 JSON 输出。我最开始图省事只打了可读文本日志结果排一次错要翻几十行改成结构化之后基本都是一两条 jq 定位问题。日志就是后悔药等出了问题再补日志是最难受的网关刚部署时就把字段定好。6.2 用日志确认模型映射和端点选择是否符合预期上面 jq 的第二个查询能快速回答一个高频问题Codex 实际请求的模型名到底是什么、被映射成了什么。我帮同事排查过一次「Codex 一直用错模型」的案例他坚持自己配置的是 DeepSeek但日志里 mapped_model 全是另一个模型名最后发现是 config.toml 里写了一个旧网关的映射表而新网关已经换了映射策略。这种问题不看日志完全猜不到。我的习惯是每次改动网关或 Codex 配置后先跑一条短任务然后去日志里确认四个点路径是不是 /v1/responses、原始模型名是什么、映射后模型名是什么、状态码是不是 200。这四个点确认完再开长任务。另外看耗时分布也很有用如果 duration_ms 普遍超过 60 秒先别急着调网关要看上游模型是不是本身就慢或者流式输出被入口层卡住了。排查顺序对了很多玄学问题最后都能落到一行日志上。希望这次的中转网关部署过程能帮你少踩几个坑也希望这套日志习惯能在你后续排查 Codex 问题时真正帮上忙。本文还有配套的精品资源点击获取
返回列表