Files
command-code-openai-bridge/README.md
T
2026-08-05 16:43:18 +08:00

326 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Command Code OpenAI Bridge
这是一个只绑定本机地址的 OpenAI 兼容转发服务。它保留 Chat Completions 文本接口,并实现单并发的 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-code`macOS/Linux 短名为 `cmd`Windows 短名为 `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` 行;最终回答位于 `finalText`usage 和耗时也在该行。
- `--no-session` 让每次请求只使用内存会话,不写入或恢复跨请求 session。
- 官方 headless 模式明确不提供键盘、问题回答或权限批准等交互。原始 TTY/TUI 与可靠的独立 `finalText` 目前不能同时获得。
- 官方退出码:0 成功;1 常规错误;3 未登录;4 权限拒绝;5 限流;6 网络错误;7 服务端错误;8 达到 turn 上限;9 无回复;10 余额不足;130 被信号中断。
官方资料:
- [Quickstart](https://commandcode.ai/docs/quickstart)
- [CLI Reference](https://commandcode.ai/docs/reference/cli)
- [Headless Mode](https://commandcode.ai/docs/headless)
- [Permissions](https://commandcode.ai/docs/core-concepts/permissions)
## 为什么使用普通子进程与结构化事件
本项目使用官方 headless 子进程和 NDJSON,不使用 PTY,也不解析 TUI 文本。
原因是 v1.10.0 没有公开嵌入 API,交互式 TUI 没有独立的结构化最终结果通道。headless 的最后一行提供稳定 `finalText`,能保证工具参数、工具结果、状态、stderr、思考事件和 ANSI 内容不会混入 API 回复。
终端会显示:请求开始、模型、turn 状态、工具事件、工具参数、工具结果、stderr、最终回答和耗时。终端不会显示原始 Ink TUI、动画、键盘快捷键、人工权限批准、人工问题回答和详细内部思考文本。
本机 v1.10.0 实测表明,headless 即使设为 `auto-accept`shell 仍会被拒绝。默认 `dangerously_skip_permissions: true`,因此实际传入 `--yolo`,让文件写入和命令工具能继续执行。它会绕过所有权限确认,只应对可信工作目录使用。若要只读,把 `dangerously_skip_permissions` 改为 `false`,同时把 `permission_mode` 改成 `plan`
## 项目结构
```text
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
├── scripts/
│ ├── install.sh
│ └── start.sh
└── examples/
├── node_client.mjs
└── python_client.py
```
## 安装
```bash
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、安装项目依赖并构建。
如未登录:
```bash
command-code login
command-code status --json
```
`command-code login` 会打开浏览器,也可以粘贴从 Command Code Studio 创建的 API key。
## 配置
编辑 `config.yaml`
```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_turns: 100
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/responses``models.<name>.effort` 会传给 Command Code 的 `--effort`Responses 请求中的 `reasoning.effort` 优先。可用 effort 由对应底层模型决定。服务强制只监听 `127.0.0.1`。客户端只能选择配置中的模型名,不能注入额外 CLI 参数。
## 启动、停止与重启
```bash
./scripts/start.sh
```
`scripts/start.sh` 只运行已经生成的 `dist/index.js`,不会自动编译 TypeScript。修改 `src/` 中的源码后,必须先重新编译,再重启服务,新代码才会生效:
```bash
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`
Bearer Token 会被忽略。所有生成接口只接受文本。图片、音频、文件、工具和其他未实现字段会返回 OpenAI 格式的 400,`code``unsupported_parameter`;服务不会静默忽略会改变行为的参数。
### Chat Completions
支持字符串 `content`,也支持由 `{ "type": "text", "text": "..." }` 组成的数组。`stream` 省略或设为 `false` 时返回普通 JSON。`stream: true` 会立即建立 SSE 连接并发送 assistant role 块,随后发送最终回答轮次的原始文本 delta、`finish_reason: "stop"`、可选 usage 块和 `[DONE]`。传入 `stream_options.include_usage: true` 时,结束前返回 usage 块。
Chat Completions 和 Responses 共用 Command Code NDJSON 实时事件执行层。服务终端会在事件到达时立即显示状态、文本和工具调用。由于 Command Code 只有在 `turn_end` 才给出 `hadToolCalls`Bridge 会按 turn 缓冲客户端文本,丢弃工具轮次,在确认 `hadToolCalls: false` 后按原 delta 边界写入 Chat SSE。连接会立即建立,最终文本不会混入工具轮次内容,文本首包仍受最终 turn 完成时间约束。
### Responses
`POST /v1/responses` 支持这些字段:
- `model`
- `input`
- `instructions`
- `stream`
- `store`
- `previous_response_id`
- `metadata`
- `reasoning.effort`
- `text.format`
`input` 可以是字符串,也可以是 message item 数组。message 支持 `system``developer``user``assistant` 角色、字符串 content,以及 `input_text``output_text` part。
普通响应包含 `id``object`、时间、状态、错误、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.completed``response.incomplete`
所有事件都包含递增的 `sequence_number`。连接使用 `no-cache, no-transform`,收到客户端断开后会终止 Command Code 子进程组。
Command Code 在 `turn_end` 之前不能保证当前 `text_delta` 属于最终回答,因为同一 turn 随后可能产生工具调用。Bridge 按 turn 缓冲文本,丢弃 `hadToolCalls: true` 的中间轮次,在 `hadToolCalls: false` 时立即按原 delta 边界发送。因此 SSE 连接、状态事件和最终 turn 输出是真实增量事件;文本 delta 会延迟到最终 turn 边界,不能承诺逐 token 的到达时延。结构化输出还会延迟到 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 }`,支持 `after``limit``order`。删除成功返回 `{ id, object: "response", deleted: true }`
### 结构化输出
支持:
```json
{ "text": { "format": { "type": "text" } } }
```
```json
{ "text": { "format": { "type": "json_object" } } }
```
```json
{
"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 的解码级保证。
官方协议依据:
- [Chat Completions streaming events](https://developers.openai.com/api/reference/resources/chat/subresources/completions/streaming-events)
- [Responses API reference](https://developers.openai.com/api/reference/resources/responses/methods/create)
- [Responses streaming events](https://developers.openai.com/api/reference/resources/responses/streaming-events)
- [List input items](https://developers.openai.com/api/reference/resources/responses/subresources/input_items/methods/list)
- [Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs)
### curl
```bash
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
```bash
python3 -m venv .venv
.venv/bin/pip install openai
.venv/bin/python examples/python_client.py
```
### OpenAI Node.js SDK
项目依赖已包含 `openai`
```bash
node examples/node_client.mjs
```
## 实际 Command Code 命令
每次请求实际执行以下固定参数,完整提示词不进入命令行:
```bash
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 实例。第二个请求直接返回 HTTP 429 和 `code: busy`。请求体超过 `max_request_bytes` 返回 413。未知模型和非文本内容返回 4xx。CLI 的详细错误留在服务终端,客户端只收到简洁的 OpenAI 格式错误。
一次 CLI 异常不会结束 HTTP 服务,后续请求仍可继续。
## 本机实际检查结果
检查日期:Chat Completions 原有检查为 2026-08-04Responses 新增检查为 2026-08-05。没有编写测试用例,以下均为构建后运行真实 CLI 和真实 HTTP/SDK 客户端得到的端到端结果。
- 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-ok``auto-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]`
- 强制 `read_file` 的 Chat 两 turn 流只向客户端输出第二个无工具 turn 的 6 个文本 delta;终端实时显示工具状态、结果和最终文本,中间轮次没有污染客户端内容。
- Chat 流式连接收到首块后断开会取消 CLI 并恢复空闲;流占用期间第二个生成请求返回 429 `busy`,取消后的普通 Chat 请求正常完成。
- 并发检查中第二个请求返回 HTTP 429 和 `code: busy`,没有启动第二个 CLI。
- 超过 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。
- Responses 运行中第二个生成请求返回 429 `busy`,第一个请求正常完成。
- `store: true` 的 Response 可通过 GET 和 input items 查询;`previous_response_id` 成功重建历史并从 `链起点-42` 得到下一轮 `42`
- `json_schema` 合格输出通过 Ajv;不可满足的合法 Schema 顺序执行两次 CLI 后返回 `incomplete``structured_output_validation_failed`usage 为两次实际用量之和。
- 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_turns`1 秒总超时返回 200 `incomplete/timeout`;主动取消返回 499 `cancelled`
- 取消后紧接着的旧 Chat Completions 请求返回 `Chat恢复成功`,证明共享单并发状态和错误恢复正常。
## 已知限制
- 没有原始 Command Code TUI、颜色布局、动画和键盘交互。
- headless 无法在服务终端进行批准、拒绝、选项选择或文字回答;`ask_user_question` 不能由等待中的 HTTP 客户端处理。
- 终端事件渲染由本项目完成,格式接近日志,无法等同原始 TUI。
- Chat Completions 和 Responses 的文本 delta 都需要等到 `turn_end.hadToolCalls: false` 才发送;SSE 连接和初始事件会立即建立。Responses 结构化输出需要再等 Bridge 校验完成。
- 只实现 Responses 文本子集,不实现图片、音频、文件、function calling、Computer Use、托管工具、原生 reasoning item、加密 reasoning 或隐藏思维过程。
- Responses 的本地文件存储只供本 Bridge 使用,没有跨进程锁、队列或多实例一致性保证。项目本身仍严格单并发。
- Responses Structured Outputs 是 Bridge 层约束,底层 Command Code 模型仍可能连续两次输出不合格 JSON;此时状态为 incomplete。
- `usage` 使用 Command Code 最终结果提供的真实 input/output tokenResponses 缺失时返回 `null`Chat 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 文本解析。