工具使用

This commit is contained in:
Sirius
2026-08-06 14:14:09 +08:00
parent d5eb882dfe
commit e236721db1
11 changed files with 745 additions and 73 deletions
+161 -23
View File
@@ -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),
};
}