UNPKG

@tehreet/conduit

Version:

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

495 lines 17.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.PluginManager = void 0; const events_1 = require("events"); const promises_1 = require("fs/promises"); const path_1 = require("path"); const log_1 = require("../utils/log"); /** * Enhanced plugin manager with lifecycle hooks and health monitoring */ class PluginManager extends events_1.EventEmitter { constructor() { super(...arguments); this.plugins = new Map(); this.pluginConfigs = new Map(); } /** * Load plugins from configuration */ async loadPlugins(configs) { for (const config of configs) { if (!config.enabled) { continue; } try { const plugin = await this.loadPlugin(config); await this.registerPlugin(plugin, config); } catch (error) { (0, log_1.log)(`Failed to load plugin ${config.name}:`, error); this.emit('plugin-error', { plugin: config.name, error: error instanceof Error ? error.message : String(error), phase: 'load' }); } } } /** * Load plugins from directory (backward compatibility) */ async loadPluginsFromDirectory(pluginDir) { try { const exists = await (0, promises_1.stat)(pluginDir) .then(() => true) .catch(() => false); if (!exists) { (0, log_1.log)(`Plugin directory ${pluginDir} does not exist, skipping plugin loading`); return; } const entries = await (0, promises_1.readdir)(pluginDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && ((0, path_1.extname)(entry.name) === '.js' || (0, path_1.extname)(entry.name) === '.ts')) { await this.loadPluginFile((0, path_1.join)(pluginDir, entry.name)); } else if (entry.isDirectory()) { // Look for index.js or index.ts in subdirectories const indexFiles = ['index.js', 'index.ts']; for (const indexFile of indexFiles) { const indexPath = (0, path_1.join)(pluginDir, entry.name, indexFile); const indexExists = await (0, promises_1.stat)(indexPath) .then(() => true) .catch(() => false); if (indexExists) { await this.loadPluginFile(indexPath); break; } } } } } catch (error) { (0, log_1.log)(`Error loading plugins from ${pluginDir}:`, error); } } /** * Load a single plugin from configuration */ async loadPlugin(config) { switch (config.source) { case 'inline': if (!config.module) { throw new Error('Inline plugin must provide module'); } return config.module; case 'npm': return require(config.name); case 'builtin': return await this.loadBuiltinPlugin(config.name); case 'file': { const filePath = config.path || config.name; return await this.loadPluginFromFile(filePath); } default: { // Try builtin first, then file try { return await this.loadBuiltinPlugin(config.name); } catch (error) { (0, log_1.log)(`Failed to load builtin plugin ${config.name}, trying as file:`, error); const filePath = config.path || config.name; return await this.loadPluginFromFile(filePath); } } } } /** * Load a builtin plugin from the conduit package */ async loadBuiltinPlugin(pluginName) { try { // Map plugin names to their exports const builtinPlugins = { 'synapse-plugin': () => Promise.resolve().then(() => __importStar(require('./synapse-plugin'))).then(m => m.SynapsePlugin), 'synapse-ide': () => Promise.resolve().then(() => __importStar(require('./synapse-plugin'))).then(m => m.SynapsePlugin), // Alias }; const pluginLoader = builtinPlugins[pluginName]; if (!pluginLoader) { throw new Error(`Unknown builtin plugin: ${pluginName}`); } const PluginClass = await pluginLoader(); const plugin = new PluginClass(); if (this.isValidPlugin(plugin)) { (0, log_1.log)(`Loaded builtin plugin: ${pluginName}`); return plugin; } else { throw new Error(`Invalid builtin plugin: ${pluginName}`); } } catch (error) { throw new Error(`Error loading builtin plugin ${pluginName}: ${error}`); } } /** * Load plugin from file */ async loadPluginFromFile(filePath) { try { // Dynamic import for ES modules and CommonJS compatibility const pluginModule = await Promise.resolve(`${filePath}`).then(s => __importStar(require(s))); const PluginClass = pluginModule.default || pluginModule; if (typeof PluginClass === 'function') { const plugin = new PluginClass(); if (this.isValidPlugin(plugin)) { return plugin; } else { throw new Error(`Invalid plugin in file ${filePath}: missing required properties`); } } else if (this.isValidPlugin(PluginClass)) { return PluginClass; } else { throw new Error(`Invalid plugin export in file ${filePath}`); } } catch (error) { throw new Error(`Error loading plugin from ${filePath}: ${error}`); } } /** * Load plugin file (backward compatibility) */ async loadPluginFile(filePath) { try { const plugin = await this.loadPluginFromFile(filePath); const config = { name: plugin.name, enabled: true, source: 'file', path: filePath }; await this.registerPlugin(plugin, config); } catch (error) { (0, log_1.log)(`Error loading plugin file ${filePath}:`, error); } } /** * Register a plugin with lifecycle management */ async registerPlugin(plugin, config) { if (this.plugins.has(plugin.name)) { (0, log_1.log)(`Plugin ${plugin.name} is already registered, replacing...`); await this.unregisterPlugin(plugin.name); } try { // Call onLoad lifecycle hook if (plugin.onLoad) { await plugin.onLoad(); } // Store plugin and config this.plugins.set(plugin.name, plugin); this.pluginConfigs.set(plugin.name, config); (0, log_1.log)(`Registered plugin: ${plugin.name} v${plugin.version}`); this.emit('plugin-loaded', { name: plugin.name, version: plugin.version, description: plugin.description }); } catch (error) { (0, log_1.log)(`Error registering plugin ${plugin.name}:`, error); this.emit('plugin-error', { plugin: plugin.name, error: error instanceof Error ? error.message : String(error), phase: 'register' }); throw error; } } /** * Unregister a plugin */ async unregisterPlugin(name) { const plugin = this.plugins.get(name); if (!plugin) { return; } try { // Call onUnload lifecycle hook if (plugin.onUnload) { await plugin.onUnload(); } this.plugins.delete(name); this.pluginConfigs.delete(name); (0, log_1.log)(`Unregistered plugin: ${name}`); this.emit('plugin-unloaded', { name }); } catch (error) { (0, log_1.log)(`Error unregistering plugin ${name}:`, error); this.emit('plugin-error', { plugin: name, error: error instanceof Error ? error.message : String(error), phase: 'unregister' }); } } /** * Execute beforeRouting hooks */ async beforeRouting(context) { let processedContext = context; for (const [pluginName, plugin] of this.plugins) { try { if (plugin.beforeRouting) { (0, log_1.log)(`Executing beforeRouting hook for plugin ${pluginName}`); processedContext = await plugin.beforeRouting(processedContext); } } catch (error) { (0, log_1.log)(`Error in beforeRouting hook for plugin ${pluginName}:`, error); this.emit('plugin-error', { plugin: pluginName, error: error instanceof Error ? error.message : String(error), phase: 'beforeRouting' }); // Continue with other plugins even if one fails } } return processedContext; } /** * Execute afterRouting hooks */ async afterRouting(decision) { let processedDecision = decision; for (const [pluginName, plugin] of this.plugins) { try { if (plugin.afterRouting) { (0, log_1.log)(`Executing afterRouting hook for plugin ${pluginName}`); processedDecision = await plugin.afterRouting(processedDecision); } } catch (error) { (0, log_1.log)(`Error in afterRouting hook for plugin ${pluginName}:`, error); this.emit('plugin-error', { plugin: pluginName, error: error instanceof Error ? error.message : String(error), phase: 'afterRouting' }); // Continue with other plugins even if one fails } } return processedDecision; } /** * Execute custom routing */ async customRouting(context) { for (const [pluginName, plugin] of this.plugins) { try { if (plugin.customRouter) { (0, log_1.log)(`Trying custom router for plugin ${pluginName}`); const result = await plugin.customRouter(context); if (result) { (0, log_1.log)(`Plugin ${pluginName} provided custom routing: ${result.model}`); return result; } } } catch (error) { (0, log_1.log)(`Error in custom router for plugin ${pluginName}:`, error); this.emit('plugin-error', { plugin: pluginName, error: error instanceof Error ? error.message : String(error), phase: 'customRouter' }); // Continue with other plugins even if one fails } } return null; } /** * Get health status from all plugins */ async getHealthStatus() { const healthStatus = {}; for (const [pluginName, plugin] of this.plugins) { try { if (plugin.getHealth) { healthStatus[pluginName] = await plugin.getHealth(); } else { // Default health status if plugin doesn't provide one healthStatus[pluginName] = { healthy: true, checks: {}, uptime: 0, startTime: new Date() }; } } catch (error) { (0, log_1.log)(`Error getting health status for plugin ${pluginName}:`, error); healthStatus[pluginName] = { healthy: false, checks: { 'health-check': { healthy: false, error: error instanceof Error ? error.message : String(error), timestamp: new Date() } }, uptime: 0, startTime: new Date() }; } } return healthStatus; } /** * Validate plugin configurations */ validateConfigs() { const results = {}; for (const [pluginName, plugin] of this.plugins) { try { const config = this.pluginConfigs.get(pluginName); if (plugin.validateConfig && config?.config) { results[pluginName] = plugin.validateConfig(config.config); } else { results[pluginName] = true; // No validation needed } } catch (error) { (0, log_1.log)(`Error validating config for plugin ${pluginName}:`, error); results[pluginName] = false; } } return results; } /** * Execute generic hook */ async executeHook(hookName, data) { let result = data; for (const [pluginName, plugin] of this.plugins) { try { const hookFn = plugin[hookName]; if (typeof hookFn === 'function') { (0, log_1.log)(`Executing ${hookName} hook for plugin ${pluginName}`); result = await hookFn.call(plugin, result); } } catch (error) { (0, log_1.log)(`Error executing ${hookName} hook for plugin ${pluginName}:`, error); this.emit('plugin-error', { plugin: pluginName, error: error instanceof Error ? error.message : String(error), phase: hookName }); // Continue with other plugins even if one fails } } return result; } /** * Get registered plugins */ getRegisteredPlugins() { return Array.from(this.plugins.keys()); } /** * Get plugin by name */ getPlugin(name) { return this.plugins.get(name); } /** * Get plugin configuration */ getPluginConfig(name) { return this.pluginConfigs.get(name); } /** * Get plugin count */ getPluginCount() { return this.plugins.size; } /** * Check if plugin is loaded */ hasPlugin(name) { return this.plugins.has(name); } /** * Validate plugin interface */ isValidPlugin(obj) { return (obj && typeof obj.name === 'string' && typeof obj.version === 'string' && (obj.beforeRouting || obj.afterRouting || obj.customRouter || obj.onLoad || obj.onUnload || obj.getHealth || obj.validateConfig)); } /** * Cleanup all plugins */ async cleanup() { (0, log_1.log)('Cleaning up plugins...'); const pluginNames = Array.from(this.plugins.keys()); for (const name of pluginNames) { await this.unregisterPlugin(name); } this.plugins.clear(); this.pluginConfigs.clear(); this.emit('cleanup'); } } exports.PluginManager = PluginManager; //# sourceMappingURL=EnhancedPluginManager.js.map