目标很明确:保留 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/responses | HTTP 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”,因为实际请求经过三层:
- DeepSeek 模型与接口定义是否支持 Response 对象。
- OpenCode Go 是否暴露兼容的
/v1/responses路由。 - 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 返回 401 | Key 是否有效、Authorization 格式 | 重写模型目录 |
/responses 返回 404 | base_url、/v1 和网关实际路由 | 增加重试次数 |
| 模型不存在 | model 字符串和 OpenCode Go 当前模型列表 | 切换 tool_mode |
missing field models | /models 与 Codex 目录 schema | 判断文本生成端点也不可用 |
Model metadata ... not found | model_catalog_json 路径和 slug | 把 Key 写进配置 |
missing field base_instructions | 自定义模型目录是否使用完整 schema | 继续手写更多猜测字段 |
Unsupported custom tool: exec | tool_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,而是确认整条链路的能力边界:
- DeepSeek 与 OpenCode Go 端点确实接受 Responses API。
- Codex Provider 使用运行时认证,不复制明文凭证。
- 自定义模型目录补全上下文和推理档位。
tool_mode必须符合网关支持的自定义工具协议。- Profile 同时切换模型、Provider 和目录。
- 原始 API、静态诊断和原生 Codex 请求必须分别验证。
- 真实
apply_patch成功后,才证明最小编码 Agent 链路成立。
最大的经验不是某一段 TOML,而是排查顺序:先验证最小协议,再接入 Codex;先用回退元数据证明主链路,再补模型目录;静态检查之后,一定运行真实工具任务。 这样每一步只有一个主要变量,失败时才能快速回到正确层级。
配置参考:Codex 配置参考、Codex 高级配置与 Profile、OpenCode Go 文档和 DeepSeek Responses API。
DISCUSSION
讨论与反馈