在官方 ChatGPT App 与自建网关之间切换 Codex:一套可回退的 macOS 方案
让 Codex 在官方 ChatGPT OAuth 与自建 OpenAI 兼容网关之间可验证、可回退地切换。
工程笔记 · 2026-07-24 · 约 6 分钟阅读 · 标签:OpenAI Compatible、Codex、ChatGPT、macOS
安全审校:2026-10-07;保留原发布日期 2026-07-24 及历史正文。此次仅静态语法、官方参考及离线安全检查,未部署、调用模型或执行生产操作;不是实时源站同步。命令前提为 Linux/macOS/WSL2 POSIX shell,Docker 需 Compose v2 与正确项目目录。
OpenAI 的 Codex CLI 和 ChatGPT macOS App 中的 Codex 功能会读取用户目录下的配置。只要自建网关兼容 OpenAI Responses API,就可以让 Codex 的代码任务经自定义接口运行;需要官方能力时,再切回 ChatGPT OAuth。
这篇文章记录一套可回退的全局切换方案,同时解释它与隔离 CODEX_HOME 启动方式的区别。
提醒:这不是修改 ChatGPT 普通聊天模型。它影响的是读取
~/.codex的 Codex 运行时。自定义 OpenAI 兼容网关通常只适合文本和代码任务,不能保证承载官方 App 内置的浏览器、Computer Use、文档、图片等专有插件。
Codex 读取哪些文件
默认 Codex Home 为:
~/.codex
两个关键文件是:
~/.codex/config.toml
~/.codex/auth.json
config.toml 保存模型、Provider、Base URL、Responses API 类型等配置;auth.json 保存官方 OAuth 或 API Key 鉴权状态。
因此,全局切换的本质就是:成对切换配置和鉴权。
为什么不能只改 Base URL
如果只把 base_url 改成自建网关,但 auth.json 仍是 chatgpt OAuth,Codex 可能继续尝试官方登录逻辑。反过来,如果只写 API Key、不指定自定义 Provider,也不会自动使用自建接口。
可靠切换需要同时处理:
官方模式:官方 config + ChatGPT OAuth auth
网关模式:自定义 config + API Key auth
切换前先验证网关
Codex 默认依赖 /v1/responses,所以不能只测试 /v1/chat/completions。
模型列表
python3 -c 'import getpass,json,sys,warnings,urllib.request,urllib.error,urllib.parse
if not sys.stdin.isatty() or not sys.stderr.isatty():
raise SystemExit("Interactive TTY required; refusing echoed input")
warnings.simplefilter("error",getpass.GetPassWarning)
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,req,fp,code,msg,headers,newurl):
raise urllib.error.HTTPError(req.full_url,code,"Redirect refused",headers,None)
url="https://gateway.example.com/v1/models"
if urllib.parse.urlsplit(url).hostname in ("gateway.example.com", "example.com"):
raise SystemExit("Replace the example HTTPS host with your reviewed exact gateway host first")
key=getpass.getpass("OpenAI API key (hidden): ")
if not key or any(c in key for c in "\r\n"):
raise SystemExit("Invalid key")
req=urllib.request.Request(url,headers={"Authorization":"Bearer "+key})
try:
with urllib.request.build_opener(NoRedirect()).open(req,timeout=30) as response:
print("HTTP",response.status)
data=json.load(response)
print("\n".join(m["id"] for m in data["data"]))
except urllib.error.HTTPError as error:
raise SystemExit("HTTP "+str(error.code)+"; response body hidden")
except (urllib.error.URLError,ValueError,KeyError):
raise SystemExit("Request failed; details hidden")
'
Responses API
python3 -c 'import getpass,json,sys,warnings,urllib.request,urllib.error,urllib.parse
if not sys.stdin.isatty() or not sys.stderr.isatty():
raise SystemExit("Interactive TTY required; refusing echoed input")
warnings.simplefilter("error",getpass.GetPassWarning)
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,req,fp,code,msg,headers,newurl):
raise urllib.error.HTTPError(req.full_url,code,"Redirect refused",headers,None)
url="https://gateway.example.com/v1/responses"
if urllib.parse.urlsplit(url).hostname in ("gateway.example.com", "example.com"):
raise SystemExit("Replace the example HTTPS host with your reviewed exact gateway host first")
key=getpass.getpass("OpenAI API key (hidden): ")
if not key or any(c in key for c in "\r\n"):
raise SystemExit("Invalid key")
req=urllib.request.Request(url,data=json.dumps({'"'"'model'"'"': '"'"'gpt-5.6-sol'"'"', '"'"'input'"'"': '"'"'Reply exactly: RESPONSES_OK'"'"', '"'"'max_output_tokens'"'"': 20}).encode(),headers={"Authorization":"Bearer "+key,"Content-Type":"application/json"})
try:
with urllib.request.build_opener(NoRedirect()).open(req,timeout=30) as response:
print("HTTP",response.status)
data=json.load(response)
print("Expected model:",data.get("model")=="gpt-5.6-sol", "Completed:",data.get("status")=="completed")
except urllib.error.HTTPError as error:
raise SystemExit("HTTP "+str(error.code)+"; response body hidden")
except (urllib.error.URLError,ValueError,KeyError):
raise SystemExit("Request failed; details hidden")
'
至少确认:
- HTTP 200;
- 状态为 completed;
- 返回内容正确;
- 模型没有被静默替换。
自定义 Provider 配置
网关模式的 config.toml 可以写成:
model = "gpt-5.6-sol"
model_provider = "my-gateway"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
[sandbox_workspace_write]
network_access = false
[model_providers.my-gateway]
name = "My OpenAI-Compatible Gateway"
base_url = "https://gateway.example.com/v1"
wire_api = "responses"
requires_openai_auth = true
request_max_retries = 3
stream_max_retries = 3
stream_idle_timeout_ms = 300000
API Key 不应写进 TOML。切换脚本把它写进权限为 600 的 auth.json:
{
"auth_mode": "apikey",
"OPENAI_API_KEY": "<API_KEY>"
}
为什么要备份官方 OAuth
官方 ChatGPT 登录包含可刷新 Token 和账户信息。切换自建接口前必须完整备份:
~/.codex/profiles/official.config.toml
~/.codex/profiles/official.auth.json
文件权限应限制为当前用户:
chmod 700 ~/.codex/profiles
chmod 600 ~/.codex/profiles/official.config.toml ~/.codex/profiles/official.auth.json
不要把这些文件提交到 Git,也不要把内容发到聊天记录、Issue 或截图里。
全局切换器
以下是概念接口,非官方 CLI;本文不提供可执行切换器,不要直接运行。优先隔离 CODEX_HOME,不覆盖现有账户。自行实现时先 umask 077 建目录、完整备份两个文件并验证恢复,用同目录临时文件原子替换,目录 700、仅已知文件 600。以下只描述接口:
# 概念,非现成工具
codex-switch official
codex-switch sub2api
codex-switch status
它分别执行:
official
- 恢复官方
config.toml; - 恢复 ChatGPT OAuth
auth.json; - 保持文件权限为
600。
sub2api
- 写入自定义 Provider 配置;
- 从受保护的环境文件读取 API Key;
- 生成 API Key 模式的
auth.json; - 不在终端输出 Key。
status
只输出:
current=official-chatgpt
或:
current=sub2api model="gpt-5.6-sol"
不显示任何凭据。
全局切换与隔离启动的区别
这两个方案不要混淆。
全局切换
修改 ~/.codex/config.toml 和 auth.json
特点:
- 终端 Codex 会改变;
- ChatGPT App 中读取同一 Codex Home 的运行时也会改变;
- 切换后建议新建会话;
- App 已经打开时,最好完全退出并重开。
隔离启动
CODEX_HOME=~/.codex-gateway codex
特点:
- 只影响这个终端进程;
- 不覆盖官方 OAuth;
- 不改变已打开的 ChatGPT App;
- 适合同时保留多个 Provider。
我的实际使用方式是:日常 CLI 优先隔离启动;需要让 App 的 Codex 走网关时,才执行全局切换。
自定义网关模式下应禁用的插件
官方 App 的部分能力依赖 OpenAI 专用服务,不是标准 Responses API 的一部分。例如:
- Browser / Chrome;
- Computer Use;
- Documents;
- PDF;
- Spreadsheets;
- Presentations;
- 图像相关工具。
自定义配置中应显式禁用这些插件,避免 Codex 发出网关无法理解的工具调用:
[plugins."browser@openai-bundled"]
enabled = false
[plugins."computer-use@openai-bundled"]
enabled = false
[plugins."documents@openai-primary-runtime"]
enabled = false
切回官方配置后再恢复它们。
真实验收
配置完成后必须运行一次 Codex,而不是只查看文件:
codex exec --skip-git-repo-check \
'Reply with exactly: CODEX_GATEWAY_OK'
启动信息应明确显示:
model: gpt-5.6-sol
provider: my-gateway
并最终返回:
CODEX_GATEWAY_OK
还应测试切回官方:
# 概念,非现成工具
codex-switch official
codex-switch status
确认 OAuth 文件完整恢复。
常见问题
1. Missing environment variable
TOML 中配置了 env_key,但官方 App 不是从你的交互式 shell 启动,无法继承该环境变量。
解决方案:全局切换时正确生成 auth.json,不要依赖 shell 临时 export。
2. 终端成功,App 不生效
通常是 App 仍在使用旧进程中的配置。完全退出 App,重新打开并新建 Codex 会话。
3. /chat/completions 正常,Codex 仍失败
大概率是 /responses 不兼容。Codex Provider 应设置:
wire_api = "responses"
同时真实测试 /v1/responses。
4. 官方插件报错
自建网关只能处理标准文本/工具协议,不能等价替代 OpenAI 专有后端。自定义模式下禁用不支持的官方插件。
小结
要让 Codex 在官方 ChatGPT 与自建网关之间可靠切换,需要同时满足四个条件:
- 网关真实支持 Responses API;
- 模型名与上游保持透明;
config.toml和auth.json成对切换;- 官方 OAuth 有安全备份并能一键恢复。
不要用覆盖官方文件却没有备份的脚本,也不要把 API Key 硬编码进配置。把全局切换和隔离启动分清楚,就能同时保留官方体验与自建路由的灵活性。
安全输入:示例须在交互 TTY 使用,拒绝无 TTY 与所有重定向;先替换并核对精确 HTTPS 网关地址与费用。模型列表不代表 Responses/chat 真实生成成功。
配置参考:https://developers.openai.com/codex/config-reference/ 。network_access 是 workspace-write 下的布尔键,保持 false,不默认放宽;其他模式不能据此推断。disable_response_storage 未在当前参考确认,移出示例,不作为存储保证。插件 ID、App 行为均依版本,未进行运行时兼容性验证。权限命令仅在备份已存在时使用,先核对目标,勿对目录使用 chmod 600。
安全程序只替换 url 的 HTTPS 地址,保留占位主机拒绝规则;Responses/chat 验证只输出布尔结果,不公开完整响应体。模型名也必须按实际账户配置核对。