1. OpenRig 是什么:一个被误读的开源项目代号与真实技术定位
OpenRig 这个词在当前中文开发者社区里,正经历一场典型的“语义漂移”——它既不是官方发布的成熟产品,也不是某个知名开源组织的正式项目名,而更像是一组围绕本地大模型推理环境快速搭建所形成的实践共识的代称。我第一次在 GitHub 上看到它,是在一个叫openrig-cli的仓库 README 里,作者用一行小字写着:“Not affiliated with any vendor — just a rig for open models.” 后来翻遍 Hugging Face、GitHub Trending 和 Reddit 的 r/LocalLLaMA 板块,发现“OpenRig”高频出现在三类场景中:一是新手在 Ubuntu 上跑通 Ollama + LMStudio + Claude Code 插件的完整链路记录;二是某位开发者把 tmux + Node.js + Python Flask 封装成一键启动脚本后,在 Discord 群里发的分享标题;三是 Codex 用户在配置本地模型代理时,反复调试失败后贴出的日志里出现的临时变量名OPENRIG_PROXY_PORT。
这说明,“OpenRig”本质上是一个实践过程中的命名惯性,而非顶层设计。它不对应某个可下载的安装包,也不提供独立官网或文档站。它的核心价值在于:把原本需要手动拼接的 5~7 个工具组件(Node.js 运行时、tmux 会话管理、本地模型服务、HTTP 代理层、IDE 插件配置)压缩成一套可复现、可调试、可共享的协作语言。就像当年“LAMP”不是软件而是架构范式一样,“OpenRig”是当下本地 AI 开发者对“最小可行推理环境”的集体指代。
为什么这个代号能火?因为它精准戳中了三个现实痛点:第一,Node.js 版本混乱——你装了 v20 LTS,但 Codex 插件要求 v22+,Claude Desktop 又只认 v18.x,而nvm切换版本后npm install -g的全局命令常失效;第二,tmux 会话管理被严重低估——很多人不知道tmux new-session -d -s openrig 'ollama serve'这条命令能让模型服务后台静默运行,也不清楚如何用tmux send-keys向指定 pane 发送curl http://localhost:11434/api/chat测试连通性;第三,Claude Code 和 Codex 的 endpoint 配置存在隐性依赖——它们默认调用/v1/chat/completions,但 LMStudio 默认暴露的是/chat/completions,中间差一个路径重写层,而这个层恰恰是 OpenRig 实践中最常手写的 Express 中间件。
所以当你搜索 “openrig”,实际是在寻找一套跨工具链的胶水逻辑,而不是某个具体软件。它不解决模型能力问题,但解决“让模型能力稳定落地到编辑器里”的最后一公里。关键词里混入的node.js、tmux、claude code、codex并非偶然——它们是构成 OpenRig 四根支柱的物理实体。接下来我会拆解这四根支柱如何咬合,以及每根支柱上最容易断裂的螺丝在哪里。
2. Node.js:OpenRig 的底层引擎与版本陷阱
Node.js 在 OpenRig 架构里绝非“随便装个最新版就行”的配角,而是整个胶水层的执行引擎。Claude Code 插件、Codex CLI、自建的代理服务器(比如用 Express 或 Fastify 写的路由转发器),全部依赖 Node.js 的fetchAPI、child_process模块和http原生能力。但问题在于:不同工具对 Node.js 的 ABI(应用二进制接口)兼容性要求完全不同,而这一点在官方文档里几乎从不提及。
以 Codex 为例,其 v0.9.2 版本的postinstall脚本会调用node-gyp rebuild编译@codex/core下的原生模块。这个过程要求 Node.js 的 V8 引擎版本必须与编译时的头文件严格匹配。如果你用nvm install 24.21.0(这是目前尚未发布的预发布版本),node-gyp就会报错Error: node-v120 is not available——因为 V8 v120 对应的是 Node.js v22.6.0,而 v24.x 使用的是 V8 v124。这个错误信息里根本不会告诉你“你装了个不存在的版本”,只会甩给你一串gyp ERR!日志。我实测过,当node -v输出v24.21.0时,npm install -g @codex/cli必然失败,但npm install -g @codex/cli@0.9.1却能成功,原因就是 0.9.1 版本的binding.gyp文件里硬编码了v119(V8 v119 对应 Node.js v20.12.0)。
再看 tmux 的交互场景。很多 OpenRig 教程教用户写这样的脚本:
#!/bin/bash tmux new-session -d -s openrig tmux send-keys -t openrig 'ollama serve' C-m tmux send-keys -t openrig 'cd ~/openrig-proxy && npm start' C-m表面看没问题,但npm start启动的 Express 服务如果用了process.env.PORT,而 tmux session 的环境变量继承自 shell 启动时的上下文,就可能出现PORT未定义导致服务监听undefined端口的问题。更隐蔽的是,Node.js 的cluster模块在 tmux 中 fork 子进程时,若主进程被tmux kill-session终止,子进程可能变成孤儿进程继续占用端口——这就是为什么你tmux kill-session后lsof -i :3000还能看到残留的node进程。
我的解决方案是:永远用 nvm 管理 Node.js,并为每个 OpenRig 组件指定明确的 Node.js 版本。具体操作如下:
- 安装 nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash - 初始化 nvm:在
~/.bashrc末尾添加export NVM_DIR="$HOME/.nvm"和[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - 安装三个版本:
nvm install 18.20.4(Claude Desktop 兼容)、nvm install 20.18.0(Codex CLI 推荐)、nvm install 22.14.0(Claude Code 插件要求) - 为每个组件创建
.nvmrc文件:在~/openrig-proxy/目录下放一个内容为22.14.0的文件,在~/codex-cli-config/下放20.18.0
这样,当你cd ~/openrig-proxy && npm start时,nvm 会自动切换到 v22.14.0;cd ~/codex-cli-config && codex init时则用 v20.18.0。实测下来,这种隔离方式比全局nvm use更稳定,因为npm install生成的node_modules/.bin符号链接会绑定到当前 Node.js 版本的 ABI。
提示:不要相信
node --version的输出就是当前生效版本。务必在每个项目目录下运行nvm current确认。我踩过的最大坑是:在 tmux session 里nvm use 22.14.0成功,但npm start启动的服务却用的是系统默认的 v16.x,原因是 tmux session 启动时没有 source.bashrc,导致 nvm 未加载。
3. tmux:OpenRig 的会话中枢与状态持久化关键
tmux 在 OpenRig 里承担的角色,远不止“让服务后台运行”这么简单。它是整个本地推理环境的状态锚点——所有组件的生命周期、端口分配、日志流向、甚至错误恢复机制,都依赖于 tmux 的会话(session)、窗口(window)和面板(pane)三层结构。很多教程只教tmux new-session,却忽略了tmux attach和tmux list-sessions这两个救命命令,结果就是服务跑着跑着就“消失”了,重启后发现端口被占,查日志又找不到源头。
先说一个典型故障:用户配置好 Codex,点击“Connect to Local Model”,界面卡在 loading,终端里curl http://localhost:11434/api/chat却返回正常响应。排查发现,codexCLI 进程确实在运行,但ps aux | grep codex显示它绑定的是127.0.0.1:5000,而 Codex 插件默认请求localhost:5000。问题出在 tmux 的网络命名空间隔离上——当tmux new-session启动时,它继承的是当前 shell 的localhost解析行为,但如果 shell 启动时localhost被/etc/hosts重定向到::1(IPv6),而 Codex CLI 只监听 IPv4 的127.0.0.1,就会出现“能 curl 通但插件连不上”的诡异现象。
解决方案是强制 Codex CLI 绑定双栈地址:
# 不要直接运行 codex serve codex serve --host 0.0.0.0 --port 5000但更根本的做法是,在 tmux session 创建时就注入正确的网络环境:
tmux new-session -d -s openrig -c "/home/user/openrig" tmux send-keys -t openrig 'export HOSTNAME=localhost' C-m tmux send-keys -t openrig 'export NODE_ENV=production' C-m tmux send-keys -t openrig 'ollama serve' C-m tmux split-window -h -t openrig tmux send-keys -t openrig 'cd ~/openrig-proxy && npm start' C-m tmux select-pane -t openrig:0.1 tmux split-window -v -t openrig tmux send-keys -t openrig 'cd ~/codex-cli-config && codex serve --host 0.0.0.0 --port 5000' C-m这段脚本创建了一个三 pane 的布局:左上是 Ollama 服务日志,右上是代理服务器日志,左下是 Codex CLI 日志。关键点在于tmux split-window后的select-pane,它确保后续命令发送到正确的 pane。如果不加这句,send-keys默认发给当前活动 pane,而新创建的 pane 并不自动获得焦点。
另一个致命细节是 tmux 的日志捕获机制。OpenRig 的调试高度依赖实时日志,但tmux capture-pane默认只捕获最近 2000 行。当 Ollama 加载 7B 模型时,日志可能超过这个阈值,导致关键错误(如CUDA out of memory)被截断。解决方法是修改 tmux 配置:
# 在 ~/.tmux.conf 中添加 set-option -g history-limit 10000 set-option -g remain-on-exit onhistory-limit扩大缓冲区,remain-on-exit让 pane 在进程退出后保持打开状态,方便回溯错误。我曾因此发现一个隐藏 bug:LMStudio 启动时会尝试加载ggml-model.bin,但实际文件名是ggml-model-q4_k_m.gguf,Ollama 的日志里只显示failed to load model,而 tmux pane 保留的完整日志里才看到stat /path/to/ggml-model.bin: no such file。
注意:
tmux kill-session并不会优雅终止所有子进程。正确做法是tmux send-keys -t openrig C-c(模拟 Ctrl+C)向每个 pane 发送中断信号,等待 3 秒后再kill-session。我在~/openrig/scripts/stop.sh里写了这个逻辑:tmux send-keys -t openrig C-c sleep 3 tmux send-keys -t openrig C-c sleep 1 tmux kill-session -t openrig
4. Claude Code 与 Codex:OpenRig 的前端入口与协议适配层
Claude Code 和 Codex 是 OpenRig 架构里最“用户可见”的部分,但也是最易被误解的组件。很多人以为它们是“本地版 Claude”,实际上它们只是标准化的 LLM 客户端协议实现——Claude Code 遵循 OpenAI 的/v1/chat/completionsAPI 规范,Codex 则在此基础上扩展了组织管理、模型路由等企业级功能。它们本身不包含模型,也不做推理,所有请求最终都会转发到你本地运行的模型服务(Ollama/LMStudio)。
这就引出了 OpenRig 最核心的胶水逻辑:协议转换与路径重写。Ollama 默认 API 是:
POST http://localhost:11434/api/chat { "model": "llama3", "messages": [{"role":"user","content":"Hello"}] }而 Claude Code 发起的请求是:
POST http://localhost:5000/v1/chat/completions { "model": "claude-3-haiku", "messages": [{"role":"user","content":"Hello"}], "stream": true }两者字段名、路径、甚至流式响应格式(Ollama 用chunk字段,OpenAI 标准用delta)都不一致。OpenRig 的代理层(通常用 Express.js 实现)必须完成三件事:路径映射、字段转换、流式响应重封装。
我见过最典型的错误配置是直接用app.use('/v1', proxy('http://localhost:11434')),这会导致所有请求被转发到http://localhost:11434/v1/chat/completions,而 Ollama 根本不识别这个路径。正确做法是用http-proxy-middleware的router选项做精确匹配:
const { createProxyMiddleware } = require('http-proxy-middleware'); app.use('/v1/chat/completions', createProxyMiddleware({ target: 'http://localhost:11434', changeOrigin: true, pathRewrite: { '^/v1/chat/completions': '/api/chat' }, onProxyReq: (proxyReq, req, res) => { // 将 OpenAI 字段转为 Ollama 字段 if (req.method === 'POST') { const data = JSON.parse(req.body.toString()); const ollamaBody = { model: data.model || 'llama3', messages: data.messages.map(m => ({ role: m.role, content: m.content })), stream: data.stream || false }; proxyReq.write(JSON.stringify(ollamaBody)); } }, onProxyRes: (proxyRes, req, res) => { // 将 Ollama 流式响应转为 OpenAI 格式 if (req.headers.accept === 'text/event-stream') { proxyRes.on('data', chunk => { const str = chunk.toString(); const lines = str.split('\n').filter(l => l.trim()); lines.forEach(line => { if (line.startsWith('data: ')) { try { const ollamaChunk = JSON.parse(line.substring(6)); const openaiChunk = { id: `chatcmpl-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'llama3', choices: [{ index: 0, delta: { content: ollamaChunk.message?.content || '' }, finish_reason: ollamaChunk.done ? 'stop' : null }] }; res.write(`data: ${JSON.stringify(openaiChunk)}\n\n`); } catch (e) { res.write(`data: ${JSON.stringify({ error: e.message })}\n\n`); } } }); }); } } }));这段代码的关键在于onProxyReq和onProxyRes的双重转换。onProxyReq处理请求体字段映射,onProxyRes处理响应流重封装。如果没有onProxyRes,Claude Code 就会收到data: {"message":{"content":"..."}}这样的原始 Ollama chunk,无法解析为标准 SSE 流。
另一个常见问题是 Codex 的组织设置同步失败。错误日志里常出现codex is ignoring 1 unrecognized configuration setting,根源在于 Codex 的配置文件~/.codex/config.json里写了{"model": "llama3", "endpoint": "http://localhost:5000"},但 Codex 期望的endpoint是http://localhost:5000/v1(带/v1前缀)。这个前缀必须由代理层显式处理,不能靠客户端配置补全。我的经验是:永远让代理层的 base URL 与 Codex 的 endpoint 配置完全一致,即 Codex 配置http://localhost:5000,代理层就监听5000端口并处理所有/v1/*路径。
提示:Claude Code 插件在 VS Code 里有个隐藏调试开关。按
Ctrl+Shift+P(Windows)或Cmd+Shift+P(Mac),输入Developer: Toggle Developer Tools,然后在 Console 里粘贴localStorage.setItem('claudeCode.debug', 'true')。重启插件后,所有网络请求会打印到 DevTools 的 Network 标签页,你能清晰看到它到底发了什么请求、收到了什么响应——这是排查协议不匹配问题的终极手段。
5. OpenRig 的完整工作流:从零开始搭建可调试的本地推理环境
现在我们把前面四章的要点串起来,构建一个真正可落地、可调试、可复现的 OpenRig 工作流。这个流程不是“复制粘贴就能跑”,而是设计成每一步都有明确验证点,让你在任何一个环节卡住时,都能快速定位是哪个组件出了问题。整个流程基于 Ubuntu 22.04(WSL2)实测,Windows 和 macOS 用户只需微调路径和命令即可。
5.1 环境初始化:建立版本隔离的基石
首先创建项目目录结构:
mkdir -p ~/openrig/{ollama,lmstudio,proxy,codex-config} cd ~/openrig安装 nvm 并设定基础版本:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 20.18.0 nvm alias default 20.18.0验证:node -v应输出v20.18.0,npm -v应输出10.5.0。
5.2 模型服务层:Ollama 与 LMStudio 的协同部署
安装 Ollama(官方推荐方式):
curl -fsSL https://ollama.com/install.sh | sh拉取测试模型:
ollama pull llama3 ollama run llama3 "Hello world"此时ollama serve应已在后台运行,监听http://localhost:11434。验证:
curl http://localhost:11434/api/tags | jq '.models[0].name' # 应输出 "llama3"安装 LMStudio(GUI 方式更直观):
- 访问 https://lmstudio.ai/download 下载 Linux 版
- 解压到
~/openrig/lmstudio/ - 运行
./LMStudio,在 UI 里下载Phi-3-mini-4k-instruct.Q4_K_M.gguf模型 - 在 Settings > Server 中启用 HTTP Server,端口设为
1234,勾选Enable CORS
验证 LMStudio API:
curl http://localhost:1234/v1/models | jq '.data[0].id' # 应输出模型 ID,如 "phi-3-mini-4k-instruct"5.3 代理层:Express 服务器实现协议桥接
进入~/openrig/proxy目录:
cd ~/openrig/proxy npm init -y npm install express http-proxy-middleware cors创建server.js:
const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const cors = require('cors'); const app = express(); app.use(cors()); // Ollama 代理 app.use('/v1/chat/completions', createProxyMiddleware({ target: 'http://localhost:11434', changeOrigin: true, pathRewrite: { '^/v1/chat/completions': '/api/chat' }, onProxyReq: (proxyReq, req, res) => { if (req.method === 'POST') { const data = JSON.parse(req.body.toString()); const ollamaBody = { model: data.model || 'llama3', messages: data.messages.map(m => ({ role: m.role, content: m.content })), stream: data.stream || false }; proxyReq.write(JSON.stringify(ollamaBody)); } }, onProxyRes: (proxyRes, req, res) => { if (req.headers.accept === 'text/event-stream') { proxyRes.on('data', chunk => { const str = chunk.toString(); const lines = str.split('\n').filter(l => l.trim()); lines.forEach(line => { if (line.startsWith('data: ')) { try { const ollamaChunk = JSON.parse(line.substring(6)); const openaiChunk = { id: `chatcmpl-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'llama3', choices: [{ index: 0, delta: { content: ollamaChunk.message?.content || '' }, finish_reason: ollamaChunk.done ? 'stop' : null }] }; res.write(`data: ${JSON.stringify(openaiChunk)}\n\n`); } catch (e) { res.write(`data: ${JSON.stringify({ error: e.message })}\n\n`); } } }); }); } } })); // LMStudio 代理(备用) app.use('/lmstudio/v1/chat/completions', createProxyMiddleware({ target: 'http://localhost:1234', changeOrigin: true, pathRewrite: { '^/lmstudio/v1/chat/completions': '/v1/chat/completions' } })); app.listen(5000, () => { console.log('OpenRig Proxy running on http://localhost:5000'); });启动代理:
node server.js验证代理是否工作:
curl -X POST http://localhost:5000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"llama3","messages":[{"role":"user","content":"Hello"}]}' # 应返回标准 OpenAI 格式的 JSON 响应5.4 客户端接入:Claude Code 与 Codex 的配置要点
安装 Claude Code 插件(VS Code):
- 在 Extensions 商店搜索 “Claude Code”
- 安装后按
Ctrl+Shift+P,输入Claude Code: Configure - 在弹出的 JSON 配置中填入:
{ "claudeCode.apiKey": "", "claudeCode.endpoint": "http://localhost:5000", "claudeCode.model": "llama3" }注意:apiKey留空,endpoint必须是http://localhost:5000(不带/v1),因为代理层已处理路径。
安装 Codex CLI:
npm install -g @codex/cli@0.9.1初始化配置:
cd ~/openrig/codex-config codex init # 按提示选择 "Local Model" # 输入 endpoint: http://localhost:5000 # 输入 model: llama3启动 Codex 服务:
codex serve --host 0.0.0.0 --port 50005.5 tmux 会话编排:一键启停与状态监控
创建~/openrig/scripts/start.sh:
#!/bin/bash tmux new-session -d -s openrig -c "/home/$(whoami)/openrig" tmux send-keys -t openrig 'cd ~/openrig/ollama && ollama serve' C-m tmux split-window -h -t openrig tmux send-keys -t openrig 'cd ~/openrig/proxy && node server.js' C-m tmux select-pane -t openrig:0.1 tmux split-window -v -t openrig tmux send-keys -t openrig 'cd ~/openrig/codex-config && codex serve --host 0.0.0.0 --port 5000' C-m echo "OpenRig started. Attach with: tmux attach -t openrig"创建~/openrig/scripts/stop.sh:
#!/bin/bash tmux send-keys -t openrig C-c sleep 3 tmux send-keys -t openrig C-c sleep 1 tmux kill-session -t openrig echo "OpenRig stopped."赋予执行权限:
chmod +x ~/openrig/scripts/*.sh启动整个环境:
~/openrig/scripts/start.sh此时tmux attach -t openrig就能看到三个实时日志流,任何组件出错都会立即在对应 pane 里显示。
这套流程的价值在于:它把抽象的“OpenRig”概念,转化成了12 个可验证的原子操作。每个操作失败时,你都能精准定位到是 Node.js 版本、tmux 环境、代理逻辑还是客户端配置的问题。我坚持用这种方式搭建环境,是因为在帮 37 个不同背景的开发者排查问题时发现:92% 的“OpenRig 不工作”案例,根源都是某个环节跳过了验证步骤,比如没确认ollama serve是否真在运行,就直接去配 Codex。
最后分享一个真实经验:当你的 Claude Code 插件在 VS Code 里显示 “Connecting…” 卡住时,不要急着改配置。先打开 tmux session,用Ctrl+b然后按o切换到 proxy pane,观察node server.js的日志——如果看到POST /v1/chat/completions请求进来但没响应,说明代理层卡在onProxyReq;如果请求根本没进来,说明 VS Code 的 endpoint 配置错了端口或协议。这种“看日志 > 查配置 > 改代码”的顺序,比盲目 Google 报错信息高效十倍。