import { LLMPort, LLMMessage, LLMResponse, LLMOptions, LLMProvider } from '../../domain/ports/LLMPort'; import { Configuration } from '../../config'; import { GoogleGenerativeAI } from '@google/generative-ai'; // Node.js 18+ has built-in fetch, no polyfill needed export interface LLMProviderInterface { generateResponse(messages: LLMMessage[], options?: LLMOptions): Promise; generateStreamingResponse( messages: LLMMessage[], options?: LLMOptions, onChunk?: (chunk: string) => void ): Promise; getAvailableModels(): Promise; isAvailable(): Promise; getModelInfo(modelName: string): Promise<{ readonly name: string; readonly maxTokens: number; readonly contextWindow: number; readonly capabilities: string[]; } | null>; } export class OpenAIProvider implements LLMProviderInterface { private apiKey: string; private baseUrl: string; constructor(apiKey: string, baseUrl?: string) { this.apiKey = apiKey; this.baseUrl = baseUrl || 'https://api.openai.com/v1'; } async generateResponse(messages: LLMMessage[], options: LLMOptions = {}): Promise { try { const response = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: options.model || 'gpt-4', messages: messages.map(msg => ({ role: msg.role, content: msg.content, })), temperature: options.temperature || 0.7, max_tokens: options.maxTokens || 2000, top_p: options.topP || 1, frequency_penalty: options.frequencyPenalty || 0, presence_penalty: options.presencePenalty || 0, }), }); if (!response.ok) { throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`); } const data = await response.json() as any; const choice = data.choices[0]; return { content: choice.message.content, usage: { promptTokens: data.usage.prompt_tokens, completionTokens: data.usage.completion_tokens, totalTokens: data.usage.total_tokens, }, metadata: { model: data.model, finishReason: choice.finish_reason, }, }; } catch (error) { throw new Error(`OpenAI API request failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async generateStreamingResponse( messages: LLMMessage[], options: LLMOptions = {}, onChunk?: (chunk: string) => void ): Promise { try { const response = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: options.model || 'gpt-4', messages: messages.map(msg => ({ role: msg.role, content: msg.content, })), temperature: options.temperature || 0.7, max_tokens: options.maxTokens || 2000, stream: true, }), }); if (!response.ok) { throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error('No response body reader available'); } let fullContent = ''; const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') continue; try { const parsed = JSON.parse(data); const content = parsed.choices[0]?.delta?.content; if (content) { fullContent += content; onChunk?.(content); } } catch (error) { // Ignore parsing errors for incomplete chunks } } } } return { content: fullContent, metadata: { model: options.model || 'gpt-4', streaming: true, }, }; } catch (error) { throw new Error(`OpenAI streaming request failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getAvailableModels(): Promise { try { const response = await fetch(`${this.baseUrl}/models`, { headers: { 'Authorization': `Bearer ${this.apiKey}`, }, }); if (!response.ok) { throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`); } const data = await response.json() as any; return data.data .filter((model: any) => model.id.startsWith('gpt-')) .map((model: any) => model.id); } catch (error) { throw new Error(`Failed to get available models: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async isAvailable(): Promise { try { await this.getAvailableModels(); return true; } catch (error) { return false; } } async getModelInfo(modelName: string): Promise<{ readonly name: string; readonly maxTokens: number; readonly contextWindow: number; readonly capabilities: string[]; } | null> { const modelInfo: Record = { 'gpt-4': { name: 'gpt-4', maxTokens: 8192, contextWindow: 8192, capabilities: ['chat', 'code', 'analysis'], }, 'gpt-4-turbo': { name: 'gpt-4-turbo', maxTokens: 4096, contextWindow: 128000, capabilities: ['chat', 'code', 'analysis'], }, 'gpt-3.5-turbo': { name: 'gpt-3.5-turbo', maxTokens: 4096, contextWindow: 4096, capabilities: ['chat', 'code'], }, }; return modelInfo[modelName] || null; } } export class OllamaProvider implements LLMProviderInterface { private baseUrl: string; constructor(baseUrl: string = 'http://localhost:11434') { this.baseUrl = baseUrl; } async generateResponse(messages: LLMMessage[], options: LLMOptions = {}): Promise { try { const response = await fetch(`${this.baseUrl}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ model: options.model || 'llama2', messages: messages.map(msg => ({ role: msg.role, content: msg.content, })), options: { temperature: options.temperature || 0.7, top_p: options.topP || 1, frequency_penalty: options.frequencyPenalty || 0, presence_penalty: options.presencePenalty || 0, }, stream: false, }), }); if (!response.ok) { throw new Error(`Ollama API error: ${response.status} ${response.statusText}`); } const data = await response.json() as any; const message = data.message; return { content: message.content, usage: { promptTokens: data.prompt_eval_count || 0, completionTokens: data.eval_count || 0, totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0), }, metadata: { model: data.model, done: data.done, }, }; } catch (error) { throw new Error(`Ollama API request failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async generateStreamingResponse( messages: LLMMessage[], options: LLMOptions = {}, onChunk?: (chunk: string) => void ): Promise { try { const response = await fetch(`${this.baseUrl}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ model: options.model || 'llama2', messages: messages.map(msg => ({ role: msg.role, content: msg.content, })), options: { temperature: options.temperature || 0.7, top_p: options.topP || 1, frequency_penalty: options.frequencyPenalty || 0, presence_penalty: options.presencePenalty || 0, }, stream: true, }), }); if (!response.ok) { throw new Error(`Ollama API error: ${response.status} ${response.statusText}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error('No response body reader available'); } let fullContent = ''; const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.trim()) { try { const parsed = JSON.parse(line); const content = parsed.message?.content; if (content) { fullContent += content; onChunk?.(content); } } catch (error) { // Ignore parsing errors for incomplete chunks } } } } return { content: fullContent, metadata: { model: options.model || 'llama2', streaming: true, }, }; } catch (error) { throw new Error(`Ollama streaming request failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getAvailableModels(): Promise { try { const response = await fetch(`${this.baseUrl}/api/tags`); if (!response.ok) { throw new Error(`Ollama API error: ${response.status} ${response.statusText}`); } const data = await response.json() as any; return data.models.map((model: any) => model.name); } catch (error) { throw new Error(`Failed to get available models: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async isAvailable(): Promise { try { await this.getAvailableModels(); return true; } catch (error) { return false; } } async getModelInfo(modelName: string): Promise<{ readonly name: string; readonly maxTokens: number; readonly contextWindow: number; readonly capabilities: string[]; } | null> { try { const response = await fetch(`${this.baseUrl}/api/show`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: modelName }), }); if (!response.ok) { return null; } const data = await response.json() as any; return { name: modelName, maxTokens: data.parameter_size || 4096, contextWindow: data.context_length || 4096, capabilities: ['chat', 'code'], }; } catch (error) { return null; } } } export class GeminiProvider implements LLMProviderInterface { private genAI: GoogleGenerativeAI; constructor(apiKey: string) { this.genAI = new GoogleGenerativeAI(apiKey); } async generateResponse(messages: LLMMessage[], options: LLMOptions = {}): Promise { try { const model = this.genAI.getGenerativeModel({ model: options.model || 'gemini-2.0-flash', generationConfig: { temperature: options.temperature || 0.7, maxOutputTokens: options.maxTokens || 2000, topP: options.topP || 1, } }); // Convert messages to Gemini format const prompt = this.convertMessagesToPrompt(messages); const result = await model.generateContent(prompt); const response = await result.response; const content = response.text(); return { content, usage: { promptTokens: 0, // Gemini doesn't provide detailed token usage in free tier completionTokens: 0, totalTokens: 0, }, metadata: { model: options.model || 'gemini-2.0-flash', provider: 'gemini', }, }; } catch (error) { throw new Error(`Gemini API request failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async generateStreamingResponse( messages: LLMMessage[], options: LLMOptions = {}, onChunk?: (chunk: string) => void ): Promise { try { const model = this.genAI.getGenerativeModel({ model: options.model || 'gemini-2.0-flash', generationConfig: { temperature: options.temperature || 0.7, maxOutputTokens: options.maxTokens || 2000, topP: options.topP || 1, } }); const prompt = this.convertMessagesToPrompt(messages); const result = await model.generateContentStream(prompt); let fullContent = ''; for await (const chunk of result.stream) { const chunkText = chunk.text(); fullContent += chunkText; onChunk?.(chunkText); } return { content: fullContent, metadata: { model: options.model || 'gemini-2.0-flash', provider: 'gemini', streaming: true, }, }; } catch (error) { throw new Error(`Gemini streaming request failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getAvailableModels(): Promise { // Gemini models available through Google AI Studio return [ 'gemini-2.0-flash', 'gemini-2.0-flash-exp', 'gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite', 'gemini-pro-latest', 'gemini-flash-latest' ]; } async isAvailable(): Promise { try { await this.getAvailableModels(); return true; } catch (error) { return false; } } async getModelInfo(modelName: string): Promise<{ readonly name: string; readonly maxTokens: number; readonly contextWindow: number; readonly capabilities: string[]; } | null> { const modelInfo: Record = { 'gemini-2.0-flash': { name: 'gemini-2.0-flash', maxTokens: 8192, contextWindow: 1048576, // 1M tokens capabilities: ['text', 'code', 'analysis', 'reasoning'], }, 'gemini-2.0-flash-exp': { name: 'gemini-2.0-flash-exp', maxTokens: 8192, contextWindow: 1048576, capabilities: ['text', 'code', 'analysis', 'reasoning', 'experimental'], }, 'gemini-2.5-flash': { name: 'gemini-2.5-flash', maxTokens: 8192, contextWindow: 1048576, capabilities: ['text', 'code', 'analysis', 'reasoning'], }, 'gemini-2.5-pro': { name: 'gemini-2.5-pro', maxTokens: 8192, contextWindow: 2097152, // 2M tokens capabilities: ['text', 'code', 'analysis', 'reasoning', 'advanced'], }, 'gemini-2.5-flash-lite': { name: 'gemini-2.5-flash-lite', maxTokens: 4096, contextWindow: 524288, capabilities: ['text', 'code', 'analysis'], }, }; return modelInfo[modelName] || null; } private convertMessagesToPrompt(messages: LLMMessage[]): string { return messages .map(msg => { switch (msg.role) { case 'system': return `System: ${msg.content}`; case 'user': return `User: ${msg.content}`; case 'assistant': return `Assistant: ${msg.content}`; default: return msg.content; } }) .join('\n\n'); } } export class LLMAdapter implements LLMPort { private providers: LLMProviderInterface[]; private currentProvider: LLMProviderInterface | null = null; constructor(config: Configuration) { this.providers = []; // Initialize providers based on configuration if (config.openai?.apiKey) { this.providers.push(new OpenAIProvider(config.openai.apiKey, config.openai.baseUrl)); } if (config.ollama?.enabled) { this.providers.push(new OllamaProvider(config.ollama.baseUrl)); } if (config.gemini?.apiKey) { this.providers.push(new GeminiProvider(config.gemini.apiKey)); } // Set current provider if (config.llm?.provider === 'openai' && config.openai?.apiKey) { this.currentProvider = this.providers.find(p => p instanceof OpenAIProvider) || null; } else if (config.llm?.provider === 'ollama' && config.ollama?.enabled) { this.currentProvider = this.providers.find(p => p instanceof OllamaProvider) || null; } else if (config.llm?.provider === 'gemini' && config.gemini?.apiKey) { this.currentProvider = this.providers.find(p => p instanceof GeminiProvider) || null; } else { // Auto-select first available provider this.currentProvider = this.providers[0] || null; } } async generateResponse(messages: LLMMessage[], options: LLMOptions = {}): Promise { if (!this.currentProvider) { throw new Error('No LLM provider available'); } return this.currentProvider.generateResponse(messages, options); } async generateStreamingResponse( messages: LLMMessage[], options: LLMOptions = {}, onChunk?: (chunk: string) => void ): Promise { if (!this.currentProvider) { throw new Error('No LLM provider available'); } return this.currentProvider.generateStreamingResponse(messages, options, onChunk); } async getAvailableModels(): Promise { if (!this.currentProvider) { return []; } return this.currentProvider.getAvailableModels(); } async isAvailable(): Promise { if (!this.currentProvider) { return false; } return this.currentProvider.isAvailable(); } async getModelInfo(modelName: string): Promise<{ readonly name: string; readonly maxTokens: number; readonly contextWindow: number; readonly capabilities: string[]; } | null> { if (!this.currentProvider) { return null; } return this.currentProvider.getModelInfo(modelName); } setProvider(provider: 'openai' | 'ollama'): void { this.currentProvider = this.providers.find(p => { if (provider === 'openai') return p instanceof OpenAIProvider; if (provider === 'ollama') return p instanceof OllamaProvider; return false; }) || null; } getCurrentProvider(): string { if (this.currentProvider instanceof OpenAIProvider) return 'openai'; if (this.currentProvider instanceof OllamaProvider) return 'ollama'; return 'none'; } }