UNPKG

naisys

Version:

NAISYS - Autonomous AI agent runner with built-in context management and cost tracking

194 lines 7.65 kB
import { LlmApiType } from "@naisys/common"; import OpenAI from "openai"; import { extractDesktopActions, formatInputWithComputerUse, } from "../../computer-use/vendors/openai-computer-use.js"; const clientCache = new Map(); function getClient(apiKey, baseURL) { const cacheKey = `${baseURL || ""}|${apiKey}`; let client = clientCache.get(cacheKey); if (!client) { client = new OpenAI({ baseURL, apiKey }); clientCache.set(cacheKey, client); } return client; } function formatOpenAiResponseFailure(response) { if (response.error) { return `OpenAI Responses error: ${response.error.code || "unknown"}: ${response.error.message || "no message"}`; } if (response.incomplete_details?.reason) { return `OpenAI Responses incomplete: ${response.incomplete_details.reason}`; } return "OpenAI Responses stream failed."; } function toOpenAiReasoningEffort(level) { return level === "max" ? "xhigh" : level; } function toOpenAiReasoning(level) { return level ? { effort: toOpenAiReasoningEffort(level) } : undefined; } async function createStreamingOpenAiResponse(openAI, params, abortSignal) { const stream = await openAI.responses.create(params, { signal: abortSignal }); let response; let outputText = ""; const outputItems = []; for await (const event of stream) { if (event.type === "response.output_text.delta") { outputText += event.delta; } else if (event.type === "response.output_item.done") { outputItems.push(event.item); } else if (event.type === "response.completed" || event.type === "response.incomplete") { response = event.response; } else if (event.type === "response.failed") { throw new Error(formatOpenAiResponseFailure(event.response)); } else if (event.type === "error") { throw new Error(event.code ? `${event.code}: ${event.message}` : event.message); } } if (!response) { throw new Error("OpenAI Responses stream ended without a final response."); } return { ...response, output: response.output?.length ? response.output : outputItems, output_text: response.output_text || outputText, }; } export async function sendWithOpenAiStandard(deps, modelKey, systemMessage, context, source, apiKey, abortSignal) { const { modelService, costTracker, tools, useToolsForLlmConsoleResponses, desktopConfig, } = deps; const model = modelService.getLlmModel(modelKey); if (!apiKey) { throw `Error, set ${model.apiKeyVar} variable`; } const openAI = getClient(apiKey, model.baseUrl); const isOpenAiOauth = model.apiType === LlmApiType.OpenAIOAuth; const useConsoleTools = source === "console" && useToolsForLlmConsoleResponses && model.supportsToolUse === true; // Build tools array — console and desktop tools can coexist const toolsDefs = []; if (useConsoleTools) { const functionTool = { type: "function", name: tools.consoleToolOpenAI.function.name, description: tools.consoleToolOpenAI.function.description, parameters: tools.consoleToolOpenAI.function .parameters, strict: false, }; toolsDefs.push(functionTool); } if (desktopConfig) { toolsDefs.push({ type: "computer" }); } // Force console tool only when desktop is not also enabled let toolChoice = undefined; if (useConsoleTools && !desktopConfig) { toolChoice = { type: "function", name: tools.consoleToolOpenAI.function.name, }; } const createParams = { model: model.versionName, instructions: systemMessage, input: (desktopConfig ? formatInputWithComputerUse(context, formatContentBlocks, formatSingleBlock) : context.map((m) => ({ role: m.role === "assistant" ? "assistant" : "user", content: formatContentBlocks(m.content, m.role), }))), reasoning: toOpenAiReasoning(model.reasoningLevel), tools: toolsDefs.length > 0 ? toolsDefs : undefined, tool_choice: toolChoice, }; const response = isOpenAiOauth ? await createStreamingOpenAiResponse(openAI, { ...createParams, store: false, stream: true, }, abortSignal) : await openAI.responses.create(createParams, { signal: abortSignal }); if (!response.usage) { throw "Error, no usage data returned from OpenAI Responses API."; } const inputTokens = response.usage.input_tokens || 0; const outputTokens = response.usage.output_tokens || 0; const messagesTokenCount = inputTokens; const cacheReadTokens = response.usage.input_tokens_details?.cached_tokens || 0; const nonCachedPromptTokens = Math.max(0, inputTokens - cacheReadTokens); costTracker.recordTokens(source, model.key, nonCachedPromptTokens, outputTokens, 0, cacheReadTokens); // Extract desktop actions (computer_call items); coords stay in scaled-pixel space const desktopActions = desktopConfig ? extractDesktopActions(response.output) : []; // Extract console commands (function_call items) const consoleCommands = useConsoleTools ? extractConsoleCommands(response.output, tools) : undefined; // Extract text const textParts = response.output_text ? [response.output_text] : []; // Desktop actions take priority (same pattern as Anthropic vendor) if (desktopActions.length > 0) { const allText = [...textParts, ...(consoleCommands || [])]; return { responses: allText, messagesTokenCount, desktopActions }; } if (consoleCommands) { return { responses: consoleCommands, messagesTokenCount }; } return { responses: textParts.length > 0 ? textParts : [""], messagesTokenCount, }; } function extractConsoleCommands(output, tools) { const toolCalls = output .filter((item) => item.type === "function_call") .map((item) => ({ type: "function", function: { name: item.name, arguments: item.arguments }, })); return tools.getCommandsFromOpenAiToolUse(toolCalls) || undefined; } // --- Content formatting helpers (shared with openai-computer-use) --- function makeTextPart(text, role) { return role === "assistant" ? { type: "output_text", text } : { type: "input_text", text }; } function formatContentBlocks(content, role) { if (typeof content === "string") { return [makeTextPart(content, role)]; } return content .map((b) => formatSingleBlock(b, role)) .filter((p) => p !== null); } function formatSingleBlock(block, role) { switch (block.type) { case "text": return makeTextPart(block.text, role); case "image": return { type: "input_image", image_url: `data:${block.mimeType};base64,${block.base64}`, detail: "auto", }; case "tool_use": // Fallback when desktop is not enabled — include as text description return makeTextPart(`ns-tool desktop-action ${JSON.stringify(block.input)}`, role); case "tool_result": return makeTextPart("[Desktop screenshot]", role); case "audio": return null; default: return null; } } //# sourceMappingURL=openai-standard.js.map