1. openrig 到底是个什么东西
第一次看到 openrig 这个名字,很多人会以为是某个硬件外设或者开源机械臂项目。实际上,结合它周边的关键词——Claude Code、Codex、YAML、Node.js——可以判断出,openrig 是一套围绕 AI 编程助手(尤其是 Claude Code 和 Codex 这类 CLI 工具)搭建的本地配置与编排方案。它的核心价值在于:把散落在各个工具里的配置、模型接入、代理转发、环境变量这些东西,用一套统一的 YAML 结构管理起来,让 Claude Code、Codex 这些工具能在同一台机器上协同工作,而不是各配各的、互相打架。
我最初接触这类需求,是因为同时用 Claude Code 写业务代码、用 Codex 处理一些脚本任务,结果两套工具的环境变量、API 端点、模型名称经常串味。Claude Code 读到了 Codex 的配置,Codex 又去请求了 Claude 的端点,报错信息还特别隐晦,比如cc switch local proxy failed while handling codex endpoint /responses这种,看半天不知道是哪个环节出了问题。openrig 要解决的就是这个痛点:用一个中心化的 YAML 文件,把每个工具的运行参数、模型映射、代理规则全部声明清楚,启动时按需加载,互不干扰。
这套方案适合谁?如果你只是偶尔用一下 Claude Code 或者 Codex,那确实没必要折腾。但如果你符合下面任意一条,openrig 这类方案就值得认真研究:一是同时使用两个以上 AI 编程 CLI 工具;二是需要在本地模型(比如通过 LM Studio 跑的模型)和云端模型之间切换;三是团队协作时需要统一配置模板;四是经常因为 Node.js 版本、YAML 格式、环境变量问题导致工具跑不起来。说白了,它是给那些把 AI 编程助手当生产力工具、而不是玩具的人准备的。
2. 整体设计思路与方案选型
2.1 为什么用 YAML 做配置中枢
openrig 选择 YAML 作为配置格式,这个决策背后有很实际的考量。JSON 虽然通用,但不支持注释,而 AI 工具的配置里经常需要标注"这个 key 从哪来的""这个模型名对应哪个端点",没有注释会非常痛苦。TOML 表达嵌套结构时又显得啰嗦,尤其是当你要描述多个工具、多个模型、多组环境变量的时候,层级会非常深。YAML 在可读性和表达力之间取得了比较好的平衡,支持注释、支持锚点和引用、支持多文档,这些特性在管理复杂配置时非常有用。
具体到 openrig 的场景,一个典型的配置需要描述这些东西:全局的 Node.js 路径和版本约束、每个工具的可执行文件位置、模型提供方的端点地址和鉴权方式、模型名称的映射关系、代理规则、以及各工具特有的环境变量。用 YAML 写出来大概是这样一种结构:
version: "1.0" runtime: node: ">=20.0.0" package_manager: npm providers: local_lmstudio: base_url: "http://127.0.0.1:1234/v1" api_key: "not-needed" models: - name: "qwen2.5-coder-7b" alias: "local-coder" cloud_deepseek: base_url: "https://api.deepseek.com/v1" api_key: "${DEEPSEEK_API_KEY}" models: - name: "deepseek-coder" alias: "ds-coder" tools: claude_code: enabled: true provider: local_lmstudio model: local-coder env: ANTHROPIC_BASE_URL: "${providers.local_lmstudio.base_url}" codex: enabled: true provider: cloud_deepseek model: ds-coder env: OPENAI_BASE_URL: "${providers.cloud_deepseek.base_url}"这种结构的好处是,当你要换模型或者换端点时,只需要改 providers 里的一个地方,所有引用它的工具都会跟着变。变量引用用${}语法,支持从环境变量读取敏感信息,避免把 API Key 硬编码进配置文件。
2.2 Node.js 版本管理为什么是绕不开的坎
热词里有一条error installing 24.21.0: node.js v24.21.0 is not yet released or is not available,这个报错太典型了。Claude Code 和 Codex 都是 Node.js 生态的工具,对 Node 版本有硬性要求。Claude Code 目前要求 Node 18 以上,Codex 的要求也类似,但如果你系统里装的是 Node 16 或者更老的版本,安装脚本会直接失败。更麻烦的是,有些工具在安装时会去拉取一个尚未正式发布的版本号,导致not yet released这种看起来莫名其妙的错误。
openrig 的设计里,runtime 部分会显式声明 Node 版本约束,启动时先检查当前 Node 版本是否满足,不满足就给出明确的升级指引,而不是让用户去猜。我自己的做法是,在项目根目录放一个.nvmrc文件,内容写20.11.0或者lts/iron,然后用 nvm 或者 fnm 来管理。这样每次进入项目目录,nvm use一下就能切到正确的版本,避免全局 Node 版本被其他项目污染。
注意:不要用
sudo npm install -g来装 Claude Code 或 Codex。用 sudo 装全局包,后续升级和卸载都会遇到权限问题,而且不同 Node 版本下的全局包路径不一样,切换版本后命令可能直接找不到。推荐用npm install -g配合 nvm,或者用npx直接运行。
2.3 代理与端点转发的核心逻辑
cc switch local proxy failed while handling codex endpoint /responses这个报错,暴露的是代理层的问题。Claude Code 和 Codex 虽然都是 AI 编程助手,但它们使用的 API 协议不完全一样。Claude Code 走的是 Anthropic 的 Messages API 格式,Codex 走的是 OpenAI 的 Responses API 格式。当你试图用一个本地代理来统一转发请求时,如果代理没有正确区分这两种格式,就会在/responses这个端点上处理失败。
openrig 的思路是,在 YAML 里为每个工具单独声明它的 API 格式和端点路径,代理层根据工具类型做格式转换。比如 Claude Code 的请求要转成 Anthropic 格式发给本地模型,Codex 的请求要转成 OpenAI 格式。这个转换逻辑可以放在一个轻量的 Node.js 脚本里,用 Express 或者 Fastify 起一个本地服务,监听不同路径,分别处理。
// proxy.js - 简化示意 const express = require('express'); const app = express(); app.use(express.json()); // Claude Code 的端点 app.post('/v1/messages', async (req, res) => { // 转换成 Anthropic 格式,转发到配置的 provider const target = resolveProvider('claude_code'); const response = await fetch(`${target.base_url}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': target.api_key, 'anthropic-version': '2023-06-01' }, body: JSON.stringify(req.body) }); const data = await response.json(); res.json(data); }); // Codex 的端点 app.post('/responses', async (req, res) => { const target = resolveProvider('codex'); // 转换成 OpenAI Responses 格式 const response = await fetch(`${target.base_url}/responses`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${target.api_key}` }, body: JSON.stringify(req.body) }); const data = await response.json(); res.json(data); }); app.listen(3456, () => console.log('openrig proxy running on 3456'));这个代理脚本的关键在于,它要根据请求路径判断是哪个工具发来的,然后查 YAML 配置找到对应的 provider,再做格式适配。如果配置里某个工具没有启用,代理就直接返回 404,避免请求被错误转发。
3. 核心细节解析与实操要点
3.1 Claude Code 的安装与配置细节
Claude Code 的安装本身不复杂,npm install -g @anthropic-ai/claude-code一条命令就能搞定。但配置环节坑比较多。首先是登录方式,Claude Code 支持订阅账号登录和 API Key 两种方式。如果你用的是订阅账号,可能会遇到your organization has disabled claude subscription access for claude code这个提示,意思是你的组织管理员关闭了 Claude Code 的订阅访问权限。这种情况下,要么找管理员开通,要么改用 API Key 方式。
用 API Key 方式时,需要设置ANTHROPIC_API_KEY环境变量。但如果你同时想用本地模型,比如通过 LM Studio 跑的模型,就需要把ANTHROPIC_BASE_URL指向本地代理地址。这里有个细节:Claude Code 默认会去请求 Anthropic 的官方端点,如果你只改了 API Key 没改 Base URL,请求还是会发到官方服务器,本地模型根本不会被调用。
在 openrig 的 YAML 里,这部分配置会写成:
tools: claude_code: enabled: true auth_mode: api_key # 或 subscription provider: local_lmstudio env: ANTHROPIC_API_KEY: "${LOCAL_API_KEY}" ANTHROPIC_BASE_URL: "http://127.0.0.1:3456" ANTHROPIC_MODEL: "local-coder"ANTHROPIC_MODEL这个变量很多人会忽略,但不设的话,Claude Code 会用它内置的默认模型名去请求,本地模型服务可能不认识这个模型名,直接返回模型不存在的错误。
3.2 Codex 的安装与模型接入
Codex 的安装方式取决于你用的是哪个版本。OpenAI 官方的 Codex CLI 可以通过 npm 安装,也有一些第三方封装的版本。安装完成后,配置文件和 Claude Code 是分开的,通常在~/.codex/config.json或者项目根目录的.codex文件里。Codex 支持接入 DeepSeek、Qwen、GLM 等第三方模型,关键是要正确设置OPENAI_BASE_URL和OPENAI_API_KEY。
热词里有一条{"detail":"the 'gpt-5.6-sol' model is not supported when using codex with a,这个报错说明 Codex 在请求一个不存在的模型名。出现这种情况,通常是因为配置文件里模型名写错了,或者代理层没有正确映射模型别名。openrig 的做法是在 YAML 里维护一个模型别名表,把工具里用的模型名映射到 provider 实际支持的模型名:
model_aliases: claude_code: "claude-sonnet-4-20250514": "local-coder" "claude-opus-4-20250514": "local-coder" codex: "gpt-5.6-sol": "ds-coder" "gpt-4o": "ds-coder"这样即使工具内部硬编码了某个模型名,代理层也能把它转换成实际可用的模型。
3.3 YAML 文件的编写规范与常见错误
YAML 对缩进极其敏感,用 Tab 还是空格、缩进几个空格,都会影响解析结果。我见过太多因为缩进问题导致配置不生效的案例。openrig 的 YAML 文件统一用两个空格缩进,禁止使用 Tab。另外,字符串值如果包含特殊字符(比如:、{、}),需要用引号包裹,否则解析器会报错。
还有一个常见问题是布尔值的写法。YAML 里yes、no、on、off都会被解析成布尔值,但有些工具期望的是字符串"yes"。如果你在配置里写enabled: yes,解析出来是true,但如果你写enabled: "yes",解析出来就是字符串。openrig 的规范是,布尔值统一用true和false,避免歧义。
# 正确写法 enabled: true timeout: 30 api_key: "sk-xxxxx" # 错误写法(缩进不一致) tools: claude_code: enabled: true # 只缩进了1个空格 provider: local # 缩进了4个空格提示:写完 YAML 后,用
python -c "import yaml; yaml.safe_load(open('config.yaml'))"快速校验一下语法,比等到工具报错再排查要高效得多。
3.4 环境变量与敏感信息管理
API Key 这类敏感信息绝对不能硬编码进 YAML 文件然后提交到 Git。openrig 的方案是用${VAR_NAME}语法引用环境变量,YAML 文件里只保留变量名,实际值放在.env文件或者系统的环境变量里。.env文件要加入.gitignore,避免误提交。
# .env 文件 DEEPSEEK_API_KEY=sk-xxxxxxxx LOCAL_API_KEY=not-needed ANTHROPIC_API_KEY=sk-ant-xxxxxxxx然后在启动脚本里用dotenv加载:
require('dotenv').config(); const yaml = require('js-yaml'); const fs = require('fs'); const rawConfig = fs.readFileSync('openrig.yaml', 'utf8'); const config = yaml.load(rawConfig); // 递归替换 ${VAR} 为环境变量值 function resolveEnv(obj) { if (typeof obj === 'string') { return obj.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] || ''); } if (Array.isArray(obj)) return obj.map(resolveEnv); if (obj && typeof obj === 'object') { return Object.fromEntries( Object.entries(obj).map(([k, v]) => [k, resolveEnv(v)]) ); } return obj; } const resolvedConfig = resolveEnv(config);这个递归替换函数能处理嵌套结构里的变量引用,包括数组和对象。实测下来,比手动逐个读取环境变量要可靠得多。
4. 实操过程与核心环节实现
4.1 从零搭建 openrig 环境的完整步骤
假设你在一台全新的 Ubuntu 或者 macOS 机器上,要从零把 openrig 跑起来,按下面的顺序操作。
第一步,安装 Node.js 版本管理器。推荐用 fnm,它比 nvm 快,而且支持自动切换版本。
# 安装 fnm curl -fsSL https://fnm.vercel.app/install | bash # 重新加载 shell 配置 source ~/.bashrc # 或 ~/.zshrc # 安装 Node 20 LTS fnm install 20 fnm use 20 fnm default 20 # 验证 node -v # 应该输出 v20.x.x npm -v第二步,安装 Claude Code 和 Codex。
npm install -g @anthropic-ai/claude-code npm install -g @openai/codex如果安装过程中遇到error installing 24.21.0: node.js v24.21.0 is not yet released,说明 npm 试图安装一个不存在的 Node 版本。这通常是因为某个包的engines字段写了一个未来版本号。解决办法是忽略 engines 检查:
npm install -g @anthropic-ai/claude-code --engine-strict=false或者用--force强制安装。但更好的做法是检查一下是不是 fnm 或 nvm 的版本列表过期了,更新一下版本管理器本身。
第三步,创建 openrig 项目目录和配置文件。
mkdir -p ~/openrig && cd ~/openrig npm init -y npm install express js-yaml dotenv然后创建openrig.yaml,内容参考前面的示例,根据你的实际 provider 和模型填写。
第四步,编写代理脚本proxy.js,实现请求转发和格式转换。这个脚本的核心逻辑前面已经展示过,实际使用时需要根据你的 provider 支持的 API 格式做调整。比如 LM Studio 的本地服务通常兼容 OpenAI 的/v1/chat/completions端点,但 Claude Code 发的是/v1/messages格式,中间需要做一次转换。
// 将 Anthropic Messages 格式转换为 OpenAI Chat Completions 格式 function anthropicToOpenAI(anthropicReq) { const messages = anthropicReq.messages.map(msg => ({ role: msg.role === 'assistant' ? 'assistant' : 'user', content: typeof msg.content === 'string' ? msg.content : msg.content.map(c => c.text).join('') })); return { model: anthropicReq.model, messages: messages, max_tokens: anthropicReq.max_tokens || 4096, temperature: anthropicReq.temperature || 0.7, stream: anthropicReq.stream || false }; }这个转换函数处理了最常见的情况:把 Anthropic 的 content 数组拍平成字符串,把 role 映射到 OpenAI 的格式。实际使用中可能还需要处理 system prompt、tool use 等更复杂的结构,但基础版本先跑通,再逐步完善。
第五步,启动代理并配置工具指向代理。
node proxy.js & # 代理运行在 3456 端口 # 配置 Claude Code export ANTHROPIC_BASE_URL=http://127.0.0.1:3456 export ANTHROPIC_API_KEY=not-needed claude # 配置 Codex export OPENAI_BASE_URL=http://127.0.0.1:3456 export OPENAI_API_KEY=not-needed codex4.2 本地模型接入的实操记录
我用 LM Studio 跑了一个 Qwen2.5-Coder-7B 的模型,LM Studio 默认在 1234 端口提供 OpenAI 兼容的 API。在 openrig 的 YAML 里,provider 配置如下:
providers: local_lmstudio: base_url: "http://127.0.0.1:1234/v1" api_key: "not-needed" models: - name: "qwen2.5-coder-7b-instruct" alias: "local-coder" context_length: 32768然后在代理脚本里,当 Claude Code 发来请求时,把模型名从claude-sonnet-4-20250514映射到qwen2.5-coder-7b-instruct,再转发给 LM Studio。实测下来,7B 的模型在代码补全和简单重构任务上表现还行,但复杂逻辑推理明显不如云端大模型。所以我的策略是:日常写代码用本地模型,遇到难题时切换到 DeepSeek 或者 Claude 的云端 API。
切换的方式也很简单,改一下 YAML 里的provider字段,重启代理即可。或者更优雅一点,在代理层根据请求的复杂度动态路由——但这个需要额外的判断逻辑,目前我还没做到那么智能。
4.3 多工具共存的端口与路径规划
同时跑 Claude Code 和 Codex 时,端口冲突是常见问题。Claude Code 默认会尝试连接localhost:3456(如果你设了 BASE_URL),Codex 可能也会用类似的端口。openrig 的方案是给每个工具分配独立的代理路径,但共用同一个端口:
http://127.0.0.1:3456/v1/messages→ Claude Codehttp://127.0.0.1:3456/responses→ Codexhttp://127.0.0.1:3456/v1/chat/completions→ 通用 OpenAI 兼容端点
这样只需要起一个代理进程,监听一个端口,根据路径分发到不同的处理逻辑。好处是资源占用少,配置简单;坏处是如果代理进程挂了,所有工具都受影响。所以我在代理脚本里加了一个简单的健康检查端点/health,配合 systemd 或者 pm2 做进程守护。
# 用 pm2 守护代理进程 npm install -g pm2 pm2 start proxy.js --name openrig-proxy pm2 save pm2 startup这样即使代理崩溃,pm2 也会自动重启它。
5. 常见问题与排查技巧实录
5.1 安装与版本类问题速查
| 报错信息 | 根本原因 | 解决方法 |
|---|---|---|
node.js v24.21.0 is not yet released | npm 包 engines 字段写了未来版本 | 加--engine-strict=false或更新版本管理器 |
your organization has disabled claude subscription access | 组织管理员关闭了订阅访问 | 改用 API Key 方式,或联系管理员 |
command not found: claude | 全局包路径不在 PATH 里 | 检查npm bin -g输出,加入 PATH |
Error: Cannot find module 'js-yaml' | 依赖未安装 | 在项目目录执行npm install js-yaml |
EACCES: permission denied | 用 sudo 装了全局包 | 卸载后改用 nvm + 非 sudo 安装 |
5.2 代理转发类问题排查
cc switch local proxy failed while handling codex endpoint /responses这个报错,排查思路是这样的:先确认代理进程是否在运行,curl http://127.0.0.1:3456/health看有没有响应。如果代理没起来,检查端口是否被占用,lsof -i :3456看看谁在用。如果代理起来了但请求失败,看代理的日志输出,确认请求路径是否匹配到了正确的处理函数。
我遇到过一次,Codex 发来的请求路径是/v1/responses而不是/responses,但代理脚本里只注册了/responses,导致 404。解决办法是在 Express 里同时注册两个路径:
app.post(['/responses', '/v1/responses'], handleCodexRequest);这种路径差异在不同版本的 Codex 里可能不一样,所以代理脚本要尽量兼容多种路径写法。
5.3 模型不识别与响应异常
the 'gpt-5.6-sol' model is not supported这个报错,说明请求里的模型名在 provider 那边不存在。排查步骤:第一,确认 YAML 里的模型别名映射是否正确;第二,确认 provider 实际支持的模型名是什么,可以通过curl ${base_url}/models列出可用模型;第三,检查代理层是否真的做了模型名替换,可以在代理脚本里加一行日志console.log('Requested model:', req.body.model),看看实际发出去的是什么。
还有一种情况是模型名对了,但响应格式不对。比如 Claude Code 期望 Anthropic 格式的响应,但代理直接返回了 OpenAI 格式的响应,Claude Code 解析不了,就会报各种奇怪的错误。这时候需要在代理层做响应格式的反向转换,把 OpenAI 的choices[0].message.content转成 Anthropic 的content[0].text。
5.4 配置文件加载失败
YAML 文件加载失败最常见的原因是缩进和特殊字符。我踩过的坑包括:在字符串值里用了未转义的冒号,导致解析器把冒号后面的内容当成了新的键值对;在注释里用了中文全角字符,某些 YAML 解析器会报编码错误;还有一次是文件末尾多了几个空格,导致解析器认为还有一个空文档。
排查方法很简单,用 Node.js 的js-yaml库加载一下,看报错信息指向哪一行:
try { const config = yaml.load(fs.readFileSync('openrig.yaml', 'utf8')); console.log('Config loaded:', JSON.stringify(config, null, 2)); } catch (e) { console.error('YAML parse error:', e.message); }报错信息通常会给出行号和列号,直接定位到问题位置。
提示:VS Code 里装一个 YAML 插件(比如 Red Hat 的 YAML Language Support),它能实时校验语法并给出提示,比等到运行时才发现问题要省事得多。
5.5 性能与稳定性优化心得
代理层如果处理大量并发请求,可能会成为瓶颈。我的做法是在代理脚本里加一个简单的请求队列,限制同时转发的请求数量,避免本地模型服务被压垮。另外,对于流式响应(streaming),代理需要正确处理text/event-stream格式,不能等整个响应结束再转发,否则 Claude Code 和 Codex 的流式输出会变成一次性输出,体验很差。
// 流式转发示例 app.post('/v1/messages', async (req, res) => { const target = resolveProvider('claude_code'); const upstream = await fetch(`${target.base_url}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(convertRequest(req.body)) }); res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); const reader = upstream.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); // 转换格式后写入响应 res.write(convertStreamChunk(chunk)); } res.end(); });流式处理是代理层最复杂的部分,因为要在数据流动的过程中做格式转换,不能等缓冲区满了再处理。我调试这部分花了差不多一个下午,最后发现关键是不要用await response.json(),而是直接用response.body.getReader()逐块读取。
6. 我个人的使用体会与后续扩展方向
这套 openrig 方案我用了大概三个月,最大的感受是:配置集中管理之后,切换模型和工具的成本从"改五个地方"变成了"改一个地方"。以前每次想试试新模型,都要去翻 Claude Code 的文档、Codex 的文档、LM Studio 的设置,现在只需要在 YAML 里加一个 provider 条目,改一下工具的 provider 引用,重启代理就完事了。
踩过的坑里,最折腾的是流式响应的格式转换。Claude Code 对响应格式的要求比较严格,如果代理返回的 SSE 事件格式不对,它会直接断开连接,而且报错信息很不明确。后来我抓包对比了官方 API 的响应格式,才把事件类型和数据结构对齐。
后续我打算把 openrig 的配置管理做成一个 CLI 工具,支持openrig init生成模板、openrig validate校验配置、openrig switch切换 provider。这样就不用每次都手动编辑 YAML 了。另外,模型路由那块也可以做得更智能,比如根据请求的 token 数量自动选择本地模型还是云端模型,小请求走本地省成本,大请求走云端保质量。
如果你也在同时折腾多个 AI 编程工具,建议先从最简单的单工具配置开始,跑通了再逐步加入第二个工具和代理层。不要一上来就搞全套,那样出了问题很难定位是哪个环节的毛病。先把 Claude Code 或者 Codex 其中一个配好,确认能正常调用模型,再引入 openrig 的 YAML 管理和代理转发,一步一步来,稳扎稳打。