工具使用

This commit is contained in:
Sirius
2026-08-06 14:14:09 +08:00
parent d5eb882dfe
commit e236721db1
11 changed files with 745 additions and 73 deletions
+28 -17
View File
@@ -1,6 +1,6 @@
# Command Code OpenAI Bridge
这是一个只绑定本机地址的 OpenAI 兼容转发服务。它保留 Chat Completions 文本接口,并实现单并发的 Responses API 文本子集。每个生成请求启动一次独立的 Command Code headless agent,将完整消息历史通过 stdin 发送给 CLI,在当前终端显示运行过程,再把结构化事件和最终 `finalText` 转换成 OpenAI 风格的 JSON 或 SSE。
这是一个只绑定本机地址的 OpenAI 兼容转发服务。它实现 Chat Completions 文本与 function calling,并提供 Responses API 文本子集。每个生成请求启动一次独立的 Command Code headless agent,将完整消息历史通过 stdin 发送给 CLI,在当前终端显示运行过程,再把结构化事件和最终 `finalText` 转换成 OpenAI 风格的 JSON 或 SSE。
默认地址:`http://127.0.0.1:18000/v1`
@@ -52,7 +52,8 @@ command-code-openai-bridge/
│ ├── request-coordinator.ts
│ ├── responses.ts
│ ├── renderer.ts
── server.ts
── server.ts
│ └── tool-calling.ts
├── scripts/
│ ├── install.sh
│ └── start.sh
@@ -90,6 +91,7 @@ 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
@@ -102,7 +104,7 @@ models:
effort: max
```
`command_code_working_directory` 决定 Command Code 能看到和操作的项目目录。`response_store_directory` 保存 `store: true` 的 Response;两个相对路径都以配置文件所在目录为基准。默认存储目录是隐藏目录 `.command-code-openai-bridge/responses``stream_thinking` 控制 Chat Completions 流是否把 Command Code 思考事件包装成 `<think>` 块;默认关闭。`models.<name>.effort` 会传给 Command Code 的 `--effort`Responses 请求中的 `reasoning.effort` 优先。可用 effort 由对应底层模型决定。服务强制只监听 `127.0.0.1`。客户端只能选择配置中的模型名,不能注入额外 CLI 参数。
`command_code_working_directory` 决定 Command Code 能看到和操作的项目目录。`response_store_directory` 保存 `store: true` 的 Response;两个相对路径都以配置文件所在目录为基准。默认存储目录是隐藏目录 `.command-code-openai-bridge/responses``max_queue_size` 是等待执行的生成请求上限,不包含当前运行中的请求;设为 `0` 可恢复忙碌时立即返回 429 的行为。`stream_thinking` 控制 Chat Completions 流是否把 Command Code 思考事件包装成 `<think>` 块;默认关闭。`models.<name>.effort` 会传给 Command Code 的 `--effort`Responses 请求中的 `reasoning.effort` 优先。可用 effort 由对应底层模型决定。服务强制只监听 `127.0.0.1`。客户端只能选择配置中的模型名,不能注入额外 CLI 参数。
### Obsidian Copilot 流式输出
@@ -134,7 +136,7 @@ npm run build
空闲时按 Ctrl+C 停止。请求运行时第一次 Ctrl+C 取消当前 Command Code 子进程组并保留服务;请求结束后再按 Ctrl+C 停止服务。重启就是再次执行启动脚本。
客户端断开连接会先给整个 Command Code 子进程组发送 SIGINT,随后按需升级为 SIGTERM 和 SIGKILL。总超时由 `timeout_seconds` 控制。
运行中的客户端断开连接会先给整个 Command Code 子进程组发送 SIGINT,随后按需升级为 SIGTERM 和 SIGKILL;排队中的客户端断开连接只会从等待队列移除。总超时由 `timeout_seconds` 控制。
## API
@@ -146,15 +148,21 @@ npm run build
- `DELETE /v1/responses/:response_id`
- `GET /v1/responses/:response_id/input_items`
Bearer Token 会被忽略。所有生成接口只接受文本。图片、音频、文件、工具和其他未实现字段会返回 OpenAI 格式的 400,`code` `unsupported_parameter`;服务不会静默忽略会改变行为的参数
`GET /health` 额外返回 `busy``queue_length` `queue_capacity`,可用于观察当前执行槽和等待队列
Bearer Token 会被忽略。所有生成接口只接受文本内容。图片、音频、文件输入和其他未实现字段会返回 OpenAI 格式的 400,`code``unsupported_parameter`;服务不会静默忽略会改变行为的参数。
### Chat Completions
支持字符串 `content`,也支持由 `{ "type": "text", "text": "..." }` 组成的数组。`stream` 省略或设为 `false` 时返回普通 JSON。`stream: true` 会立即建立 SSE 连接并发送 assistant role 块,随后在 Command Code 的 `text_delta` 到达时立即发送对应 SSE chunk,最后发送 `finish_reason: "stop"`、可选 usage 块和 `[DONE]``stream_thinking: true` 时,`thinking_delta` 会先作为 `<think>` 内容流发送;该兼容格式不是原生 OpenAI reasoning item。传入 `stream_options.include_usage: true` 时,结束前返回 usage 块
支持 `system``developer``user``assistant``tool` 消息。文本 `content` 可以是字符串,也可以是由 `{ "type": "text", "text": "..." }` 组成的数组;assistant 工具轮次还支持 `content: null``tool_calls`tool 消息支持 `tool_call_id` 和可选 `name`
Chat 接口接受标准 function tools、`tool_choice``parallel_tool_calls`。Bridge 把工具定义、完整消息历史和工具结果交给 Command Code 决定下一步,校验返回的工具名与 arguments JSON Schema;第一次不合格时在原请求总截止时间内执行一次修复。工具由 API 客户端执行,Bridge 不执行客户端工具,也不直接访问 Obsidian vault。该流程适用于 Obsidian Copilot 的 `localSearch``readNote``getFileTree``writeFile``editFile`,也适用于其他标准 function tools。协议流程参考 [OpenAI Function calling](https://developers.openai.com/api/docs/guides/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 接口还接受 `temperature``max_tokens``max_completion_tokens``top_p``frequency_penalty``presence_penalty``n: 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 实时事件执行层。服务终端会在事件到达时立即显示状态、文本和工具调用。Chat Completions 会把每个 `text_delta` 直接写入 SSE,实现生成过程中的打字机效果。Bridge 提示 Command Code 在工具调用 turn 不输出面向用户的文本;Command Code 只有在 `turn_end` 才给出 `hadToolCalls`,因此上游若仍在工具 turn 输出文本,该文本已经发送,SSE 无法撤回
Chat Completions 和 Responses 共用 Command Code NDJSON 执行层。服务终端会在事件到达时立即显示状态、文本和 Command Code 自身的工具调用。普通文本 Chat 会把每个 `text_delta` 直接写入 SSE;外部 function calling 模式会缓存 `finalText`,避免内部 JSON 决策进入客户端正文
### Responses
@@ -286,15 +294,17 @@ command-code \
随后通过子进程 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 格式错误
同一时间只运行一个 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-05。没有编写测试用例,以下均为构建后运行真实 CLI 和真实 HTTP/SDK 客户端得到的端到端结果
检查日期: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 个已知漏洞。
@@ -309,16 +319,16 @@ command-code \
- 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` 下返回 `localSearch`arguments 通过 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 并恢复空闲;流占用期间第二个生成请求返回 429 `busy`取消后的普通 Chat 请求正常完成。
- Obsidian Copilot/LangChain 风格的 `temperature: 0.1``max_tokens: 1000`、流式 usage 请求返回 HTTP 200 和正确 SSE;响应头列出两个未映射参数。可选 `top_p`、frequency/presence penalty、`max_completion_tokens``n: 1` 通过严格校验,`n: 2` 及未知 `tools` 仍返回 400。
- 并发检查中第二个请求返回 HTTP 429 和 `code: busy`,没有启动第二个 CLI。
- Chat 流式连接收到首块后断开会取消 CLI 并恢复空闲;取消后的普通 Chat 请求正常完成。
- Obsidian Copilot/LangChain 风格的 `temperature: 0.1``max_tokens: 1000`、流式 usage 请求返回 HTTP 200 和正确 SSE;响应头列出两个未映射参数。可选 `top_p`、frequency/presence penalty、`max_completion_tokens``n: 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。
- 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 和最终文本均正确。
@@ -326,15 +336,16 @@ command-code \
- `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恢复成功`,证明共享单并发状态和错误恢复正常。
- 取消后紧接着的旧 Chat Completions 请求返回 `Chat恢复成功`,证明共享执行状态和错误恢复正常。
## 已知限制
- 没有原始 Command Code TUI、颜色布局、动画和键盘交互。
- headless 无法在服务终端进行批准、拒绝、选项选择或文字回答;`ask_user_question` 不能由等待中的 HTTP 客户端处理。
- 终端事件渲染由本项目完成,格式接近日志,无法等同原始 TUI。
- Chat Completions 会实时转发 Command Code 文本 delta上游若在工具 turn 输出文本,已经发送的 SSE 内容无法撤回。Responses 的文本 delta 需要等到 `turn_end.hadToolCalls: false` 才发送,结构化输出需要等待 Bridge 校验完成。
- 只实现 Responses 文本子集,不实现图片、音频、文件、function calling、Computer Use、托管工具、原生 reasoning item、加密 reasoning 或隐藏思维过程。
- 普通文本 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 缺失时返回 `null`Chat Completions 为兼容旧行为返回 0。