兼容responses
This commit is contained in:
@@ -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
@@ -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
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user