UNPKG

naisys

Version:

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

108 lines 4.25 kB
import { isDefined } from "@naisys/common"; import OpenAI from "openai"; 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 toOpenAiReasoningEffort(level) { if (!level) return undefined; return level === "max" ? "xhigh" : level; } export async function sendWithOpenAiCompatible(deps, modelKey, systemMessage, context, source, apiKey, abortSignal) { const { modelService, costTracker, tools, useToolsForLlmConsoleResponses } = deps; const model = modelService.getLlmModel(modelKey); // API key can be blank like in cases of local LLMs const openAI = getClient(apiKey || "", model.baseUrl); const reasoningEffort = toOpenAiReasoningEffort(model.reasoningLevel); const useConsoleTools = source === "console" && useToolsForLlmConsoleResponses && model.supportsToolUse === true; const chatRequest = { model: model.versionName, stream: false, reasoning_effort: reasoningEffort, messages: [ { role: "system", content: systemMessage, }, ...context.map((m) => ({ content: formatContentForOpenAI(m.content), role: m.role, })), ], }; if (useConsoleTools) { chatRequest.tools = [tools.consoleToolOpenAI]; // Only one tool is ever offered, so "required" forces that tool while // staying compatible with endpoints that reject the object form of // tool_choice (e.g. some local OpenAI-compatible servers). chatRequest.tool_choice = "required"; } const chatResponse = await openAI.chat.completions.create(chatRequest, { signal: abortSignal, }); // Record token usage if (!chatResponse.usage) { throw "Error, no usage data returned from OpenAI API."; } const inputTokens = chatResponse.usage.prompt_tokens || 0; const outputTokens = chatResponse.usage.completion_tokens || 0; // Excludes output_tokens because it contains reasoning tokens that don't persist in context; // the actual response text is estimated locally by contextManager.getTokenCount() const messagesTokenCount = inputTokens; const cacheReadTokens = chatResponse.usage.prompt_tokens_details?.cached_tokens || 0; // Remove cached tokens so we only bill fresh tokens at the full input rate. const nonCachedPromptTokens = Math.max(0, inputTokens - cacheReadTokens); costTracker.recordTokens(source, model.key, nonCachedPromptTokens, outputTokens, 0, // OpenAI doesn't report cache write tokens separately - it's automatic cacheReadTokens); if (chatRequest.tools) { const commandsFromTool = tools.getCommandsFromOpenAiToolUse(chatResponse.choices.at(0)?.message?.tool_calls); if (commandsFromTool) { return { responses: commandsFromTool, messagesTokenCount }; } } return { responses: [chatResponse.choices[0].message.content || ""], messagesTokenCount, }; } function formatContentForOpenAI(content) { if (typeof content === "string") { return content; } return content .map((block) => { if (block.type === "text") { return { type: "text", text: block.text }; } if (block.type === "image") { return { type: "image_url", image_url: { url: `data:${block.mimeType};base64,${block.base64}`, }, }; } // tool_use/tool_result: convert to text fallback if (block.type === "tool_use") { return { type: "text", text: `ns-tool desktop-action ${JSON.stringify(block.input)}`, }; } if (block.type === "tool_result") { return { type: "text", text: "[Desktop screenshot]" }; } return null; }) .filter(isDefined); } //# sourceMappingURL=openai-compatible.js.map