目标

将 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 配置中 baseUrlhttps://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 的 baseUrlhttps://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 同时加入 upstreamresponsesUpstream 两个列表(使用不同的 name 字段)。


问题 4:模型名 ZHIPU/GLM-5.1 不被 DashScope 支持

现象

cc-switch metrics 数据库中记录的失败请求 model 字段为 ZHIPU/GLM-5.1failure_classretryableinput_tokensoutput_tokens 均为 0。

出现步骤

首次配置 Codex model = "ZHIPU/GLM-5.1" 后测试。

原因

DashScope 要求模型 ID 为裸名 glm-5.1,不接受带前缀的 ZHIPU/GLM-5.1。带前缀的模型名在 DashScope 上找不到对应模型,请求直接被拒绝。

解决

将 Codex Desktop 的 config.tomlmodelZHIPU/GLM-5.1 改为 glm-5.1


问题 5:电路熔断器 (Circuit Breaker) 反复跳闸

现象

cc-switch 返回 503,日志显示 [Responses-Circuit] 跳过 open 状态中的 Key[Responses-Error] 所有 Responses API密钥都失败了。即使配置正确后,Codex 的请求也被直接拒绝,不转发到上游。

出现步骤

每次配置变更后,如果之前有连续失败请求,cc-switch 的电路熔断器会进入 openhalf_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 数据库记录:成功的请求 modelglm-5.1,失败的请求 modelgpt-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 的 fuzzyModeEnabledoverrideModelenv.ANTHROPIC_MODEL 均无法覆盖 OpenAI Responses API 请求中的模型名——fuzzyModeEnabled 只做通道模糊匹配不做模型名替换,overrideModel 被静默忽略,ANTHROPIC_MODEL 只适用于 Claude Code 请求。

解决

在 Codex 和 cc-switch 之间增加一个 Node.js 代理层(model_override_proxy.js),监听端口 3002:

  1. 接收 Codex 的所有请求
  2. 解析 JSON body,将 model 字段强制替换为 glm-5.1
  3. 转发给 cc-switch(端口 3000)
  4. 流式转发 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.createdresponse.in_progressresponse.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

启动顺序

  1. 启动 cc-switch(端口 3000)
  2. 启动 model_override_proxy.js:node C:\Users\<用户名>\AppData\Local\Temp\opencode\model_override_proxy.js
  3. 启动 Codex Desktop

注意事项

  • 每次修改 cc-switch 配置后,需要清空 metrics.dbcircuit_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 需要手动启动,建议加入自启动脚本或后台常驻
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐