适配chat结构

This commit is contained in:
Sirius
2026-08-12 16:03:25 +08:00
parent 3e7a4c9540
commit f65ac095bb
5 changed files with 264 additions and 38 deletions
+136 -21
View File
@@ -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 } : {}),
};
}