Files
2026-08-06 14:14:09 +08:00

23 KiB
Raw Permalink Blame History

Command Code OpenAI Bridge

这是一个只绑定本机地址的 OpenAI 兼容转发服务。它实现 Chat Completions 文本与 function calling,并提供 Responses API 文本子集。每个生成请求启动一次独立的 Command Code headless agent,将完整消息历史通过 stdin 发送给 CLI,在当前终端显示运行过程,再把结构化事件和最终 finalText 转换成 OpenAI 风格的 JSON 或 SSE。

默认地址:http://127.0.0.1:18000/v1

当前机器上的调查结论

  • 系统:macOS 26.4.1 arm64。
  • Node.jsv22.22.2,符合 Command Code 的 Node.js 22+ 要求。
  • 安装方式:npm install --global command-code@latest
  • 已安装 Command Codev1.10.0npm latest 也是 v1.10.0。
  • 可执行文件:command-codemacOS/Linux 短名为 cmdWindows 短名为 cmdc
  • 登录:command-code login;检查:command-code status --json;本机已登录。
  • npm 包没有 exports 字段,main 指向会直接启动 CLI 的 dist/cli.mjs,包尾直接解析命令行,没有公开、稳定的可嵌入 API。
  • 官方 headless 调用:command-code -p --output-format json。不传查询参数时会从 stdin 读取。
  • JSON 输出是 NDJSON:运行中输出 AgentEvent,正常情况下最后输出唯一的 result 行;最终回答位于 finalTextusage 和耗时也在该行。
  • --no-session 让每次请求只使用内存会话,不写入或恢复跨请求 session。
  • 官方 headless 模式明确不提供键盘、问题回答或权限批准等交互。原始 TTY/TUI 与可靠的独立 finalText 目前不能同时获得。
  • 官方退出码:0 成功;1 常规错误;3 未登录;4 权限拒绝;5 限流;6 网络错误;7 服务端错误;8 达到 turn 上限;9 无回复;10 余额不足;130 被信号中断。

官方资料:

为什么使用普通子进程与结构化事件

本项目使用官方 headless 子进程和 NDJSON,不使用 PTY,也不解析 TUI 文本。

原因是 v1.10.0 没有公开嵌入 API,交互式 TUI 没有独立的结构化最终结果通道。headless 的最后一行提供稳定 finalText,能保证工具参数、工具结果、状态、stderr、思考事件和 ANSI 内容不会混入 API 回复。

终端会显示:请求开始、模型、turn 状态、工具事件、工具参数、工具结果、stderr、最终回答和耗时。每个 turn 结束时还会显示本轮和服务启动以来的累计 input/output/total token;服务退出时再输出一次累计,重启后从零开始。终端不会显示原始 Ink TUI、动画、键盘快捷键、人工权限批准、人工问题回答和详细内部思考文本。

本机 v1.10.0 实测表明,headless 即使设为 auto-acceptshell 仍会被拒绝。默认 dangerously_skip_permissions: true,因此实际传入 --yolo,让文件写入和命令工具能继续执行。它会绕过所有权限确认,只应对可信工作目录使用。若要只读,把 dangerously_skip_permissions 改为 false,同时把 permission_mode 改成 plan

项目结构

command-code-openai-bridge/
├── config.yaml
├── config.example.yaml
├── package.json
├── tsconfig.json
├── src/
│   ├── command-code.ts
│   ├── config.ts
│   ├── index.ts
│   ├── openai.ts
│   ├── request-coordinator.ts
│   ├── responses.ts
│   ├── renderer.ts
│   ├── server.ts
│   └── tool-calling.ts
├── scripts/
│   ├── install.sh
│   └── start.sh
└── examples/
    ├── node_client.mjs
    └── python_client.py

安装

cd /Users/zen/Documents/Codex/2026-08-04/files-mentioned-by-the-user-command/outputs/command-code-openai-bridge
./scripts/install.sh

脚本会检查 Node.js 版本、按需安装 Command Code、安装项目依赖并构建。

如未登录:

command-code login
command-code status --json

command-code login 会打开浏览器,也可以粘贴从 Command Code Studio 创建的 API key。

配置

编辑 config.yaml

host: 127.0.0.1
port: 18000
command_code_executable: command-code
command_code_working_directory: .
timeout_seconds: 1800
max_request_bytes: 20971520
max_queue_size: 8
max_turns: 100
stream_thinking: false
response_store_directory: .command-code-openai-bridge/responses
permission_mode: auto-accept
dangerously_skip_permissions: true

models:
  command-default:
    cli_model: deepseek/deepseek-v4-flash
    effort: max

command_code_working_directory 决定 Command Code 能看到和操作的项目目录。response_store_directory 保存 store: true 的 Response;两个相对路径都以配置文件所在目录为基准。默认存储目录是隐藏目录 .command-code-openai-bridge/responsesmax_queue_size 是等待执行的生成请求上限,不包含当前运行中的请求;设为 0 可恢复忙碌时立即返回 429 的行为。stream_thinking 控制 Chat Completions 流是否把 Command Code 思考事件包装成 <think> 块;默认关闭。models.<name>.effort 会传给 Command Code 的 --effortResponses 请求中的 reasoning.effort 优先。可用 effort 由对应底层模型决定。服务强制只监听 127.0.0.1。客户端只能选择配置中的模型名,不能注入额外 CLI 参数。

Obsidian Copilot 流式输出

在 Copilot 中添加自定义模型时使用:

  • Provider3rd party (openai-format)
  • Base URLhttp://127.0.0.1:18000/v1
  • API Key:任意非空文本
  • CORS:关闭
  • Streaming:开启
  • Reasoning capability:开启

Bridge 会直接响应浏览器 CORS 预检,包括 Copilot 和 OpenAI SDK 发送的自定义请求头及本机 Private Network Access。关闭 Copilot 的 CORS 绕过后,请求使用原生 fetch,可以读取 Bridge 返回的 SSE 流。将 config.yaml 中的 stream_thinking 设为 true 后,Bridge 会把实时思考事件作为 <think> 块发送,Copilot 将其显示为可折叠思考内容。思考中的 Sources 标记会被等效转义,避免 Copilot 把内部草稿误判为最终引用区并丢弃后续正文。

启动、停止与重启

./scripts/start.sh

scripts/start.sh 只运行已经生成的 dist/index.js,不会自动编译 TypeScript。修改 src/ 中的源码后,必须先重新编译,再重启服务,新代码才会生效:

npm run build
./scripts/start.sh

首次执行 scripts/install.sh 时会自动安装依赖并完成一次编译。

空闲时按 Ctrl+C 停止。请求运行时第一次 Ctrl+C 取消当前 Command Code 子进程组并保留服务;请求结束后再按 Ctrl+C 停止服务。重启就是再次执行启动脚本。

运行中的客户端断开连接会先给整个 Command Code 子进程组发送 SIGINT,随后按需升级为 SIGTERM 和 SIGKILL;排队中的客户端断开连接只会从等待队列移除。总超时由 timeout_seconds 控制。

API

  • GET /health
  • GET /v1/models
  • POST /v1/chat/completions
  • POST /v1/responses
  • GET /v1/responses/:response_id
  • DELETE /v1/responses/:response_id
  • GET /v1/responses/:response_id/input_items

GET /health 额外返回 busyqueue_lengthqueue_capacity,可用于观察当前执行槽和等待队列。

Bearer Token 会被忽略。所有生成接口只接受文本内容。图片、音频、文件输入和其他未实现字段会返回 OpenAI 格式的 400,codeunsupported_parameter;服务不会静默忽略会改变行为的参数。

Chat Completions

支持 systemdeveloperuserassistanttool 消息。文本 content 可以是字符串,也可以是由 { "type": "text", "text": "..." } 组成的数组;assistant 工具轮次还支持 content: nulltool_callstool 消息支持 tool_call_id 和可选 name

Chat 接口接受标准 function tools、tool_choiceparallel_tool_calls。Bridge 把工具定义、完整消息历史和工具结果交给 Command Code 决定下一步,校验返回的工具名与 arguments JSON Schema;第一次不合格时在原请求总截止时间内执行一次修复。工具由 API 客户端执行,Bridge 不执行客户端工具,也不直接访问 Obsidian vault。该流程适用于 Obsidian Copilot 的 localSearchreadNotegetFileTreewriteFileeditFile,也适用于其他标准 function tools。协议流程参考 OpenAI Function calling

stream 省略或设为 false 时返回普通 JSON。普通文本请求的 stream: true 会立即建立 SSE 连接并发送 assistant role 块,随后在 Command Code 的 text_delta 到达时立即发送对应 SSE chunk,最后发送 finish_reason: "stop"、可选 usage 块和 [DONE]。工具模式需要先解析和校验完整决策:最终文本会在校验后作为 content chunk 发送;工具调用会作为 delta.tool_calls 发送,并以 finish_reason: "tool_calls" 结束。stream_thinking: true 只作用于普通文本请求;该兼容格式不是原生 OpenAI reasoning item。传入 stream_options.include_usage: true 时,结束前返回 usage 块。

为兼容 Obsidian Copilot 和 LangChain OpenAI-format 客户端,Chat 接口还接受 temperaturemax_tokensmax_completion_tokenstop_pfrequency_penaltypresence_penaltyn: 1。这些采样和输出限制参数会按 OpenAI 取值范围严格校验。Command Code CLI 1.12.0 没有对应的 headless 参数,因此 Bridge 不会把它们伪装成已生效:服务终端会输出警告,HTTP 响应带 X-Command-Code-Ignored-Parameters。其他未实现字段仍返回 400 unsupported_parameter

Chat Completions 和 Responses 共用 Command Code NDJSON 执行层。服务终端会在事件到达时立即显示状态、文本和 Command Code 自身的工具调用。普通文本 Chat 会把每个 text_delta 直接写入 SSE;外部 function calling 模式会缓存 finalText,避免内部 JSON 决策进入客户端正文。

Responses

POST /v1/responses 支持这些字段:

  • model
  • input
  • instructions
  • stream
  • store
  • previous_response_id
  • metadata
  • reasoning.effort
  • text.format

input 可以是字符串,也可以是 message item 数组。message 支持 systemdeveloperuserassistant 角色、字符串 content,以及 input_textoutput_text part。

普通响应包含 idobject、时间、状态、错误、instructions、model、标准 assistant message output item、output_text、响应链 ID、effective reasoning effort、store、metadata、text format 和 usage。usage 只使用 Command Code 实际提供的 input/output token;没有 usage 时返回 null,不生成 token 明细。

stream: true 会立即建立 SSE 连接并发送:

  1. response.created
  2. response.in_progress
  3. response.output_item.added
  4. response.content_part.added
  5. 一个或多个 response.output_text.delta
  6. response.output_text.done
  7. response.content_part.done
  8. response.output_item.done
  9. response.completedresponse.incomplete

所有事件都包含递增的 sequence_number。连接使用 no-cache, no-transform,收到客户端断开后会终止 Command Code 子进程组。

Responses API 仍按 turn 缓冲文本,丢弃 hadToolCalls: true 的中间轮次,在 hadToolCalls: false 时按原 delta 边界发送。文本 delta 会延迟到最终 turn 边界。结构化输出还会延迟到 JSON 校验完成,避免把随后需要修复的无效 JSON 发给客户端。

本地存储和响应链

store 默认是 true。完成、incomplete、failed 和 cancelled Response 会写入配置的本地目录;store: false 不写入当前 Response,因此它不能在后续作为 previous_response_id 使用。

收到 previous_response_id 时,Bridge 从本地文件沿链向前读取每个 Response,按原顺序重放历史 input 和 assistant output,再追加当前 input。可以从任意仍存在的旧 Response 创建分支。删除某个祖先后,依赖该祖先的链会返回 404。Response ID 使用固定本地格式并在拼接文件路径前校验。

input items 查询返回标准 { object: "list", data, first_id, last_id, has_more },支持 afterlimitorder。删除成功返回 { id, object: "response", deleted: true }

结构化输出

支持:

{ "text": { "format": { "type": "text" } } }
{ "text": { "format": { "type": "json_object" } } }
{
  "text": {
    "format": {
      "type": "json_schema",
      "name": "result",
      "strict": true,
      "schema": { "type": "object" }
    }
  }
}

Bridge 把格式要求作为独立内部指令发送给 Command Code。最终文本必须能解析为 JSON;json_object 要求顶层对象,json_schema 使用 Ajv 校验。第一次失败后会在同一 AbortSignal 和原请求总截止时间内顺序执行一次修复,usage 是两次真实用量之和。第二次仍失败时返回 status: "incomplete"incomplete_details.reason: "structured_output_validation_failed"

这是 Bridge 层的提示、解析、校验和单次修复约束,不具备底层模型原生 Responses Structured Outputs 的解码级保证。

官方协议依据:

curl

curl -sS http://127.0.0.1:18000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer ignored' \
  -d '{
    "model": "command-default",
    "messages": [
      {"role": "user", "content": "用一句话解释递归。"}
    ],
    "stream": false
  }'

OpenAI Python SDK

python3 -m venv .venv
.venv/bin/pip install openai
.venv/bin/python examples/python_client.py

OpenAI Node.js SDK

项目依赖已包含 openai

node examples/node_client.mjs

实际 Command Code 命令

每次请求实际执行以下固定参数,完整提示词不进入命令行:

command-code \
  -p \
  --output-format json \
  --no-session \
  --skip-onboarding \
  --no-auto-update \
  --trust \
  --max-turns 100 \
  --model deepseek/deepseek-v4-flash \
  --effort max \
  --yolo

随后通过子进程 stdin 写入 UTF-8 的完整请求历史。消息序列被放进一个 JSON 对象,角色、顺序、空消息、Unicode、Markdown、代码块和自定义分隔符均不会被简单文本分隔符破坏。Bridge 不总结、不删除、不截断消息。Chat Completions 不保存会话;Responses 只在 store: true 时保存协议对象,并在新请求中把响应链重新序列化给一个新的 --no-session 进程。

有限队列和错误

同一时间只运行一个 Command Code 实例,其余生成请求按到达顺序进入等待队列。默认最多等待 8 个请求,队列已满时返回 HTTP 429 和 code: queue_full。流式请求进入队列后会立即建立 SSE 连接;非流式请求保持等待。客户端在排队期间断开会立即移出队列。timeout_seconds 从请求取得执行位置后开始计算。

请求体超过 max_request_bytes 返回 413。未知模型和非文本内容返回 4xx。CLI 的详细错误留在服务终端,客户端只收到简洁的 OpenAI 格式错误。

一次 CLI 异常不会结束 HTTP 服务,后续请求仍可继续。

本机实际检查结果

检查日期:Chat Completions 原有检查为 2026-08-04Responses 新增检查为 2026-08-05function calling 协议检查为 2026-08-06。没有编写测试用例。原有条目来自真实 CLI 和真实 HTTP/SDK 客户端;有限队列改动完成了类型检查、生产构建和协调器运行时冒烟检查,尚未重新运行真实 CLI 并发检查。function calling 完成了内部协议冒烟检查、Obsidian Copilot 当前依赖 @langchain/openai 1.2.2 的双轮 wire compatibility 检查,以及真实 Command Code 与 OpenAI Node.js SDK 的双轮 HTTP/SSE 调用;尚未在 Obsidian UI 中运行完整检查。

  • TypeScript 严格类型检查和生产构建通过。
  • npm 生产依赖审计:0 个已知漏洞。
  • /health 返回 Command Code v1.10.0、已登录、空闲。
  • /v1/models 返回两个本地映射。
  • curl 中文请求返回 HTTP 200,客户端只收到 中文接口成功 和标准 completion 字段。
  • 字符串 content 与 text part 数组均通过;非文本 part 返回 HTTP 400。
  • system、user、assistant、user 完整历史检查返回了历史 assistant 中的 蓝鲸-42
  • 约 96 KiB 的中文消息通过 stdin 完整传入并返回 长文本回退成功,没有经过命令行参数。
  • read_file 工具调用、参数和文件结果显示在服务终端,API 只返回 package name。
  • --yolo 下 shell_command 实际执行并返回 shell-tool-okauto-accept 下该工具确实被 headless 权限引擎拒绝。
  • OpenAI Node.js SDK 示例通过。
  • OpenAI Python SDK 示例通过。
  • OpenAI Node.js SDK 的 Chat stream: true 检查中,约 31 ms 收到 assistant role 块;最终文本按 10 个原始 delta 返回,随后收到 stop、真实 usage 和 [DONE]
  • LangChain 双轮检查成功把 SSE delta.tool_calls 重建为 AIMessage.tool_calls,随后发送 assistant tool call 与带 name 的 tool resultBridge Schema 接受实际请求,第二轮文本也被正确重建。
  • 真实 deepseek/deepseek-v4-flash 双轮检查中,第一轮在 tool_choice: auto 下返回 localSearcharguments 通过 JSON Schema 校验并以 finish_reason: tool_calls 结束;第二轮消费模拟 vault 结果后返回 Projects/独立决策记录.md 并以 stop 结束。两次累计 usage 为输入 62,850、输出 394、合计 63,244 tokens。
  • 强制 read_file 的 Chat 两 turn 流只向客户端输出第二个无工具 turn 的 6 个文本 delta;终端实时显示工具状态、结果和最终文本,中间轮次没有污染客户端内容。
  • Chat 流式连接收到首块后断开会取消 CLI 并恢复空闲;取消后的普通 Chat 请求正常完成。
  • Obsidian Copilot/LangChain 风格的 temperature: 0.1max_tokens: 1000、流式 usage 请求返回 HTTP 200 和正确 SSE;响应头列出两个未映射参数。可选 top_p、frequency/presence penalty、max_completion_tokensn: 1 通过严格校验,n: 2 仍返回 400。
  • 超过 20 MiB 的请求体返回 HTTP 413,服务保持可用。
  • 客户端 1 秒超时断开后,当前子进程组被清理,busy 恢复为 false,没有发现残留 headless CLI。
  • 运行中按 Ctrl+C 后客户端收到 HTTP 499,服务保持运行;紧接着的真实请求返回 中断后恢复成功
  • 原始交互模式的本机 TTY 探测确认 Ink TUI、ANSI、输入框和双 Ctrl+C 退出行为存在;该模式没有独立结构化最终结果通道。
  • OpenAI Node.js SDK 的 responses.create() 返回标准文本 Response;请求级 reasoning.effort: "max" 覆盖模型配置并实际传给 CLI。
  • store: true 的 Response 可通过 GET 和 input items 查询;previous_response_id 成功重建历史并从 链起点-42 得到下一轮 42
  • json_schema 合格输出通过 Ajv;不可满足的合法 Schema 顺序执行两次 CLI 后返回 incompletestructured_output_validation_failedusage 为两次实际用量之和。
  • OpenAI Node.js SDK 成功消费 Responses SSE;事件顺序、递增 sequence number、多个 text delta 和最终文本均正确。
  • 强制 read_file 的两 turn 请求只向 SSE 输出第二个无工具 turn 的 6 个文本 delta,中间工具轮次没有污染 output_text
  • store: false 的流式 Response 随后查询返回 404;删除已保存 Response 返回 deleted,随后查询返回 404。
  • input_image 返回 400 unsupported_parameter,并包含准确的参数路径。
  • 真实 max_turns: 1 工具请求返回 200 incomplete/max_turns1 秒总超时返回 200 incomplete/timeout;主动取消返回 499 cancelled
  • 取消后紧接着的旧 Chat Completions 请求返回 Chat恢复成功,证明共享执行状态和错误恢复正常。

已知限制

  • 没有原始 Command Code TUI、颜色布局、动画和键盘交互。
  • headless 无法在服务终端进行批准、拒绝、选项选择或文字回答;ask_user_question 不能由等待中的 HTTP 客户端处理。
  • 终端事件渲染由本项目完成,格式接近日志,无法等同原始 TUI。
  • 普通文本 Chat Completions 会实时转发 Command Code 文本 delta;外部 function calling 和 Responses 结构化输出需要等待 Bridge 校验完成。
  • Chat Completions 只实现 function tools,不实现图片、音频、文件输入、Computer Use 或托管工具。Responses 仍是文本子集,不实现 function calling、原生 reasoning item、加密 reasoning 或隐藏思维过程。
  • Chat function calling 是 Bridge 通过提示协议、JSON 解析、工具参数 Schema 校验和一次修复实现的兼容层;Command Code CLI 没有公开原生 function calling 输出接口,因此连续两次输出不合格时返回 HTTP 502 invalid_tool_decision
  • Responses 的本地文件存储只供本 Bridge 使用,没有跨进程锁、队列或多实例一致性保证。项目本身仍严格单并发。
  • Responses Structured Outputs 是 Bridge 层约束,底层 Command Code 模型仍可能连续两次输出不合格 JSON;此时状态为 incomplete。
  • usage 使用 Command Code 最终结果提供的真实 input/output tokenResponses 缺失时返回 nullChat Completions 为兼容旧行为返回 0。
  • 长文本受 HTTP 请求体上限和 Command Code 模型上下文上限共同限制,不会由桥接服务自行截断。
  • v1.10.0 的大输入会让 run_end.nextState 重复完整提示词。实测约 96 KiB 中文输入时,CLI 退出前可能截断该大事件并丢掉紧随其后的 compact result 行。桥接服务会忽略冗余 run_end,优先使用 result.finalText;若退出码为 0 且 result 缺失,只使用最后一个已完整结束、无工具调用的结构化 turn 文本和真实 turn usage,不从 TUI 文本解析。