UNPKG

@tehreet/conduit

Version:

LLM API gateway with intelligent routing, robust process management, and health monitoring

380 lines 13.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConduitRouter = void 0; const events_1 = require("events"); const EnhancedPluginManager_1 = require("../plugins/EnhancedPluginManager"); const token_counter_1 = require("../utils/token-counter"); const router_1 = require("../utils/router"); const log_1 = require("../utils/log"); /** * Standalone routing engine for Conduit */ class ConduitRouter extends events_1.EventEmitter { constructor(config, pluginManager) { super(); this.isInitialized = false; this.config = config; this.pluginManager = pluginManager || new EnhancedPluginManager_1.PluginManager(); this.providers = new Map(); } /** * Initialize the router */ async initialize() { if (this.isInitialized) { (0, log_1.log)('Router is already initialized'); return; } try { (0, log_1.log)('Initializing ConduitRouter...'); // Initialize providers if (this.config.Providers) { this.config.Providers.forEach(provider => { this.providers.set(provider.name, provider); }); (0, log_1.log)(`Loaded ${this.providers.size} providers`); } // Load plugins (only routing-related plugins) if not already loaded by ConduitServer if (this.config.plugins && this.pluginManager.getPluginCount() === 0) { const routingPlugins = this.config.plugins.filter(p => p.enabled); await this.pluginManager.loadPlugins(routingPlugins); (0, log_1.log)(`Loaded ${routingPlugins.length} routing plugins`); } else if (this.pluginManager.getPluginCount() > 0) { (0, log_1.log)(`Using existing ${this.pluginManager.getPluginCount()} plugins from ConduitServer`); } this.isInitialized = true; this.emit('initialized'); (0, log_1.log)('ConduitRouter initialized'); } catch (error) { (0, log_1.log)('Failed to initialize ConduitRouter:', error); this.emit('error', error); throw error; } } /** * Route a request and return routing decision */ async route(context) { if (!this.isInitialized) { throw new Error('Router is not initialized'); } try { // Apply pre-routing plugins const processedContext = await this.pluginManager.beforeRouting(context); // Calculate token count if not provided let tokenCount = processedContext.tokenCount; if (tokenCount === 0 && processedContext.request?.body?.messages) { const tokenResult = await token_counter_1.tokenCounter.countMessagesTokens(processedContext.request.body.messages, processedContext.request.body.model || 'claude-3-5-sonnet-20241022'); tokenCount = tokenResult.count; } // Update context with token count const contextWithTokens = { ...processedContext, tokenCount }; // Try plugin-based custom routing first const pluginDecision = await this.pluginManager.customRouting(contextWithTokens); if (pluginDecision) { const finalDecision = await this.pluginManager.afterRouting(pluginDecision); this.emit('routing-decision', { context: contextWithTokens, decision: finalDecision }); return finalDecision; } // Try custom routing rules const customDecision = await this.evaluateCustomRules(contextWithTokens); if (customDecision) { const finalDecision = await this.pluginManager.afterRouting(customDecision); this.emit('routing-decision', { context: contextWithTokens, decision: finalDecision }); return finalDecision; } // Use standard Conduit routing logic const model = await (0, router_1.getUseModel)(contextWithTokens.request, tokenCount, this.config, // Type casting to avoid conflicts contextWithTokens.env); const decision = { model, source: 'default', reason: 'Selected by standard Conduit routing', tokenCount, metadata: { routingTime: Date.now(), providerAvailable: this.isProviderAvailable(model) } }; // Apply post-routing plugins const finalDecision = await this.pluginManager.afterRouting(decision); this.emit('routing-decision', { context: contextWithTokens, decision: finalDecision }); return finalDecision; } catch (error) { (0, log_1.log)('Error during routing:', error); this.emit('error', error); // Return fallback decision const fallbackDecision = { model: this.config.Router?.default || 'claude-3-5-sonnet-20241022', source: 'default', reason: 'Fallback due to routing error', tokenCount: context.tokenCount, metadata: { error: error instanceof Error ? error.message : String(error), fallback: true } }; return fallbackDecision; } } /** * Evaluate custom routing rules */ async evaluateCustomRules(context) { const rules = this.config.Router?.customRules; if (!rules || rules.length === 0) { return null; } // Sort rules by priority (higher priority first) const sortedRules = [...rules] .filter(rule => rule.enabled) .sort((a, b) => b.priority - a.priority); for (const rule of sortedRules) { try { if (await this.evaluateRule(rule, context)) { return { model: rule.model, source: 'preset-rule', reason: `Custom rule '${rule.name}' matched`, tokenCount: context.tokenCount, metadata: { ruleId: rule.id, ruleName: rule.name, priority: rule.priority } }; } } catch (error) { (0, log_1.log)(`Error evaluating rule '${rule.name}':`, error); } } return null; } /** * Evaluate a single routing rule */ async evaluateRule(rule, context) { try { // Handle legacy string condition format if (typeof rule.condition === 'string') { return this.evaluateStringCondition(rule.condition, context); } // Handle new conditions array format if (rule.conditions && rule.conditions.length > 0) { return rule.conditions.every(condition => this.evaluateCondition(condition, context)); } return false; } catch (error) { (0, log_1.log)(`Error evaluating rule condition:`, error); return false; } } /** * Evaluate legacy string condition */ evaluateStringCondition(condition, context) { const lowerCondition = condition.toLowerCase(); // Token count conditions if (lowerCondition.includes('tokencount')) { const match = lowerCondition.match(/tokencount\s*([<>]=?)\s*(\d+)/); if (match) { const operator = match[1]; const threshold = parseInt(match[2]); switch (operator) { case '<': return context.tokenCount < threshold; case '<=': return context.tokenCount <= threshold; case '>': return context.tokenCount > threshold; case '>=': return context.tokenCount >= threshold; } } } // Model conditions if (lowerCondition.includes('model')) { const requestedModel = context.request?.body?.model || ''; if (lowerCondition.includes('haiku') && requestedModel.includes('haiku')) { return true; } if (lowerCondition.includes('sonnet') && requestedModel.includes('sonnet')) { return true; } if (lowerCondition.includes('opus') && requestedModel.includes('opus')) { return true; } } // Thinking conditions if (lowerCondition.includes('thinking')) { return Boolean(context.request?.body?.thinking); } return false; } /** * Evaluate structured condition */ evaluateCondition(condition, context) { const { field, operator, value } = condition; let fieldValue; // Extract field value from context switch (field) { case 'tokenCount': fieldValue = context.tokenCount; break; case 'model': fieldValue = context.request?.body?.model || ''; break; case 'thinking': fieldValue = Boolean(context.request?.body?.thinking); break; case 'message': fieldValue = context.request?.body?.messages?.[0]?.content || ''; break; default: fieldValue = context.metadata?.[field]; break; } // Apply operator switch (operator) { case 'gt': return fieldValue > value; case 'gte': return fieldValue >= value; case 'lt': return fieldValue < value; case 'lte': return fieldValue <= value; case 'eq': return fieldValue === value; case 'contains': return typeof fieldValue === 'string' && fieldValue.includes(value); case 'matches': return typeof fieldValue === 'string' && new RegExp(value).test(fieldValue); default: return false; } } /** * Check if provider is available for a model */ isProviderAvailable(model) { for (const provider of this.providers.values()) { if (provider.models.includes(model)) { return true; } } return false; } /** * Update router configuration */ async updateConfig(config) { (0, log_1.log)('Updating router configuration'); this.config = config; // Update providers this.providers.clear(); if (config.Providers) { config.Providers.forEach(provider => { this.providers.set(provider.name, provider); }); } // Reload plugins if needed if (config.plugins) { await this.pluginManager.cleanup(); const routingPlugins = config.plugins.filter(p => p.enabled); await this.pluginManager.loadPlugins(routingPlugins); } this.emit('config-updated', config); } /** * Get current configuration */ getConfig() { return { ...this.config }; } /** * Get available providers */ getProviders() { return Array.from(this.providers.values()); } /** * Get routing rules */ getRoutingRules() { return this.config.Router?.customRules || []; } /** * Add a routing rule */ addRoutingRule(rule) { if (!this.config.Router) { this.config.Router = {}; } if (!this.config.Router.customRules) { this.config.Router.customRules = []; } this.config.Router.customRules.push(rule); this.emit('rule-added', rule); } /** * Remove a routing rule */ removeRoutingRule(ruleId) { if (!this.config.Router?.customRules) { return false; } const index = this.config.Router.customRules.findIndex(rule => rule.id === ruleId); if (index !== -1) { const removedRule = this.config.Router.customRules.splice(index, 1)[0]; this.emit('rule-removed', removedRule); return true; } return false; } /** * Get plugin manager */ getPluginManager() { return this.pluginManager; } /** * Check if router is initialized */ isReady() { return this.isInitialized; } /** * Get router statistics */ getStats() { return { initialized: this.isInitialized, providersCount: this.providers.size, pluginsCount: this.pluginManager.getPluginCount(), rulesCount: this.config.Router?.customRules?.length || 0 }; } /** * Cleanup resources */ async cleanup() { (0, log_1.log)('Cleaning up ConduitRouter resources'); try { await this.pluginManager.cleanup(); this.providers.clear(); this.isInitialized = false; this.emit('cleanup'); } catch (error) { (0, log_1.log)('Error during router cleanup:', error); } } } exports.ConduitRouter = ConduitRouter; //# sourceMappingURL=ConduitRouter.js.map