UNPKG

naisys

Version:

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

195 lines 8.11 kB
import { Environment, FunctionCallingConfigMode, GoogleGenAI, } from "@google/genai"; import { extractDesktopActions, formatContextWithComputerUse, } from "../../computer-use/vendors/google-computer-use.js"; const clientCache = new Map(); function getClient(apiKey, baseUrl) { const cacheKey = `${baseUrl || ""}|${apiKey}`; let client = clientCache.get(cacheKey); if (!client) { client = new GoogleGenAI({ apiKey, httpOptions: baseUrl ? { baseUrl } : undefined, }); clientCache.set(cacheKey, client); } return client; } function toGoogleThinkingBudget(level) { switch (level) { case undefined: return undefined; case "none": return 0; case "low": return 1024; case "medium": return 8192; case "high": return 16384; case "max": return -1; } } export async function sendWithGoogle(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 ai = getClient(apiKey, model.baseUrl); const thinkingBudget = toGoogleThinkingBudget(model.reasoningLevel); const useConsoleTools = source === "console" && useToolsForLlmConsoleResponses && model.supportsToolUse === true; const lastMessage = context[context.length - 1]; // Build history from context (excluding last message) let history; // Last message parts formatted for sendMessage let cuLastMessageParts; if (desktopConfig) { // Format ALL messages in one pass so the tool_use ID → name map is // available when processing tool_result blocks (which may be the last message) const allFormatted = formatContextWithComputerUse(context, desktopConfig, formatPartsForGoogle); history = allFormatted.slice(0, -1); cuLastMessageParts = allFormatted[allFormatted.length - 1]?.parts; } else { history = context .filter((m) => m !== lastMessage) .map((m) => ({ role: m.role === "assistant" ? "model" : "user", parts: formatPartsForGoogle(m.content), })); } // Prepare config with system instruction const chatConfig = { model: model.versionName, config: { systemInstruction: systemMessage, thinkingConfig: thinkingBudget !== undefined ? { // -1 is dynamic thinking, 0 disables thinking. thinkingBudget, } : undefined, }, history, }; // Build tools array — console and desktop tools can coexist const toolsDefs = []; if (useConsoleTools) { // consoleToolGoogle's properties are typed as a union per // multipleCommandsEnabled; FunctionDeclaration expects a flat record, // so go through unknown to reconcile toolsDefs.push({ functionDeclarations: [ tools.consoleToolGoogle, ], }); } if (desktopConfig) { toolsDefs.push({ computerUse: { environment: Environment.ENVIRONMENT_BROWSER }, }); } if (toolsDefs.length > 0) { chatConfig.config.tools = toolsDefs; // Only force console tool when desktop is not also enabled if (useConsoleTools && !desktopConfig) { chatConfig.config.toolConfig = { functionCallingConfig: { mode: FunctionCallingConfigMode.ANY, }, }; } } const chat = ai.chats.create(chatConfig); const lastMessageParts = cuLastMessageParts || formatPartsForGoogle(lastMessage.content); const result = await chat.sendMessage({ message: lastMessageParts, // Merge abortSignal into the full config — passing { abortSignal } alone // replaces the chat config entirely, losing tools and other settings config: abortSignal ? { ...chatConfig.config, abortSignal } : undefined, }); // Use actual token counts from Google API response if (!result.usageMetadata) { throw "Error, no usage metadata returned from Google API."; } const inputTokens = result.usageMetadata.promptTokenCount || 0; const outputTokens = result.usageMetadata.candidatesTokenCount || 0; // Excludes output_tokens because it contains thinking tokens that don't persist in context; // the actual response text is estimated locally by contextManager.getTokenCount() const messagesTokenCount = inputTokens; const cachedTokenCount = result.usageMetadata.cachedContentTokenCount || 0; costTracker.recordTokens(source, model.key, inputTokens - cachedTokenCount, outputTokens, 0, // Cache write tokens (not separately reported) cachedTokenCount); // Extract desktop actions from raw response parts (not result.functionCalls) // so we can capture thoughtSignature which lives at the Part level const responseParts = result.candidates?.[0]?.content?.parts || []; // Dispatch by "is this the console tool?" rather than "is this a known // desktop action?". Anything that isn't the console tool flows into the // desktop extractor, which then routes known computer-use names to typed // actions and unknown names to a validationError-bearing DesktopAction. // This catches new Google computer-use functions we haven't mapped yet, // instead of silently dropping them in the console path. const consoleToolName = tools.consoleToolGoogle.name; const desktopActions = desktopConfig ? extractDesktopActions(responseParts.filter((p) => p.functionCall && p.functionCall.name !== consoleToolName), desktopConfig.scaledWidth, desktopConfig.scaledHeight) : []; // Extract console commands (only the declared console tool) const consoleFunctionCalls = responseParts .filter((p) => p.functionCall && p.functionCall.name === consoleToolName) .map((p) => p.functionCall); const consoleCommands = useConsoleTools ? tools.getCommandsFromGoogleToolUse(consoleFunctionCalls) : undefined; // Extract text directly from response parts to avoid the SDK warning // that fires when accessing .text on a response containing function calls const textParts = []; const candidateParts = result.candidates?.[0]?.content?.parts; if (candidateParts) { for (const part of candidateParts) { if (part.text) { textParts.push(part.text); } } } // Desktop actions take priority (same pattern as Anthropic/OpenAI vendors) 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 formatPartsForGoogle(content) { if (typeof content === "string") { return [{ text: content }]; } return content .map((block) => { if (block.type === "text") { return { text: block.text }; } if (block.type === "image" || block.type === "audio") { return { inlineData: { mimeType: block.mimeType, data: block.base64 }, }; } if (block.type === "tool_use") { return { text: `ns-tool desktop-action ${JSON.stringify(block.input)}`, }; } if (block.type === "tool_result") { return { text: "[Desktop screenshot]" }; } return null; }) .filter((p) => p !== null); } //# sourceMappingURL=google.js.map