适配chat结构
This commit is contained in:
+136
-21
@@ -12,17 +12,26 @@ import {
|
||||
import {
|
||||
buildCommandPrompt,
|
||||
buildToolDecisionRepairPrompt,
|
||||
buildToolStructuredOutputRepairPrompt,
|
||||
chatResponseTextFormat,
|
||||
chatCompletionRequestSchema,
|
||||
completionResponse,
|
||||
ignoredChatCompatibilityParameters,
|
||||
openAIError,
|
||||
toolCallsCompletionResponse,
|
||||
usesChatStructuredOutput,
|
||||
usesToolCalling,
|
||||
type ChatCompletionRequest,
|
||||
type CommandUsage,
|
||||
} from "./openai.js";
|
||||
import { RequestCoordinator } from "./request-coordinator.js";
|
||||
import { registerResponseRoutes } from "./responses.js";
|
||||
import {
|
||||
addUsage,
|
||||
buildRepairPrompt,
|
||||
createStructuredValidation,
|
||||
registerResponseRoutes,
|
||||
type ResponseTextFormat,
|
||||
type StructuredValidation,
|
||||
} from "./responses.js";
|
||||
import {
|
||||
parseToolDecision,
|
||||
ToolDecisionError,
|
||||
@@ -58,6 +67,7 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
);
|
||||
reply.raw.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
||||
reply.raw.setHeader("Access-Control-Max-Age", "600");
|
||||
reply.raw.setHeader("Access-Control-Expose-Headers", "X-Command-Code-Ignored-Parameters");
|
||||
reply.raw.setHeader(
|
||||
"Vary",
|
||||
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
|
||||
@@ -126,7 +136,7 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
));
|
||||
}
|
||||
|
||||
let parsed;
|
||||
let parsed: ChatCompletionRequest;
|
||||
try {
|
||||
parsed = chatCompletionRequestSchema.parse(request.body);
|
||||
} catch (error) {
|
||||
@@ -149,6 +159,20 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
return reply.status(400).send(openAIError(message, "invalid_request"));
|
||||
}
|
||||
|
||||
const responseFormat: ResponseTextFormat = chatResponseTextFormat(parsed);
|
||||
let structuredValidation: StructuredValidation | undefined;
|
||||
try {
|
||||
structuredValidation = createStructuredValidation(responseFormat);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Invalid JSON Schema";
|
||||
return reply.status(400).send(openAIError(
|
||||
message,
|
||||
"invalid_json_schema",
|
||||
"invalid_request_error",
|
||||
"response_format.json_schema.schema",
|
||||
));
|
||||
}
|
||||
|
||||
const model = config.models[parsed.model];
|
||||
if (!model) {
|
||||
return reply.status(404).send(openAIError(
|
||||
@@ -196,7 +220,13 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
|
||||
let writer: ChatCompletionSseWriter | undefined;
|
||||
try {
|
||||
if (parsed.stream) writer = startChatCompletionSse(reply, parsed.model, config.stream_thinking);
|
||||
if (parsed.stream) {
|
||||
writer = startChatCompletionSse(
|
||||
reply,
|
||||
parsed.model,
|
||||
config.stream_thinking && !usesChatStructuredOutput(parsed),
|
||||
);
|
||||
}
|
||||
if (request.raw.aborted || reply.raw.destroyed) cancelOnDisconnect();
|
||||
acquired = await lease.ready;
|
||||
if (!acquired) {
|
||||
@@ -212,6 +242,8 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
signal: abortController.signal,
|
||||
deadline: Date.now() + config.timeout_seconds * 1000,
|
||||
toolMode,
|
||||
format: responseFormat,
|
||||
...(structuredValidation ? { validation: structuredValidation } : {}),
|
||||
...(writer ? { writer } : {}),
|
||||
});
|
||||
const { result, decision } = execution;
|
||||
@@ -281,6 +313,21 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
));
|
||||
}
|
||||
|
||||
if (error instanceof ChatStructuredOutputError) {
|
||||
process.stderr.write(`\nChat 结构化输出连续两次校验失败:${error.errors.join("; ")}\n`);
|
||||
const payload = openAIError(
|
||||
"Command Code returned invalid structured output after one repair attempt",
|
||||
"invalid_structured_output",
|
||||
"server_error",
|
||||
"response_format",
|
||||
);
|
||||
if (writer) {
|
||||
writer.error(payload);
|
||||
return reply;
|
||||
}
|
||||
return reply.status(502).send(payload);
|
||||
}
|
||||
|
||||
const code = error instanceof CommandCodeError ? error.exitCode : undefined;
|
||||
const status = code === 5 ? 429 : code === 10 ? 402 : 502;
|
||||
const errorCode = code === 5
|
||||
@@ -323,7 +370,7 @@ interface ChatExecution {
|
||||
decision?: ToolDecision;
|
||||
}
|
||||
|
||||
async function executeChatTurn(options: {
|
||||
interface ChatTurnOptions {
|
||||
config: BridgeConfig;
|
||||
cliModel: string;
|
||||
effort: string;
|
||||
@@ -331,9 +378,21 @@ async function executeChatTurn(options: {
|
||||
signal: AbortSignal;
|
||||
deadline: number;
|
||||
toolMode: boolean;
|
||||
format: ResponseTextFormat;
|
||||
validation?: StructuredValidation;
|
||||
writer?: ChatCompletionSseWriter;
|
||||
}): Promise<ChatExecution> {
|
||||
}
|
||||
|
||||
class ChatStructuredOutputError extends Error {
|
||||
constructor(readonly errors: string[]) {
|
||||
super(errors.join("; "));
|
||||
this.name = "ChatStructuredOutputError";
|
||||
}
|
||||
}
|
||||
|
||||
async function executeChatTurn(options: ChatTurnOptions): Promise<ChatExecution> {
|
||||
const originalPrompt = buildCommandPrompt(options.request);
|
||||
const constrained = options.toolMode || options.validation !== undefined;
|
||||
const first = await runCommandCode(
|
||||
options.config,
|
||||
options.cliModel,
|
||||
@@ -342,15 +401,38 @@ async function executeChatTurn(options: {
|
||||
options.signal,
|
||||
{
|
||||
timeoutMs: options.deadline - Date.now(),
|
||||
...(!options.toolMode && options.writer
|
||||
...(!constrained && options.writer
|
||||
? { onEvent: (event: Record<string, unknown>) => options.writer?.commandEvent(event) }
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
if (!options.toolMode) return { result: first };
|
||||
if (!options.toolMode) {
|
||||
if (!options.validation) return { result: first };
|
||||
const firstValidation = options.validation.validate(first.finalText);
|
||||
if (firstValidation.valid) return { result: first };
|
||||
|
||||
process.stderr.write(`\nChat 结构化输出校验失败,正在修复:${firstValidation.errors.join("; ")}\n`);
|
||||
const second = await runCommandCode(
|
||||
options.config,
|
||||
options.cliModel,
|
||||
options.effort,
|
||||
buildRepairPrompt(
|
||||
originalPrompt,
|
||||
options.format,
|
||||
first.finalText,
|
||||
firstValidation.errors,
|
||||
),
|
||||
options.signal,
|
||||
{ timeoutMs: options.deadline - Date.now() },
|
||||
);
|
||||
const secondValidation = options.validation.validate(second.finalText);
|
||||
if (!secondValidation.valid) throw new ChatStructuredOutputError(secondValidation.errors);
|
||||
return { result: combineCommandResults(first, second) };
|
||||
}
|
||||
|
||||
let execution: ChatExecution;
|
||||
try {
|
||||
return {
|
||||
execution = {
|
||||
result: first,
|
||||
decision: parseToolDecision(first.finalText, options.request),
|
||||
};
|
||||
@@ -371,23 +453,56 @@ async function executeChatTurn(options: {
|
||||
options.signal,
|
||||
{ timeoutMs: options.deadline - Date.now() },
|
||||
);
|
||||
const usage = addUsage(first.usage, second.usage);
|
||||
const result: CommandResult = {
|
||||
finalText: second.finalText,
|
||||
durationMs: first.durationMs + second.durationMs,
|
||||
...(usage ? { usage } : {}),
|
||||
};
|
||||
return {
|
||||
result,
|
||||
execution = {
|
||||
result: combineCommandResults(first, second),
|
||||
decision: parseToolDecision(second.finalText, options.request),
|
||||
};
|
||||
}
|
||||
|
||||
if (execution.decision?.type !== "final" || !options.validation) return execution;
|
||||
const firstValidation = options.validation.validate(execution.decision.content);
|
||||
if (firstValidation.valid) return execution;
|
||||
|
||||
process.stderr.write(`\nChat 最终工具正文结构化校验失败,正在修复:${firstValidation.errors.join("; ")}\n`);
|
||||
const repair = await runCommandCode(
|
||||
options.config,
|
||||
options.cliModel,
|
||||
options.effort,
|
||||
buildToolStructuredOutputRepairPrompt(
|
||||
originalPrompt,
|
||||
options.request,
|
||||
execution.decision.content,
|
||||
firstValidation.errors,
|
||||
),
|
||||
options.signal,
|
||||
{ timeoutMs: options.deadline - Date.now() },
|
||||
);
|
||||
const result = combineCommandResults(execution.result, repair);
|
||||
let decision: ToolDecision;
|
||||
try {
|
||||
decision = parseToolDecision(repair.finalText, options.request);
|
||||
} catch (error) {
|
||||
if (error instanceof ToolDecisionError) {
|
||||
throw new ChatStructuredOutputError([
|
||||
"Structured output repair did not return a valid final decision",
|
||||
...error.errors,
|
||||
]);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (decision.type !== "final") {
|
||||
throw new ChatStructuredOutputError(["Structured output repair returned tool_calls instead of final"]);
|
||||
}
|
||||
const repairedValidation = options.validation.validate(decision.content);
|
||||
if (!repairedValidation.valid) throw new ChatStructuredOutputError(repairedValidation.errors);
|
||||
return { result, decision };
|
||||
}
|
||||
|
||||
function addUsage(left?: CommandUsage, right?: CommandUsage): CommandUsage | undefined {
|
||||
if (!left && !right) return undefined;
|
||||
function combineCommandResults(left: CommandResult, right: CommandResult): CommandResult {
|
||||
const usage = addUsage(left.usage, right.usage);
|
||||
return {
|
||||
inputTokens: (left?.inputTokens ?? 0) + (right?.inputTokens ?? 0),
|
||||
outputTokens: (left?.outputTokens ?? 0) + (right?.outputTokens ?? 0),
|
||||
finalText: right.finalText,
|
||||
durationMs: left.durationMs + right.durationMs,
|
||||
...(usage ? { usage } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user