UNPKG

erosolar-cli

Version:

Unified AI agent framework for the command line - Multi-provider support with schema-driven tools, code intelligence, and transparent reasoning

312 lines 9.7 kB
import { GoogleGenAI, } from '@google/genai'; // ============================================================================ // Error Recovery Constants // ============================================================================ const RECOVERABLE_ERROR_PATTERNS = [ 'premature close', 'premature end', 'unexpected end', 'aborted', 'fetcherror', 'invalid response body', 'gunzip', 'decompress', 'econnreset', 'econnrefused', 'epipe', 'socket hang up', 'network', 'timeout', 'rate limit', '429', '500', '502', '503', '504', ]; function isRecoverableError(error) { if (!(error instanceof Error)) return false; const message = error.message.toLowerCase(); const errorName = error.name?.toLowerCase() ?? ''; const allText = `${message} ${errorName}`; return RECOVERABLE_ERROR_PATTERNS.some(pattern => allText.includes(pattern)); } export class GoogleGenAIProvider { id; model; client; temperature; maxOutputTokens; maxRetries; constructor(options) { this.client = new GoogleGenAI({ apiKey: options.apiKey, }); this.id = options.providerId ?? 'google'; this.model = options.model; this.temperature = options.temperature; this.maxOutputTokens = options.maxOutputTokens; this.maxRetries = options.maxRetries ?? 3; } /** * Sleep for a given number of milliseconds */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Calculate exponential backoff delay with jitter */ getBackoffDelay(attempt, baseDelay = 1000, maxDelay = 30000) { const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay); return delay + Math.random() * delay * 0.1; } /** * Execute request with retry logic for transient errors */ async executeWithRetry(operation, operationName) { let lastError; for (let attempt = 0; attempt <= this.maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (isRecoverableError(error) && attempt < this.maxRetries) { const delay = this.getBackoffDelay(attempt); console.warn(`[${this.id}] ${operationName} failed (attempt ${attempt + 1}/${this.maxRetries + 1}): ` + `${lastError.message}. Retrying in ${Math.round(delay)}ms...`); await this.sleep(delay); continue; } throw error; } } throw lastError; } async generate(messages, tools) { return this.executeWithRetry(async () => { const { contents, systemInstruction } = mapConversation(messages); const config = {}; if (systemInstruction) { config.systemInstruction = systemInstruction; } if (typeof this.temperature === 'number') { config.temperature = this.temperature; } if (typeof this.maxOutputTokens === 'number') { config.maxOutputTokens = this.maxOutputTokens; } const mappedTools = mapTools(tools); if (mappedTools.length > 0) { config.tools = mappedTools; } const response = await this.client.models.generateContent({ model: this.model, contents: contents.length ? contents : createEmptyUserContent(), config: Object.keys(config).length ? config : undefined, }); const usage = mapUsage(response.usageMetadata); // Safely extract tool calls with error recovery let toolCalls = []; try { toolCalls = mapFunctionCalls(response.functionCalls ?? []); } catch (parseError) { console.warn(`[${this.id}] Failed to parse function calls, recovering: ` + `${parseError instanceof Error ? parseError.message : String(parseError)}`); } // Extract text content manually to avoid SDK warning when function calls are present // The SDK's response.text getter logs a warning when there are non-text parts const content = extractTextFromResponse(response); if (toolCalls.length > 0) { return { type: 'tool_calls', toolCalls, content, usage, }; } return { type: 'message', content, usage, }; }, 'generate'); } } function mapConversation(messages) { const contents = []; const systemPrompts = []; for (const message of messages) { switch (message.role) { case 'system': { if (message.content.trim()) { systemPrompts.push(message.content.trim()); } break; } case 'user': { contents.push({ role: 'user', parts: [{ text: message.content }], }); break; } case 'assistant': { contents.push(mapAssistantMessage(message)); break; } case 'tool': { const content = mapToolMessage(message); if (content) { contents.push(content); } break; } default: break; } } return { contents, systemInstruction: systemPrompts.length ? systemPrompts.join('\n\n') : undefined, }; } function mapAssistantMessage(message) { const parts = []; const text = message.content.trim(); if (text) { parts.push({ text }); } for (const call of message.toolCalls ?? []) { parts.push({ functionCall: { id: call.id || undefined, name: call.name, args: toRecord(call.arguments), }, }); } return { role: 'model', parts: parts.length ? parts : [{ text: '' }], }; } function mapToolMessage(message) { if (!message.toolCallId) { return null; } return { role: 'user', parts: [ { functionResponse: { id: message.toolCallId, name: message.name, response: parseToolResponse(message.content), }, }, ], }; } function parseToolResponse(content) { const trimmed = content.trim(); if (!trimmed) { return { output: '' }; } try { const parsed = JSON.parse(trimmed); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { return parsed; } } catch { } return { output: content }; } function mapFunctionCalls(calls) { return calls .filter((call) => Boolean(call.name)) .map((call) => ({ id: call.id || call.name || 'function_call', name: call.name ?? 'function_call', arguments: toRecord(call.args), })); } function createEmptyUserContent() { return [ { role: 'user', parts: [{ text: '' }], }, ]; } function mapTools(tools) { if (!tools.length) { return []; } return [ { functionDeclarations: tools.map((tool) => ({ name: tool.name, description: tool.description, parametersJsonSchema: tool.parameters ?? { type: 'object', properties: {} }, })), }, ]; } function mapUsage(metadata) { if (!metadata) { return null; } return { inputTokens: metadata.promptTokenCount ?? undefined, outputTokens: metadata.candidatesTokenCount ?? undefined, totalTokens: metadata.totalTokenCount ?? undefined, }; } function toRecord(value) { if (isPlainRecord(value)) { return value; } if (typeof value === 'string') { const trimmed = value.trim(); if (!trimmed) { return {}; } try { const parsed = JSON.parse(trimmed); return isPlainRecord(parsed) ? parsed : {}; } catch { return {}; } } return {}; } function isPlainRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } /** * Extract text content from response without using the SDK's text getter. * This avoids the SDK warning "there are non-text parts functionCall in the response" * which is logged when accessing response.text and the response contains function calls. */ function extractTextFromResponse(response) { const candidates = response.candidates; if (!candidates || candidates.length === 0) { return ''; } const content = candidates[0]?.content; if (!content?.parts) { return ''; } // Extract only text parts, ignoring function calls and other part types const textParts = content.parts .filter((part) => 'text' in part && typeof part.text === 'string') .map(part => part.text); return textParts.join('').trim(); } //# sourceMappingURL=googleProvider.js.map