agentic-flow 1.9.3 → 1.10.0
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.
- package/CHANGELOG.md +298 -0
- package/dist/cli-proxy.js +19 -1
- package/dist/core/long-running-agent.js +219 -0
- package/dist/core/provider-manager.js +434 -0
- package/dist/examples/use-provider-fallback.js +176 -0
- package/dist/proxy/adaptive-proxy.js +224 -0
- package/dist/proxy/anthropic-to-gemini.js +2 -2
- package/dist/proxy/http2-proxy-optimized.js +191 -0
- package/dist/proxy/http2-proxy.js +381 -0
- package/dist/proxy/http3-proxy-old.js +331 -0
- package/dist/proxy/http3-proxy.js +51 -0
- package/dist/proxy/websocket-proxy.js +406 -0
- package/dist/utils/auth.js +52 -0
- package/dist/utils/compression-middleware.js +149 -0
- package/dist/utils/connection-pool.js +184 -0
- package/dist/utils/rate-limiter.js +48 -0
- package/dist/utils/response-cache.js +211 -0
- package/dist/utils/streaming-optimizer.js +141 -0
- package/docs/.claude-flow/metrics/performance.json +3 -3
- package/docs/.claude-flow/metrics/task-metrics.json +3 -3
- package/docs/ISSUE-55-VALIDATION.md +152 -0
- package/docs/OPTIMIZATIONS.md +460 -0
- package/docs/README.md +217 -0
- package/docs/issues/ISSUE-xenova-transformers-dependency.md +380 -0
- package/docs/providers/LANDING-PAGE-PROVIDER-CONTENT.md +204 -0
- package/docs/providers/PROVIDER-FALLBACK-GUIDE.md +619 -0
- package/docs/providers/PROVIDER-FALLBACK-SUMMARY.md +418 -0
- package/package.json +1 -1
- package/scripts/claude +31 -0
- package/validation/test-gemini-exclusiveMinimum-fix.ts +142 -0
- package/validation/test-provider-fallback.ts +285 -0
- package/validation/validate-v1.10.0-docker.sh +296 -0
- package/wasm/reasoningbank/reasoningbank_wasm_bg.js +2 -2
- package/wasm/reasoningbank/reasoningbank_wasm_bg.wasm +0 -0
- package/docs/INDEX.md +0 -279
- package/docs/guides/.claude-flow/metrics/agent-metrics.json +0 -1
- package/docs/guides/.claude-flow/metrics/performance.json +0 -9
- package/docs/guides/.claude-flow/metrics/task-metrics.json +0 -10
- package/docs/router/.claude-flow/metrics/agent-metrics.json +0 -1
- package/docs/router/.claude-flow/metrics/performance.json +0 -9
- package/docs/router/.claude-flow/metrics/task-metrics.json +0 -10
- /package/docs/{TEST-V1.7.8.Dockerfile → docker-tests/TEST-V1.7.8.Dockerfile} +0 -0
- /package/docs/{TEST-V1.7.9-NODE20.Dockerfile → docker-tests/TEST-V1.7.9-NODE20.Dockerfile} +0 -0
- /package/docs/{TEST-V1.7.9.Dockerfile → docker-tests/TEST-V1.7.9.Dockerfile} +0 -0
- /package/docs/{v1.7.1-QUICK-START.md → guides/QUICK-START-v1.7.1.md} +0 -0
- /package/docs/{INTEGRATION-COMPLETE.md → integration-docs/INTEGRATION-COMPLETE.md} +0 -0
- /package/docs/{QUIC_FINAL_STATUS.md → quic/QUIC_FINAL_STATUS.md} +0 -0
- /package/docs/{README_QUIC_PHASE1.md → quic/README_QUIC_PHASE1.md} +0 -0
- /package/docs/{AGENTDB_TESTING.md → testing/AGENTDB_TESTING.md} +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Manager - Intelligent LLM Provider Fallback & Dynamic Switching
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Automatic failover between providers
|
|
6
|
+
* - Health monitoring and circuit breaking
|
|
7
|
+
* - Cost-based optimization
|
|
8
|
+
* - Performance tracking
|
|
9
|
+
* - Rate limit handling
|
|
10
|
+
* - Provider quality scoring
|
|
11
|
+
*/
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
export class ProviderManager {
|
|
14
|
+
providers = new Map();
|
|
15
|
+
health = new Map();
|
|
16
|
+
metrics = new Map();
|
|
17
|
+
strategy;
|
|
18
|
+
currentProvider = null;
|
|
19
|
+
healthCheckIntervals = new Map();
|
|
20
|
+
constructor(providers, strategy = {
|
|
21
|
+
type: 'priority',
|
|
22
|
+
maxFailures: 3,
|
|
23
|
+
recoveryTime: 60000, // 1 minute
|
|
24
|
+
retryBackoff: 'exponential'
|
|
25
|
+
}) {
|
|
26
|
+
this.strategy = strategy;
|
|
27
|
+
this.initializeProviders(providers);
|
|
28
|
+
this.startHealthChecks();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Initialize provider configurations
|
|
32
|
+
*/
|
|
33
|
+
initializeProviders(providers) {
|
|
34
|
+
for (const provider of providers) {
|
|
35
|
+
this.providers.set(provider.name, provider);
|
|
36
|
+
// Initialize health tracking
|
|
37
|
+
this.health.set(provider.name, {
|
|
38
|
+
provider: provider.name,
|
|
39
|
+
isHealthy: true,
|
|
40
|
+
lastChecked: new Date(),
|
|
41
|
+
consecutiveFailures: 0,
|
|
42
|
+
averageLatency: 0,
|
|
43
|
+
successRate: 1.0,
|
|
44
|
+
errorRate: 0.0,
|
|
45
|
+
circuitBreakerOpen: false
|
|
46
|
+
});
|
|
47
|
+
// Initialize metrics
|
|
48
|
+
this.metrics.set(provider.name, {
|
|
49
|
+
provider: provider.name,
|
|
50
|
+
totalRequests: 0,
|
|
51
|
+
successfulRequests: 0,
|
|
52
|
+
failedRequests: 0,
|
|
53
|
+
totalTokens: 0,
|
|
54
|
+
totalCost: 0,
|
|
55
|
+
averageLatency: 0,
|
|
56
|
+
lastUsed: new Date()
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
logger.info('Provider Manager initialized', {
|
|
60
|
+
providers: Array.from(this.providers.keys()),
|
|
61
|
+
strategy: this.strategy.type
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Start health check monitoring for all providers
|
|
66
|
+
*/
|
|
67
|
+
startHealthChecks() {
|
|
68
|
+
for (const [name, config] of this.providers.entries()) {
|
|
69
|
+
if (config.healthCheckInterval) {
|
|
70
|
+
const interval = setInterval(() => {
|
|
71
|
+
this.performHealthCheck(name);
|
|
72
|
+
}, config.healthCheckInterval);
|
|
73
|
+
this.healthCheckIntervals.set(name, interval);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Perform health check on a provider
|
|
79
|
+
*/
|
|
80
|
+
async performHealthCheck(provider) {
|
|
81
|
+
const health = this.health.get(provider);
|
|
82
|
+
if (!health)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
const startTime = Date.now();
|
|
86
|
+
// TODO: Implement actual health check API call
|
|
87
|
+
// For now, just check if circuit breaker should recover
|
|
88
|
+
const latency = Date.now() - startTime;
|
|
89
|
+
health.lastChecked = new Date();
|
|
90
|
+
health.averageLatency = (health.averageLatency + latency) / 2;
|
|
91
|
+
// Recovery logic: reset circuit breaker after recovery time
|
|
92
|
+
if (health.circuitBreakerOpen) {
|
|
93
|
+
const timeSinceLastCheck = Date.now() - health.lastChecked.getTime();
|
|
94
|
+
if (timeSinceLastCheck >= this.strategy.recoveryTime) {
|
|
95
|
+
logger.info('Circuit breaker recovering', { provider });
|
|
96
|
+
health.circuitBreakerOpen = false;
|
|
97
|
+
health.consecutiveFailures = 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
logger.debug('Health check completed', { provider, health });
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
logger.error('Health check failed', { provider, error: error.message });
|
|
104
|
+
this.recordFailure(provider, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Select the best provider based on strategy
|
|
109
|
+
*/
|
|
110
|
+
async selectProvider(taskComplexity, estimatedTokens) {
|
|
111
|
+
const availableProviders = this.getAvailableProviders();
|
|
112
|
+
if (availableProviders.length === 0) {
|
|
113
|
+
throw new Error('No healthy providers available');
|
|
114
|
+
}
|
|
115
|
+
let selectedProvider;
|
|
116
|
+
switch (this.strategy.type) {
|
|
117
|
+
case 'priority':
|
|
118
|
+
selectedProvider = this.selectByPriority(availableProviders);
|
|
119
|
+
break;
|
|
120
|
+
case 'cost-optimized':
|
|
121
|
+
selectedProvider = this.selectByCost(availableProviders, estimatedTokens);
|
|
122
|
+
break;
|
|
123
|
+
case 'performance-optimized':
|
|
124
|
+
selectedProvider = this.selectByPerformance(availableProviders);
|
|
125
|
+
break;
|
|
126
|
+
case 'round-robin':
|
|
127
|
+
selectedProvider = this.selectRoundRobin(availableProviders);
|
|
128
|
+
break;
|
|
129
|
+
default:
|
|
130
|
+
selectedProvider = availableProviders[0];
|
|
131
|
+
}
|
|
132
|
+
// Apply task complexity heuristics
|
|
133
|
+
if (taskComplexity === 'complex' && this.providers.has('anthropic')) {
|
|
134
|
+
const anthropicHealth = this.health.get('anthropic');
|
|
135
|
+
if (anthropicHealth?.isHealthy && !anthropicHealth.circuitBreakerOpen) {
|
|
136
|
+
selectedProvider = 'anthropic'; // Prefer Claude for complex tasks
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (taskComplexity === 'simple' && this.providers.has('gemini')) {
|
|
140
|
+
const geminiHealth = this.health.get('gemini');
|
|
141
|
+
if (geminiHealth?.isHealthy && !geminiHealth.circuitBreakerOpen) {
|
|
142
|
+
selectedProvider = 'gemini'; // Prefer Gemini for simple tasks (faster, cheaper)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
this.currentProvider = selectedProvider;
|
|
146
|
+
logger.info('Provider selected', {
|
|
147
|
+
provider: selectedProvider,
|
|
148
|
+
strategy: this.strategy.type,
|
|
149
|
+
taskComplexity,
|
|
150
|
+
estimatedTokens
|
|
151
|
+
});
|
|
152
|
+
return selectedProvider;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get list of healthy, available providers
|
|
156
|
+
*/
|
|
157
|
+
getAvailableProviders() {
|
|
158
|
+
const available = [];
|
|
159
|
+
for (const [name, config] of this.providers.entries()) {
|
|
160
|
+
const health = this.health.get(name);
|
|
161
|
+
if (config.enabled &&
|
|
162
|
+
health?.isHealthy &&
|
|
163
|
+
!health.circuitBreakerOpen) {
|
|
164
|
+
available.push(name);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Sort by priority
|
|
168
|
+
available.sort((a, b) => {
|
|
169
|
+
const priorityA = this.providers.get(a)?.priority ?? 999;
|
|
170
|
+
const priorityB = this.providers.get(b)?.priority ?? 999;
|
|
171
|
+
return priorityA - priorityB;
|
|
172
|
+
});
|
|
173
|
+
return available;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Select provider by priority
|
|
177
|
+
*/
|
|
178
|
+
selectByPriority(providers) {
|
|
179
|
+
return providers[0]; // Already sorted by priority
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Select provider by cost optimization
|
|
183
|
+
*/
|
|
184
|
+
selectByCost(providers, estimatedTokens) {
|
|
185
|
+
if (!estimatedTokens) {
|
|
186
|
+
return this.selectByPriority(providers);
|
|
187
|
+
}
|
|
188
|
+
let bestProvider = providers[0];
|
|
189
|
+
let lowestCost = Infinity;
|
|
190
|
+
for (const provider of providers) {
|
|
191
|
+
const config = this.providers.get(provider);
|
|
192
|
+
if (!config)
|
|
193
|
+
continue;
|
|
194
|
+
const estimatedCost = (estimatedTokens / 1_000_000) * config.costPerToken;
|
|
195
|
+
if (estimatedCost < lowestCost) {
|
|
196
|
+
lowestCost = estimatedCost;
|
|
197
|
+
bestProvider = provider;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return bestProvider;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Select provider by performance (latency + success rate)
|
|
204
|
+
*/
|
|
205
|
+
selectByPerformance(providers) {
|
|
206
|
+
let bestProvider = providers[0];
|
|
207
|
+
let bestScore = -Infinity;
|
|
208
|
+
for (const provider of providers) {
|
|
209
|
+
const health = this.health.get(provider);
|
|
210
|
+
if (!health)
|
|
211
|
+
continue;
|
|
212
|
+
// Score = success rate (weighted 70%) - normalized latency (weighted 30%)
|
|
213
|
+
const normalizedLatency = health.averageLatency / 1000; // Convert to seconds
|
|
214
|
+
const score = (health.successRate * 0.7) - (normalizedLatency * 0.3);
|
|
215
|
+
if (score > bestScore) {
|
|
216
|
+
bestScore = score;
|
|
217
|
+
bestProvider = provider;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return bestProvider;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Select provider using round-robin
|
|
224
|
+
*/
|
|
225
|
+
selectRoundRobin(providers) {
|
|
226
|
+
if (!this.currentProvider || !providers.includes(this.currentProvider)) {
|
|
227
|
+
return providers[0];
|
|
228
|
+
}
|
|
229
|
+
const currentIndex = providers.indexOf(this.currentProvider);
|
|
230
|
+
const nextIndex = (currentIndex + 1) % providers.length;
|
|
231
|
+
return providers[nextIndex];
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Execute request with automatic fallback
|
|
235
|
+
*/
|
|
236
|
+
async executeWithFallback(requestFn, taskComplexity, estimatedTokens) {
|
|
237
|
+
const startTime = Date.now();
|
|
238
|
+
let attempts = 0;
|
|
239
|
+
let lastError = null;
|
|
240
|
+
// Try each available provider
|
|
241
|
+
const availableProviders = this.getAvailableProviders();
|
|
242
|
+
const provider = await this.selectProvider(taskComplexity, estimatedTokens);
|
|
243
|
+
// Start with selected provider, then try fallbacks
|
|
244
|
+
const providersToTry = [
|
|
245
|
+
provider,
|
|
246
|
+
...availableProviders.filter(p => p !== provider)
|
|
247
|
+
];
|
|
248
|
+
for (const currentProvider of providersToTry) {
|
|
249
|
+
attempts++;
|
|
250
|
+
const config = this.providers.get(currentProvider);
|
|
251
|
+
if (!config)
|
|
252
|
+
continue;
|
|
253
|
+
try {
|
|
254
|
+
logger.info('Attempting request', { provider: currentProvider, attempt: attempts });
|
|
255
|
+
const requestStartTime = Date.now();
|
|
256
|
+
const result = await this.executeWithRetry(requestFn, currentProvider, config.maxRetries);
|
|
257
|
+
const latency = Date.now() - requestStartTime;
|
|
258
|
+
// Record success
|
|
259
|
+
this.recordSuccess(currentProvider, latency, estimatedTokens || 0);
|
|
260
|
+
logger.info('Request successful', {
|
|
261
|
+
provider: currentProvider,
|
|
262
|
+
attempts,
|
|
263
|
+
latency,
|
|
264
|
+
totalTime: Date.now() - startTime
|
|
265
|
+
});
|
|
266
|
+
return { result, provider: currentProvider, attempts };
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
lastError = error;
|
|
270
|
+
logger.warn('Provider request failed', {
|
|
271
|
+
provider: currentProvider,
|
|
272
|
+
attempt: attempts,
|
|
273
|
+
error: lastError.message
|
|
274
|
+
});
|
|
275
|
+
// Record failure
|
|
276
|
+
this.recordFailure(currentProvider, lastError);
|
|
277
|
+
// Continue to next provider
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// All providers failed
|
|
282
|
+
throw new Error(`All providers failed after ${attempts} attempts. Last error: ${lastError?.message}`);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Execute request with retry logic
|
|
286
|
+
*/
|
|
287
|
+
async executeWithRetry(requestFn, provider, maxRetries) {
|
|
288
|
+
let lastError = null;
|
|
289
|
+
for (let retry = 0; retry <= maxRetries; retry++) {
|
|
290
|
+
try {
|
|
291
|
+
// Apply backoff delay
|
|
292
|
+
if (retry > 0) {
|
|
293
|
+
const delay = this.calculateBackoffDelay(retry);
|
|
294
|
+
logger.debug('Retry backoff', { provider, retry, delay });
|
|
295
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
296
|
+
}
|
|
297
|
+
return await requestFn(provider);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
lastError = error;
|
|
301
|
+
// Check if error is retryable
|
|
302
|
+
if (!this.isRetryableError(error)) {
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
logger.debug('Retryable error, continuing', {
|
|
306
|
+
provider,
|
|
307
|
+
retry,
|
|
308
|
+
error: lastError.message
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
throw lastError || new Error('Max retries exceeded');
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Calculate backoff delay
|
|
316
|
+
*/
|
|
317
|
+
calculateBackoffDelay(retry) {
|
|
318
|
+
if (this.strategy.retryBackoff === 'exponential') {
|
|
319
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, 16s...
|
|
320
|
+
return Math.min(1000 * Math.pow(2, retry), 30000); // Max 30s
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// Linear backoff: 1s, 2s, 3s, 4s...
|
|
324
|
+
return Math.min(1000 * retry, 10000); // Max 10s
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Check if error is retryable
|
|
329
|
+
*/
|
|
330
|
+
isRetryableError(error) {
|
|
331
|
+
const retryablePatterns = [
|
|
332
|
+
/rate limit/i,
|
|
333
|
+
/timeout/i,
|
|
334
|
+
/connection/i,
|
|
335
|
+
/network/i,
|
|
336
|
+
/503/,
|
|
337
|
+
/502/,
|
|
338
|
+
/429/
|
|
339
|
+
];
|
|
340
|
+
return retryablePatterns.some(pattern => pattern.test(error.message));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Record successful request
|
|
344
|
+
*/
|
|
345
|
+
recordSuccess(provider, latency, tokens) {
|
|
346
|
+
const metrics = this.metrics.get(provider);
|
|
347
|
+
const health = this.health.get(provider);
|
|
348
|
+
const config = this.providers.get(provider);
|
|
349
|
+
if (metrics) {
|
|
350
|
+
metrics.totalRequests++;
|
|
351
|
+
metrics.successfulRequests++;
|
|
352
|
+
metrics.totalTokens += tokens;
|
|
353
|
+
metrics.totalCost += (tokens / 1_000_000) * (config?.costPerToken || 0);
|
|
354
|
+
metrics.averageLatency = (metrics.averageLatency + latency) / 2;
|
|
355
|
+
metrics.lastUsed = new Date();
|
|
356
|
+
}
|
|
357
|
+
if (health) {
|
|
358
|
+
health.consecutiveFailures = 0;
|
|
359
|
+
health.isHealthy = true;
|
|
360
|
+
health.circuitBreakerOpen = false;
|
|
361
|
+
health.successRate = metrics ? metrics.successfulRequests / metrics.totalRequests : 1.0;
|
|
362
|
+
health.errorRate = 1.0 - health.successRate;
|
|
363
|
+
health.averageLatency = latency;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Record failed request
|
|
368
|
+
*/
|
|
369
|
+
recordFailure(provider, error) {
|
|
370
|
+
const metrics = this.metrics.get(provider);
|
|
371
|
+
const health = this.health.get(provider);
|
|
372
|
+
if (metrics) {
|
|
373
|
+
metrics.totalRequests++;
|
|
374
|
+
metrics.failedRequests++;
|
|
375
|
+
}
|
|
376
|
+
if (health) {
|
|
377
|
+
health.consecutiveFailures++;
|
|
378
|
+
health.successRate = metrics ? metrics.successfulRequests / metrics.totalRequests : 0.0;
|
|
379
|
+
health.errorRate = 1.0 - health.successRate;
|
|
380
|
+
// Open circuit breaker if threshold exceeded
|
|
381
|
+
if (health.consecutiveFailures >= this.strategy.maxFailures) {
|
|
382
|
+
logger.warn('Circuit breaker opened', {
|
|
383
|
+
provider,
|
|
384
|
+
consecutiveFailures: health.consecutiveFailures,
|
|
385
|
+
threshold: this.strategy.maxFailures
|
|
386
|
+
});
|
|
387
|
+
health.circuitBreakerOpen = true;
|
|
388
|
+
health.isHealthy = false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
logger.error('Provider failure recorded', {
|
|
392
|
+
provider,
|
|
393
|
+
error: error.message,
|
|
394
|
+
consecutiveFailures: health?.consecutiveFailures,
|
|
395
|
+
circuitBreakerOpen: health?.circuitBreakerOpen
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Get current metrics for all providers
|
|
400
|
+
*/
|
|
401
|
+
getMetrics() {
|
|
402
|
+
return Array.from(this.metrics.values());
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Get health status for all providers
|
|
406
|
+
*/
|
|
407
|
+
getHealth() {
|
|
408
|
+
return Array.from(this.health.values());
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Get cost summary
|
|
412
|
+
*/
|
|
413
|
+
getCostSummary() {
|
|
414
|
+
let total = 0;
|
|
415
|
+
let totalTokens = 0;
|
|
416
|
+
const byProvider = {};
|
|
417
|
+
for (const [name, metrics] of this.metrics.entries()) {
|
|
418
|
+
total += metrics.totalCost;
|
|
419
|
+
totalTokens += metrics.totalTokens;
|
|
420
|
+
byProvider[name] = metrics.totalCost;
|
|
421
|
+
}
|
|
422
|
+
return { total, byProvider: byProvider, totalTokens };
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Cleanup and stop health checks
|
|
426
|
+
*/
|
|
427
|
+
destroy() {
|
|
428
|
+
for (const interval of this.healthCheckIntervals.values()) {
|
|
429
|
+
clearInterval(interval);
|
|
430
|
+
}
|
|
431
|
+
this.healthCheckIntervals.clear();
|
|
432
|
+
logger.info('Provider Manager destroyed');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Long-Running Agent with Provider Fallback
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* - Automatic provider fallback
|
|
6
|
+
* - Cost optimization
|
|
7
|
+
* - Health monitoring
|
|
8
|
+
* - Checkpointing for crash recovery
|
|
9
|
+
* - Budget constraints
|
|
10
|
+
*/
|
|
11
|
+
import { LongRunningAgent } from '../core/long-running-agent.js';
|
|
12
|
+
async function main() {
|
|
13
|
+
console.log('🚀 Starting Long-Running Agent with Provider Fallback\n');
|
|
14
|
+
// Configure providers with priorities and costs
|
|
15
|
+
const providers = [
|
|
16
|
+
{
|
|
17
|
+
name: 'gemini',
|
|
18
|
+
apiKey: process.env.GOOGLE_GEMINI_API_KEY,
|
|
19
|
+
priority: 1, // Try Gemini first (fastest, cheapest)
|
|
20
|
+
maxRetries: 3,
|
|
21
|
+
timeout: 30000,
|
|
22
|
+
costPerToken: 0.00015, // $0.15 per 1M tokens
|
|
23
|
+
enabled: !!process.env.GOOGLE_GEMINI_API_KEY,
|
|
24
|
+
healthCheckInterval: 60000 // Check every minute
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'anthropic',
|
|
28
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
29
|
+
priority: 2, // Fallback to Claude (higher quality)
|
|
30
|
+
maxRetries: 3,
|
|
31
|
+
timeout: 60000,
|
|
32
|
+
costPerToken: 0.003, // $3 per 1M tokens (Sonnet)
|
|
33
|
+
enabled: !!process.env.ANTHROPIC_API_KEY,
|
|
34
|
+
healthCheckInterval: 60000
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'openrouter',
|
|
38
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
39
|
+
priority: 3, // Fallback to OpenRouter
|
|
40
|
+
maxRetries: 3,
|
|
41
|
+
timeout: 60000,
|
|
42
|
+
costPerToken: 0.001, // Varies by model
|
|
43
|
+
enabled: !!process.env.OPENROUTER_API_KEY,
|
|
44
|
+
healthCheckInterval: 60000
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'onnx',
|
|
48
|
+
priority: 4, // Last resort (local, free but slower)
|
|
49
|
+
maxRetries: 2,
|
|
50
|
+
timeout: 120000,
|
|
51
|
+
costPerToken: 0, // FREE
|
|
52
|
+
enabled: true,
|
|
53
|
+
healthCheckInterval: 300000 // Check every 5 minutes
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
// Create agent with cost and performance optimization
|
|
57
|
+
const agent = new LongRunningAgent({
|
|
58
|
+
agentName: 'research-and-code-agent',
|
|
59
|
+
providers,
|
|
60
|
+
fallbackStrategy: {
|
|
61
|
+
type: 'cost-optimized', // Prefer cheaper providers
|
|
62
|
+
maxFailures: 3, // Open circuit breaker after 3 failures
|
|
63
|
+
recoveryTime: 60000, // Try recovery after 1 minute
|
|
64
|
+
retryBackoff: 'exponential',
|
|
65
|
+
costThreshold: 0.50, // Max $0.50 per request
|
|
66
|
+
latencyThreshold: 30000 // Max 30s per request
|
|
67
|
+
},
|
|
68
|
+
checkpointInterval: 30000, // Save state every 30 seconds
|
|
69
|
+
maxRuntime: 3600000, // Max 1 hour
|
|
70
|
+
costBudget: 5.00 // Max $5 total spend
|
|
71
|
+
});
|
|
72
|
+
await agent.start();
|
|
73
|
+
try {
|
|
74
|
+
// Simulate long-running workflow with different task complexities
|
|
75
|
+
console.log('\n📋 Task 1: Simple Code Generation (Gemini optimal)\n');
|
|
76
|
+
const task1 = await agent.executeTask({
|
|
77
|
+
name: 'generate-hello-world',
|
|
78
|
+
complexity: 'simple',
|
|
79
|
+
estimatedTokens: 200,
|
|
80
|
+
execute: async (provider) => {
|
|
81
|
+
console.log(` Using provider: ${provider}`);
|
|
82
|
+
// Simulated API call
|
|
83
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
84
|
+
return { code: 'console.log("Hello World");', provider };
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
console.log(` ✅ Result:`, task1);
|
|
88
|
+
console.log('\n📋 Task 2: Complex Architecture Design (Claude optimal)\n');
|
|
89
|
+
const task2 = await agent.executeTask({
|
|
90
|
+
name: 'design-microservices-architecture',
|
|
91
|
+
complexity: 'complex',
|
|
92
|
+
estimatedTokens: 5000,
|
|
93
|
+
execute: async (provider) => {
|
|
94
|
+
console.log(` Using provider: ${provider}`);
|
|
95
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
96
|
+
return {
|
|
97
|
+
architecture: 'Event-driven microservices with CQRS',
|
|
98
|
+
provider
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
console.log(` ✅ Result:`, task2);
|
|
103
|
+
console.log('\n📋 Task 3: Medium Refactoring (Auto-optimized)\n');
|
|
104
|
+
const task3 = await agent.executeTask({
|
|
105
|
+
name: 'refactor-legacy-code',
|
|
106
|
+
complexity: 'medium',
|
|
107
|
+
estimatedTokens: 1500,
|
|
108
|
+
execute: async (provider) => {
|
|
109
|
+
console.log(` Using provider: ${provider}`);
|
|
110
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
111
|
+
return {
|
|
112
|
+
refactored: true,
|
|
113
|
+
improvements: ['Better naming', 'Modular design'],
|
|
114
|
+
provider
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
console.log(` ✅ Result:`, task3);
|
|
119
|
+
// Simulate provider failure for demonstration
|
|
120
|
+
console.log('\n📋 Task 4: Testing Fallback (Simulated Failure)\n');
|
|
121
|
+
let failureCount = 0;
|
|
122
|
+
const task4 = await agent.executeTask({
|
|
123
|
+
name: 'test-fallback',
|
|
124
|
+
complexity: 'simple',
|
|
125
|
+
estimatedTokens: 300,
|
|
126
|
+
execute: async (provider) => {
|
|
127
|
+
console.log(` Attempting with provider: ${provider}`);
|
|
128
|
+
// Simulate failure on first two attempts
|
|
129
|
+
if (failureCount < 2) {
|
|
130
|
+
failureCount++;
|
|
131
|
+
throw new Error('Simulated rate limit error');
|
|
132
|
+
}
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
134
|
+
return {
|
|
135
|
+
message: 'Success after fallback!',
|
|
136
|
+
provider,
|
|
137
|
+
attempts: failureCount + 1
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
console.log(` ✅ Result:`, task4);
|
|
142
|
+
// Get final status
|
|
143
|
+
console.log('\n📊 Final Agent Status:\n');
|
|
144
|
+
const status = agent.getStatus();
|
|
145
|
+
console.log(JSON.stringify(status, null, 2));
|
|
146
|
+
console.log('\n💰 Cost Summary:\n');
|
|
147
|
+
const metrics = agent.getMetrics();
|
|
148
|
+
console.log('Total Cost:', `$${metrics.costs.total.toFixed(4)}`);
|
|
149
|
+
console.log('Total Tokens:', metrics.costs.totalTokens.toLocaleString());
|
|
150
|
+
console.log('\nBy Provider:');
|
|
151
|
+
for (const [provider, cost] of Object.entries(metrics.costs.byProvider)) {
|
|
152
|
+
console.log(` ${provider}: $${cost.toFixed(4)}`);
|
|
153
|
+
}
|
|
154
|
+
console.log('\n📈 Provider Health:\n');
|
|
155
|
+
for (const health of metrics.health) {
|
|
156
|
+
console.log(`${health.provider}:`);
|
|
157
|
+
console.log(` Healthy: ${health.isHealthy}`);
|
|
158
|
+
console.log(` Success Rate: ${(health.successRate * 100).toFixed(1)}%`);
|
|
159
|
+
console.log(` Avg Latency: ${health.averageLatency.toFixed(0)}ms`);
|
|
160
|
+
console.log(` Circuit Breaker: ${health.circuitBreakerOpen ? 'OPEN' : 'CLOSED'}`);
|
|
161
|
+
console.log('');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error('❌ Agent execution failed:', error);
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await agent.stop();
|
|
169
|
+
console.log('\n✅ Agent stopped successfully\n');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Run example
|
|
173
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
174
|
+
main().catch(console.error);
|
|
175
|
+
}
|
|
176
|
+
export { main as runProviderFallbackExample };
|