目标很明确:保留 Codex 原有的 OpenAI 登录,同时把 OpenCode Go 提供的 DeepSeek V4 Flash 接入 Codex;默认使用 DeepSeek,需要时用一条命令切回 OpenAI。

最后实现了三个入口:

# 默认:DeepSeek V4 Flash
codex

# 显式选择 DeepSeek
codex -p deepseek

# 切回 OpenAI
codex -p openai

实际过程并不是简单改一个模型名。这里同时涉及 Responses API 兼容性、Provider 认证、模型元数据、Codex 自定义工具,以及 Profile 的配置覆盖顺序。更麻烦的是,有些配置可以通过静态检查,却只会在第一次真实请求时暴露协议差异。

需求、约束与验收标准

开始修改前,我把目标拆成一组可以验证的条件:

  • 使用 OpenCode Go 中已经配置好的 deepseek-v4-flash,不再申请和复制另一份凭证。
  • Codex 直接调用 Responses API,不长期运行本地协议代理。
  • DeepSeek 保持默认,OpenAI 登录和模型仍然可用。
  • 跨 Provider 切换不能只改模型名,必须同时切换认证和模型目录。
  • 当前会话配置可追溯,修改前要保留原始 config.toml。
  • 不把 API Key 输出到终端日志、文章、Git 或截图。
  • 不能只通过 curl;还要通过 Codex 原生对话和真实文件修改。
  • 出现不兼容时,保留完整错误并定位到请求协议,而不是靠重试掩盖。

最终验收标准是:

验收项通过条件
原始 API/v1/responses 返回 HTTP 200 和 completed
Codex 配置doctor 无 warning、无 failure
模型目录能解析 100 万上下文、high/max 和正确工具模式
DeepSeek Profile启动信息显示 opencode-go + deepseek-v4-flash
OpenAI Profile启动信息显示 openai + gpt-5.6-luna
生成能力两个 Profile 分别返回约定的精确文本
Agent 工具DeepSeek 能通过 apply_patch 创建并写入指定内容
凭证安全Codex 配置不包含 API Key,相关文件权限限制为当前用户

完整排查时间线

这次接入经历了几次方向修正。按发生顺序记录如下:

阶段操作结果或判断
1确认 OpenCode Go 已保存 DeepSeek 凭证不重复保存明文 Key
2根据旧资料判断只支持 Chat Completions结论过时,方向错误
3临时安装协议兼容代理尚未启用,等待接口能力确认
4阅读 DeepSeek 创建 Response 文档确认新接口已支持 Responses API
5直连 OpenCode Go /v1/responsesHTTP 200,返回 OK
6卸载未使用的代理减少一层运行与凭证链路
7配置 Codex 自定义 Provider 和运行时认证Provider 可达,Key 未写入配置
8使用未知模型的回退元数据运行原生 Codex 返回 OK,但有模型元数据警告
9增加自定义模型目录警告消失,但首次真实请求被 exec 工具拒绝
10将 tool_mode 从 code_mode_only 改为标准模式DeepSeek 原生请求恢复成功
11增加 deepseek 与 openai 两个 Profile一条命令可切换完整 Provider 配置
12运行真实 apply_patch 文件修改文件内容和末尾换行均通过字节级验证

这条时间线有一个反直觉点:没有模型目录时请求可以成功,补上看似更完整的模型目录后反而失败。 根因不是模型质量,而是模型目录改变了 Codex 发送给上游的工具声明。

最终架构

flowchart LR
  A[Codex CLI] --> B{启动参数}
  B -->|默认或 -p deepseek| C[DeepSeek Profile]
  B -->|-p openai| D[OpenAI Profile]
  C --> E[OpenCode Go Provider]
  E --> F[deepseek-v4-flash]
  D --> G[OpenAI Provider]
  G --> H[gpt-5.6-luna]

配置分为四个文件:

~/.codex/
├── config.toml
├── deepseek.config.toml
├── openai.config.toml
└── model-catalogs/
    └── opencode-go.json

config.toml 保存默认值和 Provider 定义;两个 Profile 只覆盖模型、Provider、上下文窗口和模型目录。API Key 不写进这些文件,而是在运行时从 OpenCode 已保存的认证文件中读取。

先验证 Responses API,不要从旧资料推断

一开始我根据旧的搜索结果误以为 DeepSeek 端点只支持 Chat Completions,并准备增加一层协议代理。随后在 DeepSeek 创建 Response 文档中确认了 Responses API,再对 OpenCode Go 的实际端点做最小请求验证:

curl --request POST \
  --url https://opencode.ai/zen/go/v1/responses \
  --header "Authorization: Bearer ${OPENCODE_GO_API_KEY}" \
  --header "Content-Type: application/json" \
  --data '{
    "model": "deepseek-v4-flash",
    "input": "Reply exactly OK.",
    "max_output_tokens": 16
  }'

脱敏后的关键结果如下:

{
  "object": "response",
  "status": "completed",
  "model": "deepseek-v4-flash",
  "output_text": "OK",
  "error": null
}

接口返回 HTTP 200,响应状态为 completed,模型为 deepseek-v4-flash,文本结果为 OK。这说明不需要额外协议代理,可以让 Codex 直接使用 wire_api = "responses"。

文档、模型服务和网关是三层能力

这里不能只问“DeepSeek 是否支持 Responses API”,因为实际请求经过三层:

  1. DeepSeek 模型与接口定义是否支持 Response 对象。
  2. OpenCode Go 是否暴露兼容的 /v1/responses 路由。
  3. OpenCode Go 是否完整接受 Codex 附带的工具 schema 和流式事件。

最小文本请求只能证明前两层以及第三层的基础部分。Codex 还会发送开发者指令、工具、推理参数和流式配置,所以后面仍需要原生端到端测试。

这一步很重要:搜索索引、旧文档和第三方说明都可能滞后,最终应以目标端点的真实契约和最小请求结果为准。此前临时安装、但尚未投入使用的代理随后被卸载,避免留下重复链路。

测试时不要把真实 API Key 写进文章、脚本或 shell 历史。上面的环境变量只用于说明请求结构。

第一条弯路:为什么代理装了又卸载

最初的判断来自对旧版 DeepSeek API 的印象:如果上游只有 /chat/completions,而当前 Codex 自定义 Provider 只接受 wire_api = "responses",中间就需要一个转换层。

因此当时的备选链路是:

flowchart LR
  A[Codex Responses] --> B[本地兼容代理]
  B --> C[Chat Completions]
  C --> D[DeepSeek]

代理包已经临时安装,但没有启动服务,也没有写入 Codex 路由。看到 DeepSeek 的新文档后,我没有继续完成代理配置,而是先验证直连。直连通过后立即卸载代理。

这不是单纯清理一个 npm 包。多保留一层代理会引入:

  • 第二处 API Key 或凭证读取逻辑。
  • 独立进程、端口、健康检查和开机启动。
  • 两套超时、流式转发、重试与错误格式。
  • 工具 schema、推理字段和 Response 事件的转换风险。
  • 故障时难以判断问题来自 Codex、代理、OpenCode Go 还是模型。

只有当直连协议确实缺失,或者必须做统一审计、限流和路由时,代理才值得长期存在。

配置前先盘点本地状态

直接覆盖 config.toml 之前,先确认当前版本、默认模型、登录状态和已保存的 OpenCode 凭证。本文当时使用:

Codex CLI: 0.145.0
原默认模型: gpt-5.6-luna
原推理强度: high
OpenAI 登录: 已存在
OpenCode Go 凭证: 已存在

版本检查:

codex --version

凭证存在性检查只返回布尔结果,不打印内容:

jq -e \
  '.["opencode-go"].key | type == "string" and length > 0' \
  ~/.local/share/opencode/auth.json >/dev/null

再检查文件权限:

ls -l ~/.codex/config.toml ~/.local/share/opencode/auth.json

如果权限允许本机其他用户读取,应先收紧权限,再继续接入。不要为了确认 Key 是否存在而直接执行会把它打印到终端的 jq -r。

配置 OpenCode Go Provider

先备份当前配置:

cp ~/.codex/config.toml ~/.codex/config.toml.before-opencode-go

在 ~/.codex/config.toml 中设置默认模型:

model = "deepseek-v4-flash"
model_provider = "opencode-go"
model_context_window = 1000000
model_catalog_json = "/Users/you/.codex/model-catalogs/opencode-go.json"
model_reasoning_effort = "high"

[model_providers.opencode-go]
name = "OpenCode Go"
base_url = "https://opencode.ai/zen/go/v1"
wire_api = "responses"
request_max_retries = 4
stream_max_retries = 5
stream_idle_timeout_ms = 300000

base_url 到 /v1 为止,Codex 会根据 wire_api 请求 /responses。不要把完整的 /responses 再写进 base_url。

这里显式设置 100 万上下文,但真实可用窗口仍受模型、Provider 和网关策略共同约束。超长任务上线前应单独做边界测试,不能只依赖本地元数据。

每个字段解决什么问题

配置项作用
model发送给上游的模型标识
model_provider选择哪组 Base URL、认证和重试策略
model_context_window给 Codex 的上下文预算提示
model_catalog_json提供模型名称、推理档位、工具模式和其他能力元数据
wire_api指定 Provider 使用 Responses 协议
request_max_retries普通 HTTP 请求的最大重试次数
stream_max_retries流式响应中断后的恢复次数
stream_idle_timeout_ms流长时间无事件时的超时

model 与 model_provider 必须放在一起理解。把模型改成 DeepSeek、却仍保留 model_provider = "openai",请求不会自动路由到 OpenCode Go;反过来,在 OpenCode Go Provider 下选择仅由 OpenAI 提供的模型名也会失败。

为什么没有使用环境变量直接复制 Key

Codex Provider 也可以读取指定环境变量,但这需要保证每个启动 Codex 的终端、IDE 或桌面进程都继承同一个变量。当前凭证已经由 OpenCode 管理,所以让认证命令按需读取现有文件更符合“单一凭证来源”。

如果团队通过统一的 Secret Manager 注入环境变量,则应优先使用团队的标准方式;本文的 jq 方案适合已经使用 OpenCode 本地认证存储的个人开发机。

/models 能返回,不代表 Codex 能消费

配置 Provider 后,Codex 会尝试刷新模型列表。OpenCode Go 的 /models 当时返回 OpenAI 风格结构:

{
  "object": "list",
  "data": [{ "id": "deepseek-v4-flash", "object": "model" }]
}

当前 Codex 的模型管理器在这条自定义刷新链路中期望的是包含 models 字段的目录结构,因此记录了类似错误:

failed to decode models response:
missing field `models`

这不等于 /responses 不可用。实际上,最小 Response 和 Codex 回退元数据请求都已经成功。失败的是“自动刷新模型目录”这条辅助链路。

处理方式不是修改网关响应,也不是忽略所有日志,而是提供本地 model_catalog_json:

文本生成链路: Codex -> /responses -> 成功
模型发现链路: Codex -> /models -> schema 不匹配
替代方案:     Codex -> 本地 model_catalog_json

这样可以把模型运行和模型发现解耦,同时明确由本机配置维护 DeepSeek 的能力声明。

用运行时认证避免复制明文密钥

OpenCode 已经在本地认证文件中保存凭证,没有必要再把同一个 Key 复制到 config.toml。Codex 的自定义 Provider 支持通过命令获取认证信息:

[model_providers.opencode-go.auth]
command = "/usr/bin/jq"
args = [
  "-r",
  ".[\"opencode-go\"].key",
  "/Users/you/.local/share/opencode/auth.json",
]
timeout_ms = 5000
refresh_interval_ms = 300000

注意:args 由 Codex 直接传给命令,不一定经过 shell,因此不要假设 ~ 会自动展开。macOS 应写实际的绝对路径,Linux 则通常是 /home/用户名/...。

这种做法有几个好处:

  • Codex 配置中没有第二份明文 Key。
  • OpenCode 更新凭证后,Codex 可以按刷新周期重新读取。
  • 配置文件更适合备份和审查。

认证文件和 Codex 配置都应限制为当前用户可读,例如权限 600。任何诊断输出也不应打印 Key。

为什么需要自定义模型目录

只设置模型名时,Codex 可以使用回退元数据启动,但会提示:

Model metadata for `deepseek-v4-flash` not found.
Defaulting to fallback metadata; this can degrade performance and cause issues.

有趣的是,这个回退模式首先通过了原生 Codex 测试:启动信息显示 deepseek-v4-flash + opencode-go,并返回 OK。因此可以先得到两个结论:

  • Provider、认证与 Responses 路由本身是通的。
  • 后续失败如果只发生在自定义目录启用后,应优先比较模型元数据,而不是重新怀疑 API Key。

最小 JSON 为什么失败

我先尝试手工写一个只有名称、上下文和推理档位的目录。Codex 解析时直接拒绝:

failed to parse model_catalog_json:
missing field `base_instructions`

这说明 model_catalog_json 不是给模型选择器使用的简易清单。它还包含 Codex 运行所需的基础指令、消息模板、截断策略和工具能力。

手写几十个随版本变化的字段很容易制造新的兼容问题,所以最终改为从当前 models_cache.json 克隆一条结构完整的记录,再明确收窄 DeepSeek 能力。

为了让 Codex 认识模型名称、上下文窗口和推理档位,可以基于当前 Codex 的模型缓存生成一份自定义目录。以下命令以已有模型条目为结构模板,只替换与 DeepSeek 相关的能力字段:

mkdir -p ~/.codex/model-catalogs

jq '{
  models: [
    (.models[] | select(.slug == "gpt-5.6-luna")
      | .slug = "deepseek-v4-flash"
      | .display_name = "DeepSeek V4 Flash (OpenCode Go)"
      | .description = "通过 OpenCode Go 使用的 DeepSeek V4 Flash"
      | .context_window = 1000000
      | .max_context_window = 1000000
      | .effective_context_window_percent = 95
      | .default_reasoning_level = "high"
      | .supported_reasoning_levels = [
          {"effort": "high", "description": "适合日常编程与复杂任务"},
          {"effort": "max", "description": "为最困难任务提供最大推理强度"}
        ]
      | .tool_mode = null
      | .input_modalities = ["text"]
      | .supports_image_detail_original = false
      | .supports_search_tool = false
      | .use_responses_lite = false)
  ]
}' ~/.codex/models_cache.json > ~/.codex/model-catalogs/opencode-go.json

chmod 600 ~/.codex/model-catalogs/opencode-go.json

这里不是随便拼一个最小 JSON。模型目录的 schema 会随 Codex 版本演进,直接复用当前安装版本缓存中的完整条目,可以保留该版本要求的基础指令和必填字段。本次配置基于 Codex 0.145.0 验证;升级后应重新执行解析和端到端测试。

生成后先限制权限并检查关键字段,不需要打印长篇基础指令:

chmod 600 ~/.codex/model-catalogs/opencode-go.json

jq '.models[] | {
  slug,
  display_name,
  context_window,
  max_context_window,
  default_reasoning_level,
  tool_mode,
  input_modalities
}' ~/.codex/model-catalogs/opencode-go.json

预期关键值:

{
  "slug": "deepseek-v4-flash",
  "display_name": "DeepSeek V4 Flash (OpenCode Go)",
  "context_window": 1000000,
  "max_context_window": 1000000,
  "default_reasoning_level": "high",
  "tool_mode": null,
  "input_modalities": ["text"]
}

最隐蔽的问题:模型目录会改变工具协议

第一次生成目录时,我完整继承了 OpenAI 模型的能力声明,其中包括:

{
  "tool_mode": "code_mode_only",
  "shell_type": "shell_command",
  "apply_patch_tool_type": "freeform"
}

模型目录可以正常解析,codex doctor 也没有报错,但真实调用失败:

Unsupported custom tool: 'exec'. Only 'apply_patch' is supported.

原因是 tool_mode = "code_mode_only" 不只是一个 UI 标签。它会让 Codex 在 Responses 请求中声明 exec 自定义工具,而当时的 OpenCode Go 上游只接受 apply_patch 这种自定义工具。

把 DeepSeek 条目的工具模式改为标准模式:

{
  "tool_mode": null
}

随后同一个原生 Codex 请求成功返回 DEEPSEEK_OK。

为什么关闭 features.unified_exec 仍然失败

排查时还试过一次命令行覆盖:

codex exec \
  -p deepseek \
  -c features.unified_exec=false \
  -s read-only \
  --ephemeral \
  "Reply exactly DEEPSEEK_OK. Do not use tools."

请求仍然报同一个 Unsupported custom tool: 'exec'。这说明问题不只是全局 feature flag;模型条目中的 tool_mode = "code_mode_only" 仍在决定运行时工具协议。

真正有效的对照实验是:其他配置保持不变,只把模型目录中的 tool_mode 改为 null。修改前失败,修改后成功,因果关系才足够清晰。

用真实文件修改证明 Agent 工具可用

精确回复只能证明模型能生成文本。为了确认 Codex 不是“能聊天但不能改代码”,我在独立临时目录执行了一个最小写文件任务:

codex exec \
  -p deepseek \
  -C /private/tmp/codex-deepseek-agent-test \
  -s workspace-write \
  --ephemeral \
  --skip-git-repo-check \
  "Use apply_patch to create result.txt containing exactly DEEPSEEK_TOOL_OK followed by a newline. Then stop."

运行信息确认:

model: deepseek-v4-flash
provider: opencode-go
sandbox: workspace-write
reasoning effort: high

模型随后发起 apply_patch,Codex 显示:

apply patch
patch: completed

--- /dev/null
+++ b/result.txt
@@ -0,0 +1 @@
+DEEPSEEK_TOOL_OK

字节级验证结果为 17 字节,最后一个字节是 0a:

44 45 45 50 53 45 45 4b 5f 54 4f 4f 4c 5f 4f 4b 0a
D  E  E  P  S  E  E  K  _  T  O  O  L  _  O  K  \n

测试后删除文件和临时目录,没有把测试产物留在业务仓库。

这个问题说明:

  • 支持 Responses API,不等于支持所有 Codex 扩展工具。
  • 模型目录不是纯展示数据,它可能改变请求结构。
  • doctor 和 JSON 解析成功只代表静态配置成立,不能替代真实请求。
  • 从另一个模型复制元数据后,必须逐项核对工具、图片、搜索和推理能力。

如果网关未来增加对 exec 的原生支持,可以重新评估工具模式;不要在没有协议测试的情况下提前声明能力。

用 Profile 快速切换 Provider

Codex 交互会话中的 /model 适合切换当前 Provider 下的模型和推理强度,但 OpenAI 与 OpenCode Go 是两个不同 Provider。只改模型名可能把请求发到错误的服务,因此应使用 Codex Profile 同时覆盖模型、Provider 和模型目录。

当前 Codex 使用独立 Profile 文件:

$CODEX_HOME/deepseek.config.toml
$CODEX_HOME/openai.config.toml

不要继续使用旧式的:

profile = "deepseek"

[profiles.deepseek]
model = "deepseek-v4-flash"

从 Codex 0.134.0 起,--profile 不再从主配置里的 [profiles.<name>] 读取,而是加载同目录下的 <name>.config.toml。

DeepSeek Profile(文件名为 ~/.codex/deepseek.config.toml):

model = "deepseek-v4-flash"
model_provider = "opencode-go"
model_context_window = 1000000
model_catalog_json = "/Users/you/.codex/model-catalogs/opencode-go.json"
model_reasoning_effort = "high"
plan_mode_reasoning_effort = "max"

OpenAI Profile(文件名为 ~/.codex/openai.config.toml):

model = "gpt-5.6-luna"
model_provider = "openai"
model_context_window = 272000
model_catalog_json = "/Users/you/.codex/models_cache.json"
model_reasoning_effort = "high"
plan_mode_reasoning_effort = "xhigh"

根据自己的系统替换 /Users/you。模型上下文和推理档位也应以当前模型目录为准,不要永久照抄本文数值。

启动时切换:

codex -p deepseek
codex -p openai

非交互任务同样支持:

codex exec -p deepseek "检查当前代码"
codex exec -p openai "检查当前代码"

Profile 在新会话启动时加载。已经运行的会话不能靠修改磁盘配置无损更换底层 Provider;完成当前任务后再启动新会话更安全。

配置覆盖顺序

Profile 不是完全独立的配置,它叠加在用户主配置之上。与本文相关的优先级可以简化为:

用户 ~/.codex/config.toml
  < 启动时选择的 Profile
  < 项目 .codex/config.toml
  < CLI 专用参数或 -c/--config 覆盖

这意味着:

  • deepseek.config.toml 不需要重复 Provider 的 Base URL 和认证定义,它可以继承主配置。
  • 项目内如果固定了 model_provider,可能覆盖个人 Profile,需要用 /status 或启动信息确认。
  • 临时诊断适合用 -c,验证完成后再决定是否持久化。

/model、--model 和 --profile 的边界

方式是否新会话适合场景
/model否当前 Provider 内切模型或推理强度
codex -m <model>是单次覆盖模型名
codex -p <profile>是同时切换模型、Provider、目录与策略
codex -c key=value是临时排查任意配置项

跨 Provider 时优先用 Profile,因为 -m 不会自动选择另一套认证和 Base URL。

如果每天频繁切换,可以在个人 shell 配置中增加可选别名:

alias cdx-ds='codex -p deepseek'
alias cdx-openai='codex -p openai'

别名只是缩短启动命令,不改变 Codex 配置语义。桌面端模型选择器和 CLI Profile 也不是同一个能力;本文验证的是本地 Codex CLI。

验证不能只跑一层

最终按五层验证。每层排除的问题不同,不能互相替代。

第一层:原始 Responses 请求

确认 OpenCode Go 端点能够接受 deepseek-v4-flash,并返回 completed。这一层排除 URL、认证、模型名和基础协议问题。

第二层:模型目录解析

codex debug models

输出应包含:

deepseek-v4-flash
DeepSeek V4 Flash (OpenCode Go)
context_window: 1000000
default_reasoning_level: high
tool_mode: null

当前版本的 codex debug models 不接受 --profile;Profile 只适用于运行时命令和部分子命令。需要检查 Profile 是否真正生效时,应查看 codex exec 的启动信息或执行最小请求。

排查时曾运行:

codex -p deepseek debug models | jq ...

Codex 已明确报错 --profile only applies to runtime commands,但因为 shell 默认返回管道最后一个 jq 的退出码,整条命令表面上显示 exit=0。自动化脚本应启用 pipefail,否则可能把前半段失败误判为成功:

set -o pipefail

第三层:Codex 健康检查

codex doctor --summary --no-color

本次结果为:

17 ok · 1 idle · 1 notes · 0 warn · 0 fail

它确认配置可以加载、Provider 不需要 OpenAI 认证、目标 HTTP 端点可达,但仍不能发现工具协议不匹配。

第四层:两个 Profile 的真实请求

用只读沙箱和临时会话做最小端到端测试:

codex exec -p deepseek \
  -s read-only \
  --ephemeral \
  "Reply exactly DEEPSEEK_OK. Do not use tools."

codex exec -p openai \
  -s read-only \
  --ephemeral \
  "Reply exactly OPENAI_OK. Do not use tools."

最终分别返回:

DEEPSEEK_OK
OPENAI_OK

启动信息还应分别显示:

model: deepseek-v4-flash
provider: opencode-go

以及:

model: gpt-5.6-luna
provider: openai

这一步同时验证了 Profile 覆盖顺序、认证、模型目录、Provider 路由和 Responses 请求。

第五层:Agent 文件工具

使用 workspace-write 沙箱在隔离目录执行真实 apply_patch,再检查文件长度和最后一个换行字节。它验证的不是文本生成,而是:

  • 模型能够选择 Codex 提供的工具。
  • OpenCode Go 接受该工具声明。
  • Codex 能执行并返回补丁结果。
  • 目标文件内容符合精确要求。

只有第五层通过,才可以说这条链路具备最小的编码 Agent 能力。更复杂的 shell、并行工具、MCP、图片和 Web 搜索仍要分别验证,不能从一次 apply_patch 外推全部能力。

常见误区

把模型切换等同于 Provider 切换

/model 或 --model 主要覆盖模型名。跨服务切换时,还必须同步切换 model_provider、认证方式和模型目录。Profile 是更可靠的原子配置单元。

直接把 API Key 写进 config.toml

这会增加备份、截图、日志和误提交时的泄露风险。优先使用 bearer_token_env_var 或 Provider 的运行时认证命令,并收紧凭证文件权限。

只看 doctor,不跑真实请求

健康检查可以发现配置解析和连通性问题,却不一定触发模型网关对工具 schema 的完整校验。至少执行一次真实的只读、临时会话。

盲目复制官方模型元数据

上下文窗口、工具模式、图片输入、Web 搜索和推理档位都可能影响运行时行为。复制只能作为生成 schema 的起点,不能代表能力完全等价。

为已经支持的协议保留额外代理

如果目标端点已直接支持 Responses API,额外代理只会增加日志、凭证、重试和故障定位层级。先验证直连,再决定是否真的需要转换层。

故障诊断矩阵

遇到问题时,先按错误所在层定位,不要同时修改 URL、Key、模型目录和 Profile。

现象优先检查不要先做什么
/responses 返回 401Key 是否有效、Authorization 格式重写模型目录
/responses 返回 404base_url、/v1 和网关实际路由增加重试次数
模型不存在model 字符串和 OpenCode Go 当前模型列表切换 tool_mode
missing field models/models 与 Codex 目录 schema判断文本生成端点也不可用
Model metadata ... not foundmodel_catalog_json 路径和 slug把 Key 写进配置
missing field base_instructions自定义模型目录是否使用完整 schema继续手写更多猜测字段
Unsupported custom tool: exectool_mode 和上游自定义工具支持反复刷新 Key
--profile only applies to runtime commands子命令是否支持 Profile根据管道最后一个退出码宣称成功
Profile 启动后仍是另一模型项目配置和 CLI 参数是否覆盖个人 Profile直接删除所有 Codex 配置
doctor 通过但真实请求失败请求 body、工具 schema、模型参数把 doctor 当成端到端测试

推荐使用单变量实验:每次只调整一个字段,并保留修改前后的完整错误。像本次 tool_mode 问题,如果同时修改 feature flag、模型目录和 Provider,很难建立可靠的因果关系。

从零复现的操作清单

如果重新在另一台开发机配置,可以按以下顺序执行。

1. 确认版本与凭证

codex --version
jq -e \
  '.["opencode-go"].key | type == "string" and length > 0' \
  ~/.local/share/opencode/auth.json >/dev/null

2. 备份配置

cp ~/.codex/config.toml ~/.codex/config.toml.before-opencode-go

3. 用原始请求验证 /responses

只发送短文本,不附加工具。确认 HTTP 状态、status、model 和 output_text。

4. 添加 Provider 与运行时认证

先只配置 model_provider、base_url、wire_api 和认证,尽量保持变量最少。

5. 用回退元数据做第一次 Codex 请求

如果请求成功但出现模型元数据 warning,说明运行链路已经建立,再单独处理目录。

6. 从当前模型缓存生成本地目录

克隆完整记录,收窄上下文、输入模态、搜索和工具能力,并确保 tool_mode = null。

7. 分别建立两个 Profile

DeepSeek Profile 指向 OpenCode Go;OpenAI Profile 指向 OpenAI 和原生模型缓存。

8. 依次做静态与真实验证

codex debug models
codex doctor --summary --no-color
codex exec -p deepseek -s read-only --ephemeral "Reply exactly DEEPSEEK_OK."
codex exec -p openai -s read-only --ephemeral "Reply exactly OPENAI_OK."

9. 在隔离目录验证 apply_patch

不要把第一次工具测试直接放进重要仓库。确认文件内容后清理临时目录。

10. 记录版本并保留回滚点

Codex、网关和模型能力都会变化。把测试日期、Codex 版本、模型 slug 和已验证工具写入文档;升级后重跑相同检查。

当前已验证与尚未外推的边界

截至 2026-08-06,本次配置已经真实验证:

  • OpenCode Go /v1/responses 的非流式最小请求。
  • Codex 原生 Responses 调用。
  • high 推理档位的精确文本响应。
  • DeepSeek 与 OpenAI 两个 CLI Profile。
  • DeepSeek 选择并执行 apply_patch。
  • Codex 健康检查、模型目录解析和 Provider 连通性。

本文没有把以下能力视为已经证明:

  • 100 万上下文的极限输入和自动压缩边界。
  • 图片输入、原始图片细节和多模态输出。
  • Web Search、MCP、Browser、Computer Use 等扩展工具。
  • exec 自定义工具和统一 PTY 执行协议。
  • 长时间 SSE 中断恢复、并行工具调用和限流退避。
  • Codex 桌面端或云端会话的自定义 Provider 切换。

这种边界声明不是保守措辞,而是避免把“API 能返回一句话”误写成“所有 Codex 能力都与 OpenAI Provider 等价”。

回滚方式

如果需要完全恢复之前的 Codex 默认配置:

cp ~/.codex/config.toml.before-opencode-go ~/.codex/config.toml

Profile 文件是独立覆盖层,不影响备份内容。不再需要时可以先移出 ~/.codex,确认 Codex 正常启动后再删除。

结论

这次接入真正困难的部分,不是把 model 改成 deepseek-v4-flash,而是确认整条链路的能力边界:

  1. DeepSeek 与 OpenCode Go 端点确实接受 Responses API。
  2. Codex Provider 使用运行时认证,不复制明文凭证。
  3. 自定义模型目录补全上下文和推理档位。
  4. tool_mode 必须符合网关支持的自定义工具协议。
  5. Profile 同时切换模型、Provider 和目录。
  6. 原始 API、静态诊断和原生 Codex 请求必须分别验证。
  7. 真实 apply_patch 成功后,才证明最小编码 Agent 链路成立。

最大的经验不是某一段 TOML,而是排查顺序:先验证最小协议,再接入 Codex;先用回退元数据证明主链路,再补模型目录;静态检查之后,一定运行真实工具任务。 这样每一步只有一个主要变量,失败时才能快速回到正确层级。

配置参考:Codex 配置参考、Codex 高级配置与 Profile、OpenCode Go 文档和 DeepSeek Responses API。