diff --git a/README.md b/README.md index 57fe34e..503c4f0 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,9 @@ Chat Completions 和 Responses 共用 Command Code NDJSON 执行层。服务终 - `tool_choice` - `parallel_tool_calls` -`input` 可以是字符串,也可以是 item 数组。message 支持 `system`、`developer`、`user`、`assistant` 角色、字符串 content,以及 `input_text`、`output_text`、`input_image`、`input_file` part。工具轮次还支持 `function_call` 和 `function_call_output` item,分别携带 `call_id`、工具名、`arguments` 和工具执行结果 `output`。 +为兼容 DeepSeek Harness、OpenAI Node.js SDK 等 Responses 客户端,接口还会接受但不实际应用这些字段:`prompt_cache_key`、`prompt_cache_retention`、`prompt_cache_options`、`max_output_tokens`、`temperature`、`top_p`、`service_tier`、`stream_options`、`include`、`reasoning.generate_summary` 和 `reasoning.summary`。请求显式提供这些字段时,服务终端会输出警告,HTTP 响应的 `X-Command-Code-Ignored-Parameters` 会列出未实际应用的顶层字段。 + +`input` 可以是字符串,也可以是 item 数组。message 支持 `system`、`developer`、`user`、`assistant` 角色、字符串 content,以及 `input_text`、`output_text`、`input_image`、`input_file` part。历史 message 常见的 `id`、`status`、`phase` 和 output_text 的 `annotations` 也会被接受,但只用于兼容客户端回放,不会写入本地响应存储。工具轮次还支持 `function_call` 和 `function_call_output` item,分别携带 `call_id`、工具名、`arguments` 和工具执行结果 `output`。 `input_image.image_url` 与 Chat 一样只接受图片 data URL;`file_id`、远程 URL、`file://`、本地路径和 low/high detail 不支持。`input_file` 只接受 `file_data` 加 `filename`,其中 `file_data` 可以是严格 base64 或带受支持文本 MIME 的 base64 data URL;解码结果必须是无 NUL 的 UTF-8 文本。`file_url`、`file_id`、PDF、Office 和其他二进制会返回带准确字段路径的 400。 diff --git a/config.yaml b/config.yaml index b043db4..1943df4 100644 --- a/config.yaml +++ b/config.yaml @@ -14,15 +14,15 @@ permission_mode: auto-accept dangerously_skip_permissions: true models: - deepseek-v4-flash: + command-flash: cli_model: deepseek/deepseek-v4-flash effort: max supports_image_input: false - deepseek-v4-pro: + command-pro: cli_model: deepseek/deepseek-v4-pro effort: max supports_image_input: false - gpt-5.6-luna: + command-luna: cli_model: gpt-5.6-luna effort: max supports_image_input: true \ No newline at end of file diff --git a/src/responses.ts b/src/responses.ts index 6449b76..a19aae9 100644 --- a/src/responses.ts +++ b/src/responses.ts @@ -52,6 +52,7 @@ const inputTextPartSchema = z.object({ const outputTextPartSchema = z.object({ type: z.literal("output_text"), text: z.string(), + annotations: z.array(z.unknown()).optional(), }).strict(); const inputImagePartSchema = z.object({ @@ -94,6 +95,7 @@ const inputFilePartSchema = z.object({ const inputMessageSchema = z.object({ type: z.literal("message").optional(), + id: responseItemIdSchema.optional(), role: roleSchema, content: z.union([ z.string(), @@ -104,6 +106,8 @@ const inputMessageSchema = z.object({ inputFilePartSchema, ])).min(1), ]), + status: z.enum(["in_progress", "completed", "incomplete"]).optional(), + phase: z.string().min(1).optional(), }).strict(); const inputFunctionCallSchema = z.object({ @@ -175,17 +179,38 @@ const metadataSchema = z.record(z.string().max(512)).superRefine((metadata, cont } }); +const responseReasoningSchema = z.object({ + effort: reasoningEffortSchema, + generate_summary: z.enum(["auto", "concise", "detailed"]).nullable().optional(), + summary: z.enum(["auto", "concise", "detailed"]).nullable().optional(), +}).strict(); + +const responseStreamOptionsSchema = z.object({ + include_usage: z.boolean().optional(), +}).strict(); + +const responsePromptCacheOptionsSchema = z.object({ + mode: z.enum(["explicit"]), +}).strict(); + export const responseRequestSchema = z.object({ model: z.string().min(1), input: z.union([z.string(), z.array(responseInputItemSchema).min(1)]), instructions: z.string().nullable().optional().default(null), stream: z.boolean().optional().default(false), + stream_options: responseStreamOptionsSchema.nullable().optional(), store: z.boolean().optional().default(true), previous_response_id: responseIdSchema.nullable().optional().default(null), metadata: metadataSchema.optional().default({}), - reasoning: z.object({ - effort: reasoningEffortSchema, - }).strict().optional(), + prompt_cache_key: z.string().min(1).optional(), + prompt_cache_retention: z.enum(["in-memory", "24h"]).nullable().optional(), + prompt_cache_options: responsePromptCacheOptionsSchema.optional(), + max_output_tokens: z.number().int().positive().nullable().optional(), + temperature: z.number().min(0).max(2).nullable().optional(), + top_p: z.number().min(0).max(1).nullable().optional(), + service_tier: z.enum(["auto", "default", "flex", "scale", "priority"]).nullable().optional(), + include: z.array(z.string()).min(1).nullable().optional(), + reasoning: responseReasoningSchema.optional(), text: z.object({ format: responseTextFormatSchema, }).strict().optional().default({ format: { type: "text" } }), @@ -227,6 +252,22 @@ export type ResponseTextFormat = z.infer; type ResponseRole = z.infer; type ResponseStatus = "in_progress" | "completed" | "incomplete" | "failed" | "cancelled"; +const ignoredResponseCompatibilityParameters = [ + "prompt_cache_key", + "prompt_cache_retention", + "prompt_cache_options", + "max_output_tokens", + "temperature", + "top_p", + "service_tier", + "stream_options", + "include", +] as const; + +function ignoredResponseCompatibilityParameterList(request: ResponseRequest): string[] { + return ignoredResponseCompatibilityParameters.filter((key) => request[key] !== undefined); +} + interface ResponseInputPart { type: "input_text" | "output_text"; text: string; @@ -844,6 +885,16 @@ export async function registerResponseRoutes( "model", )); } + + const ignoredParameters = ignoredResponseCompatibilityParameterList(parsed); + if (ignoredParameters.length > 0) { + const parameterList = ignoredParameters.join(","); + reply.raw.setHeader("X-Command-Code-Ignored-Parameters", parameterList); + process.stderr.write( + `警告:Responses 参数 ${parameterList} 已为客户端兼容而接受;Command Code CLI 不支持映射,实际生成不会应用这些参数。\n`, + ); + } + const firstImage = pendingAttachments.find((attachment) => attachment.kind === "image"); if (firstImage && !model.supports_image_input) { return sendRouteError(reply, new AttachmentInputError( @@ -1042,8 +1093,10 @@ function parseResponseRequest(body: unknown): ResponseRequest { function findUnsupportedParameter(body: unknown): string | undefined { if (!isRecord(body)) return undefined; const supported = new Set([ - "model", "input", "instructions", "stream", "store", - "previous_response_id", "metadata", "reasoning", "text", + "model", "input", "instructions", "stream", "stream_options", "store", + "previous_response_id", "metadata", "prompt_cache_key", + "prompt_cache_retention", "prompt_cache_options", "max_output_tokens", + "temperature", "top_p", "service_tier", "include", "reasoning", "text", "tools", "tool_choice", "parallel_tool_calls", ]); const unknownTopLevel = Object.keys(body).find((key) => !supported.has(key));