Codex Desktop + 接入国产API排障总结
目标
将 OpenAI Codex Desktop 通过 cc-switch (CCX) 代理连接到阿里云 DashScope 上的 GLM-5.1 模型。
架构
Codex Desktop (wire_api=responses)
→ Node.js model_override_proxy (port 3002, 强制 model=glm-5.1)
→ cc-switch (port 3000, codexToolCompat=true, Responses→Chat Completions 转换)
→ DashScope (https://dashscope.aliyuncs.com/compatible-mode/v1, 实际上游)
问题 1:Base URL 错误 — Zhipu 直连 API 无法访问
现象
cc-switch 配置中 baseUrl 为 https://open.bigmodel.cn/api/paas/v4(智谱 AI 直连地址),所有请求返回错误。
出现步骤
初始配置阶段,cc-switch channel 直接指向智谱官方 API。
原因
虽然模型名叫 “GLM-5.1”,但用户实际持有的 API Key 是阿里云 DashScope 的密钥,不是智谱直连的密钥。DashScope 的 OpenAI 兼容端点是 https://dashscope.aliyuncs.com/compatible-mode/v1,与智谱直连地址完全不同。
解决
将 cc-switch 配置中两个 channel 的 baseUrl 从 https://open.bigmodel.cn/api/paas/v4 改为 https://dashscope.aliyuncs.com/compatible-mode/v1。
问题 2:DashScope 不支持 Responses API
现象
向 DashScope /v1/responses 发送请求返回 400 错误。Codex Desktop 使用 wire_api = "responses" 模式,只发送 Responses API 格式的请求。
出现步骤
验证 DashScope 直连可用性时发现。
原因
DashScope 的 OpenAI 兼容模式只支持 /v1/chat/completions(Chat Completions API),不支持 /v1/responses(Responses API)。Codex Desktop 只能发送 Responses API 格式,两者不兼容。
解决
在 cc-switch channel 配置中添加 codexToolCompat: true。该功能将 Codex 发来的 Responses API 请求自动转换为 Chat Completions 格式再转发给 DashScope,收到 DashScope 的 Chat Completions 响应后再转换回 Responses API 格式返回给 Codex。
问题 3:GLM channel 未加入 responsesUpstream
现象
Codex 发送 /v1/responses 请求时,cc-switch 返回 503 “All upstream channels are currently unavailable”。
出现步骤
首次在 Codex Desktop 中测试时出现。
原因
cc-switch 对 /v1/responses 路径只路由到 responsesUpstream 中的 channel,不路由到 upstream 中的 channel。GLM channel 当时只在 upstream 列表中,不在 responsesUpstream 中,因此 Responses API 请求找不到可用上游。
解决
将 GLM channel 同时加入 upstream 和 responsesUpstream 两个列表(使用不同的 name 字段)。
问题 4:模型名 ZHIPU/GLM-5.1 不被 DashScope 支持
现象
cc-switch metrics 数据库中记录的失败请求 model 字段为 ZHIPU/GLM-5.1,failure_class 为 retryable,input_tokens 和 output_tokens 均为 0。
出现步骤
首次配置 Codex model = "ZHIPU/GLM-5.1" 后测试。
原因
DashScope 要求模型 ID 为裸名 glm-5.1,不接受带前缀的 ZHIPU/GLM-5.1。带前缀的模型名在 DashScope 上找不到对应模型,请求直接被拒绝。
解决
将 Codex Desktop 的 config.toml 中 model 从 ZHIPU/GLM-5.1 改为 glm-5.1。
问题 5:电路熔断器 (Circuit Breaker) 反复跳闸
现象
cc-switch 返回 503,日志显示 [Responses-Circuit] 跳过 open 状态中的 Key 和 [Responses-Error] 所有 Responses API密钥都失败了。即使配置正确后,Codex 的请求也被直接拒绝,不转发到上游。
出现步骤
每次配置变更后,如果之前有连续失败请求,cc-switch 的电路熔断器会进入 open 或 half_open 状态,阻止所有请求通过。
原因
cc-switch 内置电路熔断器机制:当同一 API Key 连续失败 3-5 次后,熔断器进入 open 状态,直接拒绝所有请求而不转发到上游;一段时间后进入 half_open 状态允许尝试一次,如果仍失败则回到 open。之前的错误(ZHIPU/GLM-5.1、gpt-5.4-mini 等)已经积累了足够多的失败次数,导致熔断器跳闸,即使后续配置正确,请求也被拦截。
解决
使用 Python 脚本操作 cc-switch 的 SQLite metrics 数据库 (C:\Users\<用户名>\Downloads\.config\metrics.db),清空 circuit_states 表中的记录,将熔断器恢复到 closed 状态。每次配置变更后都需要清空熔断器状态并重启 cc-switch。
问题 6:Codex Desktop 发送 gpt-5.4-mini 模型名(根本原因)
现象
第一条消息成功(model 为 glm-5.1),第二条消息失败(model 变为 gpt-5.4-mini),后续所有消息 503。cc-switch metrics 数据库记录:成功的请求 model 为 glm-5.1,失败的请求 model 为 gpt-5.4-mini,同一时间戳出现。
出现步骤
在 Codex Desktop 中连续对话时出现。第一条消息正常回复,第二条消息开始报错。
原因
Codex Desktop 内部行为:主对话请求使用用户配置的模型名 glm-5.1,但后台/辅助请求(如推理辅助、内部规划等)使用 Codex 内置的默认模型名 gpt-5.4-mini。DashScope 不支持 gpt-5.4-mini,这些请求被拒绝,cc-switch 将其记录为失败并触发熔断器跳闸。一旦熔断器跳闸,所有请求(包括使用正确模型名的请求)都被拒绝,Codex 报 “Reconnecting… 503 Service Unavailable”。
cc-switch 的 fuzzyModeEnabled、overrideModel、env.ANTHROPIC_MODEL 均无法覆盖 OpenAI Responses API 请求中的模型名——fuzzyModeEnabled 只做通道模糊匹配不做模型名替换,overrideModel 被静默忽略,ANTHROPIC_MODEL 只适用于 Claude Code 请求。
解决
在 Codex 和 cc-switch 之间增加一个 Node.js 代理层(model_override_proxy.js),监听端口 3002:
- 接收 Codex 的所有请求
- 解析 JSON body,将
model字段强制替换为glm-5.1 - 转发给 cc-switch(端口 3000)
- 流式转发 cc-switch 的 SSE 响应回 Codex(逐 chunk 转发,不做缓冲)
Codex config.toml 中 base_url 改为 http://localhost:3002/v1,指向代理而非 cc-switch 直连。
问题 7:Python 代理 SSE 流式转发失败
现象
Codex 显示 “stream disconnected before completion: stream closed before response.completed”。非流式请求成功,但流式请求被截断。
出现步骤
使用 Python http.server + httpx 实现的代理时出现。
原因
Python 的 http.server + httpx.Client.request() 会缓冲整个响应再转发,无法实时推送 SSE 事件。Codex Desktop 需要 SSE 流式响应逐步到达(response.created → response.in_progress → response.output_item.added → … → response.completed),如果整个流被缓冲后一次性发送,Codex 会认为流已断开。
解决
用 Node.js 重写代理。Node.js 的 http.request() 原生支持流式转发——上游响应的每个 chunk 通过 upstreamRes.on('data', chunk => clientRes.write(chunk)) 立即转发,不做缓冲。SSE 事件逐条到达 Codex,response.completed 事件能正常送达。
最终配置文件
cc-switch config.json (C:\Users\<用户名>\Downloads\.config\config.json)
{
"upstream": [
{
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"apiKeys": ["sk-xxx"],
"serviceType": "openai",
"name": "glm-helmar",
"reasoningParamStyle": "reasoning",
"normalizeNonstandardChatRoles": true,
"codexToolCompat": true,
"priority": 2,
"status": "active",
"autoBlacklistBalance": true,
"normalizeMetadataUserId": true
}
],
"responsesUpstream": [
{
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"apiKeys": ["sk-xxx"],
"serviceType": "openai",
"name": "glm-helmar-responses",
"reasoningParamStyle": "reasoning",
"normalizeNonstandardChatRoles": true,
"codexToolCompat": true,
"priority": 2,
"status": "active",
"autoBlacklistBalance": true,
"normalizeMetadataUserId": true
}
],
"geminiUpstream": [],
"fuzzyModeEnabled": true,
"stripBillingHeader": true
}
Codex Desktop config.toml (C:\Users\<用户名>\.codex\config.toml)
model_provider = "custom"
model = "glm-5.1"
disable_response_storage = true
model_reasoning_effort = "medium"
[model_providers.custom]
name = "custom"
wire_api = "responses"
requires_openai_auth = false
base_url = "http://localhost:3002/v1"
model_override_proxy.js (C:\Users\<用户名>\AppData\Local\Temp\opencode\model_override_proxy.js)
const http = require('http');
const UPSTREAM_HOST = 'localhost';
const UPSTREAM_PORT = 3000;
const MODEL_OVERRIDE = 'glm-5.1';
const PROXY_PORT = 3002;
function overrideModel(body) {
try {
const data = JSON.parse(body);
if (data.model) {
data.model = MODEL_OVERRIDE;
}
return JSON.stringify(data);
} catch {
return body;
}
}
const server = http.createServer((clientReq, clientRes) => {
let bodyChunks = [];
clientReq.on('data', (chunk) => bodyChunks.push(chunk));
clientReq.on('end', () => {
let body = Buffer.concat(bodyChunks).toString('utf-8');
if (clientReq.method === 'POST') {
body = overrideModel(body);
}
const headers = { ...clientReq.headers };
headers['content-length'] = Buffer.byteLength(body);
delete headers['transfer-encoding'];
delete headers['host'];
const upstreamReq = http.request({
host: UPSTREAM_HOST,
port: UPSTREAM_PORT,
method: clientReq.method,
path: clientReq.url,
headers: headers,
}, (upstreamRes) => {
clientRes.writeHead(upstreamRes.statusCode, upstreamRes.headers);
upstreamRes.on('data', (chunk) => {
clientRes.write(chunk);
});
upstreamRes.on('end', () => {
clientRes.end();
});
});
upstreamReq.on('error', (err) => {
console.error('[model-proxy] upstream error:', err.message);
clientRes.writeHead(502, { 'content-type': 'application/json' });
clientRes.end(JSON.stringify({ error: err.message }));
});
upstreamReq.end(body);
});
});
server.listen(PROXY_PORT, '127.0.0.1', () => {
console.log(`[model-proxy] Listening on http://127.0.0.1:${PROXY_PORT}`);
console.log(`[model-proxy] Upstream: http://${UPSTREAM_HOST}:${UPSTREAM_PORT}`);
console.log(`[model-proxy] Model override: ${MODEL_OVERRIDE}`);
});
clear_circuit.py (C:\Users\<用户名>\AppData\Local\Temp\opencode\clear_circuit.py)
import sqlite3
conn = sqlite3.connect(r'C:\Users\<用户名>\Downloads\.config\metrics.db')
c = conn.cursor()
c.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = c.fetchall()
print('Tables:', tables)
c.execute("SELECT * FROM circuit_states")
states = c.fetchall()
print('Circuit states:', states)
c.execute("DELETE FROM circuit_states")
print(f'Deleted {c.rowcount} circuit state rows')
conn.commit()
conn.close()
print('Done')
关键文件路径
| 文件 | 路径 |
|---|---|
| cc-switch 配置 | C:\Users\<用户名>\Downloads\.config\config.json |
| cc-switch 二进制 | D:\Users\<用户名>\AppData\Local\Programs\CC Switch\cc-switch.exe |
| cc-switch metrics DB | C:\Users\<用户名>\Downloads\.config\metrics.db |
| Codex 配置 | C:\Users\<用户名>\.codex\config.toml |
| 模型覆盖代理 | C:\Users\<用户名>\AppData\Local\Temp\opencode\model_override_proxy.js |
| 熔断器清理脚本 | C:\Users\<用户名>\AppData\Local\Temp\opencode\clear_circuit.py |
启动顺序
- 启动 cc-switch(端口 3000)
- 启动 model_override_proxy.js:
node C:\Users\<用户名>\AppData\Local\Temp\opencode\model_override_proxy.js - 启动 Codex Desktop
注意事项
- 每次修改 cc-switch 配置后,需要清空
metrics.db的circuit_states表并重启 cc-switch - 熔断器清理:先停 cc-switch,然后
python C:\Users\<用户名>\AppData\Local\Temp\opencode\clear_circuit.py,再重启 cc-switch - 如果 cc-switch 日志出现熔断器跳闸,先停 cc-switch → 清 DB → 重启
- model_override_proxy.js 需要手动启动,建议加入自启动脚本或后台常驻
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)