chat也想支持

This commit is contained in:
Sirius
2026-08-05 17:11:55 +08:00
parent 7ec6914872
commit 7dd16e370f
3 changed files with 33 additions and 0 deletions
+3
View File
@@ -138,6 +138,8 @@ Bearer Token 会被忽略。所有生成接口只接受文本。图片、音频
支持字符串 `content`,也支持由 `{ "type": "text", "text": "..." }` 组成的数组。`stream` 省略或设为 `false` 时返回普通 JSON。`stream: true` 会立即建立 SSE 连接并发送 assistant role 块,随后发送最终回答轮次的原始文本 delta、`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 完成时间约束。
### Responses
@@ -295,6 +297,7 @@ command-code \
- 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 请求正常完成。
- 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。
- 超过 20 MiB 的请求体返回 HTTP 413,服务保持可用。
- 客户端 1 秒超时断开后,当前子进程组被清理,`busy` 恢复为 false,没有发现残留 headless CLI。
+20
View File
@@ -18,6 +18,13 @@ export const chatCompletionRequestSchema = z.object({
stream_options: z.object({
include_usage: z.boolean().optional().default(false),
}).strict().optional(),
temperature: z.number().min(0).max(2).nullable().optional(),
max_tokens: z.number().int().positive().nullable().optional(),
max_completion_tokens: z.number().int().positive().nullable().optional(),
top_p: z.number().min(0).max(1).nullable().optional(),
frequency_penalty: z.number().min(-2).max(2).nullable().optional(),
presence_penalty: z.number().min(-2).max(2).nullable().optional(),
n: z.literal(1).nullable().optional(),
}).strict();
export type ChatCompletionRequest = z.infer<typeof chatCompletionRequestSchema>;
@@ -27,6 +34,19 @@ export interface CommandUsage {
outputTokens?: number;
}
const ignoredCompatibilityParameters = [
"temperature",
"max_tokens",
"max_completion_tokens",
"top_p",
"frequency_penalty",
"presence_penalty",
] as const;
export function ignoredChatCompatibilityParameters(request: ChatCompletionRequest): string[] {
return ignoredCompatibilityParameters.filter((key) => request[key] !== undefined);
}
export function normalizeMessages(request: ChatCompletionRequest) {
return request.messages.map((message) => ({
role: message.role,
+10
View File
@@ -12,6 +12,7 @@ import {
buildCommandPrompt,
chatCompletionRequestSchema,
completionResponse,
ignoredChatCompatibilityParameters,
openAIError,
} from "./openai.js";
import { RequestCoordinator } from "./request-coordinator.js";
@@ -123,6 +124,15 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
));
}
const ignoredParameters = ignoredChatCompatibilityParameters(parsed);
if (ignoredParameters.length > 0) {
const parameterList = ignoredParameters.join(",");
reply.raw.setHeader("X-Command-Code-Ignored-Parameters", parameterList);
process.stderr.write(
`警告:Chat Completions 参数 ${parameterList} 已为客户端兼容而接受;Command Code CLI 不支持映射,实际生成不会应用这些参数。\n`,
);
}
const abortController = coordinator.begin();
if (!abortController) {
return reply.status(429).send(openAIError(