@sparkleideas/providers 3.5.2-patch.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,538 @@
1
+ /**
2
+ * V3 Provider Manager
3
+ *
4
+ * Orchestrates multiple LLM providers with:
5
+ * - Load balancing (round-robin, latency-based, cost-based)
6
+ * - Automatic failover
7
+ * - Request caching
8
+ * - Cost optimization
9
+ *
10
+ * @module @sparkleideas/providers/provider-manager
11
+ */
12
+
13
+ import { EventEmitter } from 'events';
14
+ import {
15
+ ILLMProvider,
16
+ LLMProvider,
17
+ LLMProviderConfig,
18
+ LLMRequest,
19
+ LLMResponse,
20
+ LLMStreamEvent,
21
+ LLMModel,
22
+ ProviderManagerConfig,
23
+ LoadBalancingStrategy,
24
+ HealthCheckResult,
25
+ CostEstimate,
26
+ UsageStats,
27
+ UsagePeriod,
28
+ LLMProviderError,
29
+ isLLMProviderError,
30
+ } from './types.js';
31
+ import { BaseProviderOptions, ILogger, consoleLogger } from './base-provider.js';
32
+ import { AnthropicProvider } from './anthropic-provider.js';
33
+ import { OpenAIProvider } from './openai-provider.js';
34
+ import { GoogleProvider } from './google-provider.js';
35
+ import { CohereProvider } from './cohere-provider.js';
36
+ import { OllamaProvider } from './ollama-provider.js';
37
+ import { RuVectorProvider } from './ruvector-provider.js';
38
+
39
+ /**
40
+ * Cache entry for request caching
41
+ */
42
+ interface CacheEntry {
43
+ response: LLMResponse;
44
+ timestamp: number;
45
+ hits: number;
46
+ }
47
+
48
+ /**
49
+ * Provider metrics for load balancing
50
+ */
51
+ interface ProviderMetrics {
52
+ latency: number;
53
+ errorRate: number;
54
+ cost: number;
55
+ lastUsed: number;
56
+ }
57
+
58
+ /**
59
+ * Provider Manager - Orchestrates multiple LLM providers
60
+ */
61
+ export class ProviderManager extends EventEmitter {
62
+ private providers: Map<LLMProvider, ILLMProvider> = new Map();
63
+ private cache: Map<string, CacheEntry> = new Map();
64
+ private metrics: Map<LLMProvider, ProviderMetrics> = new Map();
65
+ private roundRobinIndex = 0;
66
+ private logger: ILogger;
67
+
68
+ constructor(
69
+ private config: ProviderManagerConfig,
70
+ logger?: ILogger
71
+ ) {
72
+ super();
73
+ this.logger = logger || consoleLogger;
74
+ }
75
+
76
+ /**
77
+ * Initialize all configured providers
78
+ */
79
+ async initialize(): Promise<void> {
80
+ this.logger.info('Initializing provider manager', {
81
+ providerCount: this.config.providers.length,
82
+ });
83
+
84
+ const initPromises = this.config.providers.map(async (providerConfig) => {
85
+ try {
86
+ const provider = this.createProvider(providerConfig);
87
+ await provider.initialize();
88
+ this.providers.set(providerConfig.provider, provider);
89
+ this.metrics.set(providerConfig.provider, {
90
+ latency: 0,
91
+ errorRate: 0,
92
+ cost: 0,
93
+ lastUsed: 0,
94
+ });
95
+ this.logger.info(`Provider ${providerConfig.provider} initialized`);
96
+ } catch (error) {
97
+ this.logger.error(`Failed to initialize ${providerConfig.provider}`, error);
98
+ }
99
+ });
100
+
101
+ await Promise.all(initPromises);
102
+
103
+ this.logger.info('Provider manager initialized', {
104
+ activeProviders: Array.from(this.providers.keys()),
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Create a provider instance
110
+ */
111
+ private createProvider(config: LLMProviderConfig): ILLMProvider {
112
+ const options: BaseProviderOptions = {
113
+ config,
114
+ logger: this.logger,
115
+ };
116
+
117
+ switch (config.provider) {
118
+ case 'anthropic':
119
+ return new AnthropicProvider(options);
120
+ case 'openai':
121
+ return new OpenAIProvider(options);
122
+ case 'google':
123
+ return new GoogleProvider(options);
124
+ case 'cohere':
125
+ return new CohereProvider(options);
126
+ case 'ollama':
127
+ return new OllamaProvider(options);
128
+ case 'ruvector':
129
+ return new RuVectorProvider(options);
130
+ default:
131
+ throw new Error(`Unknown provider: ${config.provider}`);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Complete a request with automatic provider selection
137
+ */
138
+ async complete(request: LLMRequest, preferredProvider?: LLMProvider): Promise<LLMResponse> {
139
+ // Check cache first
140
+ if (this.config.cache?.enabled) {
141
+ const cached = this.getCached(request);
142
+ if (cached) {
143
+ this.logger.debug('Cache hit', { requestId: request.requestId });
144
+ return cached;
145
+ }
146
+ }
147
+
148
+ // Select provider
149
+ const provider = preferredProvider
150
+ ? this.providers.get(preferredProvider)
151
+ : await this.selectProvider(request);
152
+
153
+ if (!provider) {
154
+ throw new Error('No available providers');
155
+ }
156
+
157
+ const startTime = Date.now();
158
+
159
+ try {
160
+ const response = await provider.complete(request);
161
+ this.updateMetrics(provider.name, Date.now() - startTime, false, response.cost?.totalCost || 0);
162
+
163
+ // Cache response
164
+ if (this.config.cache?.enabled) {
165
+ this.setCached(request, response);
166
+ }
167
+
168
+ this.emit('complete', { provider: provider.name, response });
169
+ return response;
170
+ } catch (error) {
171
+ this.updateMetrics(provider.name, Date.now() - startTime, true, 0);
172
+
173
+ // Try fallback
174
+ if (this.config.fallback?.enabled && isLLMProviderError(error)) {
175
+ return this.completWithFallback(request, provider.name, error);
176
+ }
177
+
178
+ throw error;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Stream complete with automatic provider selection
184
+ */
185
+ async *streamComplete(
186
+ request: LLMRequest,
187
+ preferredProvider?: LLMProvider
188
+ ): AsyncIterable<LLMStreamEvent> {
189
+ const provider = preferredProvider
190
+ ? this.providers.get(preferredProvider)
191
+ : await this.selectProvider(request);
192
+
193
+ if (!provider) {
194
+ throw new Error('No available providers');
195
+ }
196
+
197
+ const startTime = Date.now();
198
+
199
+ try {
200
+ for await (const event of provider.streamComplete(request)) {
201
+ yield event;
202
+ }
203
+
204
+ this.updateMetrics(provider.name, Date.now() - startTime, false, 0);
205
+ } catch (error) {
206
+ this.updateMetrics(provider.name, Date.now() - startTime, true, 0);
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Select provider based on load balancing strategy
213
+ */
214
+ private async selectProvider(request: LLMRequest): Promise<ILLMProvider | undefined> {
215
+ const availableProviders = Array.from(this.providers.values()).filter(
216
+ (p) => p.getStatus().available
217
+ );
218
+
219
+ if (availableProviders.length === 0) {
220
+ // Try to use any provider
221
+ return this.providers.values().next().value;
222
+ }
223
+
224
+ const strategy = this.config.loadBalancing?.strategy || 'round-robin';
225
+
226
+ switch (strategy) {
227
+ case 'round-robin':
228
+ return this.selectRoundRobin(availableProviders);
229
+ case 'least-loaded':
230
+ return this.selectLeastLoaded(availableProviders);
231
+ case 'latency-based':
232
+ return this.selectByLatency(availableProviders);
233
+ case 'cost-based':
234
+ return this.selectByCost(availableProviders, request);
235
+ default:
236
+ return availableProviders[0];
237
+ }
238
+ }
239
+
240
+ private selectRoundRobin(providers: ILLMProvider[]): ILLMProvider {
241
+ const provider = providers[this.roundRobinIndex % providers.length];
242
+ this.roundRobinIndex++;
243
+ return provider;
244
+ }
245
+
246
+ private selectLeastLoaded(providers: ILLMProvider[]): ILLMProvider {
247
+ return providers.reduce((best, current) =>
248
+ current.getStatus().currentLoad < best.getStatus().currentLoad ? current : best
249
+ );
250
+ }
251
+
252
+ private selectByLatency(providers: ILLMProvider[]): ILLMProvider {
253
+ return providers.reduce((best, current) => {
254
+ const bestMetrics = this.metrics.get(best.name);
255
+ const currentMetrics = this.metrics.get(current.name);
256
+ return (currentMetrics?.latency || Infinity) < (bestMetrics?.latency || Infinity)
257
+ ? current
258
+ : best;
259
+ });
260
+ }
261
+
262
+ private async selectByCost(
263
+ providers: ILLMProvider[],
264
+ request: LLMRequest
265
+ ): Promise<ILLMProvider> {
266
+ const estimates = await Promise.all(
267
+ providers.map(async (p) => ({
268
+ provider: p,
269
+ cost: (await p.estimateCost(request)).estimatedCost.total,
270
+ }))
271
+ );
272
+
273
+ return estimates.reduce((best, current) =>
274
+ current.cost < best.cost ? current : best
275
+ ).provider;
276
+ }
277
+
278
+ /**
279
+ * Complete with fallback on failure
280
+ */
281
+ private async completWithFallback(
282
+ request: LLMRequest,
283
+ failedProvider: LLMProvider,
284
+ originalError: LLMProviderError
285
+ ): Promise<LLMResponse> {
286
+ const maxAttempts = this.config.fallback?.maxAttempts || 2;
287
+ let attempts = 0;
288
+ let lastError = originalError;
289
+
290
+ const remainingProviders = Array.from(this.providers.values()).filter(
291
+ (p) => p.name !== failedProvider
292
+ );
293
+
294
+ for (const provider of remainingProviders) {
295
+ if (attempts >= maxAttempts) break;
296
+ attempts++;
297
+
298
+ this.logger.info(`Attempting fallback to ${provider.name}`, {
299
+ attempt: attempts,
300
+ originalProvider: failedProvider,
301
+ });
302
+
303
+ try {
304
+ const response = await provider.complete(request);
305
+ this.emit('fallback_success', {
306
+ originalProvider: failedProvider,
307
+ fallbackProvider: provider.name,
308
+ attempts,
309
+ });
310
+ return response;
311
+ } catch (error) {
312
+ if (isLLMProviderError(error)) {
313
+ lastError = error;
314
+ }
315
+ }
316
+ }
317
+
318
+ this.emit('fallback_exhausted', {
319
+ originalProvider: failedProvider,
320
+ attempts,
321
+ });
322
+
323
+ throw lastError;
324
+ }
325
+
326
+ /**
327
+ * Update provider metrics
328
+ */
329
+ private updateMetrics(
330
+ provider: LLMProvider,
331
+ latency: number,
332
+ error: boolean,
333
+ cost: number
334
+ ): void {
335
+ const current = this.metrics.get(provider) || {
336
+ latency: 0,
337
+ errorRate: 0,
338
+ cost: 0,
339
+ lastUsed: 0,
340
+ };
341
+
342
+ // Exponential moving average for latency
343
+ const alpha = 0.3;
344
+ const newLatency = current.latency === 0 ? latency : alpha * latency + (1 - alpha) * current.latency;
345
+
346
+ // Update error rate
347
+ const errorWeight = error ? 1 : 0;
348
+ const newErrorRate = alpha * errorWeight + (1 - alpha) * current.errorRate;
349
+
350
+ this.metrics.set(provider, {
351
+ latency: newLatency,
352
+ errorRate: newErrorRate,
353
+ cost: current.cost + cost,
354
+ lastUsed: Date.now(),
355
+ });
356
+ }
357
+
358
+ /**
359
+ * Get cached response
360
+ */
361
+ private getCached(request: LLMRequest): LLMResponse | undefined {
362
+ const key = this.getCacheKey(request);
363
+ const entry = this.cache.get(key);
364
+
365
+ if (!entry) return undefined;
366
+
367
+ const ttl = this.config.cache?.ttl || 300000;
368
+ if (Date.now() - entry.timestamp > ttl) {
369
+ this.cache.delete(key);
370
+ return undefined;
371
+ }
372
+
373
+ entry.hits++;
374
+ return entry.response;
375
+ }
376
+
377
+ /**
378
+ * Set cached response
379
+ */
380
+ private setCached(request: LLMRequest, response: LLMResponse): void {
381
+ const key = this.getCacheKey(request);
382
+
383
+ // Enforce max size
384
+ const maxSize = this.config.cache?.maxSize || 1000;
385
+ if (this.cache.size >= maxSize) {
386
+ // Remove oldest entry
387
+ const oldest = Array.from(this.cache.entries()).sort(
388
+ (a, b) => a[1].timestamp - b[1].timestamp
389
+ )[0];
390
+ if (oldest) this.cache.delete(oldest[0]);
391
+ }
392
+
393
+ this.cache.set(key, {
394
+ response,
395
+ timestamp: Date.now(),
396
+ hits: 0,
397
+ });
398
+ }
399
+
400
+ /**
401
+ * Generate cache key
402
+ */
403
+ private getCacheKey(request: LLMRequest): string {
404
+ return JSON.stringify({
405
+ messages: request.messages,
406
+ model: request.model,
407
+ temperature: request.temperature,
408
+ maxTokens: request.maxTokens,
409
+ });
410
+ }
411
+
412
+ /**
413
+ * Get a specific provider
414
+ */
415
+ getProvider(name: LLMProvider): ILLMProvider | undefined {
416
+ return this.providers.get(name);
417
+ }
418
+
419
+ /**
420
+ * List all available providers
421
+ */
422
+ listProviders(): LLMProvider[] {
423
+ return Array.from(this.providers.keys());
424
+ }
425
+
426
+ /**
427
+ * Health check all providers
428
+ */
429
+ async healthCheck(): Promise<Map<LLMProvider, HealthCheckResult>> {
430
+ const results = new Map<LLMProvider, HealthCheckResult>();
431
+
432
+ await Promise.all(
433
+ Array.from(this.providers.entries()).map(async ([name, provider]) => {
434
+ const result = await provider.healthCheck();
435
+ results.set(name, result);
436
+ })
437
+ );
438
+
439
+ return results;
440
+ }
441
+
442
+ /**
443
+ * Estimate cost across providers
444
+ */
445
+ async estimateCost(request: LLMRequest): Promise<Map<LLMProvider, CostEstimate>> {
446
+ const estimates = new Map<LLMProvider, CostEstimate>();
447
+
448
+ await Promise.all(
449
+ Array.from(this.providers.entries()).map(async ([name, provider]) => {
450
+ const estimate = await provider.estimateCost(request);
451
+ estimates.set(name, estimate);
452
+ })
453
+ );
454
+
455
+ return estimates;
456
+ }
457
+
458
+ /**
459
+ * Get aggregated usage statistics
460
+ */
461
+ async getUsage(period: UsagePeriod = 'day'): Promise<UsageStats> {
462
+ let totalRequests = 0;
463
+ let totalTokens = { prompt: 0, completion: 0, total: 0 };
464
+ let totalCost = { prompt: 0, completion: 0, total: 0 };
465
+ let totalErrors = 0;
466
+ let totalLatency = 0;
467
+ let count = 0;
468
+
469
+ for (const provider of this.providers.values()) {
470
+ const usage = await provider.getUsage(period);
471
+ totalRequests += usage.requests;
472
+ totalTokens.prompt += usage.tokens.prompt;
473
+ totalTokens.completion += usage.tokens.completion;
474
+ totalTokens.total += usage.tokens.total;
475
+ totalCost.prompt += usage.cost.prompt;
476
+ totalCost.completion += usage.cost.completion;
477
+ totalCost.total += usage.cost.total;
478
+ totalErrors += usage.errors;
479
+ totalLatency += usage.averageLatency;
480
+ count++;
481
+ }
482
+
483
+ const now = new Date();
484
+ const start = new Date();
485
+ start.setDate(start.getDate() - 1);
486
+
487
+ return {
488
+ period: { start, end: now },
489
+ requests: totalRequests,
490
+ tokens: totalTokens,
491
+ cost: { ...totalCost, currency: 'USD' },
492
+ errors: totalErrors,
493
+ averageLatency: count > 0 ? totalLatency / count : 0,
494
+ modelBreakdown: {},
495
+ };
496
+ }
497
+
498
+ /**
499
+ * Get provider metrics
500
+ */
501
+ getMetrics(): Map<LLMProvider, ProviderMetrics> {
502
+ return new Map(this.metrics);
503
+ }
504
+
505
+ /**
506
+ * Clear cache
507
+ */
508
+ clearCache(): void {
509
+ this.cache.clear();
510
+ this.logger.info('Cache cleared');
511
+ }
512
+
513
+ /**
514
+ * Destroy all providers
515
+ */
516
+ destroy(): void {
517
+ for (const provider of this.providers.values()) {
518
+ provider.destroy();
519
+ }
520
+ this.providers.clear();
521
+ this.cache.clear();
522
+ this.metrics.clear();
523
+ this.removeAllListeners();
524
+ this.logger.info('Provider manager destroyed');
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Create and initialize a provider manager
530
+ */
531
+ export async function createProviderManager(
532
+ config: ProviderManagerConfig,
533
+ logger?: ILogger
534
+ ): Promise<ProviderManager> {
535
+ const manager = new ProviderManager(config, logger);
536
+ await manager.initialize();
537
+ return manager;
538
+ }