---
title: Codex 桌面 App 同时用 GPT 订阅和 DeepSeek 官方 API：本地桥接方案
slug: codex-gpt-deepseek-local-bridge
date: 2026-10-15
author: Frankie 徐
category: ai
tags: ['codex', 'deepseek', 'openai', 'proxy', 'macos']
description: 不改 App、不走第三方中转，用一个 127.0.0.1 上的 Python 桥接按模型 ID 分流：GPT 透传订阅登录，DeepSeek 用钥匙串里的官方 Key。附设计取舍、代理坑和回退方案。
permalink: https://www.210k.cc/codex-gpt-deepseek-local-bridge
---

## 想解决的问题

我在 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
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`，指向官方端点（来历见[上一篇](/codex-switch-to-chatgpt-oauth-keep-history)）。接入桥接时，`config.toml` 只改两个地方：

```toml
# 顶层：必须在第一个 [表] 之前
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
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
/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
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
{"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`，也不碰会话历史。登录文件的备份只在它确实损坏时手动恢复，不要因为桥接报错就去覆盖正常的登录。

## 排障速查

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

## 总结

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

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

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

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

参考：

- [Use API/provider credentials through a gateway](https://learn.chatgpt.com/docs/enterprise/connect-to-a-gateway)
- [Codex Configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference)