工具使用
This commit is contained in:
+161
-23
@@ -6,17 +6,29 @@ import {
|
||||
CommandCodeError,
|
||||
inspectInstallation,
|
||||
runCommandCode,
|
||||
type CommandResult,
|
||||
type InstallationStatus,
|
||||
} from "./command-code.js";
|
||||
import {
|
||||
buildCommandPrompt,
|
||||
buildToolDecisionRepairPrompt,
|
||||
chatCompletionRequestSchema,
|
||||
completionResponse,
|
||||
ignoredChatCompatibilityParameters,
|
||||
openAIError,
|
||||
toolCallsCompletionResponse,
|
||||
usesToolCalling,
|
||||
type ChatCompletionRequest,
|
||||
type CommandUsage,
|
||||
} from "./openai.js";
|
||||
import { RequestCoordinator } from "./request-coordinator.js";
|
||||
import { registerResponseRoutes } from "./responses.js";
|
||||
import {
|
||||
parseToolDecision,
|
||||
ToolDecisionError,
|
||||
validateToolSchemas,
|
||||
type ToolDecision,
|
||||
} from "./tool-calling.js";
|
||||
|
||||
export interface BridgeServer {
|
||||
app: FastifyInstance;
|
||||
@@ -31,7 +43,7 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
bodyLimit: config.max_request_bytes,
|
||||
requestTimeout: 0,
|
||||
});
|
||||
const coordinator = new RequestCoordinator();
|
||||
const coordinator = new RequestCoordinator(config.max_queue_size);
|
||||
|
||||
app.addHook("onRequest", async (request, reply) => {
|
||||
const requestedHeaders = request.headers["access-control-request-headers"];
|
||||
@@ -78,6 +90,8 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
status: installation.installed && installation.authenticated ? "ok" : "degraded",
|
||||
command_code: installation,
|
||||
busy: coordinator.busy,
|
||||
queue_length: coordinator.queueLength,
|
||||
queue_capacity: config.max_queue_size,
|
||||
}));
|
||||
|
||||
app.get("/v1/models", async () => ({
|
||||
@@ -91,14 +105,6 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
}));
|
||||
|
||||
app.post("/v1/chat/completions", async (request, reply) => {
|
||||
if (coordinator.busy) {
|
||||
return reply.status(429).send(openAIError(
|
||||
"Another Command Code request is already running",
|
||||
"busy",
|
||||
"server_error",
|
||||
));
|
||||
}
|
||||
|
||||
if (!installation.installed) {
|
||||
return reply.status(503).send(openAIError(
|
||||
"Command Code CLI is not installed",
|
||||
@@ -146,6 +152,16 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
));
|
||||
}
|
||||
|
||||
const toolSchemaErrors = validateToolSchemas(parsed);
|
||||
if (toolSchemaErrors.length > 0) {
|
||||
return reply.status(400).send(openAIError(
|
||||
toolSchemaErrors.join("; "),
|
||||
"invalid_tool_schema",
|
||||
"invalid_request_error",
|
||||
"tools",
|
||||
));
|
||||
}
|
||||
|
||||
const ignoredParameters = ignoredChatCompatibilityParameters(parsed);
|
||||
if (ignoredParameters.length > 0) {
|
||||
const parameterList = ignoredParameters.join(",");
|
||||
@@ -155,18 +171,20 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
);
|
||||
}
|
||||
|
||||
const abortController = coordinator.begin();
|
||||
if (!abortController) {
|
||||
const lease = coordinator.begin();
|
||||
if (!lease) {
|
||||
return reply.status(429).send(openAIError(
|
||||
"Another Command Code request is already running",
|
||||
"busy",
|
||||
"Command Code request queue is full",
|
||||
"queue_full",
|
||||
"server_error",
|
||||
));
|
||||
}
|
||||
const { controller: abortController } = lease;
|
||||
let responseCompleted = false;
|
||||
let acquired = false;
|
||||
|
||||
const cancelOnDisconnect = () => {
|
||||
if (!responseCompleted) abortController.abort("client disconnected");
|
||||
if (!responseCompleted) coordinator.cancel(abortController, "client disconnected");
|
||||
};
|
||||
request.raw.once("aborted", cancelOnDisconnect);
|
||||
reply.raw.once("close", cancelOnDisconnect);
|
||||
@@ -174,24 +192,52 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
let writer: ChatCompletionSseWriter | undefined;
|
||||
try {
|
||||
if (parsed.stream) writer = startChatCompletionSse(reply, parsed.model, config.stream_thinking);
|
||||
const result = await runCommandCode(
|
||||
if (request.raw.aborted || reply.raw.destroyed) cancelOnDisconnect();
|
||||
acquired = await lease.ready;
|
||||
if (!acquired) {
|
||||
responseCompleted = true;
|
||||
return reply;
|
||||
}
|
||||
const toolMode = usesToolCalling(parsed);
|
||||
const execution = await executeChatTurn({
|
||||
config,
|
||||
model.cli_model,
|
||||
model.effort,
|
||||
buildCommandPrompt(parsed),
|
||||
abortController.signal,
|
||||
writer ? { onEvent: (event) => writer?.commandEvent(event) } : {},
|
||||
);
|
||||
cliModel: model.cli_model,
|
||||
effort: model.effort,
|
||||
request: parsed,
|
||||
signal: abortController.signal,
|
||||
deadline: Date.now() + config.timeout_seconds * 1000,
|
||||
toolMode,
|
||||
...(writer ? { writer } : {}),
|
||||
});
|
||||
const { result, decision } = execution;
|
||||
responseCompleted = true;
|
||||
|
||||
if (decision?.type === "tool_calls") {
|
||||
if (writer) {
|
||||
writer.finishToolCalls(
|
||||
decision.toolCalls,
|
||||
result.usage,
|
||||
parsed.stream_options?.include_usage,
|
||||
);
|
||||
return reply;
|
||||
}
|
||||
return reply.send(toolCallsCompletionResponse(
|
||||
parsed.model,
|
||||
decision.toolCalls,
|
||||
result.usage,
|
||||
));
|
||||
}
|
||||
|
||||
const finalText = decision?.type === "final" ? decision.content : result.finalText;
|
||||
if (writer) {
|
||||
writer.finish(
|
||||
result.finalText,
|
||||
finalText,
|
||||
result.usage,
|
||||
parsed.stream_options?.include_usage,
|
||||
);
|
||||
return reply;
|
||||
}
|
||||
return reply.send(completionResponse(parsed.model, result.finalText, result.usage));
|
||||
return reply.send(completionResponse(parsed.model, finalText, result.usage));
|
||||
} catch (error) {
|
||||
responseCompleted = true;
|
||||
if (abortController.signal.aborted) {
|
||||
@@ -213,6 +259,23 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof ToolDecisionError) {
|
||||
process.stderr.write(`\n工具调用决策连续两次校验失败:${error.errors.join("; ")}\n`);
|
||||
if (writer) {
|
||||
writer.error(openAIError(
|
||||
"Command Code returned an invalid tool decision",
|
||||
"invalid_tool_decision",
|
||||
"server_error",
|
||||
));
|
||||
return reply;
|
||||
}
|
||||
return reply.status(502).send(openAIError(
|
||||
"Command Code returned an invalid tool decision",
|
||||
"invalid_tool_decision",
|
||||
"server_error",
|
||||
));
|
||||
}
|
||||
|
||||
const code = error instanceof CommandCodeError ? error.exitCode : undefined;
|
||||
const status = code === 5 ? 429 : code === 10 ? 402 : 502;
|
||||
const errorCode = code === 5
|
||||
@@ -236,6 +299,7 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
} finally {
|
||||
request.raw.off("aborted", cancelOnDisconnect);
|
||||
reply.raw.off("close", cancelOnDisconnect);
|
||||
if (!acquired) coordinator.cancel(abortController, "request ended before execution");
|
||||
coordinator.finish(abortController);
|
||||
}
|
||||
});
|
||||
@@ -248,3 +312,77 @@ export async function createServer(config: BridgeConfig): Promise<BridgeServer>
|
||||
abortActive: () => coordinator.abortActive(),
|
||||
};
|
||||
}
|
||||
|
||||
interface ChatExecution {
|
||||
result: CommandResult;
|
||||
decision?: ToolDecision;
|
||||
}
|
||||
|
||||
async function executeChatTurn(options: {
|
||||
config: BridgeConfig;
|
||||
cliModel: string;
|
||||
effort: string;
|
||||
request: ChatCompletionRequest;
|
||||
signal: AbortSignal;
|
||||
deadline: number;
|
||||
toolMode: boolean;
|
||||
writer?: ChatCompletionSseWriter;
|
||||
}): Promise<ChatExecution> {
|
||||
const originalPrompt = buildCommandPrompt(options.request);
|
||||
const first = await runCommandCode(
|
||||
options.config,
|
||||
options.cliModel,
|
||||
options.effort,
|
||||
originalPrompt,
|
||||
options.signal,
|
||||
{
|
||||
timeoutMs: options.deadline - Date.now(),
|
||||
...(!options.toolMode && options.writer
|
||||
? { onEvent: (event: Record<string, unknown>) => options.writer?.commandEvent(event) }
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
if (!options.toolMode) return { result: first };
|
||||
|
||||
try {
|
||||
return {
|
||||
result: first,
|
||||
decision: parseToolDecision(first.finalText, options.request),
|
||||
};
|
||||
} catch (error) {
|
||||
if (!(error instanceof ToolDecisionError)) throw error;
|
||||
process.stderr.write(`\n工具调用决策校验失败,正在修复:${error.errors.join("; ")}\n`);
|
||||
|
||||
const repairPrompt = buildToolDecisionRepairPrompt(
|
||||
originalPrompt,
|
||||
first.finalText,
|
||||
error.errors,
|
||||
);
|
||||
const second = await runCommandCode(
|
||||
options.config,
|
||||
options.cliModel,
|
||||
options.effort,
|
||||
repairPrompt,
|
||||
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,
|
||||
decision: parseToolDecision(second.finalText, options.request),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function addUsage(left?: CommandUsage, right?: CommandUsage): CommandUsage | undefined {
|
||||
if (!left && !right) return undefined;
|
||||
return {
|
||||
inputTokens: (left?.inputTokens ?? 0) + (right?.inputTokens ?? 0),
|
||||
outputTokens: (left?.outputTokens ?? 0) + (right?.outputTokens ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user