@sparkleideas/integration 3.0.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,1168 @@
1
+ /**
2
+ * ProviderAdapter - Multi-Provider Support for AI Models
3
+ *
4
+ * Provides a unified interface for working with multiple AI providers
5
+ * (Anthropic, OpenAI, local models, etc.) with automatic selection,
6
+ * failover, and load balancing.
7
+ *
8
+ * Features:
9
+ * - Provider registration and management
10
+ * - Requirement-based provider selection
11
+ * - Automatic failover on provider errors
12
+ * - Rate limiting and quota management
13
+ * - Cost tracking and optimization
14
+ * - Provider health monitoring
15
+ *
16
+ * Compatible with @sparkleideas/agentic-flow's provider manager patterns.
17
+ *
18
+ * @module v3/integration/provider-adapter
19
+ * @version 3.0.0-alpha.1
20
+ */
21
+
22
+ import { EventEmitter } from 'events';
23
+ import type { Task } from './agentic-flow-agent.js';
24
+
25
+ /**
26
+ * Provider interface for AI model providers
27
+ */
28
+ export interface Provider {
29
+ /** Unique provider identifier */
30
+ id: string;
31
+ /** Provider name */
32
+ name: string;
33
+ /** Provider type */
34
+ type: ProviderType;
35
+ /** Available models */
36
+ models: ModelInfo[];
37
+ /** Provider capabilities */
38
+ capabilities: ProviderCapability[];
39
+ /** Provider status */
40
+ status: ProviderStatus;
41
+ /** Rate limits */
42
+ rateLimits: RateLimits;
43
+ /** Cost per token (input/output) */
44
+ costPerToken: CostInfo;
45
+ /** Provider-specific configuration */
46
+ config?: Record<string, unknown>;
47
+ }
48
+
49
+ /**
50
+ * Provider types
51
+ */
52
+ export type ProviderType =
53
+ | 'anthropic'
54
+ | 'openai'
55
+ | 'google'
56
+ | 'azure'
57
+ | 'aws'
58
+ | 'ollama'
59
+ | 'huggingface'
60
+ | 'custom';
61
+
62
+ /**
63
+ * Provider capabilities
64
+ */
65
+ export type ProviderCapability =
66
+ | 'text-completion'
67
+ | 'chat'
68
+ | 'embeddings'
69
+ | 'vision'
70
+ | 'code-generation'
71
+ | 'function-calling'
72
+ | 'streaming'
73
+ | 'fine-tuning'
74
+ | 'batch-processing'
75
+ | 'long-context';
76
+
77
+ /**
78
+ * Provider status
79
+ */
80
+ export type ProviderStatus =
81
+ | 'available'
82
+ | 'degraded'
83
+ | 'unavailable'
84
+ | 'rate-limited'
85
+ | 'maintenance';
86
+
87
+ /**
88
+ * Model information
89
+ */
90
+ export interface ModelInfo {
91
+ /** Model identifier */
92
+ id: string;
93
+ /** Model display name */
94
+ name: string;
95
+ /** Maximum context length */
96
+ maxContextLength: number;
97
+ /** Maximum output tokens */
98
+ maxOutputTokens: number;
99
+ /** Supported capabilities */
100
+ capabilities: ProviderCapability[];
101
+ /** Model-specific configuration */
102
+ config?: Record<string, unknown>;
103
+ }
104
+
105
+ /**
106
+ * Rate limit configuration
107
+ */
108
+ export interface RateLimits {
109
+ /** Requests per minute */
110
+ requestsPerMinute: number;
111
+ /** Tokens per minute */
112
+ tokensPerMinute: number;
113
+ /** Current request count */
114
+ currentRequests: number;
115
+ /** Current token count */
116
+ currentTokens: number;
117
+ /** Reset timestamp */
118
+ resetAt: number;
119
+ }
120
+
121
+ /**
122
+ * Cost information
123
+ */
124
+ export interface CostInfo {
125
+ /** Cost per 1K input tokens in USD */
126
+ inputPer1K: number;
127
+ /** Cost per 1K output tokens in USD */
128
+ outputPer1K: number;
129
+ /** Currency */
130
+ currency: string;
131
+ }
132
+
133
+ /**
134
+ * Provider requirements for selection
135
+ */
136
+ export interface ProviderRequirements {
137
+ /** Required capabilities */
138
+ capabilities?: ProviderCapability[];
139
+ /** Minimum context length */
140
+ minContextLength?: number;
141
+ /** Maximum cost per 1K tokens */
142
+ maxCostPer1K?: number;
143
+ /** Preferred provider types */
144
+ preferredTypes?: ProviderType[];
145
+ /** Excluded provider IDs */
146
+ excludeProviders?: string[];
147
+ /** Required model ID */
148
+ modelId?: string;
149
+ /** Require streaming support */
150
+ streaming?: boolean;
151
+ /** Require vision support */
152
+ vision?: boolean;
153
+ /** Custom filters */
154
+ customFilters?: ((provider: Provider) => boolean)[];
155
+ }
156
+
157
+ /**
158
+ * Provider selection result
159
+ */
160
+ export interface ProviderSelectionResult {
161
+ /** Selected provider */
162
+ provider: Provider;
163
+ /** Selected model */
164
+ model: ModelInfo;
165
+ /** Selection score */
166
+ score: number;
167
+ /** Selection reasoning */
168
+ reasons: string[];
169
+ /** Alternative providers */
170
+ alternatives: Array<{
171
+ provider: Provider;
172
+ model: ModelInfo;
173
+ score: number;
174
+ }>;
175
+ }
176
+
177
+ /**
178
+ * Execution options
179
+ */
180
+ export interface ExecutionOptions {
181
+ /** Model to use (overrides automatic selection) */
182
+ modelId?: string;
183
+ /** Temperature for generation */
184
+ temperature?: number;
185
+ /** Maximum tokens to generate */
186
+ maxTokens?: number;
187
+ /** Enable streaming */
188
+ stream?: boolean;
189
+ /** Stop sequences */
190
+ stopSequences?: string[];
191
+ /** Timeout in milliseconds */
192
+ timeout?: number;
193
+ /** Retry configuration */
194
+ retry?: {
195
+ maxAttempts: number;
196
+ backoffMs: number;
197
+ backoffMultiplier: number;
198
+ };
199
+ /** Metadata */
200
+ metadata?: Record<string, unknown>;
201
+ }
202
+
203
+ /**
204
+ * Execution result
205
+ */
206
+ export interface ExecutionResult {
207
+ /** Success indicator */
208
+ success: boolean;
209
+ /** Output content */
210
+ content: string;
211
+ /** Provider used */
212
+ providerId: string;
213
+ /** Model used */
214
+ modelId: string;
215
+ /** Token usage */
216
+ usage: {
217
+ inputTokens: number;
218
+ outputTokens: number;
219
+ totalTokens: number;
220
+ };
221
+ /** Cost in USD */
222
+ cost: number;
223
+ /** Execution latency in milliseconds */
224
+ latencyMs: number;
225
+ /** Error if failed */
226
+ error?: Error;
227
+ /** Metadata */
228
+ metadata?: Record<string, unknown>;
229
+ }
230
+
231
+ /**
232
+ * Provider metrics
233
+ */
234
+ export interface ProviderMetrics {
235
+ /** Total requests */
236
+ totalRequests: number;
237
+ /** Successful requests */
238
+ successfulRequests: number;
239
+ /** Failed requests */
240
+ failedRequests: number;
241
+ /** Average latency in milliseconds */
242
+ avgLatencyMs: number;
243
+ /** Total tokens used */
244
+ totalTokens: number;
245
+ /** Total cost in USD */
246
+ totalCost: number;
247
+ /** Last request timestamp */
248
+ lastRequest: number;
249
+ /** Uptime percentage */
250
+ uptimePercent: number;
251
+ }
252
+
253
+ /**
254
+ * Provider adapter configuration
255
+ */
256
+ export interface ProviderAdapterConfig {
257
+ /** Default provider ID */
258
+ defaultProviderId?: string;
259
+ /** Default model ID */
260
+ defaultModelId?: string;
261
+ /** Enable automatic failover */
262
+ enableFailover?: boolean;
263
+ /** Maximum failover attempts */
264
+ maxFailoverAttempts?: number;
265
+ /** Enable cost tracking */
266
+ enableCostTracking?: boolean;
267
+ /** Cost limit per hour in USD */
268
+ costLimitPerHour?: number;
269
+ /** Enable provider health checks */
270
+ enableHealthChecks?: boolean;
271
+ /** Health check interval in milliseconds */
272
+ healthCheckInterval?: number;
273
+ /** Enable request caching */
274
+ enableCaching?: boolean;
275
+ /** Cache TTL in milliseconds */
276
+ cacheTTL?: number;
277
+ }
278
+
279
+ /**
280
+ * ProviderAdapter - Multi-provider AI model management
281
+ *
282
+ * Usage:
283
+ * ```typescript
284
+ * const adapter = new ProviderAdapter({
285
+ * enableFailover: true,
286
+ * enableCostTracking: true,
287
+ * });
288
+ *
289
+ * // Register providers
290
+ * adapter.registerProvider({
291
+ * id: 'anthropic',
292
+ * name: 'Anthropic',
293
+ * type: 'anthropic',
294
+ * models: [...],
295
+ * capabilities: ['chat', 'code-generation'],
296
+ * status: 'available',
297
+ * rateLimits: { ... },
298
+ * costPerToken: { ... },
299
+ * });
300
+ *
301
+ * // Select provider based on requirements
302
+ * const result = adapter.selectProvider({
303
+ * capabilities: ['code-generation'],
304
+ * maxCostPer1K: 0.01,
305
+ * });
306
+ *
307
+ * // Execute task
308
+ * const output = await adapter.executeWithProvider(task, result.provider);
309
+ * ```
310
+ */
311
+ export class ProviderAdapter extends EventEmitter {
312
+ /** Registered providers */
313
+ providers: Map<string, Provider>;
314
+
315
+ /** Provider metrics */
316
+ private metrics: Map<string, ProviderMetrics>;
317
+
318
+ /** Adapter configuration */
319
+ private config: ProviderAdapterConfig;
320
+
321
+ /** Health check timer */
322
+ private healthCheckTimer: NodeJS.Timeout | null = null;
323
+
324
+ /** Request cache */
325
+ private cache: Map<string, { result: ExecutionResult; timestamp: number }>;
326
+
327
+ /** Hourly cost tracking */
328
+ private hourlyCost: { amount: number; resetAt: number };
329
+
330
+ /**
331
+ * Create a new ProviderAdapter instance
332
+ *
333
+ * @param config - Adapter configuration
334
+ */
335
+ constructor(config: ProviderAdapterConfig = {}) {
336
+ super();
337
+
338
+ this.providers = new Map();
339
+ this.metrics = new Map();
340
+ this.cache = new Map();
341
+
342
+ this.config = {
343
+ enableFailover: config.enableFailover ?? true,
344
+ maxFailoverAttempts: config.maxFailoverAttempts ?? 3,
345
+ enableCostTracking: config.enableCostTracking ?? true,
346
+ costLimitPerHour: config.costLimitPerHour ?? 10,
347
+ enableHealthChecks: config.enableHealthChecks ?? true,
348
+ healthCheckInterval: config.healthCheckInterval ?? 60000,
349
+ enableCaching: config.enableCaching ?? false,
350
+ cacheTTL: config.cacheTTL ?? 300000,
351
+ ...config,
352
+ };
353
+
354
+ this.hourlyCost = { amount: 0, resetAt: Date.now() + 3600000 };
355
+
356
+ this.emit('adapter-created', { config: this.config });
357
+ }
358
+
359
+ /**
360
+ * Initialize the adapter
361
+ */
362
+ async initialize(): Promise<void> {
363
+ // Start health checks if enabled
364
+ if (this.config.enableHealthChecks) {
365
+ this.startHealthChecks();
366
+ }
367
+
368
+ this.emit('adapter-initialized');
369
+ }
370
+
371
+ /**
372
+ * Shutdown the adapter
373
+ */
374
+ async shutdown(): Promise<void> {
375
+ this.stopHealthChecks();
376
+ this.providers.clear();
377
+ this.metrics.clear();
378
+ this.cache.clear();
379
+
380
+ this.emit('adapter-shutdown');
381
+ }
382
+
383
+ /**
384
+ * Register a provider
385
+ *
386
+ * @param provider - Provider to register
387
+ */
388
+ registerProvider(provider: Provider): void {
389
+ this.providers.set(provider.id, provider);
390
+
391
+ // Initialize metrics
392
+ this.metrics.set(provider.id, {
393
+ totalRequests: 0,
394
+ successfulRequests: 0,
395
+ failedRequests: 0,
396
+ avgLatencyMs: 0,
397
+ totalTokens: 0,
398
+ totalCost: 0,
399
+ lastRequest: 0,
400
+ uptimePercent: 100,
401
+ });
402
+
403
+ this.emit('provider-registered', { providerId: provider.id });
404
+ }
405
+
406
+ /**
407
+ * Unregister a provider
408
+ *
409
+ * @param providerId - Provider ID to remove
410
+ */
411
+ unregisterProvider(providerId: string): boolean {
412
+ const removed = this.providers.delete(providerId);
413
+ this.metrics.delete(providerId);
414
+
415
+ if (removed) {
416
+ this.emit('provider-unregistered', { providerId });
417
+ }
418
+
419
+ return removed;
420
+ }
421
+
422
+ /**
423
+ * Get a provider by ID
424
+ *
425
+ * @param providerId - Provider ID
426
+ * @returns Provider or undefined
427
+ */
428
+ getProvider(providerId: string): Provider | undefined {
429
+ return this.providers.get(providerId);
430
+ }
431
+
432
+ /**
433
+ * Get all registered providers
434
+ */
435
+ getAllProviders(): Provider[] {
436
+ return Array.from(this.providers.values());
437
+ }
438
+
439
+ /**
440
+ * Get available providers (not unavailable or rate-limited)
441
+ */
442
+ getAvailableProviders(): Provider[] {
443
+ return Array.from(this.providers.values()).filter(
444
+ (p) => p.status === 'available' || p.status === 'degraded'
445
+ );
446
+ }
447
+
448
+ /**
449
+ * Select the best provider based on requirements
450
+ *
451
+ * @param requirements - Selection requirements
452
+ * @returns Selection result with provider and model
453
+ */
454
+ selectProvider(requirements: ProviderRequirements = {}): ProviderSelectionResult {
455
+ const candidates: Array<{
456
+ provider: Provider;
457
+ model: ModelInfo;
458
+ score: number;
459
+ reasons: string[];
460
+ }> = [];
461
+
462
+ for (const provider of this.getAvailableProviders()) {
463
+ // Check exclusions
464
+ if (requirements.excludeProviders?.includes(provider.id)) {
465
+ continue;
466
+ }
467
+
468
+ // Check capabilities
469
+ if (requirements.capabilities) {
470
+ const hasAllCapabilities = requirements.capabilities.every((cap) =>
471
+ provider.capabilities.includes(cap)
472
+ );
473
+ if (!hasAllCapabilities) {
474
+ continue;
475
+ }
476
+ }
477
+
478
+ // Check preferred types
479
+ if (
480
+ requirements.preferredTypes &&
481
+ !requirements.preferredTypes.includes(provider.type)
482
+ ) {
483
+ continue;
484
+ }
485
+
486
+ // Check custom filters
487
+ if (requirements.customFilters) {
488
+ const passesFilters = requirements.customFilters.every((filter) =>
489
+ filter(provider)
490
+ );
491
+ if (!passesFilters) {
492
+ continue;
493
+ }
494
+ }
495
+
496
+ // Find suitable model
497
+ for (const model of provider.models) {
498
+ // Check context length
499
+ if (
500
+ requirements.minContextLength &&
501
+ model.maxContextLength < requirements.minContextLength
502
+ ) {
503
+ continue;
504
+ }
505
+
506
+ // Check model ID
507
+ if (requirements.modelId && model.id !== requirements.modelId) {
508
+ continue;
509
+ }
510
+
511
+ // Check streaming
512
+ if (requirements.streaming && !model.capabilities.includes('streaming')) {
513
+ continue;
514
+ }
515
+
516
+ // Check vision
517
+ if (requirements.vision && !model.capabilities.includes('vision')) {
518
+ continue;
519
+ }
520
+
521
+ // Check cost
522
+ const avgCost =
523
+ (provider.costPerToken.inputPer1K + provider.costPerToken.outputPer1K) / 2;
524
+ if (requirements.maxCostPer1K && avgCost > requirements.maxCostPer1K) {
525
+ continue;
526
+ }
527
+
528
+ // Calculate score
529
+ const { score, reasons } = this.calculateProviderScore(
530
+ provider,
531
+ model,
532
+ requirements
533
+ );
534
+
535
+ candidates.push({ provider, model, score, reasons });
536
+ }
537
+ }
538
+
539
+ if (candidates.length === 0) {
540
+ throw new Error('No providers match the requirements');
541
+ }
542
+
543
+ // Sort by score
544
+ candidates.sort((a, b) => b.score - a.score);
545
+
546
+ const best = candidates[0];
547
+ const alternatives = candidates.slice(1, 4).map(({ provider, model, score }) => ({
548
+ provider,
549
+ model,
550
+ score,
551
+ }));
552
+
553
+ this.emit('provider-selected', {
554
+ providerId: best.provider.id,
555
+ modelId: best.model.id,
556
+ score: best.score,
557
+ });
558
+
559
+ return {
560
+ provider: best.provider,
561
+ model: best.model,
562
+ score: best.score,
563
+ reasons: best.reasons,
564
+ alternatives,
565
+ };
566
+ }
567
+
568
+ /**
569
+ * Execute a task with a specific provider
570
+ *
571
+ * @param task - Task to execute
572
+ * @param provider - Provider to use
573
+ * @param options - Execution options
574
+ * @returns Execution result
575
+ */
576
+ async executeWithProvider(
577
+ task: Task,
578
+ provider: Provider,
579
+ options: ExecutionOptions = {}
580
+ ): Promise<ExecutionResult> {
581
+ // Check cache
582
+ if (this.config.enableCaching) {
583
+ const cached = this.getCachedResult(task, provider.id);
584
+ if (cached) {
585
+ return cached;
586
+ }
587
+ }
588
+
589
+ // Check cost limits
590
+ if (this.config.enableCostTracking) {
591
+ this.checkCostLimits();
592
+ }
593
+
594
+ // Check rate limits
595
+ this.checkRateLimits(provider);
596
+
597
+ const startTime = Date.now();
598
+ let result: ExecutionResult;
599
+ let attempt = 0;
600
+ const maxAttempts = options.retry?.maxAttempts ?? 1;
601
+
602
+ while (attempt < maxAttempts) {
603
+ try {
604
+ result = await this.executeRequest(task, provider, options);
605
+
606
+ // Update metrics on success
607
+ this.updateMetrics(provider.id, result);
608
+
609
+ // Update rate limits
610
+ this.updateRateLimits(provider, result.usage.totalTokens);
611
+
612
+ // Cache result
613
+ if (this.config.enableCaching) {
614
+ this.cacheResult(task, provider.id, result);
615
+ }
616
+
617
+ return result;
618
+ } catch (error) {
619
+ attempt++;
620
+
621
+ this.emit('execution-error', {
622
+ providerId: provider.id,
623
+ attempt,
624
+ error: error as Error,
625
+ });
626
+
627
+ if (attempt >= maxAttempts) {
628
+ // Try failover if enabled
629
+ if (this.config.enableFailover) {
630
+ const failoverResult = await this.tryFailover(task, provider, options);
631
+ if (failoverResult) {
632
+ return failoverResult;
633
+ }
634
+ }
635
+
636
+ // Update failure metrics
637
+ const metrics = this.metrics.get(provider.id);
638
+ if (metrics) {
639
+ metrics.failedRequests++;
640
+ }
641
+
642
+ return {
643
+ success: false,
644
+ content: '',
645
+ providerId: provider.id,
646
+ modelId: options.modelId || provider.models[0]?.id || 'unknown',
647
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
648
+ cost: 0,
649
+ latencyMs: Date.now() - startTime,
650
+ error: error as Error,
651
+ };
652
+ }
653
+
654
+ // Wait before retry
655
+ const delay =
656
+ (options.retry?.backoffMs ?? 1000) *
657
+ Math.pow(options.retry?.backoffMultiplier ?? 2, attempt - 1);
658
+ await this.delay(delay);
659
+ }
660
+ }
661
+
662
+ throw new Error('Execution failed after all attempts');
663
+ }
664
+
665
+ /**
666
+ * Get provider metrics
667
+ *
668
+ * @param providerId - Provider ID
669
+ * @returns Provider metrics or undefined
670
+ */
671
+ getProviderMetrics(providerId: string): ProviderMetrics | undefined {
672
+ return this.metrics.get(providerId);
673
+ }
674
+
675
+ /**
676
+ * Get all provider metrics
677
+ */
678
+ getAllMetrics(): Map<string, ProviderMetrics> {
679
+ return new Map(this.metrics);
680
+ }
681
+
682
+ /**
683
+ * Update provider status
684
+ *
685
+ * @param providerId - Provider ID
686
+ * @param status - New status
687
+ */
688
+ updateProviderStatus(providerId: string, status: ProviderStatus): void {
689
+ const provider = this.providers.get(providerId);
690
+ if (provider) {
691
+ provider.status = status;
692
+
693
+ this.emit('provider-status-changed', { providerId, status });
694
+ }
695
+ }
696
+
697
+ // ===== Private Methods =====
698
+
699
+ /**
700
+ * Calculate provider score for selection
701
+ */
702
+ private calculateProviderScore(
703
+ provider: Provider,
704
+ model: ModelInfo,
705
+ requirements: ProviderRequirements
706
+ ): { score: number; reasons: string[] } {
707
+ let score = 100;
708
+ const reasons: string[] = [];
709
+
710
+ // Base availability score
711
+ if (provider.status === 'available') {
712
+ score += 20;
713
+ reasons.push('Provider is fully available');
714
+ } else if (provider.status === 'degraded') {
715
+ score -= 10;
716
+ reasons.push('Provider is degraded');
717
+ }
718
+
719
+ // Cost efficiency
720
+ const avgCost =
721
+ (provider.costPerToken.inputPer1K + provider.costPerToken.outputPer1K) / 2;
722
+ if (avgCost < 0.01) {
723
+ score += 15;
724
+ reasons.push('Low cost provider');
725
+ } else if (avgCost > 0.05) {
726
+ score -= 10;
727
+ }
728
+
729
+ // Rate limit headroom
730
+ const rateHeadroom = 1 - provider.rateLimits.currentRequests / provider.rateLimits.requestsPerMinute;
731
+ score += rateHeadroom * 10;
732
+ if (rateHeadroom > 0.5) {
733
+ reasons.push('Good rate limit headroom');
734
+ }
735
+
736
+ // Historical performance
737
+ const metrics = this.metrics.get(provider.id);
738
+ if (metrics) {
739
+ const successRate =
740
+ metrics.totalRequests > 0
741
+ ? metrics.successfulRequests / metrics.totalRequests
742
+ : 1;
743
+ score += successRate * 20;
744
+
745
+ if (successRate > 0.95) {
746
+ reasons.push('High success rate');
747
+ }
748
+
749
+ // Latency penalty
750
+ if (metrics.avgLatencyMs > 5000) {
751
+ score -= 15;
752
+ reasons.push('High latency');
753
+ }
754
+ }
755
+
756
+ // Preferred provider bonus
757
+ if (requirements.preferredTypes?.includes(provider.type)) {
758
+ score += 10;
759
+ reasons.push('Preferred provider type');
760
+ }
761
+
762
+ // Context length bonus
763
+ if (model.maxContextLength > 100000) {
764
+ score += 5;
765
+ reasons.push('Long context support');
766
+ }
767
+
768
+ return { score, reasons };
769
+ }
770
+
771
+ /**
772
+ * Execute a request to the provider
773
+ */
774
+ private async executeRequest(
775
+ task: Task,
776
+ provider: Provider,
777
+ options: ExecutionOptions
778
+ ): Promise<ExecutionResult> {
779
+ const startTime = Date.now();
780
+
781
+ // Execute with provider-like latency (actual API calls via external integrations)
782
+ await this.delay(100);
783
+
784
+ const model = options.modelId
785
+ ? provider.models.find((m) => m.id === options.modelId) || provider.models[0]
786
+ : provider.models[0];
787
+
788
+ const inputTokens = Math.ceil(task.description.length / 4);
789
+ const outputTokens = Math.ceil(inputTokens * 1.5);
790
+
791
+ const cost =
792
+ (inputTokens / 1000) * provider.costPerToken.inputPer1K +
793
+ (outputTokens / 1000) * provider.costPerToken.outputPer1K;
794
+
795
+ return {
796
+ success: true,
797
+ content: `Executed task ${task.id} with ${provider.name}`,
798
+ providerId: provider.id,
799
+ modelId: model?.id || 'default',
800
+ usage: {
801
+ inputTokens,
802
+ outputTokens,
803
+ totalTokens: inputTokens + outputTokens,
804
+ },
805
+ cost,
806
+ latencyMs: Date.now() - startTime,
807
+ };
808
+ }
809
+
810
+ /**
811
+ * Try failover to alternative providers
812
+ */
813
+ private async tryFailover(
814
+ task: Task,
815
+ failedProvider: Provider,
816
+ options: ExecutionOptions
817
+ ): Promise<ExecutionResult | null> {
818
+ for (let attempt = 0; attempt < this.config.maxFailoverAttempts!; attempt++) {
819
+ try {
820
+ const selection = this.selectProvider({
821
+ excludeProviders: [failedProvider.id],
822
+ capabilities: failedProvider.capabilities,
823
+ });
824
+
825
+ this.emit('failover-attempt', {
826
+ fromProvider: failedProvider.id,
827
+ toProvider: selection.provider.id,
828
+ attempt: attempt + 1,
829
+ });
830
+
831
+ const result = await this.executeRequest(
832
+ task,
833
+ selection.provider,
834
+ options
835
+ );
836
+
837
+ this.updateMetrics(selection.provider.id, result);
838
+
839
+ this.emit('failover-success', {
840
+ fromProvider: failedProvider.id,
841
+ toProvider: selection.provider.id,
842
+ });
843
+
844
+ return result;
845
+ } catch (error) {
846
+ this.emit('failover-error', {
847
+ attempt: attempt + 1,
848
+ error: error as Error,
849
+ });
850
+ }
851
+ }
852
+
853
+ this.emit('failover-exhausted', { originalProvider: failedProvider.id });
854
+ return null;
855
+ }
856
+
857
+ /**
858
+ * Update provider metrics
859
+ */
860
+ private updateMetrics(providerId: string, result: ExecutionResult): void {
861
+ const metrics = this.metrics.get(providerId);
862
+ if (!metrics) return;
863
+
864
+ metrics.totalRequests++;
865
+ metrics.lastRequest = Date.now();
866
+
867
+ if (result.success) {
868
+ metrics.successfulRequests++;
869
+ } else {
870
+ metrics.failedRequests++;
871
+ }
872
+
873
+ // Update average latency
874
+ const totalLatency = metrics.avgLatencyMs * (metrics.totalRequests - 1) + result.latencyMs;
875
+ metrics.avgLatencyMs = totalLatency / metrics.totalRequests;
876
+
877
+ // Update token and cost totals
878
+ metrics.totalTokens += result.usage.totalTokens;
879
+ metrics.totalCost += result.cost;
880
+
881
+ // Update hourly cost
882
+ if (this.config.enableCostTracking) {
883
+ this.hourlyCost.amount += result.cost;
884
+ }
885
+ }
886
+
887
+ /**
888
+ * Update rate limits after request
889
+ */
890
+ private updateRateLimits(provider: Provider, tokensUsed: number): void {
891
+ provider.rateLimits.currentRequests++;
892
+ provider.rateLimits.currentTokens += tokensUsed;
893
+
894
+ // Check if rate limited
895
+ if (
896
+ provider.rateLimits.currentRequests >= provider.rateLimits.requestsPerMinute ||
897
+ provider.rateLimits.currentTokens >= provider.rateLimits.tokensPerMinute
898
+ ) {
899
+ provider.status = 'rate-limited';
900
+
901
+ // Schedule reset
902
+ setTimeout(() => {
903
+ provider.rateLimits.currentRequests = 0;
904
+ provider.rateLimits.currentTokens = 0;
905
+ provider.status = 'available';
906
+ }, 60000);
907
+ }
908
+ }
909
+
910
+ /**
911
+ * Check rate limits before request
912
+ */
913
+ private checkRateLimits(provider: Provider): void {
914
+ // Reset rate limits if needed
915
+ const now = Date.now();
916
+ if (now >= provider.rateLimits.resetAt) {
917
+ provider.rateLimits.currentRequests = 0;
918
+ provider.rateLimits.currentTokens = 0;
919
+ provider.rateLimits.resetAt = now + 60000;
920
+ if (provider.status === 'rate-limited') {
921
+ provider.status = 'available';
922
+ }
923
+ }
924
+
925
+ if (provider.status === 'rate-limited') {
926
+ throw new Error(`Provider ${provider.id} is rate limited`);
927
+ }
928
+ }
929
+
930
+ /**
931
+ * Check cost limits
932
+ */
933
+ private checkCostLimits(): void {
934
+ const now = Date.now();
935
+
936
+ // Reset hourly cost if needed
937
+ if (now >= this.hourlyCost.resetAt) {
938
+ this.hourlyCost.amount = 0;
939
+ this.hourlyCost.resetAt = now + 3600000;
940
+ }
941
+
942
+ if (this.hourlyCost.amount >= this.config.costLimitPerHour!) {
943
+ throw new Error(
944
+ `Hourly cost limit exceeded: $${this.hourlyCost.amount.toFixed(2)} / $${this.config.costLimitPerHour}`
945
+ );
946
+ }
947
+ }
948
+
949
+ /**
950
+ * Get cached result
951
+ */
952
+ private getCachedResult(
953
+ task: Task,
954
+ providerId: string
955
+ ): ExecutionResult | null {
956
+ const cacheKey = `${providerId}:${task.id}:${task.description}`;
957
+ const cached = this.cache.get(cacheKey);
958
+
959
+ if (cached && Date.now() - cached.timestamp < this.config.cacheTTL!) {
960
+ this.emit('cache-hit', { taskId: task.id, providerId });
961
+ return cached.result;
962
+ }
963
+
964
+ return null;
965
+ }
966
+
967
+ /**
968
+ * Cache result
969
+ */
970
+ private cacheResult(
971
+ task: Task,
972
+ providerId: string,
973
+ result: ExecutionResult
974
+ ): void {
975
+ const cacheKey = `${providerId}:${task.id}:${task.description}`;
976
+ this.cache.set(cacheKey, { result, timestamp: Date.now() });
977
+ }
978
+
979
+ /**
980
+ * Start health check timer
981
+ */
982
+ private startHealthChecks(): void {
983
+ this.healthCheckTimer = setInterval(() => {
984
+ this.performHealthChecks();
985
+ }, this.config.healthCheckInterval!);
986
+ }
987
+
988
+ /**
989
+ * Stop health check timer
990
+ */
991
+ private stopHealthChecks(): void {
992
+ if (this.healthCheckTimer) {
993
+ clearInterval(this.healthCheckTimer);
994
+ this.healthCheckTimer = null;
995
+ }
996
+ }
997
+
998
+ /**
999
+ * Perform health checks on all providers
1000
+ */
1001
+ private performHealthChecks(): void {
1002
+ for (const provider of Array.from(this.providers.values())) {
1003
+ const metrics = this.metrics.get(provider.id);
1004
+ if (!metrics) continue;
1005
+
1006
+ // Calculate uptime based on recent success rate
1007
+ const successRate =
1008
+ metrics.totalRequests > 0
1009
+ ? metrics.successfulRequests / metrics.totalRequests
1010
+ : 1;
1011
+ metrics.uptimePercent = successRate * 100;
1012
+
1013
+ // Update provider status based on metrics
1014
+ if (successRate < 0.5 && metrics.totalRequests > 10) {
1015
+ provider.status = 'unavailable';
1016
+ } else if (successRate < 0.8 && metrics.totalRequests > 5) {
1017
+ provider.status = 'degraded';
1018
+ } else if (provider.status !== 'rate-limited') {
1019
+ provider.status = 'available';
1020
+ }
1021
+
1022
+ this.emit('provider-health-check', {
1023
+ providerId: provider.id,
1024
+ status: provider.status,
1025
+ uptimePercent: metrics.uptimePercent,
1026
+ });
1027
+ }
1028
+ }
1029
+
1030
+ /**
1031
+ * Utility delay function
1032
+ */
1033
+ private delay(ms: number): Promise<void> {
1034
+ return new Promise((resolve) => setTimeout(resolve, ms));
1035
+ }
1036
+ }
1037
+
1038
+ /**
1039
+ * Create a provider adapter with the given configuration
1040
+ *
1041
+ * @param config - Adapter configuration
1042
+ * @returns Configured ProviderAdapter
1043
+ */
1044
+ export function createProviderAdapter(
1045
+ config: ProviderAdapterConfig = {}
1046
+ ): ProviderAdapter {
1047
+ return new ProviderAdapter(config);
1048
+ }
1049
+
1050
+ /**
1051
+ * Create default provider configurations for common providers
1052
+ */
1053
+ export function createDefaultProviders(): Provider[] {
1054
+ return [
1055
+ {
1056
+ id: 'anthropic-claude',
1057
+ name: 'Anthropic Claude',
1058
+ type: 'anthropic',
1059
+ models: [
1060
+ {
1061
+ id: 'claude-3-opus-20240229',
1062
+ name: 'Claude 3 Opus',
1063
+ maxContextLength: 200000,
1064
+ maxOutputTokens: 4096,
1065
+ capabilities: ['chat', 'code-generation', 'vision', 'long-context'],
1066
+ },
1067
+ {
1068
+ id: 'claude-3-sonnet-20240229',
1069
+ name: 'Claude 3 Sonnet',
1070
+ maxContextLength: 200000,
1071
+ maxOutputTokens: 4096,
1072
+ capabilities: ['chat', 'code-generation', 'vision', 'long-context'],
1073
+ },
1074
+ {
1075
+ id: 'claude-3-5-sonnet-20241022',
1076
+ name: 'Claude 3.5 Sonnet',
1077
+ maxContextLength: 200000,
1078
+ maxOutputTokens: 8192,
1079
+ capabilities: ['chat', 'code-generation', 'vision', 'long-context', 'streaming'],
1080
+ },
1081
+ ],
1082
+ capabilities: ['chat', 'code-generation', 'vision', 'streaming', 'long-context'],
1083
+ status: 'available',
1084
+ rateLimits: {
1085
+ requestsPerMinute: 60,
1086
+ tokensPerMinute: 100000,
1087
+ currentRequests: 0,
1088
+ currentTokens: 0,
1089
+ resetAt: Date.now() + 60000,
1090
+ },
1091
+ costPerToken: {
1092
+ inputPer1K: 0.015,
1093
+ outputPer1K: 0.075,
1094
+ currency: 'USD',
1095
+ },
1096
+ },
1097
+ {
1098
+ id: 'openai-gpt4',
1099
+ name: 'OpenAI GPT-4',
1100
+ type: 'openai',
1101
+ models: [
1102
+ {
1103
+ id: 'gpt-4-turbo',
1104
+ name: 'GPT-4 Turbo',
1105
+ maxContextLength: 128000,
1106
+ maxOutputTokens: 4096,
1107
+ capabilities: ['chat', 'code-generation', 'vision', 'function-calling'],
1108
+ },
1109
+ {
1110
+ id: 'gpt-4o',
1111
+ name: 'GPT-4o',
1112
+ maxContextLength: 128000,
1113
+ maxOutputTokens: 4096,
1114
+ capabilities: ['chat', 'code-generation', 'vision', 'function-calling', 'streaming'],
1115
+ },
1116
+ ],
1117
+ capabilities: ['chat', 'code-generation', 'vision', 'function-calling', 'streaming'],
1118
+ status: 'available',
1119
+ rateLimits: {
1120
+ requestsPerMinute: 500,
1121
+ tokensPerMinute: 150000,
1122
+ currentRequests: 0,
1123
+ currentTokens: 0,
1124
+ resetAt: Date.now() + 60000,
1125
+ },
1126
+ costPerToken: {
1127
+ inputPer1K: 0.01,
1128
+ outputPer1K: 0.03,
1129
+ currency: 'USD',
1130
+ },
1131
+ },
1132
+ {
1133
+ id: 'ollama-local',
1134
+ name: 'Ollama Local',
1135
+ type: 'ollama',
1136
+ models: [
1137
+ {
1138
+ id: 'llama3',
1139
+ name: 'Llama 3',
1140
+ maxContextLength: 8192,
1141
+ maxOutputTokens: 2048,
1142
+ capabilities: ['chat', 'code-generation'],
1143
+ },
1144
+ {
1145
+ id: 'codellama',
1146
+ name: 'Code Llama',
1147
+ maxContextLength: 16384,
1148
+ maxOutputTokens: 2048,
1149
+ capabilities: ['chat', 'code-generation'],
1150
+ },
1151
+ ],
1152
+ capabilities: ['chat', 'code-generation'],
1153
+ status: 'available',
1154
+ rateLimits: {
1155
+ requestsPerMinute: 1000,
1156
+ tokensPerMinute: 1000000,
1157
+ currentRequests: 0,
1158
+ currentTokens: 0,
1159
+ resetAt: Date.now() + 60000,
1160
+ },
1161
+ costPerToken: {
1162
+ inputPer1K: 0,
1163
+ outputPer1K: 0,
1164
+ currency: 'USD',
1165
+ },
1166
+ },
1167
+ ];
1168
+ }