UNPKG

@xsai/shared-chat

Version:

extra-small AI SDK.

170 lines (162 loc) 6.25 kB
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 };