UNPKG

jorel

Version:

A unified wrapper for working with LLMs from multiple providers, including streams, images, documents & automatic tool use.

387 lines (386 loc) 16.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GoogleGenerativeAIProvider = void 0; const genai_1 = require("@google/genai"); const zod_1 = require("zod"); const __1 = require(".."); const shared_1 = require("../../shared"); const convert_llm_message_1 = require("./convert-llm-message"); class GoogleGenerativeAIProvider { constructor(options = {}) { this.name = options.name || GoogleGenerativeAIProvider.defaultName; const apiKey = options.apiKey || process.env.GOOGLE_AI_API_KEY; if (!apiKey) { throw new Error("[GoogleGenerativeAIProvider] Missing API key. Either pass it as config.apiKey or set the GOOGLE_AI_API_KEY environment variable"); } this.client = new genai_1.GoogleGenAI({ apiKey }); this.safetySettings = options.safetySettings; } async generateResponse(model, messages, config = {}) { const start = Date.now(); try { const { contents, systemInstruction } = (0, convert_llm_message_1.convertLlmMessagesToGoogleGenerativeAiMessages)(messages); const requestConfig = this.prepareGenerationConfig(config); // Add system instruction to config if present if (systemInstruction) { requestConfig.systemInstruction = systemInstruction; } // Add abort signal to config if present if (config.abortSignal) { requestConfig.abortSignal = config.abortSignal; } let result; try { result = await this.client.models.generateContent({ model, contents, config: requestConfig, }); } catch (error) { if (error instanceof Error && error.message.toLowerCase().includes("aborted")) { throw new shared_1.JorElAbortError("Request was aborted"); } const { message, type } = this.parseGoogleApiError(error); throw new shared_1.JorElLlmError(`[GoogleGenerativeAIProvider] Error generating content: ${message}`, type); } const candidate = result.candidates?.[0]; const contentParts = candidate?.content?.parts || []; const textParts = contentParts.filter((p) => p.text && !p.thought); const content = textParts.map((p) => p.text).join(""); const reasoningParts = contentParts.filter((p) => p.thought); const reasoningContent = reasoningParts.length > 0 ? reasoningParts.map((p) => p.text).join("") : null; const toolCalls = []; for (const part of contentParts) { if (part.functionCall) { toolCalls.push({ id: (0, shared_1.generateUniqueId)(), request: { id: (0, shared_1.generateRandomId)(), function: { name: part.functionCall.name ?? "", arguments: part.functionCall.args ?? {}, }, providerMetadata: part.thoughtSignature ? { google: { thoughtSignature: part.thoughtSignature } } : undefined, }, approvalState: config.tools?.getTool(part.functionCall.name ?? "")?.requiresConfirmation ? "requiresApproval" : "noApprovalRequired", executionState: "pending", result: null, error: null, }); } } const durationMs = Date.now() - start; return { ...(0, __1.generateAssistantMessage)(content, reasoningContent, toolCalls.length > 0 ? toolCalls : undefined), meta: { model, provider: this.name, temperature: config.temperature ?? undefined, durationMs, inputTokens: undefined, outputTokens: undefined, }, }; } catch (error) { if (error instanceof shared_1.JorElAbortError) { throw error; } throw error; } } async *generateResponseStream(model, messages, config = {}) { const start = Date.now(); const { contents, systemInstruction } = (0, convert_llm_message_1.convertLlmMessagesToGoogleGenerativeAiMessages)(messages); const requestConfig = this.prepareGenerationConfig(config); // Add system instruction to config if present if (systemInstruction) { requestConfig.systemInstruction = systemInstruction; } // Add abort signal to config if present if (config.abortSignal) { requestConfig.abortSignal = config.abortSignal; } let streamResult; try { streamResult = await this.client.models.generateContentStream({ model, contents, config: requestConfig, }); } catch (error) { const isAbort = error instanceof Error && (error.message.toLowerCase().includes("aborted") || error.name === "AbortError"); const stopReason = isAbort ? "userCancelled" : "generationError"; const { message: errorMessage, type: errorType } = stopReason === "generationError" ? this.parseGoogleApiError(error) : { message: "", type: "unknown" }; yield { type: "response", role: "assistant", content: "", reasoningContent: null, meta: { model, provider: this.name, temperature: config.temperature ?? undefined, durationMs: 0, inputTokens: undefined, outputTokens: undefined, }, stopReason, error: stopReason === "generationError" ? { message: errorMessage, type: errorType, } : undefined, }; return; } let fullContent = ""; let fullReasoningContent = ""; const toolCalls = []; let error; try { for await (const chunk of streamResult) { const candidate = chunk.candidates?.[0]; const parts = candidate?.content?.parts || []; const textParts = parts.filter((p) => p.text && !p.thought); const chunkText = textParts.map((p) => p.text).join(""); fullContent += chunkText; // Extract reasoning from parts const reasoningParts = parts.filter((p) => p.thought); const chunkReasoning = reasoningParts.map((p) => p.text).join(""); fullReasoningContent += chunkReasoning; // Check for function calls for (const part of parts) { if (part.functionCall) { // Check if this function call is already in our toolCalls array const existingToolCall = toolCalls.find((tc) => tc.request.function.name === (part.functionCall.name ?? "") && JSON.stringify(tc.request.function.arguments) === JSON.stringify(part.functionCall.args ?? {})); if (!existingToolCall) { toolCalls.push({ id: (0, shared_1.generateUniqueId)(), request: { id: (0, shared_1.generateRandomId)(), function: { name: part.functionCall.name ?? "", arguments: part.functionCall.args ?? {}, }, providerMetadata: part.thoughtSignature ? { google: { thoughtSignature: part.thoughtSignature } } : undefined, }, approvalState: config.tools?.getTool(part.functionCall.name ?? "")?.requiresConfirmation ? "requiresApproval" : "noApprovalRequired", executionState: "pending", result: null, error: null, }); } } } if (chunkText) { yield { type: "chunk", content: chunkText, chunkId: (0, shared_1.generateUniqueId)() }; } if (chunkReasoning) { yield { type: "reasoningChunk", content: chunkReasoning, chunkId: (0, shared_1.generateUniqueId)() }; } } } catch (e) { // Map Google GenAI SDK errors to our error types const { message: errorMessage, type } = this.parseGoogleApiError(e); error = { message: errorMessage, type, }; } const durationMs = Date.now() - start; // Determine stop reason and error message const stopReason = config.abortSignal?.aborted ? "userCancelled" : error ? "generationError" : "completed"; // Log non-abort errors if (error && stopReason === "generationError") { config.logger?.error("GoogleGenerativeAIProvider", `Stream error: ${error.message}`); } const meta = { model, provider: this.name, temperature: config.temperature ?? undefined, durationMs, inputTokens: undefined, outputTokens: undefined, }; // If we have tool calls, yield a response with tools if (toolCalls.length > 0) { yield { type: "response", role: "assistant_with_tools", content: fullContent, reasoningContent: fullReasoningContent || null, toolCalls, meta, stopReason, error: stopReason === "generationError" ? error : undefined, }; } else { yield { type: "response", role: "assistant", content: fullContent, reasoningContent: fullReasoningContent || null, meta, stopReason, error: stopReason === "generationError" ? error : undefined, }; } } async getAvailableModels() { return []; } async createEmbedding(model, text, abortSignal) { let result; try { result = await this.client.models.embedContent({ model, contents: [{ role: "user", parts: [{ text }] }], config: abortSignal ? { abortSignal } : undefined, }); } catch (error) { if (error.name === "AbortError" || (error.message && error.message.toLowerCase().includes("aborted"))) { throw new shared_1.JorElAbortError("Request was aborted"); } throw error; } if (!result.embeddings || result.embeddings.length === 0) { throw new Error("No embedding returned"); } return result.embeddings[0].values ?? []; } // Helper method for parsing Google API errors parseGoogleApiError(error) { let errorMessage; let errorType = "unknown"; const status = error instanceof genai_1.ApiError ? error.status : undefined; errorMessage = error instanceof Error ? error.message : String(error); // Try to parse the error message if it's a JSON string from Google API if (error instanceof genai_1.ApiError && errorMessage.startsWith("{")) { try { const parsedError = JSON.parse(errorMessage); if (parsedError.error?.message) { // The error message itself might be a JSON string if (parsedError.error.message.startsWith("{")) { try { const innerError = JSON.parse(parsedError.error.message); errorMessage = innerError.error?.message || parsedError.error.message; } catch { errorMessage = parsedError.error.message; } } else { errorMessage = parsedError.error.message; } } } catch { // If parsing fails, use the original message } } // Map status codes to error types if (status === 400) { errorType = "invalid_request"; } else if (status === 401) { errorType = "authentication_error"; } else if (status === 403) { // 403 can mean quota exceeded or permission denied const lowerMessage = errorMessage.toLowerCase(); if (lowerMessage.includes("quota") || lowerMessage.includes("resource exhausted")) { errorType = "quota_exceeded"; } else { errorType = "authentication_error"; } } else if (status === 404) { errorType = "invalid_request"; } else if (status === 429) { errorType = "rate_limit"; } else if (status && status >= 500) { errorType = "server_error"; } return { message: errorMessage, type: errorType }; } // Helper method for preparing request configuration prepareGenerationConfig(config) { const requestConfig = { safetySettings: this.safetySettings, }; if (config.reasoningEffort) { requestConfig.thinkingConfig = { includeThoughts: true, thinkingBudget: config.reasoningEffort === "minimal" ? 0 : undefined, thinkingLevel: config.reasoningEffort === "minimal" ? undefined : config.reasoningEffort === "high" || config.reasoningEffort === "medium" ? genai_1.ThinkingLevel.HIGH : genai_1.ThinkingLevel.LOW, }; } // Add generation config if (config.temperature !== undefined || config.maxTokens !== undefined || config.json) { requestConfig.temperature = config.temperature ?? undefined; requestConfig.maxOutputTokens = config.maxTokens ?? undefined; if (config.json) { requestConfig.responseMimeType = "application/json"; if (typeof config.json !== "boolean") { requestConfig.responseJsonSchema = config.json instanceof zod_1.ZodObject ? (0, shared_1.zodSchemaToJsonSchema)(config.json) : config.json; } } } // Add tools if (config.tools?.asLlmFunctions?.length) { requestConfig.tools = [ { functionDeclarations: config.tools.asLlmFunctions.map((f) => ({ name: f.function.name, description: f.function.description, parameters: f.function.parameters, // TODO: Improve types })), }, ]; } // Add tool config if (config.tools?.hasTools && config.toolChoice) { let mode = genai_1.FunctionCallingConfigMode.AUTO; if (config.toolChoice === "none") { mode = genai_1.FunctionCallingConfigMode.NONE; } else if (config.toolChoice === "required") { mode = genai_1.FunctionCallingConfigMode.ANY; } requestConfig.toolConfig = { functionCallingConfig: { mode, }, }; } return requestConfig; } } exports.GoogleGenerativeAIProvider = GoogleGenerativeAIProvider; GoogleGenerativeAIProvider.defaultName = "google-generative-ai";