适配opencode
This commit is contained in:
@@ -59,7 +59,8 @@ command-code-openai-bridge/
|
||||
│ └── start.sh
|
||||
└── examples/
|
||||
├── node_client.mjs
|
||||
└── python_client.py
|
||||
├── python_client.py
|
||||
└── opencode.json
|
||||
```
|
||||
|
||||
## 安装
|
||||
@@ -119,6 +120,26 @@ models:
|
||||
|
||||
Bridge 会直接响应浏览器 CORS 预检,包括 Copilot 和 OpenAI SDK 发送的自定义请求头及本机 Private Network Access。关闭 Copilot 的 CORS 绕过后,请求使用原生 `fetch`,可以读取 Bridge 返回的 SSE 流。将 `config.yaml` 中的 `stream_thinking` 设为 `true` 后,Bridge 会把实时思考事件作为 `<think>` 块发送,Copilot 将其显示为可折叠思考内容。思考中的 `Sources` 标记会被等效转义,避免 Copilot 把内部草稿误判为最终引用区并丢弃后续正文。
|
||||
|
||||
### OpenCode 自定义 provider
|
||||
|
||||
已在本机验证 **OpenCode 1.18.15** 通过 `@ai-sdk/openai-compatible` 自定义 provider 连接 Bridge,并完成真实工具循环(`glob` 单工具、并行 `glob`、流式 `tool_calls` 与工具结果后的文本轮次)。
|
||||
|
||||
1. 启动 Bridge(默认 `http://127.0.0.1:18000/v1`)。
|
||||
2. 将 `examples/opencode.json` 复制到项目根目录,或通过环境变量指向它:
|
||||
|
||||
```bash
|
||||
export OPENCODE_CONFIG="/path/to/command-code-openai-bridge/examples/opencode.json"
|
||||
```
|
||||
|
||||
3. OpenCode 仍需配置任意非空 API key(Bridge 忽略 Authorization,但客户端要求填写)。示例配置使用 `options.apiKey: "local-bridge"`。
|
||||
4. 选择模型 `command-bridge/command-default`(与 `config.yaml` 中的 `models.command-default` 映射一致)。
|
||||
|
||||
OpenCode 1.18.15 实际发送的 Chat 请求包含 `stream: true`、`stream_options.include_usage: true`、`max_tokens`(默认约 32000)、`store: false`、`tools`(约 24 个内置工具)、`tool_choice: auto`。Bridge 接受 `store` 与采样/输出限制参数但不改变无状态行为;未映射参数通过 `X-Command-Code-Ignored-Parameters` 标出。工具 `parameters` 中的 JSON Schema `$schema`(draft 2020-12)会在校验前剥离元数据字段,避免 AJV 拒绝 OpenCode 工具定义。
|
||||
|
||||
OpenCode 还会在会话标题生成等场景发送无 `tools` 的压缩/摘要请求(`stream: true`、较长 system 提示、无 `store` 或 `store: false`)。这些请求与主 agent 请求共用同一 provider。
|
||||
|
||||
已知限制:Bridge 不实现 OpenCode hosted tools、图片/音频输入、Computer Use 或 Responses `/v1/responses` 路径;OpenCode 自定义 provider 走 `/v1/chat/completions`。`temperature`、`max_tokens` 等采样参数不会传给 Command Code CLI。
|
||||
|
||||
## 启动、停止与重启
|
||||
|
||||
```bash
|
||||
@@ -160,7 +181,7 @@ Chat 接口接受标准 function tools、`tool_choice` 和 `parallel_tool_calls`
|
||||
|
||||
`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`。
|
||||
为兼容 Obsidian Copilot、OpenCode 和 LangChain OpenAI-format 客户端,Chat 接口还接受 `temperature`、`max_tokens`、`max_completion_tokens`、`top_p`、`frequency_penalty`、`presence_penalty`、`n: 1` 和 `store`。这些采样、输出限制与会话存储参数会按 OpenAI 取值范围严格校验。Command Code CLI 没有对应的 headless 参数,Chat Completions 也不保存跨请求会话;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 外部 function calling 同样缓存 `finalText`,工具轮次不向客户端输出 assistant 文本。
|
||||
|
||||
@@ -315,7 +336,7 @@ command-code \
|
||||
|
||||
## 本机实际检查结果
|
||||
|
||||
检查日期:Chat Completions 原有检查为 2026-08-04;Responses 文本子集检查为 2026-08-05;Chat function calling 协议检查为 2026-08-06;Responses function calling 检查为 2026-08-12。没有编写测试用例。原有条目来自真实 CLI 和真实 HTTP/SDK 客户端;有限队列改动完成了类型检查、生产构建和协调器运行时冒烟检查,尚未重新运行真实 CLI 并发检查。Chat function calling 完成了内部协议冒烟检查、Obsidian Copilot 当前依赖 `@langchain/openai 1.2.2` 的双轮 wire compatibility 检查,以及真实 Command Code 与 OpenAI Node.js SDK 的双轮 HTTP/SSE 调用;尚未在 Obsidian UI 中运行完整检查。Responses function calling 完成了类型检查、生产构建、真实 HTTP 双轮检查、流式 function call SSE 检查和 OpenAI Node.js SDK 双轮检查。
|
||||
检查日期:Chat Completions 原有检查为 2026-08-04;Responses 文本子集检查为 2026-08-05;Chat function calling 协议检查为 2026-08-06;Responses function calling 检查为 2026-08-12;OpenCode 1.18.15 Chat Completions E2E 检查为 2026-08-12。没有编写测试用例。原有条目来自真实 CLI 和真实 HTTP/SDK 客户端;有限队列改动完成了类型检查、生产构建和协调器运行时冒烟检查,尚未重新运行真实 CLI 并发检查。Chat function calling 完成了内部协议冒烟检查、Obsidian Copilot 当前依赖 `@langchain/openai 1.2.2` 的双轮 wire compatibility 检查,以及真实 Command Code 与 OpenAI Node.js SDK 的双轮 HTTP/SSE 调用;尚未在 Obsidian UI 中运行完整检查。Responses function calling 完成了类型检查、生产构建、真实 HTTP 双轮检查、流式 function call SSE 检查和 OpenAI Node.js SDK 双轮检查。OpenCode 1.18.15 完成了真实 `opencode run` 文本回复、`glob` 工具循环、并行 `glob`、流式 `tool_calls`/`stop` SSE、`store: false` 与 `max_tokens` 接受、工具 Schema `$schema` 兼容,以及客户端断开后的取消恢复检查。
|
||||
|
||||
- TypeScript 严格类型检查和生产构建通过。
|
||||
- npm 生产依赖审计:0 个已知漏洞。
|
||||
@@ -352,6 +373,7 @@ command-code \
|
||||
- Responses 流式 function call 发送 `output_item.added`、`function_call_arguments.delta/done`、`output_item.done` 和 `completed`,无 `output_text.delta`。
|
||||
- OpenAI Node.js SDK 的 Responses 双轮检查:第一轮返回 `multiply` function call,第二轮消费 `function_call_output` 后返回最终文本。
|
||||
- Responses 无效工具名、无效 arguments JSON、孤立 `function_call_output` 和 hosted tool 类型分别返回带 `param` 的 400 错误。
|
||||
- OpenCode 1.18.15 自定义 provider:`store: false` 与 `max_tokens: 32000` 返回 HTTP 200;`stream: true` 工具轮次以 `finish_reason: tool_calls` 结束,文本轮次以 `stop` 结束;`glob` 单工具与并行 `glob` 完成完整工具循环;工具 `parameters.$schema`(draft 2020-12)不再触发 400。
|
||||
|
||||
## 已知限制
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ export const chatCompletionRequestSchema = z.object({
|
||||
tools: z.array(functionToolSchema).min(1).max(128).optional(),
|
||||
tool_choice: toolChoiceSchema.optional(),
|
||||
parallel_tool_calls: z.boolean().optional(),
|
||||
store: z.boolean().optional().default(true),
|
||||
}).strict().superRefine((request, context) => {
|
||||
const toolNames = new Set<string>();
|
||||
for (const [index, tool] of (request.tools ?? []).entries()) {
|
||||
@@ -153,6 +154,7 @@ const ignoredCompatibilityParameters = [
|
||||
"top_p",
|
||||
"frequency_penalty",
|
||||
"presence_penalty",
|
||||
"store",
|
||||
] as const;
|
||||
|
||||
export function ignoredChatCompatibilityParameters(request: ChatCompletionRequest): string[] {
|
||||
|
||||
+38
-1
@@ -250,7 +250,44 @@ function normalizeArguments(
|
||||
}
|
||||
|
||||
function createValidator(schema: Record<string, unknown>): ValidateFunction {
|
||||
return new Ajv({ allErrors: true, strict: false }).compile(schema);
|
||||
return new Ajv({ allErrors: true, strict: false }).compile(sanitizeToolParametersSchema(schema));
|
||||
}
|
||||
|
||||
function sanitizeToolParametersSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
||||
const clone = structuredClone(schema);
|
||||
stripJsonSchemaMeta(clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
function stripJsonSchemaMeta(value: unknown): void {
|
||||
if (!isRecord(value)) return;
|
||||
|
||||
delete value.$schema;
|
||||
delete value.$id;
|
||||
delete value.$vocabulary;
|
||||
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
if (key === "properties" && isRecord(child)) {
|
||||
for (const property of Object.values(child)) stripJsonSchemaMeta(property);
|
||||
continue;
|
||||
}
|
||||
if (key === "items") {
|
||||
if (Array.isArray(child)) for (const item of child) stripJsonSchemaMeta(item);
|
||||
else stripJsonSchemaMeta(child);
|
||||
continue;
|
||||
}
|
||||
if ((key === "$defs" || key === "definitions") && isRecord(child)) {
|
||||
for (const definition of Object.values(child)) stripJsonSchemaMeta(definition);
|
||||
continue;
|
||||
}
|
||||
if ((key === "anyOf" || key === "oneOf" || key === "allOf" || key === "prefixItems") && Array.isArray(child)) {
|
||||
for (const item of child) stripJsonSchemaMeta(item);
|
||||
continue;
|
||||
}
|
||||
if (key === "not" || key === "if" || key === "then" || key === "else" || key === "contains") {
|
||||
stripJsonSchemaMeta(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function effectiveToolChoice(request: FunctionToolDecisionRequest): FunctionToolChoice {
|
||||
|
||||
Reference in New Issue
Block a user