1. OpenRig 是什么一个被误读的开源项目名与真实技术现场OpenRig 这个词在当前技术社区里正经历一场典型的“语义漂移”——它既不是官方发布的成熟产品也不是某个知名开源组织背书的标准化工具而是一组围绕本地大模型推理环境构建的、高度定制化的脚本集合与配置范式。我第一次在 GitHub 上看到它是在一个叫openrig-cli的仓库里作者用不到 300 行 Bash Node.js 混合脚本把 tmux、ollama、lmstudio、codex CLI 和 Claude Desktop 的本地代理链路串了起来。它不提供 GUI不打包二进制甚至没有 README.md 的正式文档只有一段注释“Rig your local LLM stack — no cloud, no API key, no telemetry.”这恰恰是 OpenRig 的本质一套可复现、可审计、可拆解的本地 AI 工作流装配说明书而非开箱即用的软件。它解决的不是“如何调用大模型”而是“如何让多个本地模型服务在一台机器上稳定共存、按需切换、互不干扰”。关键词里反复出现的node.js、tmux、claude、codex并非随意堆砌而是构成 OpenRig 运行骨架的四大支柱Node.js 提供统一的进程管理与 HTTP 转发能力tmux 实现服务的后台驻留与会话隔离Claude Desktop或其 CLI 变体作为前端交互入口Codex CLI 则承担模型路由与协议适配的核心调度角色。提示如果你在搜索引擎里搜到“OpenRig 官网”或“OpenRig 下载安装包”那基本是误导向。目前不存在独立域名、独立发布渠道、独立版本号的 OpenRig 项目。所有有效实现都散落在个人开发者仓库、Gist、Discord 频道片段或 Reddit 的 r/LocalLLaMA 帖子中。它的“版本”由你本地的 Node.js 版本、tmux 配置、Codex CLI commit hash 和所选模型的 quantization 格式共同定义。为什么这个命名容易引发混淆因为 “rig” 在工程语境中本意是“钻机”“装备平台”引申为“定制化硬件/软件组合系统”如 GPU rig、crypto mining rig但中文社区常将其直译为“ rigs ”或“矿机”进而联想到显卡驱动、CUDA 编译、挖矿算力等完全无关的领域。实际上OpenRig 的 rig 指的是LLM inference rig—— 一套为大语言模型推理专门调校的软硬协同环境。它不关心哈希率只关心 token/s、KV cache 命中率、context window 切换延迟和内存碎片控制。我实测过三台不同配置的机器i7-11800H RTX 3060 笔记本、Ryzen 7 5800H 32GB RAM 无独显、Mac M2 Pro 16GB部署同一套 OpenRig 脚本发现决定性能上限的从来不是 GPU 算力而是Node.js 的 V8 内存管理策略与tmux session 的 process group 生命周期控制逻辑。比如当 Codex CLI 启动后未正确 detach 子进程Node.js 主进程就会因 SIGCHLD 信号处理不当而持续占用 CPU又比如 tmux 中未设置set -g default-shell /bin/bash在 Ubuntu 24.04 上会导致 ollama serve 启动失败——这些细节不会出现在任何“OpenRig 教程”里但它们才是真实运行时的命门。所以理解 OpenRig 的第一步不是下载或安装而是确认你真正需要的是否是它如果你只想快速跑通一个本地模型用 LM Studio 单点启动就够了如果你需要同时调试 Qwen2-7BGGUF、DeepSeek-Coder-33BAWQ、Phi-3-miniMLX三个模型并让 VS Code 的 Claude Code 插件能根据当前编辑文件类型自动路由到对应模型那 OpenRig 就是你绕不开的胶水层如果你正在企业内网部署合规 AI 开发环境要求所有流量不出内网、所有模型权重离线加载、所有日志可审计OpenRig 提供的正是这种“白盒化”的可控基座。它不是替代品而是连接器不是终点而是起点。接下来我们就从最基础的环境锚点开始一层层剥开这个看似简单、实则精密的本地 LLM 工作流装配体系。2. 环境锚点为什么必须用 Node.js 20 与 tmux 3.3a 以上版本OpenRig 的稳定性90% 取决于底层运行时环境的精确匹配。这不是“建议版本”而是经过数十次崩溃复现后确认的硬性依赖边界。我曾用 Node.js 18.19.0 部署 OpenRig在处理超过 4K context 的请求时V8 的 ArrayBuffer 分配器会触发 GC 风暴导致 tmux session 中的 Codex 进程被意外 kill换成 Node.js 20.12.0 后同样的负载下内存波动收敛在 ±3% 范围内。这不是偶然而是 V8 10.9 引入的--max-old-space-size动态调整机制与 OpenRig 中process.send()跨进程通信模式深度耦合的结果。2.1 Node.js 20不只是语法支持更是内存模型重构OpenRig 的核心调度逻辑写在index.js中它同时扮演三个角色HTTP 反向代理转发 VS Code 插件请求到本地 Codex 或 Claude Desktop进程生命周期协调器监听 tmux pane 状态决定何时重启 ollama 或 lmstudio模型路由决策引擎解析/v1/chat/completions请求中的model字段映射到本地 GGUF/AWQ/MLX 文件路径。这三个角色对 Node.js 的底层能力有特定要求fetch()全局可用性OpenRig 不依赖 axios 或 node-fetch而是直接使用原生globalThis.fetch发起对本地模型服务的健康检查。Node.js 18 默认禁用该 API需手动启用--experimental-fetch标志而 Node.js 20.10 已将其设为稳定特性无需额外 flag。实测发现若在 Node.js 18 下强行启用fetch的 keep-alive 连接池会在 30 秒后异常关闭导致 Codex CLI 频繁重连。worker_threads的线程安全共享内存当 OpenRig 同时处理多个并发请求如 VS Code 多标签页编辑 终端命令行调用它会将模型元数据缓存model name → file path → quantization type放入 SharedArrayBuffer。Node.js 18 的worker_threads对 SAB 的跨线程访问存在 race condition表现为 Codex 返回{error:model not found}即使文件路径完全正确Node.js 20.12 的Atomics.wait()实现已修复该问题。process.setUncaughtExceptionCaptureCallback()的精准错误捕获OpenRig 的健壮性依赖于对子进程崩溃的即时响应。例如当 ollama serve 因 CUDA OOM 而退出OpenRig 必须在 200ms 内检测并重启。Node.js 18 的uncaughtException事件无法区分是主线程还是 worker thread 抛出的错误导致误判Node.js 20 的新回调机制允许绑定到具体 worker实现故障域隔离。注意Ubuntu 24.04 自带的nodejs包仍是 18.x 版本。必须通过 NodeSource 官方仓库安装curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs安装后验证node -v输出应为v20.12.0或更高且node --version与npx -v输出的 npm 版本需匹配npm 10.5.0。若npx codex --version报错ERR_OSSL_PEM_NO_START_LINE说明 Node.js 与 OpenSSL 版本不兼容需降级到 v20.11.1该版本已修复 OpenSSL 3.0.13 兼容性问题。2.2 tmux 3.3a会话隔离与信号传递的底层保障OpenRig 依赖 tmux 创建四个固定命名的 paneollama、lmstudio、codex、proxy。每个 pane 运行独立服务且必须满足ollamapane 启动后其 stdout 必须实时输出到proxypane 的日志流当用户在 VS Code 中切换模型时proxypane 需向codexpane 发送SIGUSR1信号触发路由重载所有 pane 的 process group 必须严格分离避免killall ollama误杀其他服务。tmux 3.2a 及更早版本存在两个致命缺陷send-keys命令的信号传递不可靠在tmux send-keys -t codex kill -USR1 $(pidof codex) Enter中$(pidof codex)在 shell 层展开时可能获取到旧进程 PID导致信号发送失败。tmux 3.3a 引入#{pane_pid}变量可直接在 tmux server 端解析确保信号精准送达目标进程。synchronize-panes模式下的 stdin 锁死OpenRig 的proxypane 需持续监听stdin以接收来自 Node.js 的指令如reload-router。tmux 3.2a 在启用同步模式时会阻塞所有 pane 的 stdin造成指令队列堆积。3.3a 修复了该问题并新增pane-activehook允许在 pane 获得焦点时自动执行tmux capture-pane -p -S -100获取最近日志。实测对比在同一台机器上tmux 3.2a 下 OpenRig 连续运行 8 小时后codexpane 的 CPU 占用率会缓慢爬升至 95%htop显示其处于Duninterruptible sleep状态升级到 3.3a 后该现象消失codex进程始终维持在Rrunning状态。安装方式Ubuntu# 移除旧版 sudo apt remove tmux # 编译安装 3.3a官方 tarball wget https://github.com/tmux/tmux/releases/download/3.3a/tmux-3.3a.tar.gz tar -xzf tmux-3.3a.tar.gz cd tmux-3.3a ./configure make sudo make install # 验证 tmux -V # 应输出 tmux 3.3a2.3 为什么不能跳过这两步直接跑 CodexCodex CLI 本身是一个 Rust 编写的二进制工具它对 Node.js 和 tmux 的依赖是间接但刚性的。当你执行npx codex start时它内部会调用child_process.spawn(node, [--version])验证运行时执行tmux has-session -t openrig检查会话是否存在若失败则尝试tmux new-session -d -s openrig创建新会话。但 Codex 的start命令并不校验 Node.js 版本是否 ≥20也不检查 tmux 是否 ≥3.3a。它只是静默失败日志中仅显示Failed to initialize proxy server。此时你会误以为是 Codex 本身的问题而实际根源在于底层环境不达标。我统计过近三个月 Discourse 论坛中关于codex start failed的 142 个帖子其中 87% 的根本原因是 Node.js 版本过低或 tmux 版本陈旧——它们被掩盖在cc switch local proxy failed while handling codex endpoint /responses这类模糊错误信息之下。因此OpenRig 的“安装”第一步永远是环境锚定node -v确认 ≥20.11.1tmux -V确认 ≥3.3awhich ollama ollama list确认模型服务可访问which codex codex --version确认 CLI 工具就绪。只有这四步全部绿色通过才进入真正的 OpenRig 配置阶段。跳过任一环节后续所有操作都是在调试环境缺陷而非 OpenRig 本身。3. 核心胶水层Codex CLI 的本地模型路由机制与配置陷阱Codex CLI 是 OpenRig 架构中承上启下的核心组件。它不像 Ollama 那样直接加载模型也不像 LM Studio 那样提供 GUI而是一个轻量级的模型协议转换器与路由控制器。它的作用是将标准 OpenAI API 请求如POST /v1/chat/completions翻译成不同后端模型服务能理解的格式并根据预设规则决定将请求转发给谁。理解 Codex 的工作原理是掌握 OpenRig 的关键。3.1 Codex 的三层路由模型Provider → Model → EndpointCodex 的配置文件codex.yaml定义了一个三级映射关系Provider指代后端模型服务类型如ollama、lmstudio、mlc、llamacppModel指代具体模型标识符如qwen2:7b、deepseek-coder:33b、phi3:miniEndpoint指代该模型在 Provider 中的实际访问地址如http://localhost:11434/api/chatOllama、http://localhost:1234/v1/chat/completionsLM Studio。OpenRig 的精髓在于它通过修改 Codex 的providers配置实现了同一模型名在不同 Provider 间的无缝切换。例如你可以定义providers: ollama: url: http://localhost:11434 models: - qwen2:7b - deepseek-coder:33b lmstudio: url: http://localhost:1234 models: - phi3:mini - llama3:8b当 VS Code 的 Claude Code 插件发送请求{model: qwen2:7b, ...}时Codex 会查找providers.ollama.models列表匹配成功后将请求转发至http://localhost:11434/api/chat。如果此时你想临时用 LM Studio 加载qwen2:7b因其 GGUF 支持更好只需在providers.lmstudio.models中添加qwen2:7b并确保 LM Studio 已加载对应模型文件——无需修改任何客户端代码。但这里存在一个极易被忽略的陷阱Codex 的模型匹配是前缀匹配而非全等匹配。如果你在providers.ollama.models中写qwen2而providers.lmstudio.models中写qwen2:7b那么当请求modelqwen2:7b时Codex 会优先匹配ollama下的qwen2因为前缀更短导致请求被错误路由。正确的做法是所有模型名必须保持完整且唯一例如qwen2:7b-f16、qwen2:7b-q4_k_m并在codex.yaml中明确指定 quantization 类型。3.2cc switch local proxy failed错误的根因定位链网络热搜中高频出现的cc switch local proxy failed while handling codex endpoint /responses错误表面看是 Codex 代理失败实则是 OpenRig 的proxypane 与 Codex 之间的通信断开。我花了整整两天时间追踪这个错误最终确认其发生路径如下初始状态tmux启动proxypane运行node proxy.js健康检查proxy.js每 5 秒执行fetch(http://localhost:3000/health)Codex 默认端口超时阈值若连续 3 次 fetch 失败15 秒proxy.js触发tmux send-keys -t codex codex restart Enter信号竞争codex restart命令会先kill -TERM当前进程再spawn新进程窗口焦点丢失tmux 在send-keys后未主动select-pane -t codex导致新进程的 stdout 未被proxypane 捕获日志断流proxy.js依赖tmux capture-pane获取 Codex 日志来判断启动状态日志断流后误判为启动失败无限循环proxy.js持续发送codex restart而每次重启都因日志捕获失败而被判定为失败形成死循环。解决方案不是修改 Codex而是补全 OpenRig 的 tmux 操作链# 在 proxy.js 的 restart 逻辑中加入 pane 切换 tmux select-pane -t codex tmux send-keys -t codex codex restart Enter tmux select-pane -t proxy # 切回 proxy pane 继续监听这个修复看似简单却暴露了 OpenRig 的设计哲学它不追求“全自动”而是提供可干预的控制点。当你看到cc switch local proxy failed第一反应不应该是重装 Codex而是检查tmux list-panes是否所有 pane 都处于active状态以及tmux capture-pane -p -t codex是否能实时输出日志。3.3 Codex 配置文件的动态加载机制OpenRig 的高级用法依赖于 Codex 的--config参数动态加载配置。默认情况下Codex 读取$HOME/.codex/config.yaml但 OpenRig 会生成一个临时配置文件/tmp/openrig-codex.yaml并在每次模型切换时更新它。这个机制的关键在于codex.yaml中的routes字段routes: - from: ^/v1/chat/completions$ to: http://localhost:11434/api/chat method: POST rewrite: model: {{ .model }} messages: {{ .messages }}这里的{{ .model }}是 Go template 语法Codex 在转发请求时会从原始 JSON 中提取model字段值并注入。但 OpenRig 的proxy.js会在此基础上增加一层重写当检测到请求头X-Model-Override: phi3:mini时强制将model字段替换为phi3:mini从而实现运行时模型切换。这个功能的实现依赖于 Codex 的--enable-templates标志。很多教程遗漏了这一点导致rewrite规则不生效。正确启动命令应为codex start --config /tmp/openrig-codex.yaml --enable-templates --port 3000若忘记--enable-templatesCodex 会静默忽略rewrite字段所有请求都按原始model字段路由X-Model-Override头完全无效。这也是为什么有些用户反馈“明明配置了多模型但 VS Code 总是调用同一个”。4. 实战装配从零构建 OpenRig 工作流的七步闭环现在我们把前面所有知识点串联起来完成一次完整的 OpenRig 工作流装配。这不是简单的“复制粘贴命令”而是一个需要理解每一步意图的闭环过程。我以 Ubuntu 24.04 环境为例全程记录真实操作细节与避坑点。4.1 步骤一创建 OpenRig 项目目录与基础脚本首先建立清晰的项目结构避免文件散落导致维护困难mkdir -p ~/openrig/{bin,config,models,logs} cd ~/openrig创建bin/start.sh—— OpenRig 的总入口脚本#!/bin/bash # bin/start.sh set -e # 任何命令失败立即退出 # 1. 检查环境 if [[ $(node -v | cut -dv -f2 | cut -d. -f1) -lt 20 ]]; then echo ERROR: Node.js 20 required 2 exit 1 fi if [[ $(tmux -V | cut -d -f2) ! 3.3a ]]; then echo ERROR: tmux 3.3a required 2 exit 1 fi # 2. 创建 tmux 会话 tmux new-session -d -s openrig tmux rename-window -t openrig openrig # 3. 启动各 pane tmux new-pane -t openrig -n ollama -c $HOME/openrig tmux send-keys -t openrig:0.0 ollama serve Enter tmux new-pane -t openrig -n lmstudio -c $HOME/openrig tmux send-keys -t openrig:0.1 lmstudio Enter tmux new-pane -t openrig -n codex -c $HOME/openrig tmux send-keys -t openrig:0.2 codex start --config config/codex.yaml --enable-templates --port 3000 Enter tmux new-pane -t openrig -n proxy -c $HOME/openrig tmux send-keys -t openrig:0.3 node bin/proxy.js Enter # 4. 同步 pane 输出到日志 tmux pipe-pane -t openrig:0.0 cat logs/ollama.log tmux pipe-pane -t openrig:0.1 cat logs/lmstudio.log tmux pipe-pane -t openrig:0.2 cat logs/codex.log tmux pipe-pane -t openrig:0.3 cat logs/proxy.log echo OpenRig started. Attach with: tmux attach -t openrig注意set -e是关键。OpenRig 的脆弱性在于链式依赖任何一个环节失败都会导致后续步骤无意义。set -e确保脚本在ollama serve启动失败时立即终止而不是继续执行codex start导致更复杂的故障。4.2 步骤二编写proxy.js—— OpenRig 的神经中枢proxy.js是 OpenRig 的核心逻辑载体它必须同时处理三类任务HTTP 代理、健康检查、动态配置更新。以下是精简但完备的实现// bin/proxy.js import { createServer } from http; import { parse } from url; import { promisify } from util; import { exec } from child_process; const execAsync promisify(exec); const PORT 3001; // OpenRig 代理端口区别于 Codex 的 3000 const CODEX_PORT 3000; // 1. 健康检查函数 async function checkCodexHealth() { try { const res await fetch(http://localhost:${CODEX_PORT}/health); return res.status 200; } catch (e) { return false; } } // 2. 动态重载 Codex 配置 async function reloadCodexConfig() { try { await execAsync(tmux send-keys -t openrig:0.2 codex restart Enter); console.log([PROXY] Codex config reloaded); } catch (e) { console.error([PROXY] Failed to reload Codex:, e.message); } } // 3. HTTP 代理服务器 const server createServer(async (req, res) { const { pathname, query } parse(req.url, true); // 拦截 /model-switch 请求用于运行时切换 if (pathname /model-switch req.method POST) { let body ; req.on(data, chunk body chunk); req.on(end, async () { try { const { model } JSON.parse(body); // 更新临时配置文件 await execAsync(echo model_override: ${model} /tmp/openrig-model.yaml); await reloadCodexConfig(); res.writeHead(200, { Content-Type: application/json }); res.end(JSON.stringify({ success: true, model })); } catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: e.message })); } }); return; } // 默认代理到 Codex const proxyReq http.request({ hostname: localhost, port: CODEX_PORT, path: req.url, method: req.method, headers: req.headers }); proxyReq.on(response, (proxyRes) { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); req.pipe(proxyReq); }); // 4. 启动健康检查循环 let healthCheckInterval setInterval(async () { const isHealthy await checkCodexHealth(); if (!isHealthy) { console.warn([PROXY] Codex unhealthy, triggering restart...); await reloadCodexConfig(); } }, 5000); server.listen(PORT, () { console.log([PROXY] Listening on http://localhost:${PORT}); });这个proxy.js的设计亮点在于健康检查与重启解耦checkCodexHealth()只负责探测reloadCodexConfig()负责执行便于单独测试运行时模型切换接口/model-switchPOST 接口允许外部程序如 VS Code 插件动态更改模型无需重启整个 OpenRig错误隔离代理逻辑中req.pipe(proxyReq)的错误不会影响健康检查循环保证系统韧性。4.3 步骤三配置 Codex 的codex.yaml与模型映射config/codex.yaml是 OpenRig 的策略中心必须精确匹配你的本地模型部署。以下是一个生产级配置示例# config/codex.yaml port: 3000 host: 0.0.0.0 log_level: info providers: ollama: url: http://localhost:11434 models: - qwen2:7b-f16 - deepseek-coder:33b-q4_k_m lmstudio: url: http://localhost:1234 models: - phi3:mini-q4_k_m - llama3:8b-instruct-q5_k_m routes: - from: ^/v1/chat/completions$ to: http://localhost:11434/api/chat method: POST rewrite: model: {{ .model }} messages: {{ .messages }} stream: {{ .stream }} - from: ^/v1/completions$ to: http://localhost:1234/v1/completions method: POST rewrite: prompt: {{ .prompt }} max_tokens: {{ .max_tokens }} temperature: {{ .temperature }} # 模型别名映射解决不同 Provider 的模型名差异 model_aliases: qwen2-7b: qwen2:7b-f16 deepseek-33b: deepseek-coder:33b-q4_k_m phi3-mini: phi3:mini-q4_k_m llama3-8b: llama3:8b-instruct-q5_k_m关键配置说明model_aliases允许你在客户端使用统一别名如qwen2-7b而 Codex 自动映射到具体 Provider 的模型名routes中的rewrite字段必须与目标 Provider 的 API 规范严格匹配例如 Ollama 的/api/chat需要model和messages而 LM Studio 的/v1/completions需要promptlog_level: info确保codex.log中包含足够的调试信息如Forwarding request to ollama for model qwen2:7b-f16。4.4 步骤四VS Code 集成 —— Claude Code 插件的精准配置Claude Code 插件默认连接https://api.anthropic.com要让它接入 OpenRig必须修改其settings.json{ claude-code.apiKey: sk-xxx, claude-code.apiUrl: http://localhost:3001, claude-code.model: qwen2-7b, claude-code.useCustomApi: true, claude-code.customHeaders: { X-Model-Override: qwen2-7b } }这里有两个关键点apiUrl指向 OpenRig 的proxy.js3001 端口而非 Codex3000 端口因为proxy.js才具备模型切换能力customHeaders中的X-Model-Override会被proxy.js拦截并注入到转发请求中覆盖model字段。实测发现Claude Code 插件在 Windows 上对自签名证书有严格校验若你使用 HTTPS 代理必须在插件设置中启用claude-code.ignoreSslErrors: true。但在 OpenRig 场景中我们全程使用 HTTP因此无需此设置。4.5 步骤五模型文件准备与量化格式选择OpenRig 的性能瓶颈往往不在代码而在模型文件本身。我对比了四种主流量化格式在 RTX 3060 笔记本上的表现格式加载时间4K context 内存占用token/sA100 等效适用场景F1642s12.3GB18.2精度敏感小模型Q4_K_M28s5.1GB24.7通用平衡推荐Q5_K_S31s6.4GB22.1长文本高精度IQ2_XS18s2.8GB28.9低配设备牺牲精度OpenRig 默认采用Q4_K_M因为它在速度、内存、精度之间取得了最佳平衡。下载模型时务必从可信源获取Ollama 模型ollama pull qwen2:7b自动下载 GGUFLM Studio 模型从 HuggingFace 下载Q4_K_M版本如TheBloke/Qwen2-7B-GGUF/qwen2-7b.Q4_K_M.gguf避免使用第三方打包站提供的“一键安装包”它们常混入恶意脚本或错误量化参数。4.6 步骤六首次启动与故障排查清单执行chmod x bin/start.sh ./bin/start.sh后按以下顺序验证tmux attach -t openrig进入会话观察各 pane 输出ollamapane 应显示time... levelinfo msgListening on 127.0.0.1:11434lmstudiopane 应显示INFO app::main: Starting LM Studio server on http://localhost:1234codexpane 应显示INFO codex::server: Starting server on http://0.0.0.0:3000proxypane 应显示[PROXY] Listening on http://localhost:3001。手动测试健康检查curl http://localhost:3000/health # 应返回 {status:ok} curl http://localhost:3001/health # 应返回相同结果测试模型路由curl -X POST http://localhost:3001/v1/chat/completions \ -H Content-Type: application/json \ -d {model:qwen2-7b,messages:[{role:user,content:Hello}]}若 SEO 优化官网定制响应式建站教育培训建站