在官方 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 与自建网关之间可靠切换,需要同时满足四个条件:

  1. 网关真实支持 Responses API;
  2. 模型名与上游保持透明;
  3. config.toml 和 auth.json 成对切换;
  4. 官方 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 验证只输出布尔结果,不公开完整响应体。模型名也必须按实际账户配置核对。

全部文章 · 返回 OneMJJ 首页

OneMJJ · VPS、自托管与 AI 工具与历史维护笔记。少踩坑,多留传家宝。

部分工具由第三方运营;本站命令仅供复制,不会自动执行。运行前请核对环境、阅读源码并备份重要数据。本站可能展示 Google AdSense 广告,详见隐私政策。