@xsai/shared-chat
Version:
extra-small AI SDK.
170 lines (162 loc) • 6.25 kB
JavaScript
import { postJSON, InvalidToolCallError, InvalidToolInputError, ToolExecutionError } from '@xsai/shared';
const chat = async (options) => postJSON("chat/completions", {
...options,
maxSteps: void 0,
onEvent: void 0,
onFinish: void 0,
onStepFinish: void 0,
postToolCall: void 0,
prepareStep: void 0,
preToolCall: void 0,
stopWhen: void 0,
tools: options.tools?.map(({ execute: _execute, ...tool }) => tool)
});
const toToolMessageContent = (result) => {
if (typeof result === "string")
return result;
if (Array.isArray(result)) {
if (result.every((item) => item !== null && typeof item === "object" && "type" in item && ["file", "image_url", "input_audio", "text"].includes(item.type))) {
return result;
}
}
return JSON.stringify(result);
};
const isAbortError = (error, abortSignal) => abortSignal?.aborted === true && error === abortSignal.reason || error instanceof Error && error.name === "AbortError";
const parseToolInput = (toolName, input) => {
try {
return JSON.parse(input.trim() || "{}");
} catch (cause) {
throw new InvalidToolInputError(`Failed to parse tool input for "${toolName}".`, {
cause,
toolInput: input,
toolName
});
}
};
const runTool = async (tool, options) => {
try {
return await tool.execute(options.parsedArgs, options.toolExecuteOptions);
} catch (cause) {
if (isAbortError(cause, options.toolExecuteOptions.abortSignal))
throw cause;
throw new ToolExecutionError(`Tool "${tool.function.name}" execution failed.`, {
cause,
toolCallId: options.toolExecuteOptions.toolCallId,
toolInput: options.parsedArgs,
toolName: tool.function.name
});
}
};
const assertSameToolCallId = (source, next, label) => {
if (source === next.toolCallId)
return;
throw new InvalidToolCallError(`${label} must preserve toolCallId "${source}".`, {
reason: "tool_call_id_mismatch",
toolCall: next
});
};
const findTool = (tools, toolName, toolCall) => {
const tool = tools?.find((tool2) => tool2.function.name === toolName);
if (!tool) {
const availableTools = tools?.map((tool2) => tool2.function.name);
const availableToolsErrorMsg = availableTools == null || availableTools.length === 0 ? "No tools are available" : `Available tools: ${availableTools.join(", ")}`;
throw new InvalidToolCallError(`Model tried to call unavailable tool "${toolName}", ${availableToolsErrorMsg}.`, {
availableTools,
reason: "unknown_tool",
toolCall,
toolName
});
}
return tool;
};
const executeTool = async ({ abortSignal, messages, postToolCall, preToolCall, toolCall, tools, wrapResult }) => {
const wrap = wrapResult ?? toToolMessageContent;
const toolName = toolCall.function.name;
const toolArguments = toolCall.function.arguments;
if (toolName == null) {
throw new InvalidToolCallError(`Missing toolCall.function.name: ${JSON.stringify(toolCall)}`, {
reason: "missing_name",
toolCall
});
}
if (toolArguments == null) {
throw new InvalidToolCallError(`Missing toolCall.function.arguments: ${JSON.stringify(toolCall)}`, {
reason: "missing_arguments",
toolCall
});
}
const toolExecuteOptions = {
abortSignal,
messages,
toolCallId: toolCall.id
};
let completionToolCall = {
args: toolArguments,
toolCallId: toolCall.id,
toolCallType: "function",
toolName
};
let completionToolResult;
const preToolCallResult = await preToolCall?.(completionToolCall, toolExecuteOptions);
if (preToolCallResult) {
assertSameToolCallId(completionToolCall.toolCallId, preToolCallResult, "preToolCallResult");
if ("result" in preToolCallResult)
completionToolResult = preToolCallResult;
else
completionToolCall = preToolCallResult;
}
if (completionToolResult == null) {
const tool = findTool(tools, completionToolCall.toolName, completionToolCall);
const parsedArgs = parseToolInput(completionToolCall.toolName, completionToolCall.args);
const result = await runTool(tool, {
parsedArgs,
toolExecuteOptions
});
completionToolResult = {
args: parsedArgs,
result,
toolCallId: completionToolCall.toolCallId,
toolName: completionToolCall.toolName
};
}
const postToolCallResult = await postToolCall?.(completionToolResult, toolExecuteOptions);
if (postToolCallResult) {
assertSameToolCallId(completionToolResult.toolCallId, postToolCallResult, "postToolCallResult");
completionToolResult = postToolCallResult;
}
return {
completionToolCall,
completionToolResult,
result: wrap(completionToolResult.result)
};
};
const resolvePrepareStep = async ({ input, model, prepareStep, stepNumber, steps, toolChoice }) => {
const prepared = prepareStep == null ? void 0 : await prepareStep({
input: structuredClone(input),
model,
stepNumber,
steps: structuredClone(steps)
});
return {
input: prepared?.input != null ? structuredClone(prepared.input) : input,
model: prepared?.model ?? model,
toolChoice: prepared?.toolChoice ?? toolChoice
};
};
const and = (...conditions) => (context) => conditions.every((condition) => condition(context));
const or = (...conditions) => (context) => conditions.some((condition) => condition(context));
const not = (condition) => (context) => !condition(context);
const stepCountAtLeast = (count) => ({ steps }) => steps.length >= count;
const hasToolCall = (name) => ({ step }) => step.toolCalls.some((toolCall) => name == null || toolCall.toolName === name);
const shouldStop = (stopWhen, context) => stopWhen(context);
const computeTotalUsage = (totalUsage, usage) => totalUsage == null ? usage : {
inputTokens: totalUsage.inputTokens + usage.inputTokens,
outputTokens: totalUsage.outputTokens + usage.outputTokens,
totalTokens: totalUsage.totalTokens + usage.totalTokens
};
const normalizeChatCompletionUsage = (usage) => ({
inputTokens: usage.prompt_tokens,
outputTokens: usage.completion_tokens,
totalTokens: usage.total_tokens
});
export { and, chat, computeTotalUsage, executeTool, hasToolCall, normalizeChatCompletionUsage, not, or, resolvePrepareStep, shouldStop, stepCountAtLeast };