agentic-qe 2.2.0 → 2.2.2
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/.claude/agents/qe-security-scanner.md +26 -0
- package/.claude/skills/agentic-quality-engineering/SKILL.md +4 -4
- package/.claude/skills/cicd-pipeline-qe-orchestrator/README.md +14 -11
- package/.claude/skills/skills-manifest.json +2 -2
- package/CHANGELOG.md +75 -0
- package/README.md +92 -214
- package/dist/agents/BaseAgent.d.ts +5 -1
- package/dist/agents/BaseAgent.d.ts.map +1 -1
- package/dist/agents/BaseAgent.js +32 -17
- package/dist/agents/BaseAgent.js.map +1 -1
- package/dist/agents/index.js +3 -3
- package/dist/agents/index.js.map +1 -1
- package/dist/cli/commands/improve/index.d.ts +8 -1
- package/dist/cli/commands/improve/index.d.ts.map +1 -1
- package/dist/cli/commands/improve/index.js +18 -16
- package/dist/cli/commands/improve/index.js.map +1 -1
- package/dist/cli/commands/learn/index.d.ts +10 -2
- package/dist/cli/commands/learn/index.d.ts.map +1 -1
- package/dist/cli/commands/learn/index.js +99 -63
- package/dist/cli/commands/learn/index.js.map +1 -1
- package/dist/cli/commands/patterns/index.d.ts +8 -1
- package/dist/cli/commands/patterns/index.d.ts.map +1 -1
- package/dist/cli/commands/patterns/index.js +79 -45
- package/dist/cli/commands/patterns/index.js.map +1 -1
- package/dist/cli/commands/routing/index.d.ts +5 -0
- package/dist/cli/commands/routing/index.d.ts.map +1 -1
- package/dist/cli/commands/routing/index.js +11 -10
- package/dist/cli/commands/routing/index.js.map +1 -1
- package/dist/cli/init/agents.d.ts +1 -1
- package/dist/cli/init/agents.js +2 -2
- package/dist/cli/init/database-init.d.ts +7 -0
- package/dist/cli/init/database-init.d.ts.map +1 -1
- package/dist/cli/init/database-init.js +29 -48
- package/dist/cli/init/database-init.js.map +1 -1
- package/dist/core/memory/HNSWVectorMemory.d.ts +261 -0
- package/dist/core/memory/HNSWVectorMemory.d.ts.map +1 -0
- package/dist/core/memory/HNSWVectorMemory.js +647 -0
- package/dist/core/memory/HNSWVectorMemory.js.map +1 -0
- package/dist/core/memory/SwarmMemoryManager.d.ts +7 -0
- package/dist/core/memory/SwarmMemoryManager.d.ts.map +1 -1
- package/dist/core/memory/SwarmMemoryManager.js +9 -0
- package/dist/core/memory/SwarmMemoryManager.js.map +1 -1
- package/dist/core/memory/index.d.ts +2 -0
- package/dist/core/memory/index.d.ts.map +1 -1
- package/dist/core/memory/index.js +11 -1
- package/dist/core/memory/index.js.map +1 -1
- package/dist/learning/ExplainableLearning.d.ts +191 -0
- package/dist/learning/ExplainableLearning.d.ts.map +1 -0
- package/dist/learning/ExplainableLearning.js +441 -0
- package/dist/learning/ExplainableLearning.js.map +1 -0
- package/dist/learning/GossipPatternSharingProtocol.d.ts +228 -0
- package/dist/learning/GossipPatternSharingProtocol.d.ts.map +1 -0
- package/dist/learning/GossipPatternSharingProtocol.js +590 -0
- package/dist/learning/GossipPatternSharingProtocol.js.map +1 -0
- package/dist/learning/LearningEngine.d.ts +4 -4
- package/dist/learning/LearningEngine.d.ts.map +1 -1
- package/dist/learning/LearningEngine.js +20 -13
- package/dist/learning/LearningEngine.js.map +1 -1
- package/dist/learning/PerformanceOptimizer.d.ts +268 -0
- package/dist/learning/PerformanceOptimizer.d.ts.map +1 -0
- package/dist/learning/PerformanceOptimizer.js +552 -0
- package/dist/learning/PerformanceOptimizer.js.map +1 -0
- package/dist/learning/PrivacyManager.d.ts +197 -0
- package/dist/learning/PrivacyManager.d.ts.map +1 -0
- package/dist/learning/PrivacyManager.js +551 -0
- package/dist/learning/PrivacyManager.js.map +1 -0
- package/dist/learning/TransferLearningManager.d.ts +212 -0
- package/dist/learning/TransferLearningManager.d.ts.map +1 -0
- package/dist/learning/TransferLearningManager.js +497 -0
- package/dist/learning/TransferLearningManager.js.map +1 -0
- package/dist/learning/algorithms/MAMLMetaLearner.d.ts +218 -0
- package/dist/learning/algorithms/MAMLMetaLearner.d.ts.map +1 -0
- package/dist/learning/algorithms/MAMLMetaLearner.js +532 -0
- package/dist/learning/algorithms/MAMLMetaLearner.js.map +1 -0
- package/dist/learning/algorithms/index.d.ts +4 -1
- package/dist/learning/algorithms/index.d.ts.map +1 -1
- package/dist/learning/algorithms/index.js +7 -1
- package/dist/learning/algorithms/index.js.map +1 -1
- package/dist/learning/index.d.ts +8 -0
- package/dist/learning/index.d.ts.map +1 -1
- package/dist/learning/index.js +17 -1
- package/dist/learning/index.js.map +1 -1
- package/dist/mcp/server-instructions.d.ts +1 -1
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/providers/HybridRouter.d.ts +217 -0
- package/dist/providers/HybridRouter.d.ts.map +1 -0
- package/dist/providers/HybridRouter.js +679 -0
- package/dist/providers/HybridRouter.js.map +1 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +7 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/telemetry/LearningTelemetry.d.ts +190 -0
- package/dist/telemetry/LearningTelemetry.d.ts.map +1 -0
- package/dist/telemetry/LearningTelemetry.js +403 -0
- package/dist/telemetry/LearningTelemetry.js.map +1 -0
- package/dist/telemetry/index.d.ts +1 -0
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +20 -2
- package/dist/telemetry/index.js.map +1 -1
- package/dist/telemetry/instrumentation/agent.d.ts +1 -1
- package/dist/telemetry/instrumentation/agent.js +1 -1
- package/dist/telemetry/instrumentation/index.d.ts +1 -1
- package/dist/telemetry/instrumentation/index.js +1 -1
- package/dist/utils/math.d.ts +11 -0
- package/dist/utils/math.d.ts.map +1 -0
- package/dist/utils/math.js +16 -0
- package/dist/utils/math.js.map +1 -0
- package/docs/reference/agents.md +1 -1
- package/docs/reference/skills.md +3 -3
- package/docs/reference/usage.md +4 -4
- package/package.json +14 -1
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* HybridRouter - Intelligent LLM Provider Router
|
|
4
|
+
*
|
|
5
|
+
* Provides intelligent routing between local (ruvllm) and cloud (Claude, GPT) providers
|
|
6
|
+
* with circuit breakers, cost optimization, and adaptive learning.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Task complexity analysis for optimal routing
|
|
10
|
+
* - Latency-aware provider selection
|
|
11
|
+
* - Cost tracking and savings estimation
|
|
12
|
+
* - Circuit breaker pattern for failing providers
|
|
13
|
+
* - Priority queuing for urgent requests
|
|
14
|
+
* - Learning from routing outcomes
|
|
15
|
+
* - Privacy-first routing for sensitive data
|
|
16
|
+
*
|
|
17
|
+
* @module providers/HybridRouter
|
|
18
|
+
* @version 1.0.0
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.HybridRouter = exports.RoutingStrategy = exports.TaskComplexity = exports.RequestPriority = void 0;
|
|
22
|
+
const ILLMProvider_1 = require("./ILLMProvider");
|
|
23
|
+
const ClaudeProvider_1 = require("./ClaudeProvider");
|
|
24
|
+
const RuvllmProvider_1 = require("./RuvllmProvider");
|
|
25
|
+
const Logger_1 = require("../utils/Logger");
|
|
26
|
+
/**
|
|
27
|
+
* Request priority levels
|
|
28
|
+
*/
|
|
29
|
+
var RequestPriority;
|
|
30
|
+
(function (RequestPriority) {
|
|
31
|
+
RequestPriority[RequestPriority["LOW"] = 0] = "LOW";
|
|
32
|
+
RequestPriority[RequestPriority["NORMAL"] = 1] = "NORMAL";
|
|
33
|
+
RequestPriority[RequestPriority["HIGH"] = 2] = "HIGH";
|
|
34
|
+
RequestPriority[RequestPriority["URGENT"] = 3] = "URGENT";
|
|
35
|
+
})(RequestPriority || (exports.RequestPriority = RequestPriority = {}));
|
|
36
|
+
/**
|
|
37
|
+
* Task complexity classification
|
|
38
|
+
*/
|
|
39
|
+
var TaskComplexity;
|
|
40
|
+
(function (TaskComplexity) {
|
|
41
|
+
TaskComplexity["SIMPLE"] = "simple";
|
|
42
|
+
TaskComplexity["MODERATE"] = "moderate";
|
|
43
|
+
TaskComplexity["COMPLEX"] = "complex";
|
|
44
|
+
TaskComplexity["VERY_COMPLEX"] = "very_complex"; // Advanced analysis, architectural design
|
|
45
|
+
})(TaskComplexity || (exports.TaskComplexity = TaskComplexity = {}));
|
|
46
|
+
/**
|
|
47
|
+
* Routing strategy
|
|
48
|
+
*/
|
|
49
|
+
var RoutingStrategy;
|
|
50
|
+
(function (RoutingStrategy) {
|
|
51
|
+
RoutingStrategy["COST_OPTIMIZED"] = "cost_optimized";
|
|
52
|
+
RoutingStrategy["LATENCY_OPTIMIZED"] = "latency_optimized";
|
|
53
|
+
RoutingStrategy["QUALITY_OPTIMIZED"] = "quality_optimized";
|
|
54
|
+
RoutingStrategy["BALANCED"] = "balanced";
|
|
55
|
+
RoutingStrategy["PRIVACY_FIRST"] = "privacy_first"; // Always use local
|
|
56
|
+
})(RoutingStrategy || (exports.RoutingStrategy = RoutingStrategy = {}));
|
|
57
|
+
/**
|
|
58
|
+
* Circuit breaker state
|
|
59
|
+
*/
|
|
60
|
+
var CircuitState;
|
|
61
|
+
(function (CircuitState) {
|
|
62
|
+
CircuitState["CLOSED"] = "closed";
|
|
63
|
+
CircuitState["OPEN"] = "open";
|
|
64
|
+
CircuitState["HALF_OPEN"] = "half_open"; // Testing if provider recovered
|
|
65
|
+
})(CircuitState || (CircuitState = {}));
|
|
66
|
+
/**
|
|
67
|
+
* HybridRouter - Intelligent provider routing implementation
|
|
68
|
+
*
|
|
69
|
+
* Routes LLM requests between local and cloud providers based on:
|
|
70
|
+
* - Task complexity and required capabilities
|
|
71
|
+
* - Latency and cost constraints
|
|
72
|
+
* - Provider health and circuit breaker state
|
|
73
|
+
* - Request priority
|
|
74
|
+
* - Privacy requirements
|
|
75
|
+
*/
|
|
76
|
+
class HybridRouter {
|
|
77
|
+
constructor(config = {}) {
|
|
78
|
+
this.logger = Logger_1.Logger.getInstance();
|
|
79
|
+
this.config = {
|
|
80
|
+
name: config.name || 'hybrid-router',
|
|
81
|
+
debug: config.debug ?? false,
|
|
82
|
+
timeout: config.timeout ?? 120000,
|
|
83
|
+
maxRetries: config.maxRetries ?? 2,
|
|
84
|
+
defaultStrategy: config.defaultStrategy ?? RoutingStrategy.BALANCED,
|
|
85
|
+
enableCircuitBreaker: config.enableCircuitBreaker ?? true,
|
|
86
|
+
circuitBreakerThreshold: config.circuitBreakerThreshold ?? 5,
|
|
87
|
+
circuitBreakerTimeout: config.circuitBreakerTimeout ?? 60000,
|
|
88
|
+
maxLocalLatency: config.maxLocalLatency ?? 5000,
|
|
89
|
+
enableLearning: config.enableLearning ?? true,
|
|
90
|
+
privacyKeywords: config.privacyKeywords ?? [
|
|
91
|
+
'secret', 'password', 'token', 'key', 'credential',
|
|
92
|
+
'private', 'confidential', 'internal', 'api_key'
|
|
93
|
+
],
|
|
94
|
+
claude: config.claude,
|
|
95
|
+
ruvllm: config.ruvllm
|
|
96
|
+
};
|
|
97
|
+
this.circuitBreakers = new Map();
|
|
98
|
+
this.routingHistory = [];
|
|
99
|
+
this.isInitialized = false;
|
|
100
|
+
this.totalCost = 0;
|
|
101
|
+
this.requestCount = 0;
|
|
102
|
+
this.localRequestCount = 0;
|
|
103
|
+
this.cloudRequestCount = 0;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Initialize the hybrid router and its providers
|
|
107
|
+
*/
|
|
108
|
+
async initialize() {
|
|
109
|
+
if (this.isInitialized) {
|
|
110
|
+
this.logger.warn('HybridRouter already initialized');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Initialize local provider (ruvllm)
|
|
114
|
+
try {
|
|
115
|
+
this.localProvider = new RuvllmProvider_1.RuvllmProvider(this.config.ruvllm ?? {});
|
|
116
|
+
await this.localProvider.initialize();
|
|
117
|
+
this.initCircuitBreaker('local');
|
|
118
|
+
this.logger.info('Local provider (ruvllm) initialized');
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
this.logger.warn('Failed to initialize local provider', {
|
|
122
|
+
error: error.message
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Initialize cloud provider (Claude)
|
|
126
|
+
try {
|
|
127
|
+
this.cloudProvider = new ClaudeProvider_1.ClaudeProvider(this.config.claude ?? {});
|
|
128
|
+
await this.cloudProvider.initialize();
|
|
129
|
+
this.initCircuitBreaker('cloud');
|
|
130
|
+
this.logger.info('Cloud provider (Claude) initialized');
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
this.logger.warn('Failed to initialize cloud provider', {
|
|
134
|
+
error: error.message
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (!this.localProvider && !this.cloudProvider) {
|
|
138
|
+
throw new ILLMProvider_1.LLMProviderError('Failed to initialize any providers', 'hybrid-router', 'INIT_ERROR', false);
|
|
139
|
+
}
|
|
140
|
+
this.isInitialized = true;
|
|
141
|
+
this.logger.info('HybridRouter initialized', {
|
|
142
|
+
hasLocal: !!this.localProvider,
|
|
143
|
+
hasCloud: !!this.cloudProvider,
|
|
144
|
+
strategy: this.config.defaultStrategy
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Complete a prompt with intelligent routing
|
|
149
|
+
*/
|
|
150
|
+
async complete(options) {
|
|
151
|
+
this.ensureInitialized();
|
|
152
|
+
const startTime = Date.now();
|
|
153
|
+
// Analyze request and make routing decision
|
|
154
|
+
const decision = this.makeRoutingDecision(options);
|
|
155
|
+
this.logger.debug('Routing decision made', {
|
|
156
|
+
provider: decision.provider,
|
|
157
|
+
reason: decision.reason,
|
|
158
|
+
complexity: decision.complexity
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
let response;
|
|
162
|
+
if (decision.provider === 'local' && this.localProvider) {
|
|
163
|
+
response = await this.executeWithCircuitBreaker('local', () => this.localProvider.complete(options));
|
|
164
|
+
this.localRequestCount++;
|
|
165
|
+
}
|
|
166
|
+
else if (decision.provider === 'cloud' && this.cloudProvider) {
|
|
167
|
+
response = await this.executeWithCircuitBreaker('cloud', () => this.cloudProvider.complete(options));
|
|
168
|
+
this.cloudRequestCount++;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
throw new ILLMProvider_1.LLMProviderError(`Selected provider (${decision.provider}) not available`, 'hybrid-router', 'PROVIDER_UNAVAILABLE', true);
|
|
172
|
+
}
|
|
173
|
+
this.requestCount++;
|
|
174
|
+
// Track cost
|
|
175
|
+
const actualCost = this.calculateCost(response, decision.provider);
|
|
176
|
+
this.totalCost += actualCost;
|
|
177
|
+
// Record outcome for learning
|
|
178
|
+
if (this.config.enableLearning) {
|
|
179
|
+
this.recordOutcome({
|
|
180
|
+
decision,
|
|
181
|
+
actualCost,
|
|
182
|
+
actualLatency: Date.now() - startTime,
|
|
183
|
+
success: true
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// Add routing metadata to response
|
|
187
|
+
response.metadata = {
|
|
188
|
+
...response.metadata,
|
|
189
|
+
routingDecision: decision,
|
|
190
|
+
actualCost
|
|
191
|
+
};
|
|
192
|
+
return response;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
// Record failure
|
|
196
|
+
if (this.config.enableLearning) {
|
|
197
|
+
this.recordOutcome({
|
|
198
|
+
decision,
|
|
199
|
+
actualCost: 0,
|
|
200
|
+
actualLatency: Date.now() - startTime,
|
|
201
|
+
success: false,
|
|
202
|
+
error: error.message
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// Try fallback if enabled
|
|
206
|
+
if (this.config.enableCircuitBreaker) {
|
|
207
|
+
return this.executeFallback(options, decision.provider);
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Stream completion with routing
|
|
214
|
+
*/
|
|
215
|
+
async *streamComplete(options) {
|
|
216
|
+
this.ensureInitialized();
|
|
217
|
+
const decision = this.makeRoutingDecision(options);
|
|
218
|
+
try {
|
|
219
|
+
if (decision.provider === 'local' && this.localProvider) {
|
|
220
|
+
yield* this.localProvider.streamComplete(options);
|
|
221
|
+
this.localRequestCount++;
|
|
222
|
+
}
|
|
223
|
+
else if (decision.provider === 'cloud' && this.cloudProvider) {
|
|
224
|
+
yield* this.cloudProvider.streamComplete(options);
|
|
225
|
+
this.cloudRequestCount++;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
throw new ILLMProvider_1.LLMProviderError(`Selected provider (${decision.provider}) not available`, 'hybrid-router', 'PROVIDER_UNAVAILABLE', true);
|
|
229
|
+
}
|
|
230
|
+
this.requestCount++;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
throw new ILLMProvider_1.LLMProviderError(`Stream failed: ${error.message}`, 'hybrid-router', 'STREAM_ERROR', true, error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Generate embeddings (prefer local)
|
|
238
|
+
*/
|
|
239
|
+
async embed(options) {
|
|
240
|
+
this.ensureInitialized();
|
|
241
|
+
// Always prefer local for embeddings (privacy + cost)
|
|
242
|
+
if (this.localProvider && this.isProviderAvailable('local')) {
|
|
243
|
+
try {
|
|
244
|
+
return await this.localProvider.embed(options);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
this.logger.warn('Local embedding failed, no fallback available', {
|
|
248
|
+
error: error.message
|
|
249
|
+
});
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
throw new ILLMProvider_1.LLMProviderError('Embeddings not available. Local provider required.', 'hybrid-router', 'UNSUPPORTED', false);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Count tokens
|
|
257
|
+
*/
|
|
258
|
+
async countTokens(options) {
|
|
259
|
+
// Use local provider if available (faster, free)
|
|
260
|
+
if (this.localProvider) {
|
|
261
|
+
return this.localProvider.countTokens(options);
|
|
262
|
+
}
|
|
263
|
+
if (this.cloudProvider) {
|
|
264
|
+
return this.cloudProvider.countTokens(options);
|
|
265
|
+
}
|
|
266
|
+
throw new ILLMProvider_1.LLMProviderError('No provider available for token counting', 'hybrid-router', 'PROVIDER_UNAVAILABLE', false);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Health check all providers
|
|
270
|
+
*/
|
|
271
|
+
async healthCheck() {
|
|
272
|
+
const checks = [];
|
|
273
|
+
if (this.localProvider) {
|
|
274
|
+
try {
|
|
275
|
+
const status = await this.localProvider.healthCheck();
|
|
276
|
+
checks.push({ provider: 'local', status });
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
checks.push({
|
|
280
|
+
provider: 'local',
|
|
281
|
+
status: {
|
|
282
|
+
healthy: false,
|
|
283
|
+
error: error.message,
|
|
284
|
+
timestamp: new Date()
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (this.cloudProvider) {
|
|
290
|
+
try {
|
|
291
|
+
const status = await this.cloudProvider.healthCheck();
|
|
292
|
+
checks.push({ provider: 'cloud', status });
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
checks.push({
|
|
296
|
+
provider: 'cloud',
|
|
297
|
+
status: {
|
|
298
|
+
healthy: false,
|
|
299
|
+
error: error.message,
|
|
300
|
+
timestamp: new Date()
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const healthyCount = checks.filter(c => c.status.healthy).length;
|
|
306
|
+
return {
|
|
307
|
+
healthy: healthyCount > 0,
|
|
308
|
+
timestamp: new Date(),
|
|
309
|
+
metadata: {
|
|
310
|
+
providers: checks,
|
|
311
|
+
circuitBreakers: Object.fromEntries(this.circuitBreakers),
|
|
312
|
+
requestCount: this.requestCount,
|
|
313
|
+
localRequests: this.localRequestCount,
|
|
314
|
+
cloudRequests: this.cloudRequestCount
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Get metadata (aggregated from all providers)
|
|
320
|
+
*/
|
|
321
|
+
getMetadata() {
|
|
322
|
+
const metadata = [];
|
|
323
|
+
if (this.localProvider) {
|
|
324
|
+
metadata.push(this.localProvider.getMetadata());
|
|
325
|
+
}
|
|
326
|
+
if (this.cloudProvider) {
|
|
327
|
+
metadata.push(this.cloudProvider.getMetadata());
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
name: 'hybrid-router',
|
|
331
|
+
version: '1.0.0',
|
|
332
|
+
models: [...new Set(metadata.flatMap(m => m.models))],
|
|
333
|
+
capabilities: {
|
|
334
|
+
streaming: metadata.some(m => m.capabilities.streaming),
|
|
335
|
+
caching: metadata.some(m => m.capabilities.caching),
|
|
336
|
+
embeddings: metadata.some(m => m.capabilities.embeddings),
|
|
337
|
+
vision: metadata.some(m => m.capabilities.vision)
|
|
338
|
+
},
|
|
339
|
+
costs: {
|
|
340
|
+
inputPerMillion: Math.min(...metadata.map(m => m.costs.inputPerMillion)),
|
|
341
|
+
outputPerMillion: Math.min(...metadata.map(m => m.costs.outputPerMillion))
|
|
342
|
+
},
|
|
343
|
+
location: 'cloud' // Hybrid, but defaults to cloud
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Shutdown all providers
|
|
348
|
+
*/
|
|
349
|
+
async shutdown() {
|
|
350
|
+
const shutdownPromises = [];
|
|
351
|
+
if (this.localProvider) {
|
|
352
|
+
shutdownPromises.push(this.localProvider.shutdown());
|
|
353
|
+
}
|
|
354
|
+
if (this.cloudProvider) {
|
|
355
|
+
shutdownPromises.push(this.cloudProvider.shutdown());
|
|
356
|
+
}
|
|
357
|
+
await Promise.allSettled(shutdownPromises);
|
|
358
|
+
this.isInitialized = false;
|
|
359
|
+
this.logger.info('HybridRouter shutdown', {
|
|
360
|
+
totalRequests: this.requestCount,
|
|
361
|
+
totalCost: this.totalCost
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Track cost (aggregated across providers)
|
|
366
|
+
*/
|
|
367
|
+
trackCost(usage) {
|
|
368
|
+
// This is handled internally per request
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Get cost savings report
|
|
373
|
+
*/
|
|
374
|
+
getCostSavingsReport() {
|
|
375
|
+
// Estimate what cloud cost would have been for all requests
|
|
376
|
+
const cloudCostPerRequest = 0.01; // Rough estimate
|
|
377
|
+
const estimatedCloudCost = this.requestCount * cloudCostPerRequest;
|
|
378
|
+
const savings = estimatedCloudCost - this.totalCost;
|
|
379
|
+
const savingsPercentage = estimatedCloudCost > 0
|
|
380
|
+
? (savings / estimatedCloudCost) * 100
|
|
381
|
+
: 0;
|
|
382
|
+
return {
|
|
383
|
+
totalRequests: this.requestCount,
|
|
384
|
+
localRequests: this.localRequestCount,
|
|
385
|
+
cloudRequests: this.cloudRequestCount,
|
|
386
|
+
totalCost: this.totalCost,
|
|
387
|
+
estimatedCloudCost,
|
|
388
|
+
savings,
|
|
389
|
+
savingsPercentage
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Get routing statistics
|
|
394
|
+
*/
|
|
395
|
+
getRoutingStats() {
|
|
396
|
+
const successCount = this.routingHistory.filter(o => o.success).length;
|
|
397
|
+
const localOutcomes = this.routingHistory.filter(o => o.decision.provider === 'local');
|
|
398
|
+
const cloudOutcomes = this.routingHistory.filter(o => o.decision.provider === 'cloud');
|
|
399
|
+
const avgLocalLatency = localOutcomes.length > 0
|
|
400
|
+
? localOutcomes.reduce((sum, o) => sum + o.actualLatency, 0) / localOutcomes.length
|
|
401
|
+
: 0;
|
|
402
|
+
const avgCloudLatency = cloudOutcomes.length > 0
|
|
403
|
+
? cloudOutcomes.reduce((sum, o) => sum + o.actualLatency, 0) / cloudOutcomes.length
|
|
404
|
+
: 0;
|
|
405
|
+
return {
|
|
406
|
+
totalDecisions: this.routingHistory.length,
|
|
407
|
+
localDecisions: localOutcomes.length,
|
|
408
|
+
cloudDecisions: cloudOutcomes.length,
|
|
409
|
+
averageLocalLatency: avgLocalLatency,
|
|
410
|
+
averageCloudLatency: avgCloudLatency,
|
|
411
|
+
successRate: this.routingHistory.length > 0
|
|
412
|
+
? (successCount / this.routingHistory.length) * 100
|
|
413
|
+
: 0
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Make intelligent routing decision
|
|
418
|
+
*/
|
|
419
|
+
makeRoutingDecision(options, strategy, priority = RequestPriority.NORMAL) {
|
|
420
|
+
const activeStrategy = strategy || this.config.defaultStrategy;
|
|
421
|
+
const complexity = this.analyzeComplexity(options);
|
|
422
|
+
// Check for privacy-sensitive content
|
|
423
|
+
if (this.containsPrivacySensitiveData(options)) {
|
|
424
|
+
return this.createDecision('local', 'ruvllm', 'Privacy-sensitive data detected', complexity, priority);
|
|
425
|
+
}
|
|
426
|
+
// PRIVACY_FIRST strategy
|
|
427
|
+
if (activeStrategy === RoutingStrategy.PRIVACY_FIRST && this.isProviderAvailable('local')) {
|
|
428
|
+
return this.createDecision('local', 'ruvllm', 'Privacy-first strategy', complexity, priority);
|
|
429
|
+
}
|
|
430
|
+
// Check circuit breakers
|
|
431
|
+
const localAvailable = this.isProviderAvailable('local');
|
|
432
|
+
const cloudAvailable = this.isProviderAvailable('cloud');
|
|
433
|
+
if (!localAvailable && !cloudAvailable) {
|
|
434
|
+
throw new ILLMProvider_1.LLMProviderError('No providers available (circuit breakers open)', 'hybrid-router', 'NO_PROVIDERS', true);
|
|
435
|
+
}
|
|
436
|
+
// COST_OPTIMIZED strategy
|
|
437
|
+
if (activeStrategy === RoutingStrategy.COST_OPTIMIZED) {
|
|
438
|
+
if (localAvailable) {
|
|
439
|
+
return this.createDecision('local', 'ruvllm', 'Cost optimization (local is free)', complexity, priority);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// LATENCY_OPTIMIZED strategy
|
|
443
|
+
if (activeStrategy === RoutingStrategy.LATENCY_OPTIMIZED) {
|
|
444
|
+
// Use historical data to choose faster provider
|
|
445
|
+
const stats = this.getRoutingStats();
|
|
446
|
+
if (stats.averageLocalLatency > 0 && stats.averageCloudLatency > 0) {
|
|
447
|
+
if (stats.averageLocalLatency < stats.averageCloudLatency && localAvailable) {
|
|
448
|
+
return this.createDecision('local', 'ruvllm', 'Latency optimization (local faster)', complexity, priority);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// QUALITY_OPTIMIZED or BALANCED strategy
|
|
453
|
+
// Route based on complexity
|
|
454
|
+
switch (complexity) {
|
|
455
|
+
case TaskComplexity.SIMPLE:
|
|
456
|
+
case TaskComplexity.MODERATE:
|
|
457
|
+
// Simple tasks → local
|
|
458
|
+
if (localAvailable) {
|
|
459
|
+
return this.createDecision('local', 'ruvllm', `${complexity} task suitable for local`, complexity, priority);
|
|
460
|
+
}
|
|
461
|
+
break;
|
|
462
|
+
case TaskComplexity.COMPLEX:
|
|
463
|
+
// Complex tasks → prefer cloud for quality, but local if cost-sensitive
|
|
464
|
+
if (activeStrategy === RoutingStrategy.BALANCED && localAvailable) {
|
|
465
|
+
return this.createDecision('local', 'ruvllm', 'Balanced strategy, trying local first', complexity, priority);
|
|
466
|
+
}
|
|
467
|
+
if (cloudAvailable) {
|
|
468
|
+
return this.createDecision('cloud', 'claude', `${complexity} task needs cloud quality`, complexity, priority);
|
|
469
|
+
}
|
|
470
|
+
break;
|
|
471
|
+
case TaskComplexity.VERY_COMPLEX:
|
|
472
|
+
// Very complex → always cloud if available
|
|
473
|
+
if (cloudAvailable) {
|
|
474
|
+
return this.createDecision('cloud', 'claude', `${complexity} task requires cloud`, complexity, priority);
|
|
475
|
+
}
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
// Fallback: use whatever is available
|
|
479
|
+
if (cloudAvailable) {
|
|
480
|
+
return this.createDecision('cloud', 'claude', 'Fallback to cloud', complexity, priority);
|
|
481
|
+
}
|
|
482
|
+
if (localAvailable) {
|
|
483
|
+
return this.createDecision('local', 'ruvllm', 'Fallback to local', complexity, priority);
|
|
484
|
+
}
|
|
485
|
+
throw new ILLMProvider_1.LLMProviderError('No providers available', 'hybrid-router', 'NO_PROVIDERS', true);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Analyze task complexity
|
|
489
|
+
*/
|
|
490
|
+
analyzeComplexity(options) {
|
|
491
|
+
const maxTokens = options.maxTokens || 0;
|
|
492
|
+
const messageCount = options.messages.length;
|
|
493
|
+
const totalContent = options.messages
|
|
494
|
+
.map(m => typeof m.content === 'string' ? m.content : m.content.map(c => c.text || '').join(''))
|
|
495
|
+
.join(' ');
|
|
496
|
+
const contentLength = totalContent.length;
|
|
497
|
+
// Complexity heuristics
|
|
498
|
+
const codePatterns = /```|function|class|import|export|const|let|var/gi;
|
|
499
|
+
const hasCode = codePatterns.test(totalContent);
|
|
500
|
+
const complexKeywords = /architect|design|analyze|optimize|refactor|debug/gi;
|
|
501
|
+
const hasComplexKeywords = complexKeywords.test(totalContent);
|
|
502
|
+
// Scoring
|
|
503
|
+
let score = 0;
|
|
504
|
+
if (contentLength > 5000)
|
|
505
|
+
score += 2;
|
|
506
|
+
else if (contentLength > 2000)
|
|
507
|
+
score += 1;
|
|
508
|
+
if (maxTokens > 4000)
|
|
509
|
+
score += 2;
|
|
510
|
+
else if (maxTokens > 1000)
|
|
511
|
+
score += 1;
|
|
512
|
+
if (messageCount > 5)
|
|
513
|
+
score += 1;
|
|
514
|
+
if (hasCode)
|
|
515
|
+
score += 1;
|
|
516
|
+
if (hasComplexKeywords)
|
|
517
|
+
score += 1;
|
|
518
|
+
// Classification
|
|
519
|
+
if (score >= 6)
|
|
520
|
+
return TaskComplexity.VERY_COMPLEX;
|
|
521
|
+
if (score >= 4)
|
|
522
|
+
return TaskComplexity.COMPLEX;
|
|
523
|
+
if (score >= 2)
|
|
524
|
+
return TaskComplexity.MODERATE;
|
|
525
|
+
return TaskComplexity.SIMPLE;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Check for privacy-sensitive data
|
|
529
|
+
*/
|
|
530
|
+
containsPrivacySensitiveData(options) {
|
|
531
|
+
const allContent = options.messages
|
|
532
|
+
.map(m => typeof m.content === 'string' ? m.content : m.content.map(c => c.text || '').join(''))
|
|
533
|
+
.join(' ')
|
|
534
|
+
.toLowerCase();
|
|
535
|
+
return this.config.privacyKeywords.some(keyword => allContent.includes(keyword.toLowerCase()));
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Check if provider is available (circuit breaker check)
|
|
539
|
+
*/
|
|
540
|
+
isProviderAvailable(provider) {
|
|
541
|
+
if (provider === 'local' && !this.localProvider)
|
|
542
|
+
return false;
|
|
543
|
+
if (provider === 'cloud' && !this.cloudProvider)
|
|
544
|
+
return false;
|
|
545
|
+
if (!this.config.enableCircuitBreaker)
|
|
546
|
+
return true;
|
|
547
|
+
const breaker = this.circuitBreakers.get(provider);
|
|
548
|
+
if (!breaker)
|
|
549
|
+
return true;
|
|
550
|
+
if (breaker.state === CircuitState.OPEN) {
|
|
551
|
+
// Check if timeout expired
|
|
552
|
+
if (breaker.nextAttemptTime && new Date() >= breaker.nextAttemptTime) {
|
|
553
|
+
breaker.state = CircuitState.HALF_OPEN;
|
|
554
|
+
this.logger.info(`Circuit breaker for ${provider} entering HALF_OPEN state`);
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Execute with circuit breaker protection
|
|
563
|
+
*/
|
|
564
|
+
async executeWithCircuitBreaker(provider, operation) {
|
|
565
|
+
const breaker = this.circuitBreakers.get(provider);
|
|
566
|
+
if (!breaker) {
|
|
567
|
+
return operation();
|
|
568
|
+
}
|
|
569
|
+
if (breaker.state === CircuitState.OPEN) {
|
|
570
|
+
throw new ILLMProvider_1.LLMProviderError(`Circuit breaker OPEN for ${provider}`, 'hybrid-router', 'CIRCUIT_OPEN', true);
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
const result = await operation();
|
|
574
|
+
// Success
|
|
575
|
+
breaker.successCount++;
|
|
576
|
+
breaker.failureCount = 0;
|
|
577
|
+
if (breaker.state === CircuitState.HALF_OPEN) {
|
|
578
|
+
// Recovered, close circuit
|
|
579
|
+
breaker.state = CircuitState.CLOSED;
|
|
580
|
+
this.logger.info(`Circuit breaker for ${provider} closed (recovered)`);
|
|
581
|
+
}
|
|
582
|
+
return result;
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
// Failure
|
|
586
|
+
breaker.failureCount++;
|
|
587
|
+
breaker.lastFailureTime = new Date();
|
|
588
|
+
if (breaker.failureCount >= this.config.circuitBreakerThreshold) {
|
|
589
|
+
breaker.state = CircuitState.OPEN;
|
|
590
|
+
breaker.nextAttemptTime = new Date(Date.now() + this.config.circuitBreakerTimeout);
|
|
591
|
+
this.logger.error(`Circuit breaker for ${provider} opened after ${breaker.failureCount} failures`);
|
|
592
|
+
}
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Execute fallback to alternative provider
|
|
598
|
+
*/
|
|
599
|
+
async executeFallback(options, failedProvider) {
|
|
600
|
+
const fallbackProvider = failedProvider === 'local' ? 'cloud' : 'local';
|
|
601
|
+
this.logger.warn(`Attempting fallback to ${fallbackProvider} after ${failedProvider} failure`);
|
|
602
|
+
if (!this.isProviderAvailable(fallbackProvider)) {
|
|
603
|
+
throw new ILLMProvider_1.LLMProviderError(`Fallback provider ${fallbackProvider} not available`, 'hybrid-router', 'FALLBACK_FAILED', false);
|
|
604
|
+
}
|
|
605
|
+
if (fallbackProvider === 'local' && this.localProvider) {
|
|
606
|
+
return this.localProvider.complete(options);
|
|
607
|
+
}
|
|
608
|
+
if (fallbackProvider === 'cloud' && this.cloudProvider) {
|
|
609
|
+
return this.cloudProvider.complete(options);
|
|
610
|
+
}
|
|
611
|
+
throw new ILLMProvider_1.LLMProviderError('Fallback failed: no alternative provider', 'hybrid-router', 'FALLBACK_FAILED', false);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Initialize circuit breaker for a provider
|
|
615
|
+
*/
|
|
616
|
+
initCircuitBreaker(provider) {
|
|
617
|
+
this.circuitBreakers.set(provider, {
|
|
618
|
+
state: CircuitState.CLOSED,
|
|
619
|
+
failureCount: 0,
|
|
620
|
+
successCount: 0
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Create routing decision object
|
|
625
|
+
*/
|
|
626
|
+
createDecision(provider, providerName, reason, complexity, priority) {
|
|
627
|
+
return {
|
|
628
|
+
provider,
|
|
629
|
+
providerName,
|
|
630
|
+
reason,
|
|
631
|
+
estimatedCost: provider === 'local' ? 0 : 0.01, // Rough estimate
|
|
632
|
+
estimatedLatency: provider === 'local' ? 2000 : 1000, // Rough estimate
|
|
633
|
+
complexity,
|
|
634
|
+
priority,
|
|
635
|
+
timestamp: new Date()
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Calculate actual cost from response
|
|
640
|
+
*/
|
|
641
|
+
calculateCost(response, provider) {
|
|
642
|
+
if (provider === 'local') {
|
|
643
|
+
return 0; // Local is free
|
|
644
|
+
}
|
|
645
|
+
// Use cloud provider's tracking
|
|
646
|
+
if (this.cloudProvider) {
|
|
647
|
+
return this.cloudProvider.trackCost(response.usage);
|
|
648
|
+
}
|
|
649
|
+
return 0;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Record routing outcome for learning
|
|
653
|
+
*/
|
|
654
|
+
recordOutcome(outcome) {
|
|
655
|
+
this.routingHistory.push(outcome);
|
|
656
|
+
// Keep only last 1000 outcomes
|
|
657
|
+
if (this.routingHistory.length > 1000) {
|
|
658
|
+
this.routingHistory.shift();
|
|
659
|
+
}
|
|
660
|
+
if (this.config.debug) {
|
|
661
|
+
this.logger.debug('Routing outcome recorded', {
|
|
662
|
+
provider: outcome.decision.provider,
|
|
663
|
+
success: outcome.success,
|
|
664
|
+
latency: outcome.actualLatency,
|
|
665
|
+
cost: outcome.actualCost
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Ensure router is initialized
|
|
671
|
+
*/
|
|
672
|
+
ensureInitialized() {
|
|
673
|
+
if (!this.isInitialized) {
|
|
674
|
+
throw new ILLMProvider_1.LLMProviderError('HybridRouter not initialized. Call initialize() first.', 'hybrid-router', 'NOT_INITIALIZED', false);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
exports.HybridRouter = HybridRouter;
|
|
679
|
+
//# sourceMappingURL=HybridRouter.js.map
|