@tehreet/conduit
Version:
LLM API gateway with intelligent routing, robust process management, and health monitoring
380 lines • 13.8 kB
JavaScript
"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