支持responses
This commit is contained in:
+507
-39
@@ -15,10 +15,23 @@ import type { BridgeConfig } from "./config.js";
|
||||
import { FinalTurnAccumulator } from "./final-turn.js";
|
||||
import { openAIError, type CommandUsage } from "./openai.js";
|
||||
import type { RequestCoordinator } from "./request-coordinator.js";
|
||||
import {
|
||||
parseFunctionToolDecision,
|
||||
parseFunctionToolArguments,
|
||||
ToolDecisionError,
|
||||
validateFunctionToolArguments,
|
||||
validateFunctionToolSchemas,
|
||||
type FunctionToolCall,
|
||||
type FunctionToolChoice,
|
||||
type FunctionToolDecisionRequest,
|
||||
type FunctionToolDefinition,
|
||||
} from "./tool-calling.js";
|
||||
|
||||
const responseIdPattern = /^resp_[A-Za-z0-9_-]{1,128}$/;
|
||||
const responseIdSchema = z.string().regex(responseIdPattern, "Invalid response ID");
|
||||
const roleSchema = z.enum(["system", "developer", "user", "assistant"]);
|
||||
const functionNameSchema = z.string().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/);
|
||||
const responseItemIdSchema = z.string().min(1).max(128);
|
||||
|
||||
const inputTextPartSchema = z.object({
|
||||
type: z.literal("input_text"),
|
||||
@@ -39,6 +52,42 @@ const inputMessageSchema = z.object({
|
||||
]),
|
||||
}).strict();
|
||||
|
||||
const inputFunctionCallSchema = z.object({
|
||||
id: responseItemIdSchema.optional(),
|
||||
type: z.literal("function_call"),
|
||||
call_id: responseItemIdSchema,
|
||||
name: functionNameSchema,
|
||||
arguments: z.string(),
|
||||
status: z.enum(["in_progress", "completed", "incomplete"]).optional(),
|
||||
}).strict();
|
||||
|
||||
const inputFunctionCallOutputSchema = z.object({
|
||||
id: responseItemIdSchema.optional(),
|
||||
type: z.literal("function_call_output"),
|
||||
call_id: responseItemIdSchema,
|
||||
output: z.string(),
|
||||
status: z.enum(["in_progress", "completed", "incomplete"]).optional(),
|
||||
}).strict();
|
||||
|
||||
const responseInputItemSchema = z.union([
|
||||
inputMessageSchema,
|
||||
inputFunctionCallSchema,
|
||||
inputFunctionCallOutputSchema,
|
||||
]);
|
||||
|
||||
const responseFunctionToolSchema = z.object({
|
||||
type: z.literal("function"),
|
||||
name: functionNameSchema,
|
||||
description: z.string().nullable().optional(),
|
||||
parameters: z.union([z.record(z.unknown()), z.null()]).optional(),
|
||||
strict: z.boolean().nullable().optional(),
|
||||
}).strict();
|
||||
|
||||
const responseToolChoiceSchema = z.union([
|
||||
z.enum(["none", "auto", "required"]),
|
||||
z.object({ type: z.literal("function"), name: functionNameSchema }).strict(),
|
||||
]);
|
||||
|
||||
const textFormatSchema = z.object({
|
||||
type: z.literal("text"),
|
||||
}).strict();
|
||||
@@ -74,7 +123,7 @@ const metadataSchema = z.record(z.string().max(512)).superRefine((metadata, cont
|
||||
|
||||
export const responseRequestSchema = z.object({
|
||||
model: z.string().min(1),
|
||||
input: z.union([z.string(), z.array(inputMessageSchema).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),
|
||||
store: z.boolean().optional().default(true),
|
||||
@@ -86,7 +135,38 @@ export const responseRequestSchema = z.object({
|
||||
text: z.object({
|
||||
format: responseTextFormatSchema,
|
||||
}).strict().optional().default({ format: { type: "text" } }),
|
||||
}).strict();
|
||||
tools: z.array(responseFunctionToolSchema).min(1).max(128).optional(),
|
||||
tool_choice: responseToolChoiceSchema.optional(),
|
||||
parallel_tool_calls: z.boolean().optional(),
|
||||
}).strict().superRefine((request, context) => {
|
||||
const toolNames = new Set<string>();
|
||||
for (const [index, tool] of (request.tools ?? []).entries()) {
|
||||
if (toolNames.has(tool.name)) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["tools", index, "name"],
|
||||
message: `Duplicate tool name '${tool.name}'`,
|
||||
});
|
||||
}
|
||||
toolNames.add(tool.name);
|
||||
}
|
||||
|
||||
if (request.tool_choice !== undefined && request.tools === undefined) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["tool_choice"],
|
||||
message: "tool_choice requires tools",
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof request.tool_choice === "object" && !toolNames.has(request.tool_choice.name)) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["tool_choice", "name"],
|
||||
message: `Unknown forced tool '${request.tool_choice.name}'`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type ResponseRequest = z.infer<typeof responseRequestSchema>;
|
||||
export type ResponseTextFormat = z.infer<typeof responseTextFormatSchema>;
|
||||
@@ -98,13 +178,32 @@ interface ResponseInputPart {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface ResponseInputItem {
|
||||
interface ResponseInputMessage {
|
||||
id: string;
|
||||
type: "message";
|
||||
role: ResponseRole;
|
||||
content: ResponseInputPart[];
|
||||
}
|
||||
|
||||
interface ResponseInputFunctionCall {
|
||||
id: string;
|
||||
type: "function_call";
|
||||
call_id: string;
|
||||
name: string;
|
||||
arguments: string;
|
||||
status: "in_progress" | "completed" | "incomplete";
|
||||
}
|
||||
|
||||
interface ResponseInputFunctionCallOutput {
|
||||
id: string;
|
||||
type: "function_call_output";
|
||||
call_id: string;
|
||||
output: string;
|
||||
status: "in_progress" | "completed" | "incomplete";
|
||||
}
|
||||
|
||||
type ResponseInputItem = ResponseInputMessage | ResponseInputFunctionCall | ResponseInputFunctionCallOutput;
|
||||
|
||||
interface ResponseOutputText {
|
||||
type: "output_text";
|
||||
text: string;
|
||||
@@ -119,6 +218,17 @@ interface ResponseOutputMessage {
|
||||
content: ResponseOutputText[];
|
||||
}
|
||||
|
||||
interface ResponseOutputFunctionCall {
|
||||
id: string;
|
||||
type: "function_call";
|
||||
call_id: string;
|
||||
name: string;
|
||||
arguments: string;
|
||||
status: "completed" | "incomplete";
|
||||
}
|
||||
|
||||
type ResponseOutputItem = ResponseOutputMessage | ResponseOutputFunctionCall;
|
||||
|
||||
interface ResponseUsage {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
@@ -140,26 +250,34 @@ interface LocalResponse {
|
||||
incomplete_details: { reason: string } | null;
|
||||
instructions: string | null;
|
||||
model: string;
|
||||
output: ResponseOutputMessage[];
|
||||
output: ResponseOutputItem[];
|
||||
output_text: string;
|
||||
previous_response_id: string | null;
|
||||
reasoning: { effort: string | null; summary: null };
|
||||
store: boolean;
|
||||
metadata: Record<string, string>;
|
||||
text: { format: ResponseTextFormat };
|
||||
tools: ResponseFunctionTool[];
|
||||
tool_choice: ResponseToolChoice;
|
||||
parallel_tool_calls: boolean;
|
||||
usage: ResponseUsage | null;
|
||||
}
|
||||
|
||||
interface ResponseFunctionTool {
|
||||
type: "function";
|
||||
name: string;
|
||||
description?: string | null;
|
||||
parameters: Record<string, unknown>;
|
||||
strict: boolean | null;
|
||||
}
|
||||
|
||||
type ResponseToolChoice = "none" | "auto" | "required" | { type: "function"; name: string };
|
||||
|
||||
interface StoredResponse {
|
||||
response: LocalResponse;
|
||||
input_items: ResponseInputItem[];
|
||||
}
|
||||
|
||||
interface PromptMessage {
|
||||
role: ResponseRole;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface StructuredValidation {
|
||||
validate: (text: string) => { valid: boolean; errors: string[] };
|
||||
}
|
||||
@@ -174,6 +292,7 @@ interface ExecutionResult {
|
||||
chunks: string[];
|
||||
usage?: CommandUsage;
|
||||
validationErrors?: string[];
|
||||
toolCalls?: FunctionToolCall[];
|
||||
}
|
||||
|
||||
class ResponseApiError extends Error {
|
||||
@@ -347,10 +466,17 @@ class ResponsesSseWriter {
|
||||
}
|
||||
|
||||
finish(response: LocalResponse, terminalEvent: "response.completed" | "response.incomplete"): void {
|
||||
if (response.output.some((item) => item.type === "function_call")) {
|
||||
this.addToolCalls(response);
|
||||
this.event(terminalEvent, { response });
|
||||
return;
|
||||
}
|
||||
this.ensureFinalText(response.output_text);
|
||||
if (this.outputStarted) {
|
||||
const outputMessage = response.output[0];
|
||||
if (!outputMessage) throw new Error("Response output message is missing");
|
||||
if (!outputMessage || outputMessage.type !== "message") {
|
||||
throw new Error("Response output message is missing");
|
||||
}
|
||||
const part = outputMessage.content[0];
|
||||
if (!part) throw new Error("Response output text part is missing");
|
||||
|
||||
@@ -371,6 +497,27 @@ class ResponsesSseWriter {
|
||||
this.event(terminalEvent, { response });
|
||||
}
|
||||
|
||||
private addToolCalls(response: LocalResponse): void {
|
||||
for (const [outputIndex, item] of response.output.entries()) {
|
||||
if (item.type !== "function_call") continue;
|
||||
const addedItem = { ...item, arguments: "", status: "in_progress" as const };
|
||||
this.event("response.output_item.added", { output_index: outputIndex, item: addedItem });
|
||||
if (item.arguments !== "") {
|
||||
this.event("response.function_call_arguments.delta", {
|
||||
item_id: item.id,
|
||||
output_index: outputIndex,
|
||||
delta: item.arguments,
|
||||
});
|
||||
}
|
||||
this.event("response.function_call_arguments.done", {
|
||||
item_id: item.id,
|
||||
output_index: outputIndex,
|
||||
arguments: item.arguments,
|
||||
});
|
||||
this.event("response.output_item.done", { output_index: outputIndex, item });
|
||||
}
|
||||
}
|
||||
|
||||
failed(response: LocalResponse): void {
|
||||
this.event("response.failed", { response });
|
||||
}
|
||||
@@ -479,6 +626,7 @@ export async function registerResponseRoutes(
|
||||
try {
|
||||
parsed = parseResponseRequest(request.body);
|
||||
validation = createStructuredValidation(parsed.text.format);
|
||||
validateResponseToolRequest(parsed);
|
||||
} catch (error) {
|
||||
return sendRouteError(reply, error);
|
||||
}
|
||||
@@ -535,8 +683,10 @@ export async function registerResponseRoutes(
|
||||
const chain = parsed.previous_response_id
|
||||
? await store.loadChain(parsed.previous_response_id)
|
||||
: [];
|
||||
validateResponseFunctionHistory(parsed, chain, inputItems);
|
||||
const prompt = buildResponsesPrompt(parsed, chain, inputItems);
|
||||
const deadline = Date.now() + config.timeout_seconds * 1000;
|
||||
const toolRequest = responseFunctionToolDecisionRequest(parsed);
|
||||
|
||||
const execution = await executeResponse({
|
||||
config,
|
||||
@@ -544,6 +694,7 @@ export async function registerResponseRoutes(
|
||||
effort: effectiveEffort,
|
||||
prompt,
|
||||
format: parsed.text.format,
|
||||
...(toolRequest ? { toolRequest } : {}),
|
||||
signal: abortController.signal,
|
||||
deadline,
|
||||
...(validation ? { validation } : {}),
|
||||
@@ -558,6 +709,7 @@ export async function registerResponseRoutes(
|
||||
effort: effectiveEffort,
|
||||
status: execution.validationErrors ? "incomplete" : "completed",
|
||||
finalText: execution.finalText,
|
||||
...(execution.toolCalls ? { toolCalls: execution.toolCalls } : {}),
|
||||
...(execution.usage ? { usage: execution.usage } : {}),
|
||||
...(execution.validationErrors ? { incompleteReason: "structured_output_validation_failed" } : {}),
|
||||
});
|
||||
@@ -668,6 +820,7 @@ function findUnsupportedParameter(body: unknown): string | undefined {
|
||||
const supported = new Set([
|
||||
"model", "input", "instructions", "stream", "store",
|
||||
"previous_response_id", "metadata", "reasoning", "text",
|
||||
"tools", "tool_choice", "parallel_tool_calls",
|
||||
]);
|
||||
const unknownTopLevel = Object.keys(body).find((key) => !supported.has(key));
|
||||
if (unknownTopLevel) return unknownTopLevel;
|
||||
@@ -679,10 +832,27 @@ function findUnsupportedParameter(body: unknown): string | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(body.tools)) {
|
||||
for (const [toolIndex, tool] of body.tools.entries()) {
|
||||
if (!isRecord(tool)) continue;
|
||||
if (tool.type !== "function") return `tools.${toolIndex}.type`;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRecord(body.tool_choice) && body.tool_choice.type !== "function") {
|
||||
return "tool_choice.type";
|
||||
}
|
||||
|
||||
if (!Array.isArray(body.input)) return undefined;
|
||||
for (const [itemIndex, item] of body.input.entries()) {
|
||||
if (!isRecord(item)) continue;
|
||||
if (item.type !== undefined && item.type !== "message") return `input.${itemIndex}.type`;
|
||||
if (
|
||||
item.type !== undefined
|
||||
&& item.type !== "message"
|
||||
&& item.type !== "function_call"
|
||||
&& item.type !== "function_call_output"
|
||||
) return `input.${itemIndex}.type`;
|
||||
if (item.type === "function_call" || item.type === "function_call_output") continue;
|
||||
if (!Array.isArray(item.content)) continue;
|
||||
for (const [partIndex, part] of item.content.entries()) {
|
||||
if (!isRecord(part)) continue;
|
||||
@@ -704,14 +874,35 @@ function normalizeInput(input: ResponseRequest["input"]): ResponseInputItem[] {
|
||||
}];
|
||||
}
|
||||
|
||||
return input.map((message) => ({
|
||||
id: newMessageId(),
|
||||
type: "message",
|
||||
role: message.role,
|
||||
content: typeof message.content === "string"
|
||||
? [{ type: message.role === "assistant" ? "output_text" : "input_text", text: message.content }]
|
||||
: message.content.map((part) => ({ type: part.type, text: part.text })),
|
||||
}));
|
||||
return input.map((item) => {
|
||||
if (item.type === "function_call") {
|
||||
return {
|
||||
id: item.id ?? newFunctionCallItemId(),
|
||||
type: "function_call",
|
||||
call_id: item.call_id,
|
||||
name: item.name,
|
||||
arguments: item.arguments,
|
||||
status: item.status ?? "completed",
|
||||
};
|
||||
}
|
||||
if (item.type === "function_call_output") {
|
||||
return {
|
||||
id: item.id ?? newFunctionCallOutputItemId(),
|
||||
type: "function_call_output",
|
||||
call_id: item.call_id,
|
||||
output: item.output,
|
||||
status: item.status ?? "completed",
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: newMessageId(),
|
||||
type: "message",
|
||||
role: item.role,
|
||||
content: typeof item.content === "string"
|
||||
? [{ type: item.role === "assistant" ? "output_text" : "input_text", text: item.content }]
|
||||
: item.content.map((part) => ({ type: part.type, text: part.text })),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function buildResponsesPrompt(
|
||||
@@ -719,24 +910,44 @@ function buildResponsesPrompt(
|
||||
chain: StoredResponse[],
|
||||
currentInput: ResponseInputItem[],
|
||||
): string {
|
||||
const messages: PromptMessage[] = [];
|
||||
const history: Array<ResponseInputItem | ResponseOutputItem> = [];
|
||||
for (const stored of chain) {
|
||||
messages.push(...stored.input_items.map(inputItemToPromptMessage));
|
||||
if (stored.response.output_text !== "") {
|
||||
messages.push({ role: "assistant", content: stored.response.output_text });
|
||||
}
|
||||
history.push(...stored.input_items, ...stored.response.output);
|
||||
}
|
||||
messages.push(...currentInput.map(inputItemToPromptMessage));
|
||||
history.push(...currentInput);
|
||||
|
||||
const envelope = {
|
||||
protocol: "openai-responses-text-history-v1",
|
||||
protocol: usesResponseToolCalling(request, history)
|
||||
? "openai-responses-tools-history-v1"
|
||||
: "openai-responses-text-history-v1",
|
||||
instructions: request.instructions,
|
||||
messages,
|
||||
input: history,
|
||||
tools: normalizeResponseTools(request.tools),
|
||||
tool_choice: effectiveResponseToolChoice(request),
|
||||
parallel_tool_calls: request.parallel_tool_calls ?? true,
|
||||
};
|
||||
|
||||
if (usesResponseToolCalling(request, history)) {
|
||||
return [
|
||||
"下面 JSON 对象是外部客户端提交的一次 OpenAI Responses 工具调用任务,input 包含完整历史。",
|
||||
"你只负责决定当前这一轮应该调用外部工具还是返回最终回答。外部工具由客户端执行,你不能模拟工具结果。",
|
||||
"不要使用 Command Code 自身的文件、终端、搜索、网络或其他工具替代外部 tools;只依据 input、tools 和 function_call_output 作出决定。",
|
||||
"严格执行 input 中的 system、developer、user 指令和当前 instructions,并结合 message、function_call、function_call_output 及其他已有 output item 理解完整历史。",
|
||||
"只返回以下两个 JSON 对象之一,禁止 Markdown 代码围栏、前后说明和额外字段:",
|
||||
"最终回答:{\"type\":\"final\",\"content\":\"面向用户的完整回答\"}",
|
||||
"调用工具:{\"type\":\"tool_calls\",\"calls\":[{\"name\":\"工具名称\",\"arguments\":{}}]}",
|
||||
"调用工具时,name 必须与 tools 中的名称完全一致,arguments 必须是符合该工具 parameters JSON Schema 的对象。",
|
||||
"tool_choice 为 none 时必须返回 final;为 required 或指定函数时必须返回 tool_calls。parallel_tool_calls 为 false 时 calls 只能有一项。",
|
||||
"需要工具提供信息或执行动作时返回 tool_calls 并立即结束;已有 function_call_output 且信息足够时返回 final。工具调用轮次禁止生成面向用户的正文。",
|
||||
formatDirective(request.text.format),
|
||||
"JSON 数据开始:",
|
||||
JSON.stringify(envelope),
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
return [
|
||||
"下面 JSON 对象是外部客户端提交的完整 Responses 文本任务和本地重建的响应链。",
|
||||
"按 system、developer、user、assistant 的角色与顺序理解上下文;当前 instructions 是本次请求的高优先级开发者指令。",
|
||||
"按 input 中的 system、developer、user、assistant 角色与顺序理解上下文;当前 instructions 是本次请求的高优先级开发者指令。",
|
||||
"严格执行最后一个用户任务。不要复述 JSON,不要输出角色标签,不要暴露内部思考。",
|
||||
formatDirective(request.text.format),
|
||||
"JSON 数据开始:",
|
||||
@@ -780,12 +991,17 @@ async function executeResponse(options: {
|
||||
effort: string;
|
||||
prompt: string;
|
||||
format: ResponseTextFormat;
|
||||
toolRequest?: FunctionToolDecisionRequest;
|
||||
validation?: StructuredValidation;
|
||||
signal: AbortSignal;
|
||||
deadline: number;
|
||||
writer?: ResponsesSseWriter;
|
||||
}): Promise<ExecutionResult> {
|
||||
const first = await executeAttempt(options, options.prompt, options.validation ? undefined : options.writer);
|
||||
const constrained = options.validation !== undefined || options.toolRequest !== undefined;
|
||||
const first = await executeAttempt(options, options.prompt, constrained ? undefined : options.writer);
|
||||
if (options.toolRequest) {
|
||||
return executeToolDecision({ ...options, toolRequest: options.toolRequest }, first);
|
||||
}
|
||||
if (!options.validation) {
|
||||
return {
|
||||
finalText: first.result.finalText,
|
||||
@@ -821,6 +1037,91 @@ async function executeResponse(options: {
|
||||
};
|
||||
}
|
||||
|
||||
async function executeToolDecision(
|
||||
options: {
|
||||
config: BridgeConfig;
|
||||
cliModel: string;
|
||||
effort: string;
|
||||
prompt: string;
|
||||
format: ResponseTextFormat;
|
||||
toolRequest: FunctionToolDecisionRequest;
|
||||
validation?: StructuredValidation;
|
||||
signal: AbortSignal;
|
||||
deadline: number;
|
||||
},
|
||||
first: AttemptResult,
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
return executionFromToolDecision(
|
||||
parseFunctionToolDecision(first.result.finalText, options.toolRequest),
|
||||
first,
|
||||
);
|
||||
} catch (error) {
|
||||
if (!(error instanceof ToolDecisionError)) throw error;
|
||||
process.stderr.write(`\nResponses 工具调用决策校验失败,正在修复:${error.errors.join("; ")}\n`);
|
||||
|
||||
const repairPrompt = buildToolDecisionRepairPrompt(
|
||||
options.prompt,
|
||||
first.result.finalText,
|
||||
error.errors,
|
||||
);
|
||||
const second = await executeAttempt(options, repairPrompt);
|
||||
let decision;
|
||||
try {
|
||||
decision = parseFunctionToolDecision(second.result.finalText, options.toolRequest);
|
||||
} catch (repairError) {
|
||||
if (repairError instanceof ToolDecisionError) {
|
||||
throw new ResponseApiError(
|
||||
`Invalid tool decision after repair: ${repairError.errors.join("; ")}`,
|
||||
502,
|
||||
"invalid_tool_decision",
|
||||
"server_error",
|
||||
);
|
||||
}
|
||||
throw repairError;
|
||||
}
|
||||
const result = executionFromToolDecision(decision, second);
|
||||
const usage = addUsage(first.result.usage, second.result.usage);
|
||||
return { ...result, ...(usage ? { usage } : {}) };
|
||||
}
|
||||
}
|
||||
|
||||
function executionFromToolDecision(
|
||||
decision: ReturnType<typeof parseFunctionToolDecision>,
|
||||
attempt: AttemptResult,
|
||||
): ExecutionResult {
|
||||
if (decision.type === "tool_calls") {
|
||||
return {
|
||||
finalText: "",
|
||||
chunks: [],
|
||||
toolCalls: decision.toolCalls,
|
||||
...(attempt.result.usage ? { usage: attempt.result.usage } : {}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
finalText: decision.content,
|
||||
chunks: [decision.content],
|
||||
...(attempt.result.usage ? { usage: attempt.result.usage } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildToolDecisionRepairPrompt(
|
||||
originalPrompt: string,
|
||||
invalidOutput: string,
|
||||
errors: string[],
|
||||
): string {
|
||||
return [
|
||||
"上一次输出不符合 Responses 外部工具调用传输协议。保持原来的决策意图,只修复 JSON 结构、工具名称或参数。",
|
||||
"只返回原任务要求的 final 或 tool_calls JSON 对象,禁止 Markdown 代码围栏、前后说明和额外字段。",
|
||||
"校验错误:",
|
||||
errors.join("\n"),
|
||||
"上一次输出:",
|
||||
invalidOutput,
|
||||
"原始任务:",
|
||||
originalPrompt,
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
async function executeAttempt(
|
||||
options: {
|
||||
config: BridgeConfig;
|
||||
@@ -910,14 +1211,32 @@ function createResponse(options: {
|
||||
effort: string;
|
||||
status: ResponseStatus;
|
||||
finalText?: string;
|
||||
toolCalls?: FunctionToolCall[];
|
||||
usage?: CommandUsage;
|
||||
error?: ResponseError;
|
||||
incompleteReason?: string;
|
||||
}): LocalResponse {
|
||||
const finalText = options.finalText ?? "";
|
||||
const terminal = options.status !== "in_progress";
|
||||
const hasOutput = options.finalText !== undefined;
|
||||
const outputStatus = options.status === "completed" ? "completed" : "incomplete";
|
||||
const output: ResponseOutputItem[] = options.toolCalls
|
||||
? options.toolCalls.map((call) => ({
|
||||
id: newFunctionCallItemId(),
|
||||
type: "function_call",
|
||||
call_id: call.callId,
|
||||
name: call.name,
|
||||
arguments: call.arguments,
|
||||
status: outputStatus,
|
||||
}))
|
||||
: options.finalText !== undefined
|
||||
? [{
|
||||
id: options.messageId,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
status: outputStatus,
|
||||
content: [{ type: "output_text", text: finalText, annotations: [] }],
|
||||
}]
|
||||
: [];
|
||||
return {
|
||||
id: options.id,
|
||||
object: "response",
|
||||
@@ -928,19 +1247,16 @@ function createResponse(options: {
|
||||
incomplete_details: options.incompleteReason ? { reason: options.incompleteReason } : null,
|
||||
instructions: options.request.instructions,
|
||||
model: options.request.model,
|
||||
output: hasOutput ? [{
|
||||
id: options.messageId,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
status: outputStatus,
|
||||
content: [{ type: "output_text", text: finalText, annotations: [] }],
|
||||
}] : [],
|
||||
output,
|
||||
output_text: finalText,
|
||||
previous_response_id: options.request.previous_response_id,
|
||||
reasoning: { effort: options.effort, summary: null },
|
||||
store: options.request.store,
|
||||
metadata: options.request.metadata,
|
||||
text: { format: options.request.text.format },
|
||||
tools: normalizeResponseTools(options.request.tools),
|
||||
tool_choice: effectiveResponseToolChoice(options.request),
|
||||
parallel_tool_calls: options.request.parallel_tool_calls ?? true,
|
||||
usage: toResponseUsage(options.usage),
|
||||
};
|
||||
}
|
||||
@@ -1115,8 +1431,152 @@ function responseNotFound(id: string): ResponseApiError {
|
||||
);
|
||||
}
|
||||
|
||||
function inputItemToPromptMessage(item: ResponseInputItem): PromptMessage {
|
||||
return { role: item.role, content: item.content.map((part) => part.text).join("") };
|
||||
function validateResponseToolRequest(request: ResponseRequest): void {
|
||||
const errors = validateFunctionToolSchemas(responseFunctionTools(request));
|
||||
if (errors.length > 0) {
|
||||
throw new ResponseApiError(
|
||||
errors.join("; "),
|
||||
400,
|
||||
"invalid_tool_schema",
|
||||
"invalid_request_error",
|
||||
"tools",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateResponseFunctionHistory(
|
||||
request: ResponseRequest,
|
||||
chain: StoredResponse[],
|
||||
currentInput: ResponseInputItem[],
|
||||
): void {
|
||||
const tools = new Map(responseFunctionTools(request).map((tool) => [tool.name, tool]));
|
||||
const callIds = new Set<string>();
|
||||
const outputCallIds = new Set<string>();
|
||||
const items: Array<{ item: ResponseInputItem | ResponseOutputItem; param: string }> = [];
|
||||
|
||||
for (const stored of chain) {
|
||||
for (const item of stored.input_items) items.push({ item, param: "previous_response_id" });
|
||||
for (const item of stored.response.output) items.push({ item, param: "previous_response_id" });
|
||||
}
|
||||
for (const [index, item] of currentInput.entries()) items.push({ item, param: `input.${index}` });
|
||||
|
||||
for (const { item, param } of items) {
|
||||
if (item.type === "function_call") {
|
||||
if (callIds.has(item.call_id)) {
|
||||
throw new ResponseApiError(
|
||||
`Duplicate function call_id '${item.call_id}'`,
|
||||
400,
|
||||
"invalid_function_call",
|
||||
"invalid_request_error",
|
||||
`${param}.call_id`,
|
||||
);
|
||||
}
|
||||
callIds.add(item.call_id);
|
||||
const tool = tools.get(item.name);
|
||||
if (!tool) {
|
||||
throw new ResponseApiError(
|
||||
`Function call references unknown tool '${item.name}'`,
|
||||
400,
|
||||
"invalid_tool_name",
|
||||
"invalid_request_error",
|
||||
`${param}.name`,
|
||||
);
|
||||
}
|
||||
|
||||
let args: Record<string, unknown>;
|
||||
try {
|
||||
args = parseFunctionToolArguments(item.arguments, `${param}.arguments`);
|
||||
} catch (error) {
|
||||
if (error instanceof ToolDecisionError) {
|
||||
throw new ResponseApiError(
|
||||
error.errors.join("; "),
|
||||
400,
|
||||
"invalid_tool_arguments",
|
||||
"invalid_request_error",
|
||||
`${param}.arguments`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const errors = validateFunctionToolArguments(tool, item.name, args);
|
||||
if (errors.length > 0) {
|
||||
throw new ResponseApiError(
|
||||
errors.join("; "),
|
||||
400,
|
||||
"invalid_tool_arguments",
|
||||
"invalid_request_error",
|
||||
`${param}.arguments`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.type === "function_call_output") {
|
||||
if (!callIds.has(item.call_id)) {
|
||||
throw new ResponseApiError(
|
||||
`No preceding function_call found for call_id '${item.call_id}'`,
|
||||
400,
|
||||
"invalid_function_call_output",
|
||||
"invalid_request_error",
|
||||
`${param}.call_id`,
|
||||
);
|
||||
}
|
||||
if (outputCallIds.has(item.call_id)) {
|
||||
throw new ResponseApiError(
|
||||
`Duplicate function_call_output for call_id '${item.call_id}'`,
|
||||
400,
|
||||
"invalid_function_call_output",
|
||||
"invalid_request_error",
|
||||
`${param}.call_id`,
|
||||
);
|
||||
}
|
||||
outputCallIds.add(item.call_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function responseFunctionTools(request: ResponseRequest): FunctionToolDefinition[] {
|
||||
return normalizeResponseTools(request.tools).map((tool) => ({
|
||||
name: tool.name,
|
||||
...(tool.description !== undefined ? { description: tool.description } : {}),
|
||||
parameters: tool.parameters,
|
||||
strict: tool.strict,
|
||||
}));
|
||||
}
|
||||
|
||||
function responseFunctionToolDecisionRequest(
|
||||
request: ResponseRequest,
|
||||
): FunctionToolDecisionRequest | undefined {
|
||||
if (!request.tools) return undefined;
|
||||
const toolChoice = effectiveResponseToolChoice(request);
|
||||
return {
|
||||
tools: responseFunctionTools(request),
|
||||
toolChoice: typeof toolChoice === "string" ? toolChoice : { name: toolChoice.name },
|
||||
parallelToolCalls: request.parallel_tool_calls ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeResponseTools(tools: ResponseRequest["tools"]): ResponseFunctionTool[] {
|
||||
return (tools ?? []).map((tool) => ({
|
||||
type: "function",
|
||||
name: tool.name,
|
||||
...(tool.description !== undefined ? { description: tool.description } : {}),
|
||||
parameters: tool.parameters ?? { type: "object", properties: {} },
|
||||
strict: tool.strict ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
function effectiveResponseToolChoice(request: ResponseRequest): ResponseToolChoice {
|
||||
return request.tool_choice ?? (request.tools ? "auto" : "none");
|
||||
}
|
||||
|
||||
function usesResponseToolCalling(
|
||||
request: ResponseRequest,
|
||||
history: Array<ResponseInputItem | ResponseOutputItem>,
|
||||
): boolean {
|
||||
return request.tools !== undefined || history.some((item) => (
|
||||
item.type === "function_call" || item.type === "function_call_output"
|
||||
));
|
||||
}
|
||||
|
||||
function toResponseUsage(usage?: CommandUsage): ResponseUsage | null {
|
||||
@@ -1155,6 +1615,14 @@ function newMessageId(): string {
|
||||
return `msg_local_${randomHexId()}`;
|
||||
}
|
||||
|
||||
function newFunctionCallItemId(): string {
|
||||
return `fc_local_${randomHexId()}`;
|
||||
}
|
||||
|
||||
function newFunctionCallOutputItemId(): string {
|
||||
return `fco_local_${randomHexId()}`;
|
||||
}
|
||||
|
||||
function randomHexId(): string {
|
||||
return randomUUID().replaceAll("-", "");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user