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