import { LLMProvider } from './LLMProvider'; import type { StreamChunk } from './StreamingTypes'; import type { Message } from '../core/AgentState'; import { ConfigManager } from '../../core/config/ConfigManager'; /** * Google Gemini API provider implementation */ export class GeminiProvider extends LLMProvider { readonly name = 'Gemini'; private apiKey: string; private model: string; private baseURL: string = 'https://generativelanguage.googleapis.com'; private apiEndpoint: string; constructor() { super(); const configManager = ConfigManager.instance(); this.apiKey = configManager.get('general.gemini.api_key') as string; this.model = configManager.get('general.gemini.model') as string; this.apiEndpoint = `${this.baseURL}/v1beta/models/${this.model}`; } /** * Convert internal messages to Gemini's format */ private convertMessages(messages: Message[], systemPrompt?: string): { systemInstruction?: { parts: Array<{ text: string }> }; contents: Array<{ role: 'user' | 'model'; parts: Array<{ text: string }> }>; } { const geminiMessages: Array<{ role: 'user' | 'model'; parts: Array<{ text: string }> }> = []; for (const msg of messages) { let role: 'user' | 'model'; if (msg.role === 'assistant') { role = 'model'; } else { // Treat system and user messages as 'user' role role = 'user'; } geminiMessages.push({ role, parts: [{ text: msg.content }] }); } const result: { systemInstruction?: { parts: Array<{ text: string }> }; contents: Array<{ role: 'user' | 'model'; parts: Array<{ text: string }> }>; } = { contents: geminiMessages }; if (systemPrompt) { result.systemInstruction = { parts: [{ text: systemPrompt }] }; } return result; } /** * Parse Gemini's streaming response chunks */ private parseGeminiStreamChunk(chunk: string): { content?: string; isDone?: boolean } { try { const json = JSON.parse(chunk.trim()); // Gemini streaming format if (json.candidates && json.candidates.length > 0) { const candidate = json.candidates[0]; // Check if generation is finished if (candidate.finishReason && candidate.finishReason !== 'STOP') { return { isDone: true }; } // Extract text content const content = candidate.content?.parts?.[0]?.text; if (content) { return { content, isDone: false }; } } // Check for explicit done signal if (json.done === true) { return { isDone: true }; } return {}; } catch { return {}; // Skip invalid JSON } } async *generateStream( messages: Message[], systemPrompt?: string, tools?: Record[] ): AsyncIterableIterator { const { systemInstruction, contents } = this.convertMessages(messages, systemPrompt); const requestBody: { contents: Array<{ role: 'user' | 'model'; parts: Array<{ text: string }> }>; generationConfig: { temperature: number; maxOutputTokens: number }; systemInstruction?: { parts: Array<{ text: string }> }; tools?: Record[]; } = { contents, generationConfig: { temperature: 0.7, maxOutputTokens: 8192, } }; if (systemInstruction) { requestBody.systemInstruction = systemInstruction; } if (tools && tools.length > 0) { requestBody.tools = tools; } const response = await fetch(`${this.apiEndpoint}:streamGenerateContent?key=${this.apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(`Gemini API error: ${response.status} ${response.statusText}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error('Failed to get response reader'); } const decoder = new TextDecoder(); let buffer = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // Gemini sends JSON objects separated by newlines const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine) continue; const parseResult = this.parseGeminiStreamChunk(trimmedLine); if (parseResult.isDone) { yield { type: 'done', content: '' }; return; } if (parseResult.content) { yield { type: 'text', content: parseResult.content }; } } } } finally { reader.releaseLock(); } } }