claude-llm-gateway
Version:
🧠 Intelligent API gateway with automatic model selection - connects Claude Code to 36+ LLM providers with smart task detection and cost optimization
454 lines (378 loc) • 12.9 kB
JavaScript
const { LLMInterface } = require('llm-interface');
class ProviderRouter {
constructor() {
this.providerConfig = new Map();
this.healthStatus = new Map();
this.requestCounts = new Map();
this.lastHealthCheck = null;
this.roundRobinIndex = 0;
}
/**
* Initialize provider configuration
*/
async initialize(providers) {
if (!providers || typeof providers !== 'object') {
console.warn('⚠️ No providers configuration found in ProviderRouter');
this.providerConfig = new Map();
return;
}
this.providerConfig = new Map(Object.entries(providers));
// Initialize request count
for (const providerName of this.providerConfig.keys()) {
this.requestCounts.set(providerName, 0);
}
// Start health checks
await this.startHealthChecks();
console.log(`🚀 Provider router initialization completed, supporting ${this.providerConfig.size} providers`);
}
/**
* Select best provider
*/
async selectProvider(request, options = {}) {
try {
// 1. Check if specific provider is specified
if (options.preferredProvider) {
const provider = options.preferredProvider;
if (this.isProviderHealthy(provider)) {
console.log(`🎯 Using specified provider: ${provider}`);
return provider;
}
}
// 2. Select provider based on model type
const modelBasedProvider = this.selectByModel(request.model);
if (modelBasedProvider && this.isProviderHealthy(modelBasedProvider)) {
console.log(`🎯 Provider selected based on model: ${modelBasedProvider} (model: ${request.model})`);
return modelBasedProvider;
}
// 3. Get healthy provider list
const healthyProviders = this.getHealthyProviders();
if (healthyProviders.length === 0) {
throw new Error('No healthy providers available');
}
// 4. Select based on load balancing strategy
const selectedProvider = this.loadBalance(healthyProviders, options.strategy);
console.log(`⚖️ Load balancer selected provider: ${selectedProvider}`);
return selectedProvider;
} catch (error) {
console.error('❌ Provider selection failed:', error);
// Return default provider as last resort
return this.getDefaultProvider();
}
}
/**
* Select provider based on model type
*/
selectByModel(model) {
if (!model) return null;
const modelLower = model.toLowerCase();
// OpenAI model
if (modelLower.includes('gpt')) {
return 'openai';
}
// Google model
if (modelLower.includes('gemini')) {
return 'google';
}
// Anthropic model
if (modelLower.includes('claude')) {
return 'anthropic';
}
// Mistral model
if (modelLower.includes('mistral')) {
return 'mistral';
}
// Ollama local model
if (modelLower.includes('llama') || modelLower.includes('codellama')) {
return 'ollama';
}
// Cohere model
if (modelLower.includes('command')) {
return 'cohere';
}
return null;
}
/**
* Get healthy provider list
*/
getHealthyProviders() {
const healthy = [];
for (const [providerName, config] of this.providerConfig.entries()) {
if (config.enabled && this.isProviderHealthy(providerName)) {
healthy.push(providerName);
}
}
// Sort by priority
return healthy.sort((a, b) => {
const priorityA = this.providerConfig.get(a)?.priority || 10;
const priorityB = this.providerConfig.get(b)?.priority || 10;
return priorityA - priorityB;
});
}
/**
* Check if provider is healthy
*/
isProviderHealthy(providerName) {
const status = this.healthStatus.get(providerName);
if (!status) return false;
// Check health status and last check time
const maxAge = 5 * 60 * 1000; // 5 minutes
const isRecent = (Date.now() - status.lastCheck) < maxAge;
return status.healthy && isRecent;
}
/**
* Load balancing algorithm
*/
loadBalance(providers, strategy = 'priority') {
if (providers.length === 0) {
throw new Error('No available providers');
}
if (providers.length === 1) {
return providers[0];
}
switch (strategy) {
case 'round_robin':
return this.roundRobinBalance(providers);
case 'least_requests':
return this.leastRequestsBalance(providers);
case 'cost_optimized':
return this.costOptimizedBalance(providers);
case 'random':
return providers[Math.floor(Math.random() * providers.length)];
case 'priority':
default:
return this.priorityBalance(providers);
}
}
/**
* Round-robin load balancing
*/
roundRobinBalance(providers) {
const provider = providers[this.roundRobinIndex % providers.length];
this.roundRobinIndex++;
return provider;
}
/**
* Least requests load balancing
*/
leastRequestsBalance(providers) {
let minRequests = Infinity;
let selectedProvider = providers[0];
for (const provider of providers) {
const requestCount = this.requestCounts.get(provider) || 0;
if (requestCount < minRequests) {
minRequests = requestCount;
selectedProvider = provider;
}
}
return selectedProvider;
}
/**
* Cost-optimized load balancing
*/
costOptimizedBalance(providers) {
// Sort by cost, select lowest cost available provider
const sortedByCost = providers.sort((a, b) => {
const costA = this.providerConfig.get(a)?.cost_per_1k_tokens || 0;
const costB = this.providerConfig.get(b)?.cost_per_1k_tokens || 0;
return costA - costB;
});
return sortedByCost[0];
}
/**
* Priority-based load balancing
*/
priorityBalance(providers) {
// providers are already sorted by priority, return the first one
return providers[0];
}
/**
* Record request
*/
recordRequest(provider) {
const currentCount = this.requestCounts.get(provider) || 0;
this.requestCounts.set(provider, currentCount + 1);
}
/**
* Get default provider
*/
getDefaultProvider() {
// return the first enabled provider sorted by priority
const enabledProviders = Array.from(this.providerConfig.entries())
.filter(([name, config]) => config.enabled)
.sort((a, b) => (a[1].priority || 10) - (b[1].priority || 10));
if (enabledProviders.length > 0) {
return enabledProviders[0][0];
}
// if no enabled providers, return openai as default
return 'openai';
}
/**
* Start health checks
*/
async startHealthChecks() {
console.log('🔍 Starting provider health checks...');
// Perform health check immediately
await this.performHealthCheck();
// Set up periodic health checks
setInterval(async () => {
await this.performHealthCheck();
}, 30000); // check every 30 seconds
}
/**
* Perform health check
*/
async performHealthCheck() {
const enabledProviders = Array.from(this.providerConfig.entries())
.filter(([name, config]) => config.enabled)
.map(([name]) => name);
console.log(`🩺 Performing health check (${enabledProviders.length} providers)...`);
for (const provider of enabledProviders) {
try {
await this.checkProviderHealth(provider);
} catch (error) {
console.warn(`⚠️ Provider ${provider} health check failed: ${error.message}`);
}
}
this.lastHealthCheck = Date.now();
}
/**
* Check individual provider health status
*/
async checkProviderHealth(provider) {
try {
const startTime = Date.now();
// Send simple ping request
const testMessage = 'ping';
const response = await LLMInterface.sendMessage(provider, testMessage, {
max_tokens: 5,
timeout: 10000 // 10 seconds timeout
});
const responseTime = Date.now() - startTime;
// Record health status
this.healthStatus.set(provider, {
healthy: true,
lastCheck: Date.now(),
responseTime: responseTime,
error: null
});
console.log(`✅ ${provider}: healthy (${responseTime}ms)`);
} catch (error) {
// Determine error type and provide friendly message
let status = 'unhealthy';
let friendlyMessage = error.message;
// Check for authentication/API key errors
if (error.message.includes('HTTP 401') || error.message.includes('HTTP 403') ||
error.message.includes('Unauthorized') || error.message.includes('Forbidden') ||
error.message.includes('API key not found') || error.message.includes('Invalid API key') ||
error.message.includes('Authentication failed') || error.message.includes('Access denied')) {
status = 'no_api_key';
friendlyMessage = 'API key not configured or invalid';
}
// Check for network/connection errors
else if (error.message.includes('HTTP 404') || error.message.includes('Not Found') ||
error.message.includes('ECONNREFUSED') || error.message.includes('ENOTFOUND') ||
error.message.includes('ECONNRESET') || error.message.includes('ETIMEDOUT')) {
status = 'unreachable';
friendlyMessage = 'Service unavailable or not configured';
}
// Check for rate limiting
else if (error.message.includes('HTTP 429') || error.message.includes('Rate limit') ||
error.message.includes('Too many requests')) {
status = 'rate_limited';
friendlyMessage = 'Rate limit exceeded';
}
// Record status with friendly message
this.healthStatus.set(provider, {
healthy: false,
lastCheck: Date.now(),
responseTime: null,
error: friendlyMessage,
status: status
});
// Display appropriate emoji and message
const emoji = status === 'no_api_key' ? '🔑' :
status === 'unreachable' ? '🔌' :
status === 'rate_limited' ? '⏳' : '❌';
console.log(`${emoji} ${provider}: ${friendlyMessage}`);
}
}
/**
* Get all provider status
*/
getProviderStatus() {
const status = {};
for (const [providerName, config] of this.providerConfig.entries()) {
const health = this.healthStatus.get(providerName);
const requests = this.requestCounts.get(providerName) || 0;
status[providerName] = {
enabled: config.enabled,
priority: config.priority,
healthy: health?.healthy || false,
status_type: health?.status || 'unknown',
last_check: health?.lastCheck || null,
response_time: health?.responseTime || null,
error: health?.error || null,
request_count: requests,
models: config.models || [],
local: config.local || false,
cost_per_1k_tokens: config.cost_per_1k_tokens || 0
};
}
return status;
}
/**
* Manually set provider health status
*/
setProviderHealth(provider, healthy, error = null) {
this.healthStatus.set(provider, {
healthy: healthy,
lastCheck: Date.now(),
responseTime: null,
error: error
});
}
/**
* Reset provider statistics
*/
resetStats() {
this.requestCounts.clear();
this.roundRobinIndex = 0;
console.log('📊 Provider statistics reset');
}
/**
* Get load balancing statistics
*/
getStats() {
const totalRequests = Array.from(this.requestCounts.values()).reduce((sum, count) => sum + count, 0);
const healthyCount = Array.from(this.healthStatus.values()).filter(status => status.healthy).length;
const totalProviders = this.providerConfig.size;
return {
total_requests: totalRequests,
healthy_providers: healthyCount,
total_providers: totalProviders,
last_health_check: this.lastHealthCheck,
request_distribution: Object.fromEntries(this.requestCounts),
round_robin_index: this.roundRobinIndex
};
}
/**
* Get list of healthy providers
*/
getHealthyProviders() {
const healthyProviders = [];
this.providerConfig.forEach((config, provider) => {
const health = this.healthStatus.get(provider);
if (config.enabled && health && health.healthy) {
healthyProviders.push(provider);
}
});
// Sort by priority (lower numbers = hellogher priority)
healthyProviders.sort((a, b) => {
const priorityA = this.providerConfig.get(a)?.priority || 999;
const priorityB = this.providerConfig.get(b)?.priority || 999;
return priorityA - priorityB;
});
return healthyProviders;
}
}
module.exports = ProviderRouter;