UNPKG

claude-flow-multilang

Version:

Revolutionary multilingual AI orchestration framework with cultural awareness and DDD architecture

654 lines (561 loc) 18.2 kB
/** * Provider Manager - Central orchestration for multi-LLM providers * Handles provider selection, fallback, load balancing, and cost optimization */ import { EventEmitter } from 'events'; import { ILogger } from '../core/logger.js'; import { ConfigManager } from '../config/config-manager.js'; import { ILLMProvider, LLMProvider, LLMProviderConfig, LLMRequest, LLMResponse, LLMStreamEvent, LLMModel, FallbackStrategy, FallbackRule, LoadBalancer, ProviderMetrics, CostOptimizer, CostConstraints, OptimizationResult, RateLimiter, ProviderMonitor, CacheConfig, LLMProviderError, RateLimitError, isRateLimitError, } from './types.js'; // Import providers import { AnthropicProvider } from './anthropic-provider.js'; import { OpenAIProvider } from './openai-provider.js'; import { GoogleProvider } from './google-provider.js'; import { CohereProvider } from './cohere-provider.js'; import { OllamaProvider } from './ollama-provider.js'; export interface ProviderManagerConfig { providers: Record<LLMProvider, LLMProviderConfig>; defaultProvider: LLMProvider; fallbackStrategy?: FallbackStrategy; loadBalancing?: { enabled: boolean; strategy: 'round-robin' | 'least-loaded' | 'latency-based' | 'cost-based'; }; costOptimization?: { enabled: boolean; maxCostPerRequest?: number; preferredProviders?: LLMProvider[]; }; caching?: CacheConfig; monitoring?: { enabled: boolean; metricsInterval: number; }; } export class ProviderManager extends EventEmitter { private providers: Map<LLMProvider, ILLMProvider> = new Map(); private logger: ILogger; private config: ProviderManagerConfig; private requestCount: Map<LLMProvider, number> = new Map(); private lastUsed: Map<LLMProvider, Date> = new Map(); private providerMetrics: Map<LLMProvider, ProviderMetrics[]> = new Map(); private cache: Map<string, { response: LLMResponse; timestamp: Date }> = new Map(); private currentProviderIndex = 0; constructor(logger: ILogger, configManager: ConfigManager, config: ProviderManagerConfig) { super(); this.logger = logger; this.config = config; // Initialize providers this.initializeProviders(); // Start monitoring if enabled if (config.monitoring?.enabled) { this.startMonitoring(); } } /** * Initialize all configured providers */ private async initializeProviders(): Promise<void> { for (const [providerName, providerConfig] of Object.entries(this.config.providers)) { try { const provider = await this.createProvider(providerName as LLMProvider, providerConfig); if (provider) { this.providers.set(providerName as LLMProvider, provider); this.requestCount.set(providerName as LLMProvider, 0); this.logger.info(`Initialized ${providerName} provider`); } } catch (error) { this.logger.error(`Failed to initialize ${providerName} provider`, error); } } if (this.providers.size === 0) { throw new Error('No providers could be initialized'); } } /** * Create a provider instance */ private async createProvider(name: LLMProvider, config: LLMProviderConfig): Promise<ILLMProvider | null> { const providerOptions = { logger: this.logger, config, }; try { let provider: ILLMProvider; switch (name) { case 'anthropic': provider = new AnthropicProvider(providerOptions); break; case 'openai': provider = new OpenAIProvider(providerOptions); break; case 'google': provider = new GoogleProvider(providerOptions); break; case 'cohere': provider = new CohereProvider(providerOptions); break; case 'ollama': provider = new OllamaProvider(providerOptions); break; default: this.logger.warn(`Unknown provider: ${name}`); return null; } await provider.initialize(); // Set up event listeners provider.on('response', (data) => this.handleProviderResponse(name, data)); provider.on('error', (error) => this.handleProviderError(name, error)); provider.on('health_check', (result) => this.handleHealthCheck(name, result)); return provider; } catch (error) { this.logger.error(`Failed to create ${name} provider`, error); return null; } } /** * Complete a request using the appropriate provider */ async complete(request: LLMRequest): Promise<LLMResponse> { // Check cache first if (this.config.caching?.enabled) { const cached = this.checkCache(request); if (cached) { this.logger.debug('Returning cached response'); return cached; } } // Select provider based on strategy const provider = await this.selectProvider(request); try { const response = await provider.complete(request); // Cache successful response if (this.config.caching?.enabled) { this.cacheResponse(request, response); } // Update metrics this.updateProviderMetrics(provider.name, { success: true, latency: response.latency || 0, cost: response.cost?.totalCost || 0, }); return response; } catch (error) { // Handle error and potentially fallback return this.handleRequestError(error, request, provider); } } /** * Stream complete a request */ async *streamComplete(request: LLMRequest): AsyncIterable<LLMStreamEvent> { const provider = await this.selectProvider(request); try { yield* provider.streamComplete(request); // Update metrics this.updateProviderMetrics(provider.name, { success: true, latency: 0, // Will be updated by stream events cost: 0, // Will be updated by stream events }); } catch (error) { // Handle error and potentially fallback const fallbackProvider = await this.getFallbackProvider(error, provider); if (fallbackProvider) { this.logger.info(`Falling back to ${fallbackProvider.name} provider`); yield* fallbackProvider.streamComplete(request); } else { throw error; } } } /** * Select the best provider for a request */ private async selectProvider(request: LLMRequest): Promise<ILLMProvider> { // If specific provider requested if (request.providerOptions?.preferredProvider) { const provider = this.providers.get(request.providerOptions.preferredProvider); if (provider && this.isProviderAvailable(provider)) { return provider; } } // Cost optimization if (this.config.costOptimization?.enabled && request.costConstraints) { const optimized = await this.selectOptimalProvider(request); if (optimized) { return optimized; } } // Load balancing if (this.config.loadBalancing?.enabled) { return this.selectLoadBalancedProvider(); } // Default provider const defaultProvider = this.providers.get(this.config.defaultProvider); if (defaultProvider && this.isProviderAvailable(defaultProvider)) { return defaultProvider; } // First available provider for (const provider of this.providers.values()) { if (this.isProviderAvailable(provider)) { return provider; } } throw new Error('No available providers'); } /** * Select provider based on cost optimization */ private async selectOptimalProvider(request: LLMRequest): Promise<ILLMProvider | null> { let bestProvider: ILLMProvider | null = null; let bestCost = Infinity; for (const provider of this.providers.values()) { if (!this.isProviderAvailable(provider)) continue; try { const estimate = await provider.estimateCost(request); if (estimate.estimatedCost.total < bestCost && (!request.costConstraints?.maxCostPerRequest || estimate.estimatedCost.total <= request.costConstraints.maxCostPerRequest)) { bestCost = estimate.estimatedCost.total; bestProvider = provider; } } catch (error) { this.logger.warn(`Failed to estimate cost for ${provider.name}`, error); } } return bestProvider; } /** * Select provider using load balancing */ private selectLoadBalancedProvider(): ILLMProvider { const availableProviders = Array.from(this.providers.values()).filter(p => this.isProviderAvailable(p) ); if (availableProviders.length === 0) { throw new Error('No available providers'); } switch (this.config.loadBalancing?.strategy) { case 'round-robin': return this.roundRobinSelect(availableProviders); case 'least-loaded': return this.leastLoadedSelect(availableProviders); case 'latency-based': return this.latencyBasedSelect(availableProviders); case 'cost-based': return this.costBasedSelect(availableProviders); default: return availableProviders[0]; } } /** * Round-robin provider selection */ private roundRobinSelect(providers: ILLMProvider[]): ILLMProvider { const provider = providers[this.currentProviderIndex % providers.length]; this.currentProviderIndex++; return provider; } /** * Select least loaded provider */ private leastLoadedSelect(providers: ILLMProvider[]): ILLMProvider { let minLoad = Infinity; let selectedProvider = providers[0]; for (const provider of providers) { const status = provider.getStatus(); if (status.currentLoad < minLoad) { minLoad = status.currentLoad; selectedProvider = provider; } } return selectedProvider; } /** * Select provider with lowest latency */ private latencyBasedSelect(providers: ILLMProvider[]): ILLMProvider { let minLatency = Infinity; let selectedProvider = providers[0]; for (const provider of providers) { const metrics = this.providerMetrics.get(provider.name); if (metrics && metrics.length > 0) { const avgLatency = metrics.reduce((sum, m) => sum + m.latency, 0) / metrics.length; if (avgLatency < minLatency) { minLatency = avgLatency; selectedProvider = provider; } } } return selectedProvider; } /** * Select provider with lowest cost */ private costBasedSelect(providers: ILLMProvider[]): ILLMProvider { let minCost = Infinity; let selectedProvider = providers[0]; for (const provider of providers) { const metrics = this.providerMetrics.get(provider.name); if (metrics && metrics.length > 0) { const avgCost = metrics.reduce((sum, m) => sum + m.cost, 0) / metrics.length; if (avgCost < minCost) { minCost = avgCost; selectedProvider = provider; } } } return selectedProvider; } /** * Check if provider is available */ private isProviderAvailable(provider: ILLMProvider): boolean { const status = provider.getStatus(); return status.available; } /** * Handle request error with fallback */ private async handleRequestError( error: unknown, request: LLMRequest, failedProvider: ILLMProvider ): Promise<LLMResponse> { this.logger.error(`Provider ${failedProvider.name} failed`, error); // Update metrics this.updateProviderMetrics(failedProvider.name, { success: false, latency: 0, cost: 0, }); // Try fallback const fallbackProvider = await this.getFallbackProvider(error, failedProvider); if (fallbackProvider) { this.logger.info(`Falling back to ${fallbackProvider.name} provider`); return fallbackProvider.complete(request); } throw error; } /** * Get fallback provider based on error */ private async getFallbackProvider( error: unknown, failedProvider: ILLMProvider ): Promise<ILLMProvider | null> { if (!this.config.fallbackStrategy?.enabled) { return null; } const errorCondition = this.getErrorCondition(error); const fallbackRule = this.config.fallbackStrategy.rules.find(rule => rule.condition === errorCondition ); if (!fallbackRule) { return null; } // Find first available fallback provider for (const providerName of fallbackRule.fallbackProviders) { const provider = this.providers.get(providerName); if (provider && provider !== failedProvider && this.isProviderAvailable(provider)) { return provider; } } return null; } /** * Determine error condition for fallback */ private getErrorCondition(error: unknown): FallbackRule['condition'] { if (isRateLimitError(error)) { return 'rate_limit'; } if (error instanceof LLMProviderError) { if (error.statusCode === 503) { return 'unavailable'; } if (error.code === 'TIMEOUT') { return 'timeout'; } } return 'error'; } /** * Cache management */ private checkCache(request: LLMRequest): LLMResponse | null { const cacheKey = this.generateCacheKey(request); const cached = this.cache.get(cacheKey); if (cached) { const age = Date.now() - cached.timestamp.getTime(); if (age < (this.config.caching?.ttl || 3600) * 1000) { return cached.response; } // Remove expired entry this.cache.delete(cacheKey); } return null; } private cacheResponse(request: LLMRequest, response: LLMResponse): void { const cacheKey = this.generateCacheKey(request); this.cache.set(cacheKey, { response, timestamp: new Date(), }); // Cleanup old cache entries if (this.cache.size > 1000) { const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); } } private generateCacheKey(request: LLMRequest): string { return JSON.stringify({ model: request.model, messages: request.messages, temperature: request.temperature, maxTokens: request.maxTokens, }); } /** * Update provider metrics */ private updateProviderMetrics( provider: LLMProvider, metrics: { success: boolean; latency: number; cost: number } ): void { const count = this.requestCount.get(provider) || 0; this.requestCount.set(provider, count + 1); this.lastUsed.set(provider, new Date()); const providerMetricsList = this.providerMetrics.get(provider) || []; const errorRate = metrics.success ? 0 : 1; const successRate = metrics.success ? 1 : 0; providerMetricsList.push({ provider, timestamp: new Date(), latency: metrics.latency, errorRate, successRate, load: this.providers.get(provider)?.getStatus().currentLoad || 0, cost: metrics.cost, availability: this.providers.get(provider)?.getStatus().available ? 1 : 0, }); // Keep only recent metrics (last 100) if (providerMetricsList.length > 100) { providerMetricsList.shift(); } this.providerMetrics.set(provider, providerMetricsList); } /** * Event handlers */ private handleProviderResponse(provider: LLMProvider, data: any): void { this.emit('provider_response', { provider, ...data }); } private handleProviderError(provider: LLMProvider, error: any): void { this.emit('provider_error', { provider, error }); } private handleHealthCheck(provider: LLMProvider, result: any): void { this.emit('health_check', { provider, result }); } /** * Start monitoring */ private startMonitoring(): void { setInterval(() => { this.emitMetrics(); }, this.config.monitoring?.metricsInterval || 60000); } /** * Emit aggregated metrics */ private emitMetrics(): void { const metrics = { providers: {} as Record<LLMProvider, any>, totalRequests: 0, totalCost: 0, averageLatency: 0, }; for (const [provider, count] of this.requestCount.entries()) { const providerMetricsList = this.providerMetrics.get(provider) || []; const avgLatency = providerMetricsList.length > 0 ? providerMetricsList.reduce((sum, m) => sum + m.latency, 0) / providerMetricsList.length : 0; const totalCost = providerMetricsList.reduce((sum, m) => sum + m.cost, 0); metrics.providers[provider] = { requests: count, averageLatency: avgLatency, totalCost, lastUsed: this.lastUsed.get(provider), available: this.providers.get(provider)?.getStatus().available, }; metrics.totalRequests += count; metrics.totalCost += totalCost; } if (metrics.totalRequests > 0) { let totalLatency = 0; let latencyCount = 0; for (const providerMetricsList of this.providerMetrics.values()) { for (const metric of providerMetricsList) { totalLatency += metric.latency; latencyCount++; } } metrics.averageLatency = latencyCount > 0 ? totalLatency / latencyCount : 0; } this.emit('metrics', metrics); } /** * Get available providers */ getAvailableProviders(): LLMProvider[] { return Array.from(this.providers.keys()).filter(name => { const provider = this.providers.get(name); return provider && this.isProviderAvailable(provider); }); } /** * Get provider by name */ getProvider(name: LLMProvider): ILLMProvider | undefined { return this.providers.get(name); } /** * Get all providers */ getAllProviders(): Map<LLMProvider, ILLMProvider> { return new Map(this.providers); } /** * Clean up resources */ destroy(): void { for (const provider of this.providers.values()) { provider.destroy(); } this.providers.clear(); this.cache.clear(); this.providerMetrics.clear(); this.removeAllListeners(); } }