anon-identity
Version:
Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure
753 lines • 27.9 kB
JavaScript
"use strict";
/**
* Provider Selection Engine for MCP
*
* Intelligent provider selection based on capabilities, performance, cost, and availability
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProviderSelector = exports.SelectionStrategy = void 0;
const events_1 = require("events");
const types_1 = require("../types");
/**
* Provider selection strategy
*/
var SelectionStrategy;
(function (SelectionStrategy) {
SelectionStrategy["PERFORMANCE"] = "performance";
SelectionStrategy["COST_OPTIMIZED"] = "cost_optimized";
SelectionStrategy["RELIABILITY"] = "reliability";
SelectionStrategy["CAPABILITY_MATCH"] = "capability_match";
SelectionStrategy["LOAD_BALANCED"] = "load_balanced";
SelectionStrategy["SMART_ADAPTIVE"] = "smart_adaptive";
})(SelectionStrategy || (exports.SelectionStrategy = SelectionStrategy = {}));
/**
* Provider Selector
*/
class ProviderSelector extends events_1.EventEmitter {
constructor(providers, config = {
scoringWeights: {
performance: 0.25,
reliability: 0.25,
capability: 0.2,
cost: 0.15,
availability: 0.1,
preference: 0.05
},
loadBalancing: {
strategy: 'latency_based',
stickySession: false,
healthCheckInterval: 30000,
failoverThreshold: 3
},
circuitBreakerThreshold: 5,
circuitBreakerTimeout: 60000,
adaptiveLearningEnabled: true,
fallbackChainLength: 3
}) {
super();
this.providers = providers;
this.config = config;
this.providerMetrics = new Map();
this.providerHealth = new Map();
this.selectionHistory = [];
this.circuitBreakers = new Map();
this.loadBalancer = new LoadBalancer(this.config.loadBalancing);
this.adaptiveLearning = new AdaptiveLearning();
this.initializeProviderHealth();
this.startHealthChecks();
}
/**
* Select optimal provider for request
*/
async selectProvider(request, criteria, strategy = SelectionStrategy.SMART_ADAPTIVE) {
// Filter available providers
const availableProviders = await this.getAvailableProviders(criteria);
if (availableProviders.length === 0) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.PROVIDER_UNAVAILABLE,
message: 'No providers available for request criteria',
timestamp: new Date(),
retryable: true
});
}
// Score providers based on strategy
const scoredProviders = await this.scoreProviders(availableProviders, criteria, strategy);
// Select primary and fallback providers
const primaryProvider = scoredProviders[0].provider;
const fallbackProviders = scoredProviders
.slice(1, this.config.fallbackChainLength + 1)
.map(sp => sp.provider);
// Calculate estimates
const estimatedCost = await this.estimateCost(primaryProvider, request);
const estimatedLatency = await this.estimateLatency(primaryProvider, criteria);
// Build result
const result = {
primaryProvider,
fallbackProviders,
reasoning: this.buildReasoning(scoredProviders[0], criteria, strategy),
score: scoredProviders[0].score,
estimatedCost,
estimatedLatency,
alternativeOptions: scoredProviders.slice(1, 4).map(sp => ({
provider: sp.provider,
score: sp.score,
tradeoffs: this.calculateTradeoffs(sp.provider, primaryProvider)
}))
};
// Record selection
this.recordSelection(criteria, result);
// Apply load balancing if needed
if (scoredProviders.length > 1 &&
scoredProviders[0].score - scoredProviders[1].score < 0.1) {
const balancedProvider = await this.loadBalancer.selectProvider(scoredProviders.slice(0, 3).map(sp => sp.provider));
if (balancedProvider && balancedProvider.id !== primaryProvider.id) {
result.primaryProvider = balancedProvider;
result.reasoning += ' (Load balanced)';
}
}
this.emit('provider_selected', { request, criteria, result });
return result;
}
/**
* Get available providers based on criteria
*/
async getAvailableProviders(criteria) {
const available = [];
for (const [id, provider] of this.providers) {
// Check circuit breaker
const breaker = this.circuitBreakers.get(id);
if (breaker?.state === 'open') {
continue;
}
// Check health
const health = this.providerHealth.get(id);
if (!health || health.status !== 'healthy') {
continue;
}
// Check capabilities
if (!this.checkCapabilities(provider, criteria)) {
continue;
}
// Check regulatory compliance
if (criteria.context?.regulatoryRequirements) {
if (!this.checkCompliance(provider, criteria.context.regulatoryRequirements)) {
continue;
}
}
available.push(provider);
}
return available;
}
/**
* Score providers based on strategy
*/
async scoreProviders(providers, criteria, strategy) {
const scored = [];
for (const provider of providers) {
const breakdown = await this.calculateProviderScore(provider, criteria, strategy);
const score = this.aggregateScore(breakdown, strategy);
scored.push({ provider, score, breakdown });
}
// Sort by score (highest first)
scored.sort((a, b) => b.score - a.score);
// Apply adaptive learning adjustments
if (this.config.adaptiveLearningEnabled) {
this.adaptiveLearning.adjustScores(scored, criteria);
}
return scored;
}
/**
* Calculate individual provider score
*/
async calculateProviderScore(provider, criteria, strategy) {
const metrics = this.providerMetrics.get(provider.id);
const health = this.providerHealth.get(provider.id);
const breakdown = {
performance: 0,
reliability: 0,
capability: 0,
cost: 0,
availability: 0,
preference: 0
};
// Performance score
if (metrics) {
const avgLatency = metrics.averageLatency;
const targetLatency = criteria.requirements.maxLatency || 5000;
breakdown.performance = Math.max(0, 1 - (avgLatency / targetLatency));
// Throughput consideration
const throughputScore = Math.min(1, metrics.requestsPerSecond / 10);
breakdown.performance = (breakdown.performance + throughputScore) / 2;
}
// Reliability score
if (metrics && health) {
breakdown.reliability = (metrics.successRate + health.uptime) / 2;
}
// Capability score
breakdown.capability = this.calculateCapabilityScore(provider, criteria);
// Cost score (inverse - lower cost = higher score)
const estimatedCost = await this.estimateCost(provider, {
id: 'estimate',
type: criteria.requestType,
prompt: 'sample',
agentDID: criteria.context?.agentDID || 'unknown',
sessionId: 'estimate'
});
const maxAcceptableCost = criteria.requirements.costConstraint || 1.0;
breakdown.cost = Math.max(0, 1 - (estimatedCost / maxAcceptableCost));
// Availability score
if (health) {
breakdown.availability = health.responseTime < 1000 ? 1 :
health.responseTime < 3000 ? 0.7 : 0.3;
}
// Preference score (based on domain, agent, or historical success)
breakdown.preference = this.calculatePreferenceScore(provider, criteria);
return breakdown;
}
/**
* Aggregate scores based on strategy
*/
aggregateScore(breakdown, strategy) {
let weights = { ...this.config.scoringWeights };
// Adjust weights based on strategy
switch (strategy) {
case SelectionStrategy.PERFORMANCE:
weights.performance = 0.5;
weights.reliability = 0.3;
break;
case SelectionStrategy.COST_OPTIMIZED:
weights.cost = 0.5;
weights.capability = 0.3;
break;
case SelectionStrategy.RELIABILITY:
weights.reliability = 0.5;
weights.availability = 0.3;
break;
case SelectionStrategy.CAPABILITY_MATCH:
weights.capability = 0.6;
weights.performance = 0.2;
break;
}
// Normalize weights
const totalWeight = Object.values(weights).reduce((sum, w) => sum + w, 0);
for (const key of Object.keys(weights)) {
weights[key] /= totalWeight;
}
// Calculate weighted score
let score = 0;
for (const [metric, value] of Object.entries(breakdown)) {
score += value * (weights[metric] || 0);
}
return Math.min(1, Math.max(0, score));
}
/**
* Check provider capabilities
*/
checkCapabilities(provider, criteria) {
const caps = provider.capabilities;
// Check basic capability for request type
switch (criteria.requestType) {
case types_1.LLMRequestType.COMPLETION:
if (!caps.completion)
return false;
break;
case types_1.LLMRequestType.FUNCTION_CALL:
if (!caps.functionCalling)
return false;
break;
case types_1.LLMRequestType.EMBEDDING:
if (!caps.embeddings)
return false;
break;
case types_1.LLMRequestType.MODERATION:
if (!caps.moderation)
return false;
break;
}
// Check specific requirements
if (criteria.requirements.streaming && !caps.streaming) {
return false;
}
if (criteria.requirements.preferredModels) {
const hasPreferredModel = criteria.requirements.preferredModels.some(model => provider.models.includes(model));
if (!hasPreferredModel)
return false;
}
return true;
}
/**
* Check regulatory compliance
*/
checkCompliance(provider, requirements) {
// This would check against provider compliance certifications
// For now, implement basic checks
const providerCompliance = provider.config.compliance || [];
for (const requirement of requirements) {
if (!providerCompliance.includes(requirement)) {
return false;
}
}
return true;
}
/**
* Calculate capability score
*/
calculateCapabilityScore(provider, criteria) {
let score = 0.5; // Base score
// Exact capability match
if (this.checkCapabilities(provider, criteria)) {
score += 0.3;
}
// Model quality (based on known model rankings)
const modelQualityScore = this.getModelQualityScore(provider.models);
score += modelQualityScore * 0.2;
// Additional features
if (provider.capabilities.multimodal && criteria.requestType === types_1.LLMRequestType.COMPLETION) {
score += 0.1;
}
if (provider.capabilities.jsonMode) {
score += 0.05;
}
return Math.min(1, score);
}
/**
* Get model quality score
*/
getModelQualityScore(models) {
// Known model quality rankings (simplified)
const modelRankings = {
'gpt-4': 1.0,
'gpt-4-turbo': 0.95,
'claude-3-opus': 0.95,
'claude-3-sonnet': 0.9,
'gpt-3.5-turbo': 0.8,
'claude-3-haiku': 0.75
};
let maxScore = 0;
for (const model of models) {
const score = modelRankings[model] || 0.5;
maxScore = Math.max(maxScore, score);
}
return maxScore;
}
/**
* Calculate preference score
*/
calculatePreferenceScore(provider, criteria) {
let score = 0.5; // Neutral
// Domain-specific preferences
if (criteria.context?.domain) {
const domainHistory = this.selectionHistory.filter(h => h.criteria.context?.domain === criteria.context?.domain &&
h.result.primaryProvider.id === provider.id &&
h.actualPerformance?.success);
if (domainHistory.length > 0) {
const successRate = domainHistory.length /
this.selectionHistory.filter(h => h.criteria.context?.domain === criteria.context?.domain).length;
score = successRate;
}
}
// Agent-specific preferences
if (criteria.context?.agentDID) {
const agentHistory = this.selectionHistory.filter(h => h.criteria.context?.agentDID === criteria.context?.agentDID &&
h.result.primaryProvider.id === provider.id &&
h.actualPerformance?.success);
if (agentHistory.length > 0) {
const avgPerformance = agentHistory.reduce((sum, h) => sum + (h.actualPerformance?.success ? 1 : 0), 0) / agentHistory.length;
score = (score + avgPerformance) / 2;
}
}
return score;
}
/**
* Estimate cost for request
*/
async estimateCost(provider, request) {
// Simplified cost estimation based on token count and provider rates
const estimatedTokens = this.estimateTokens(request.prompt);
const providerCost = provider.config.costPer1kTokens || 0.01;
return (estimatedTokens / 1000) * providerCost;
}
/**
* Estimate latency
*/
async estimateLatency(provider, criteria) {
const metrics = this.providerMetrics.get(provider.id);
if (metrics) {
// Adjust based on request complexity
let baseLatency = metrics.averageLatency;
if (criteria.requestType === types_1.LLMRequestType.FUNCTION_CALL) {
baseLatency *= 1.3; // Function calls typically take longer
}
if (criteria.requirements.maxTokens && criteria.requirements.maxTokens > 1000) {
baseLatency *= 1.2; // Longer responses take more time
}
return baseLatency;
}
return 3000; // Default estimate
}
/**
* Estimate token count
*/
estimateTokens(text) {
// Rough estimate: 1 token per 4 characters
return Math.ceil(text.length / 4);
}
/**
* Build reasoning for selection
*/
buildReasoning(selectedProvider, criteria, strategy) {
const { provider, breakdown } = selectedProvider;
const topFactors = Object.entries(breakdown)
.sort(([, a], [, b]) => b - a)
.slice(0, 3)
.map(([factor, score]) => `${factor}: ${(score * 100).toFixed(1)}%`);
return `Selected ${provider.name} (${provider.id}) for ${criteria.requestType} request using ${strategy} strategy. ` +
`Top scoring factors: ${topFactors.join(', ')}. ` +
`Overall score: ${(selectedProvider.score * 100).toFixed(1)}%`;
}
/**
* Calculate tradeoffs between providers
*/
calculateTradeoffs(provider, primaryProvider) {
const tradeoffs = [];
const providerMetrics = this.providerMetrics.get(provider.id);
const primaryMetrics = this.providerMetrics.get(primaryProvider.id);
if (providerMetrics && primaryMetrics) {
if (providerMetrics.averageLatency > primaryMetrics.averageLatency) {
tradeoffs.push('Higher latency');
}
if (providerMetrics.successRate < primaryMetrics.successRate) {
tradeoffs.push('Lower reliability');
}
}
// Compare capabilities
if (provider.capabilities.functionCalling && !primaryProvider.capabilities.functionCalling) {
tradeoffs.push('Better function calling support');
}
if (provider.capabilities.streaming && !primaryProvider.capabilities.streaming) {
tradeoffs.push('Streaming support');
}
return tradeoffs;
}
/**
* Record selection for learning
*/
recordSelection(criteria, result) {
this.selectionHistory.push({
timestamp: new Date(),
criteria,
result
});
// Keep only recent history
if (this.selectionHistory.length > 1000) {
this.selectionHistory = this.selectionHistory.slice(-500);
}
}
/**
* Record actual performance for learning
*/
async recordPerformance(requestId, providerId, performance) {
// Find the selection record
const selectionRecord = this.selectionHistory.find(h => h.result.primaryProvider.id === providerId &&
Math.abs(h.timestamp.getTime() - Date.now()) < 300000 // Within 5 minutes
);
if (selectionRecord) {
selectionRecord.actualPerformance = performance;
}
// Update provider metrics
this.updateProviderMetrics(providerId, performance);
// Update circuit breaker
this.updateCircuitBreaker(providerId, performance.success);
// Feed to adaptive learning
if (this.config.adaptiveLearningEnabled) {
this.adaptiveLearning.recordPerformance(providerId, performance);
}
this.emit('performance_recorded', { providerId, performance });
}
/**
* Update provider metrics
*/
updateProviderMetrics(providerId, performance) {
let metrics = this.providerMetrics.get(providerId);
if (!metrics) {
metrics = {
providerId,
requestCount: 0,
successCount: 0,
errorCount: 0,
totalLatency: 0,
averageLatency: 0,
successRate: 1,
requestsPerSecond: 0,
tokensPerSecond: 0,
costPer1kTokens: 0,
lastUpdated: new Date()
};
}
metrics.requestCount++;
if (performance.success) {
metrics.successCount++;
}
else {
metrics.errorCount++;
}
metrics.totalLatency += performance.latency;
metrics.averageLatency = metrics.totalLatency / metrics.requestCount;
metrics.successRate = metrics.successCount / metrics.requestCount;
metrics.lastUpdated = new Date();
this.providerMetrics.set(providerId, metrics);
}
/**
* Update circuit breaker
*/
updateCircuitBreaker(providerId, success) {
let breaker = this.circuitBreakers.get(providerId);
if (!breaker) {
breaker = {
failures: 0,
lastFailure: new Date(),
state: 'closed'
};
}
if (success) {
// Reset on success
if (breaker.state === 'half_open') {
breaker.state = 'closed';
breaker.failures = 0;
}
}
else {
breaker.failures++;
breaker.lastFailure = new Date();
// Trip circuit breaker
if (breaker.failures >= this.config.circuitBreakerThreshold) {
breaker.state = 'open';
// Set timer to try half-open
setTimeout(() => {
const currentBreaker = this.circuitBreakers.get(providerId);
if (currentBreaker?.state === 'open') {
currentBreaker.state = 'half_open';
}
}, this.config.circuitBreakerTimeout);
}
}
this.circuitBreakers.set(providerId, breaker);
}
/**
* Initialize provider health
*/
initializeProviderHealth() {
for (const [id, provider] of this.providers) {
this.providerHealth.set(id, {
providerId: id,
status: 'healthy',
lastCheck: new Date(),
responseTime: 0,
uptime: 1,
errorRate: 0,
connectionCount: 0,
version: provider.version || '1.0.0'
});
}
}
/**
* Start health checks
*/
startHealthChecks() {
setInterval(() => {
this.performHealthChecks();
}, this.config.loadBalancing.healthCheckInterval);
}
/**
* Perform health checks
*/
async performHealthChecks() {
const healthPromises = Array.from(this.providers.entries()).map(async ([id, provider]) => {
try {
const startTime = Date.now();
// Simple health check request
const healthResult = await this.performHealthCheck(provider);
const responseTime = Date.now() - startTime;
const health = {
providerId: id,
status: healthResult.healthy ? 'healthy' : 'unhealthy',
lastCheck: new Date(),
responseTime,
uptime: healthResult.uptime || 1,
errorRate: healthResult.errorRate || 0,
connectionCount: healthResult.connections || 0,
version: provider.version || '1.0.0'
};
this.providerHealth.set(id, health);
}
catch (error) {
this.providerHealth.set(id, {
providerId: id,
status: 'unhealthy',
lastCheck: new Date(),
responseTime: 0,
uptime: 0,
errorRate: 1,
connectionCount: 0,
version: 'unknown'
});
}
});
await Promise.allSettled(healthPromises);
}
/**
* Perform individual health check
*/
async performHealthCheck(provider) {
// This would implement actual health check logic for each provider
// For now, return mock data
return {
healthy: true,
uptime: 0.99,
errorRate: 0.01,
connections: 10
};
}
/**
* Get provider statistics
*/
getStatistics() {
const totalSelections = this.selectionHistory.length;
const providerUsage = {};
let totalScore = 0;
let failovers = 0;
let totalCost = 0;
for (const record of this.selectionHistory) {
const providerId = record.result.primaryProvider.id;
providerUsage[providerId] = (providerUsage[providerId] || 0) + 1;
totalScore += record.result.score;
totalCost += record.result.estimatedCost;
if (record.actualPerformance && !record.actualPerformance.success) {
failovers++;
}
}
return {
totalSelections,
providerUsage,
averageScore: totalSelections > 0 ? totalScore / totalSelections : 0,
failoverRate: totalSelections > 0 ? failovers / totalSelections : 0,
costSavings: 0 // Would calculate against baseline
};
}
/**
* Shutdown
*/
shutdown() {
this.loadBalancer.shutdown();
this.removeAllListeners();
}
}
exports.ProviderSelector = ProviderSelector;
/**
* Load Balancer
*/
class LoadBalancer {
constructor(config) {
this.config = config;
this.currentIndex = 0;
this.connectionCounts = new Map();
}
async selectProvider(providers) {
if (providers.length === 0)
return null;
switch (this.config.strategy) {
case 'round_robin':
return this.roundRobin(providers);
case 'weighted':
return this.weighted(providers);
case 'least_connections':
return this.leastConnections(providers);
case 'latency_based':
return this.latencyBased(providers);
default:
return providers[0];
}
}
roundRobin(providers) {
const provider = providers[this.currentIndex % providers.length];
this.currentIndex++;
return provider;
}
weighted(providers) {
const weights = this.config.weights || new Map();
const totalWeight = providers.reduce((sum, p) => sum + (weights.get(p.id) || 1), 0);
const random = Math.random() * totalWeight;
let current = 0;
for (const provider of providers) {
current += weights.get(provider.id) || 1;
if (random <= current) {
return provider;
}
}
return providers[0];
}
leastConnections(providers) {
let minConnections = Infinity;
let selected = providers[0];
for (const provider of providers) {
const connections = this.connectionCounts.get(provider.id) || 0;
if (connections < minConnections) {
minConnections = connections;
selected = provider;
}
}
return selected;
}
latencyBased(providers) {
// Select provider with lowest latency (mock implementation)
return providers[0];
}
shutdown() {
this.connectionCounts.clear();
}
}
/**
* Adaptive Learning
*/
class AdaptiveLearning {
constructor() {
this.performanceHistory = new Map();
}
adjustScores(scoredProviders, criteria) {
// Apply learning adjustments based on historical performance
for (const scored of scoredProviders) {
const history = this.performanceHistory.get(scored.provider.id);
if (history && history.length > 5) {
const recentPerformance = history.slice(-10);
const avgSuccess = recentPerformance.reduce((sum, p) => sum + (p.success ? 1 : 0), 0) / recentPerformance.length;
const avgLatency = recentPerformance.reduce((sum, p) => sum + p.latency, 0) / recentPerformance.length;
// Adjust score based on actual performance vs. predicted
if (avgSuccess < 0.9) {
scored.score *= 0.9; // Penalize for low success rate
}
if (avgLatency > 5000) {
scored.score *= 0.95; // Penalize for high latency
}
}
}
// Re-sort after adjustments
scoredProviders.sort((a, b) => b.score - a.score);
}
recordPerformance(providerId, performance) {
let history = this.performanceHistory.get(providerId);
if (!history) {
history = [];
this.performanceHistory.set(providerId, history);
}
history.push({
timestamp: new Date(),
...performance
});
// Keep only recent history
if (history.length > 100) {
history.splice(0, history.length - 50);
}
}
}
exports.default = ProviderSelector;
//# sourceMappingURL=provider-selector.js.map