兼容responses

This commit is contained in:
Sirius
2026-08-20 14:32:30 +08:00
parent d84b6f863d
commit b03d47c891
3 changed files with 64 additions and 9 deletions
+3 -1
View File
@@ -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。
+3 -3
View File
@@ -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
+58 -5
View File
@@ -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<typeof responseTextFormatSchema>;
type ResponseRole = z.infer<typeof roleSchema>;
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));