在 Codex 桌面 App 里同时用 GPT 订阅和 DeepSeek 官方 API

October, 15th 2026 11 min read • Markdown •

想解决的问题

我在 Codex 桌面 App 里用的是 ChatGPT 订阅。想在同一个模型菜单里再加上 DeepSeek,有几个要求:

  • GPT 继续用我自己的订阅登录,不换成 API Key。
  • DeepSeek 直接调官方 API,用我自己的 Key,不经过任何第三方中转。
  • 不改 App 本身,坏了能一键退回原样。

最后的做法是:在本机 127.0.0.1 上跑一个很小的 HTTP 桥接,再给 App 一份合并后的模型目录。环境是 macOS,ChatGPT App 26.1007.x,内置 Codex CLI 0.162.0-alpha。

架构

text
123456
Codex 桌面 App
    │
    └── 127.0.0.1:19427 本机桥接
          ├── GPT 模型        → chatgpt.com/backend-api/codex → 原订阅登录(透传)
          ├── deepseek-flash  → api.deepseek.com              → 自己的 DeepSeek Key
          └── deepseek-v4-pro → api.deepseek.com              → 自己的 DeepSeek Key

App 从合并后的模型目录读到菜单,所有 Responses 请求都发到本机桥接。桥接只看请求体里的 model 字段,决定发往哪个上游。

需要说清楚这个方案没做的事:

  • 没给 App 原生代码加 Provider。
  • GPT 请求不会用 DeepSeek Key。
  • 没把 Responses 转成 Chat Completions,也不翻译模型 ID。DeepSeek 官方本身就提供 Responses 兼容的接口。

菜单里多出两项:

菜单名称请求中的模型 ID
DeepSeek Flash(官方 API)deepseek-flash
DeepSeek Pro(官方 API)deepseek-v4-pro

配置层:只动两处

我的 Codex 已经有一个自定义的订阅 Provider,叫 crs,指向官方端点(来历见上一篇)。接入桥接时,config.toml 只改两个地方:

toml
123456
# 顶层:必须在第一个 [表] 之前
model_catalog_json = "~/.codex/deepseek-bridge/catalog.json"

# 原 Provider:其他字段不动,只换 base_url
[model_providers.crs]
base_url = "http://127.0.0.1:19427/<随机能力值>/backend-api/codex"

(model_catalog_json 实际要写绝对路径。)

catalog.json 怎么来的:

  • GPT 部分:直接用本机 ~/.codex/models_cache.json 里的条目,原样保留。每个人能用哪些 GPT 模型,由自己的账户和 App 版本决定,不能拿别人的模型目录替代。
  • DeepSeek 部分:模型元数据取自 DeepSeek 官方的 Codex 安装脚本。我只下载下来读里面的模型 JSON,没有执行这个远程脚本,只改了显示名称。

如果你用的是内置的 openai Provider,那它不能被覆盖,得另外新建一个 Provider 表,不要写 [model_providers.openai]。

桥接的核心逻辑

桥接是一个只用 Python 标准库的 http.server,路由部分很短:

python
12345678910111213
model = payload.get('model')
vendor = model in settings['deepseek_models']
base = settings['deepseek']['base_url'] if vendor else settings['native_base_url']

if vendor:
    # DeepSeek:自己构造认证头,不携带任何 ChatGPT 认证信息
    headers = {'Authorization': 'Bearer ' + read_deepseek_key(),
               'Content-Type': 'application/json',
               'X-Request-ID': correlation}
else:
    # GPT:去掉 hop-by-hop 头,其余原样透传,包括订阅的认证头
    headers = {k: v for k, v in self.headers.items() if k.lower() not in HOP_HEADERS}
    headers['X-Request-ID'] = correlation

响应用 read1() 按块读,再以 chunked 编码写回去,这样 SSE 流式输出能实时到达 App。

几个我认为重要的设计取舍:

设计原因
只监听 127.0.0.1启动时用断言检查,防止被误配成对局域网开放
URL 路径带随机能力值没有这个值的本机程序调不到模型端点,不能白用我的 Key。/health 不需要这个值
禁止跟随重定向带认证头的请求一旦被重定向,凭据可能被带到别的地址
不重试、不跨厂商降级DeepSeek 报错就是报错,不会偷偷改用 GPT,避免账单和行为出现意外
关掉默认访问日志请求 URL 里有能力值,不能写进日志

注意,能力值只能挡住随手调用的本机程序,挡不住同一用户权限下的恶意软件。它不是安全隔离。

Key 放在钥匙串,不落盘

DeepSeek Key 存在 macOS 钥匙串里。桥接通过 ctypes 调 Security.framework 读取,运行时只认钥匙串,不会退回去读环境变量。

导入时用隐藏输入,Key 不会出现在命令参数、shell 历史或任何文件里:

bash
12345678910111213
/usr/bin/python3 - <<'PY'
import getpass, os, sys
from pathlib import Path
sys.path.insert(0, str(Path.home() / '.codex/deepseek-bridge'))
import bridge

os.environ['DEEPSEEK_API_KEY'] = getpass.getpass('DeepSeek API Key(隐藏输入):')
sys.argv = ['bridge.py', 'import-key']
try:
    bridge.main()
finally:
    os.environ.pop('DEEPSEEK_API_KEY', None)
PY

导入函数写完会马上读回来比对。如果已经存在同名条目,直接报错,不会覆盖。

这样做还有个好处:从 Dock 启动的 App 拿不到终端里的环境变量,但钥匙串里的 Key 照样能用。

用 launchd 常驻

桥接用 LaunchAgent 跑在当前用户下,配置 KeepAlive:

bash
12345
launchctl bootstrap "gui/$(id -u)" \
  "$HOME/Library/LaunchAgents/local.codex-deepseek-bridge.plist"

curl --noproxy '*' --fail --silent http://127.0.0.1:19427/health
# {"service":"codex-deepseek-bridge","status":"ready"}

用 $(id -u),不要写死 501,也不要加 sudo。

/health 正常只说明桥接进程在响应,不说明上游认证和模型调用没问题。

坑:系统代理吃掉了本地请求

第一次接通后,App 发请求直接得到 503,桥接日志里却一条请求都没有。

原因是 macOS 开着系统代理。Codex 的 HTTP 客户端没有遵守系统代理里的「本地地址例外」,把发往 127.0.0.1 的请求也交给了代理。

我的处理:桥接启动时,在 launchd 的 NO_PROXY 原有值后面追加 127.0.0.1,localhost。这个设置只对之后启动的 GUI 进程生效,所以要退出 App 再重新打开。原来的值提前备份,回退时恢复。

这是个兼容补丁。等客户端能正确处理回环地址例外,就应该把它去掉。

怎么确认请求真的去了该去的地方

不要靠问模型「你是谁」来判断,模型的自我介绍不可靠。我看的是桥接的结构化日志 requests.jsonl,每个请求按 correlation_id 对应一条 request 和一条 response:

json
12
{"event":"request","correlation_id":"…","provider":"deepseek","model":"deepseek-flash","upstream_host":"api.deepseek.com","input_bytes":1234}
{"event":"response","correlation_id":"…","provider":"deepseek","status":200,"output_bytes":5678,"duration_ms":2100}

日志只记录模型、目标主机、状态码、字节数和耗时,不记录提示词、认证头或完整 URL。

另外写了一个付费的端到端验证脚本。它用临时 CODEX_HOME 和登录副本(refresh token 清空,防止刷新动到原来的登录),跑六项断言:

  1. GPT 返回指定标记
  2. Flash 返回指定标记,并记住一个合成口令
  3. 在同一会话里切到 Pro,能说出那个口令(验证跨模型的上下文延续)
  4. Flash 调 shell 执行 printf,退出码为 0
  5. Pro 做同样的 shell 检查
  6. 再开一个 GPT 会话,仍然正常

六项都通过。离线部分另外有 15 个单元测试(传输和认证边界 7 个,安装和回退 8 个),用临时 HTTP 服务和假认证跑,不调用真实模型。

可用性边界

GPT 也要经过桥接。 DeepSeek 出问题不会影响 GPT 的认证;但如果桥接进程挂了或卡住,GPT 也用不了。KeepAlive 只负责进程退出后自动拉起,处理不了进程卡死,也没有自动切回官方 Provider 的能力。

所以我准备了一个不依赖任何东西的回退脚本:zsh 加系统工具,不需要 Python、桥接、网络或 Codex。步骤是:

  1. ⌘Q 完全退出 App。
  2. 运行回退脚本,输入 RESTORE 确认。
  3. 脚本校验备份,然后恢复接入前的 config.toml 和 models_cache.json,卸载 LaunchAgent,还原 NO_PROXY。
  4. 重新打开 App,用 GPT 确认可用。

回退不动 auth.json,也不碰会话历史。登录文件的备份只在它确实损坏时手动恢复,不要因为桥接报错就去覆盖正常的登录。

排障速查

现象先查
菜单里没有 DeepSeekApp 是否完全退出后重开;model_catalog_json 是否误放进了某个表里;路径是否存在
报错,但桥接日志没有 request桥接健康状态、端口、GUI 进程的 NO_PROXY、是否还是旧的 App 进程
DeepSeek 返回 401/403Key、钥匙串授权、账户权限。不要去动 GPT 登录
DeepSeek 返回 429/5xx上游状态和账户限额。桥接不会改用 GPT,也不会重发
GPT 认证错误订阅登录是否仍有效,必要时先回退再重新登录
进程反复退出service.stderr.log、解释器路径、端口冲突

总结

这个方案的核心就是三件事:

  1. 一份合并的模型目录,让菜单里出现 DeepSeek。
  2. 一个只看 model 字段分流的本地桥接。
  3. 一个不依赖桥接也能运行的回退脚本。

GPT 的请求原样透传,DeepSeek 用自己的 Key,两边的凭据不会混在一起。代价是 GPT 多了一个本地单点,所以回退方案一定要在接入之前准备好。

以上只在 macOS 和上面提到的 App 版本上验证过。Windows/Linux、其他 App 版本、图片输入、联网搜索、长会话压缩这些情况都没测过,不保证兼容。

参考: