支持CROS

This commit is contained in:
Sirius
2026-08-05 17:37:41 +08:00
parent 7dd16e370f
commit bd18678963
4 changed files with 24 additions and 5 deletions
+4 -4
View File
@@ -136,11 +136,11 @@ Bearer Token 会被忽略。所有生成接口只接受文本。图片、音频
### 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 块。
支持字符串 `content`,也支持由 `{ "type": "text", "text": "..." }` 组成的数组。`stream` 省略或设为 `false` 时返回普通 JSON。`stream: true` 会立即建立 SSE 连接并发送 assistant role 块,随后在 Command Code 的 `text_delta` 到达时立即发送对应 SSE chunk,最后发送 `finish_reason: "stop"`、可选 usage 块和 `[DONE]`。传入 `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 实时事件执行层。服务终端会在事件到达时立即显示状态、文本和工具调用。由于 Command Code 只有在 `turn_end` 才给出 `hadToolCalls`Bridge 会按 turn 缓冲客户端文本,丢弃工具轮次,在确认 `hadToolCalls: false` 后按原 delta 边界写入 Chat SSE。连接会立即建立,最终文本不会混入工具轮次内容,文本首包仍受最终 turn 完成时间约束
Chat Completions 和 Responses 共用 Command Code NDJSON 实时事件执行层。服务终端会在事件到达时立即显示状态、文本和工具调用。Chat Completions 会把每个 `text_delta` 直接写入 SSE,实现生成过程中的打字机效果。Bridge 提示 Command Code 在工具调用 turn 不输出面向用户的文本;Command Code 只有在 `turn_end` 才给出 `hadToolCalls`,因此上游若仍在工具 turn 输出文本,该文本已经发送,SSE 无法撤回
### Responses
@@ -174,7 +174,7 @@ Chat Completions 和 Responses 共用 Command Code NDJSON 实时事件执行层
所有事件都包含递增的 `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 发给客户端。
Responses API 仍按 turn 缓冲文本,丢弃 `hadToolCalls: true` 的中间轮次,在 `hadToolCalls: false` 时按原 delta 边界发送。文本 delta 会延迟到最终 turn 边界。结构化输出还会延迟到 JSON 校验完成,避免把随后需要修复的无效 JSON 发给客户端。
### 本地存储和响应链
@@ -319,7 +319,7 @@ command-code \
- 没有原始 Command Code TUI、颜色布局、动画和键盘交互。
- headless 无法在服务终端进行批准、拒绝、选项选择或文字回答;`ask_user_question` 不能由等待中的 HTTP 客户端处理。
- 终端事件渲染由本项目完成,格式接近日志,无法等同原始 TUI。
- Chat Completions Responses 的文本 delta 需要等到 `turn_end.hadToolCalls: false` 才发送;SSE 连接和初始事件会立即建立。Responses 结构化输出需要等 Bridge 校验完成。
- Chat Completions 会实时转发 Command Code 文本 delta;上游若在工具 turn 输出文本,已经发送的 SSE 内容无法撤回。Responses 的文本 delta 需要等到 `turn_end.hadToolCalls: false` 才发送结构化输出需要等 Bridge 校验完成。
- 只实现 Responses 文本子集,不实现图片、音频、文件、function calling、Computer Use、托管工具、原生 reasoning item、加密 reasoning 或隐藏思维过程。
- Responses 的本地文件存储只供本 Bridge 使用,没有跨进程锁、队列或多实例一致性保证。项目本身仍严格单并发。
- Responses Structured Outputs 是 Bridge 层约束,底层 Command Code 模型仍可能连续两次输出不合格 JSON;此时状态为 incomplete。
+11 -1
View File
@@ -21,7 +21,17 @@ export class ChatCompletionSseWriter {
commandEvent(event: Record<string, unknown>): void {
const chunks = this.accumulator.event(event);
if (chunks) this.addText(chunks);
if (event.type === "text_delta" && typeof event.delta === "string") {
this.addText([event.delta]);
return;
}
if (event.type !== "turn_end") return;
if (event.hadToolCalls === true) {
this.outputText = "";
return;
}
if (chunks && this.outputText === "") this.addText(chunks);
}
finish(finalText: string, usage?: CommandUsage, includeUsage = false): void {
+1
View File
@@ -67,6 +67,7 @@ export function buildCommandPrompt(request: ChatCompletionRequest): string {
"严格执行 system 消息和最后一条 user 消息中的具体指令,并结合此前消息理解上下文。",
"调用方可能要求改写问题、生成检索词、提取数据、分类或输出特定格式。这些属于内部处理任务,也必须严格执行。",
"如果最后一条 user 消息要求改写、压缩、提取或输出指定格式,只返回要求的结果,不回答消息中包含的问题。",
"如需调用工具,调用工具的 turn 只发起工具调用,不输出面向用户的文本;所有工具结束后,仅在不再调用工具的最终 turn 输出回答。",
"不要复述 JSON,不要输出角色标签,不要添加指令未要求的解释或格式。",
"JSON 数据开始:",
JSON.stringify(envelope),
+8
View File
@@ -33,6 +33,14 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
});
const coordinator = new RequestCoordinator();
app.addHook("onRequest", async (_request, reply) => {
reply.raw.setHeader("Access-Control-Allow-Origin", "*");
reply.raw.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
reply.raw.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
});
app.options("/*", async (_request, reply) => reply.status(204).send());
app.setErrorHandler((error, _request, reply) => {
const fastifyError = error as Error & { code?: string; statusCode?: number };
if (fastifyError.code === "FST_ERR_CTP_BODY_TOO_LARGE") {