UNPKG

@tehreet/conduit

Version:

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

819 lines (676 loc) 22.2 kB
# Conduit-Synapse Scaling Implementation Plan ## Executive Summary This document outlines the comprehensive plan to transform Conduit from a CLI-focused tool into a scalable Node.js library that can be seamlessly integrated with Synapse while maintaining backward compatibility with existing CLI usage. ## Current State Analysis ### Conduit Architecture Overview Conduit currently operates as a sophisticated CLI tool with the following components: ``` conduit/ ├── src/ │ ├── cli.ts # CLI entry point │ ├── index.ts # Library exports (basic) │ ├── server.ts # Basic server │ ├── serverWrapper.ts # Enhanced server with health checks │ ├── core/ # Core functionality │ │ ├── claude-wrapper.ts # Claude CLI wrapper with routing │ │ ├── metadata-handler.ts # Stream metadata injection │ │ └── index.ts # Core exports │ ├── features/ # Feature modules │ │ ├── synapse-features.ts # Synapse-specific features │ │ └── index.ts # Feature exports │ ├── utils/ # Utility modules │ │ ├── config-presets.ts # Configuration presets │ │ ├── context-extractor.ts # Context extraction │ │ ├── token-counter.ts # Token counting │ │ ├── router.ts # Model routing logic │ │ └── pidManager.ts # Process management │ └── plugins/ # Plugin system │ ├── plugin-interface.ts # Plugin interface │ ├── plugin-manager.ts # Plugin management │ └── synapse-plugin.ts # Synapse plugin ├── dist/ │ ├── cli.js # Bundled CLI executable │ └── tiktoken_bg.wasm # WebAssembly token counter └── config/ └── synapse-preset.json # Synapse configuration preset ``` ### Current Limitations 1. **CLI-Only Build**: Only builds bundled CLI executable 2. **No Library API**: No clean programmatic interface 3. **No TypeScript Definitions**: No `.d.ts` files for library consumers 4. **Process Management**: Tied to CLI commands and file system 5. **Configuration Loading**: Expects JSON files on disk 6. **Logging**: Hardcoded to file system ## Implementation Plan ### Phase 1: Core Library Infrastructure (Week 1) #### 1.1 Dual Build System Setup **Update Build Configuration** ```json // package.json { "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", "bin": { "conduit": "./dist/cli.js" }, "exports": { ".": { "import": "./dist/lib/index.js", "require": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts" }, "./server": { "import": "./dist/lib/server.js", "require": "./dist/lib/server.js", "types": "./dist/lib/server.d.ts" }, "./router": { "import": "./dist/lib/router.js", "require": "./dist/lib/router.js", "types": "./dist/lib/router.d.ts" }, "./plugins": { "import": "./dist/lib/plugins.js", "require": "./dist/lib/plugins.js", "types": "./dist/lib/plugins.d.ts" } }, "scripts": { "build": "npm run build:lib && npm run build:cli", "build:lib": "tsc -p tsconfig.lib.json", "build:cli": "esbuild src/cli.ts --bundle --platform=node --outfile=dist/cli.js && shx cp node_modules/tiktoken/tiktoken_bg.wasm dist/tiktoken_bg.wasm", "build:watch": "npm run build:lib -- --watch", "dev": "npm run build:watch" } } ``` **Create Library-Specific TypeScript Config** ```json // tsconfig.lib.json { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./dist/lib", "declaration": true, "declarationMap": true, "module": "CommonJS", "target": "ES2020", "moduleResolution": "node", "esModuleInterop": true, "allowSyntheticDefaultImports": true }, "include": ["src/**/*.ts"], "exclude": ["src/cli.ts", "**/*.test.ts"] } ``` #### 1.2 Enhanced Library Exports **Create Main Library Entry Point** ```typescript // src/lib.ts export * from './core'; export * from './features'; export * from './utils/config-presets'; export * from './utils/context-extractor'; export * from './utils/token-counter'; export * from './utils/router'; export * from './plugins'; // Enhanced server exports export { createConduitServer, ConduitServer, type EnhancedServerConfig } from './serverWrapper'; // Main run function export { run } from './index'; // Convenience factory functions export const createConduitClient = (config: ConduitConfig) => { return new ConduitServer({ initialConfig: config }); }; export const createConduitRouter = (config: ConduitConfig) => { return new ConduitRouter(config); }; ``` **Update Index File** ```typescript // src/index.ts (updated) export * from './lib'; // Keep existing run function for backward compatibility export { run } from './server'; ``` #### 1.3 Core Type Definitions **Create Comprehensive Type Definitions** ```typescript // src/types/index.ts export interface ConduitConfig { preset?: string; presetConfig?: ConfigPreset; Providers?: ProviderConfig[]; Router?: RouterConfig; plugins?: PluginConfig[]; server?: ServerConfig; } export interface ServerConfig { port?: number; host?: string; cors?: boolean; healthCheck?: boolean; gracefulShutdown?: boolean; timeout?: number; } export interface ProviderConfig { name: string; api_base_url: string; api_key: string; models: string[]; transformer?: { use: string[]; }; } export interface RouterConfig { default?: string; longContext?: string; background?: string; think?: string; customRules?: RoutingRule[]; } export interface RoutingRule { id: string; name: string; conditions: RuleCondition[]; target: string; priority: number; enabled: boolean; } export interface RuleCondition { field: string; operator: 'gt' | 'lt' | 'eq' | 'contains' | 'matches'; value: any; } export interface RoutingContext { tokenCount: number; model: string; message: string; metadata: Record<string, any>; } export interface RoutingDecision { model: string; provider: string; reason: string; confidence: number; metadata: Record<string, any>; } ``` ### Phase 2: Enhanced API Design (Week 2) #### 2.1 Programmatic Server Management **Create Enhanced Server Class** ```typescript // src/server/ConduitServer.ts export class ConduitServer { private server: any; private config: ConduitConfig; private plugins: PluginManager; private router: ConduitRouter; private healthMonitor: HealthMonitor; constructor(config: ConduitConfig) { this.config = config; this.plugins = new PluginManager(); this.router = new ConduitRouter(config); this.healthMonitor = new HealthMonitor(); } async start(): Promise<void> { // Load plugins await this.plugins.loadPlugins(this.config.plugins || []); // Initialize router await this.router.initialize(); // Start server this.server = await this.createServer(); // Start health monitoring this.healthMonitor.start(); console.log(`Conduit server started on port ${this.config.server?.port || 3456}`); } async stop(): Promise<void> { this.healthMonitor.stop(); if (this.server) { await this.server.close(); } await this.plugins.cleanup(); } async route(context: RoutingContext): Promise<RoutingDecision> { return this.router.route(context); } getHealth(): HealthStatus { return this.healthMonitor.getStatus(); } async updateConfig(config: Partial<ConduitConfig>): Promise<void> { this.config = { ...this.config, ...config }; await this.router.updateConfig(this.config); } } ``` #### 2.2 Standalone Router Class **Create Standalone Router** ```typescript // src/router/ConduitRouter.ts export class ConduitRouter { private config: ConduitConfig; private tokenCounter: TokenCounter; private plugins: PluginManager; private providers: Map<string, ProviderConfig>; constructor(config: ConduitConfig) { this.config = config; this.tokenCounter = new TokenCounter(); this.plugins = new PluginManager(); this.providers = new Map(); } async initialize(): Promise<void> { // Initialize providers this.config.Providers?.forEach(provider => { this.providers.set(provider.name, provider); }); // Load plugins await this.plugins.loadPlugins(this.config.plugins || []); } async route(context: RoutingContext): Promise<RoutingDecision> { // Apply pre-routing plugins const processedContext = await this.plugins.beforeRouting(context); // Count tokens const tokenCount = await this.tokenCounter.count(processedContext.message); // Make routing decision const decision = await this.makeRoutingDecision({ ...processedContext, tokenCount }); // Apply post-routing plugins const finalDecision = await this.plugins.afterRouting(decision); return finalDecision; } private async makeRoutingDecision(context: RoutingContext): Promise<RoutingDecision> { // Custom routing rules const customDecision = await this.evaluateCustomRules(context); if (customDecision) { return customDecision; } // Token-based routing if (context.tokenCount > 60000) { return this.createDecision(this.config.Router?.longContext || 'default', 'token-based'); } // Default routing return this.createDecision(this.config.Router?.default || 'default', 'default'); } async updateConfig(config: ConduitConfig): Promise<void> { this.config = config; await this.initialize(); } } ``` #### 2.3 Enhanced Plugin System **Improved Plugin Interface** ```typescript // src/plugins/PluginInterface.ts export interface ConduitPlugin { name: string; version: string; description?: string; // Lifecycle hooks onLoad?(): Promise<void>; onUnload?(): Promise<void>; // Routing hooks beforeRouting?(context: RoutingContext): Promise<RoutingContext>; afterRouting?(decision: RoutingDecision): Promise<RoutingDecision>; // Custom routing customRouter?(context: RoutingContext): Promise<RoutingDecision | null>; // Health monitoring getHealth?(): HealthStatus; // Configuration validateConfig?(config: any): boolean; } export interface PluginConfig { name: string; enabled: boolean; config?: any; source?: 'file' | 'npm' | 'inline'; path?: string; module?: ConduitPlugin; } ``` **Enhanced Plugin Manager** ```typescript // src/plugins/PluginManager.ts export class PluginManager { private plugins: Map<string, ConduitPlugin> = new Map(); private pluginConfigs: Map<string, PluginConfig> = new Map(); async loadPlugins(configs: PluginConfig[]): Promise<void> { for (const config of configs) { if (!config.enabled) continue; try { const plugin = await this.loadPlugin(config); this.plugins.set(config.name, plugin); this.pluginConfigs.set(config.name, config); if (plugin.onLoad) { await plugin.onLoad(); } } catch (error) { console.error(`Failed to load plugin ${config.name}:`, error); } } } private async loadPlugin(config: PluginConfig): Promise<ConduitPlugin> { switch (config.source) { case 'inline': return config.module!; case 'npm': return require(config.name); case 'file': default: return require(config.path || config.name); } } async beforeRouting(context: RoutingContext): Promise<RoutingContext> { let processedContext = context; for (const plugin of this.plugins.values()) { if (plugin.beforeRouting) { processedContext = await plugin.beforeRouting(processedContext); } } return processedContext; } async afterRouting(decision: RoutingDecision): Promise<RoutingDecision> { let processedDecision = decision; for (const plugin of this.plugins.values()) { if (plugin.afterRouting) { processedDecision = await plugin.afterRouting(processedDecision); } } return processedDecision; } async customRouting(context: RoutingContext): Promise<RoutingDecision | null> { for (const plugin of this.plugins.values()) { if (plugin.customRouter) { const decision = await plugin.customRouter(context); if (decision) { return decision; } } } return null; } async cleanup(): Promise<void> { for (const plugin of this.plugins.values()) { if (plugin.onUnload) { await plugin.onUnload(); } } this.plugins.clear(); this.pluginConfigs.clear(); } } ``` ### Phase 3: Synapse Integration Layer (Week 3) #### 3.1 Synapse-Specific Features **Create Synapse Integration Module** ```typescript // src/integrations/synapse.ts export class SynapseIntegration { private router: ConduitRouter; private usageTracker: UsageTracker; private contextExtractor: ContextExtractor; constructor(config: ConduitConfig) { this.router = new ConduitRouter(config); this.usageTracker = new UsageTracker(); this.contextExtractor = new ContextExtractor(); } async routeClaudeRequest( args: string[], env: NodeJS.ProcessEnv ): Promise<{ model: string; provider: string; metadata: any; }> { // Extract Synapse context const context = this.contextExtractor.extractFromEnvironment(env); // Create routing context const routingContext: RoutingContext = { tokenCount: await this.estimateTokenCount(args), model: this.extractModelFromArgs(args), message: this.extractMessageFromArgs(args), metadata: { projectId: context.projectId, agentId: context.agentId, agentType: context.agentType, ...context.projectModelConfig, ...context.agentModelConfig } }; // Route request const decision = await this.router.route(routingContext); // Track usage this.usageTracker.trackRequest(routingContext, decision); return { model: decision.model, provider: decision.provider, metadata: decision.metadata }; } async trackUsage(usage: UsageData): Promise<void> { await this.usageTracker.track(usage); } getUsageStats(projectId: string): Promise<UsageStats> { return this.usageTracker.getStats(projectId); } } ``` #### 3.2 Configuration Management **Enhanced Configuration System** ```typescript // src/config/ConfigManager.ts export class ConfigManager { private config: ConduitConfig; private presetManager: ConfigPresetManager; constructor(initialConfig?: ConduitConfig) { this.config = initialConfig || {}; this.presetManager = new ConfigPresetManager(); } async loadPreset(presetName: string): Promise<void> { const preset = await this.presetManager.loadBuiltinPreset(presetName); this.config = this.mergeConfigs(this.config, preset); } updateConfig(updates: Partial<ConduitConfig>): void { this.config = this.mergeConfigs(this.config, updates); } getConfig(): ConduitConfig { return { ...this.config }; } validateConfig(): ValidationResult { return this.presetManager.validateConfig(this.config); } private mergeConfigs(base: ConduitConfig, updates: Partial<ConduitConfig>): ConduitConfig { // Deep merge logic return { ...base, ...updates, Router: { ...base.Router, ...updates.Router }, server: { ...base.server, ...updates.server } }; } } ``` ### Phase 4: Usage and Monitoring (Week 4) #### 4.1 Enhanced Usage Tracking **Usage Tracking System** ```typescript // src/monitoring/UsageTracker.ts export class UsageTracker { private storage: UsageStorage; private metrics: MetricsCollector; constructor(config: UsageConfig) { this.storage = new UsageStorage(config.storage); this.metrics = new MetricsCollector(config.metrics); } async trackRequest(context: RoutingContext, decision: RoutingDecision): Promise<void> { const usage: UsageRecord = { id: generateId(), timestamp: new Date(), projectId: context.metadata.projectId, agentId: context.metadata.agentId, model: decision.model, provider: decision.provider, tokenCount: context.tokenCount, cost: this.calculateCost(context.tokenCount, decision.model), routingReason: decision.reason, metadata: { ...context.metadata, ...decision.metadata } }; await this.storage.store(usage); this.metrics.record(usage); } async getStats(projectId: string, timeRange?: TimeRange): Promise<UsageStats> { return this.storage.getStats(projectId, timeRange); } async getCostBreakdown(projectId: string): Promise<CostBreakdown> { return this.storage.getCostBreakdown(projectId); } } ``` #### 4.2 Health Monitoring **Health Monitoring System** ```typescript // src/monitoring/HealthMonitor.ts export class HealthMonitor { private checks: Map<string, HealthCheck> = new Map(); private status: HealthStatus = { healthy: true, checks: {}, uptime: 0, startTime: new Date() }; start(): void { // Register default health checks this.registerCheck('server', () => this.checkServer()); this.registerCheck('router', () => this.checkRouter()); this.registerCheck('plugins', () => this.checkPlugins()); this.registerCheck('storage', () => this.checkStorage()); // Start monitoring setInterval(() => this.runHealthChecks(), 30000); } registerCheck(name: string, check: HealthCheck): void { this.checks.set(name, check); } async runHealthChecks(): Promise<void> { for (const [name, check] of this.checks) { try { const result = await check(); this.status.checks[name] = result; } catch (error) { this.status.checks[name] = { healthy: false, error: error.message, timestamp: new Date() }; } } this.status.healthy = Object.values(this.status.checks) .every(check => check.healthy); this.status.uptime = Date.now() - this.status.startTime.getTime(); } getStatus(): HealthStatus { return { ...this.status }; } } ``` ### Phase 5: Documentation and Testing (Week 5) #### 5.1 Comprehensive Documentation **Create Usage Examples** ```typescript // docs/examples/synapse-integration.ts import { ConduitServer, SynapseIntegration } from '@tehreet/conduit'; // Example 1: Basic Synapse integration const conduit = new ConduitServer({ preset: 'synapse', server: { port: 3456, healthCheck: true }, Router: { default: 'anthropic,claude-3-5-sonnet-20241022', longContext: 'anthropic,claude-3-5-sonnet-20241022', background: 'anthropic,claude-3-5-haiku-20241022' } }); await conduit.start(); // Example 2: Programmatic routing const integration = new SynapseIntegration(config); const routingDecision = await integration.routeClaudeRequest( ['--message', 'Hello world'], process.env ); // Example 3: Usage tracking const stats = await integration.getUsageStats('project-123'); console.log('Total cost:', stats.totalCost); console.log('Request count:', stats.requestCount); ``` #### 5.2 Testing Strategy **Unit Tests** ```typescript // tests/unit/ConduitServer.test.ts describe('ConduitServer', () => { let server: ConduitServer; beforeEach(() => { server = new ConduitServer(testConfig); }); it('should start and stop cleanly', async () => { await server.start(); expect(server.getHealth().healthy).toBe(true); await server.stop(); expect(server.getHealth().healthy).toBe(false); }); it('should route requests correctly', async () => { const decision = await server.route({ tokenCount: 1000, model: 'claude-3-5-sonnet-20241022', message: 'Test message', metadata: {} }); expect(decision.model).toBe('claude-3-5-sonnet-20241022'); expect(decision.provider).toBe('anthropic'); }); }); ``` ## Migration Strategy ### Backward Compatibility 1. **CLI Interface**: Maintain existing CLI commands and functionality 2. **Configuration**: Support existing configuration file formats 3. **Plugin System**: Ensure existing plugins continue to work 4. **API Endpoints**: Maintain existing HTTP API endpoints ### Migration Path for Existing Users 1. **Phase 1**: Update package, CLI continues to work as before 2. **Phase 2**: Introduce library usage alongside CLI 3. **Phase 3**: Migrate to library usage gradually 4. **Phase 4**: Deprecate CLI-only features (with long deprecation period) ## Success Metrics 1. **Performance**: Library usage should be 10x faster than CLI spawning 2. **Memory Usage**: Memory footprint should be 50% lower than CLI spawning 3. **Error Handling**: 99.9% of errors should be properly captured and handled 4. **Compatibility**: 100% backward compatibility with existing CLI usage 5. **Adoption**: 80% of new integrations should use library interface ## Timeline - **Week 1**: Core library infrastructure and build system - **Week 2**: Enhanced API design and programmatic interfaces - **Week 3**: Synapse integration layer and configuration management - **Week 4**: Usage tracking and monitoring systems - **Week 5**: Documentation, testing, and migration tools ## Risk Mitigation 1. **Breaking Changes**: Comprehensive testing and gradual rollout 2. **Performance**: Benchmarking and optimization at each phase 3. **Compatibility**: Extensive testing with existing configurations 4. **Security**: Security audit of all new APIs and interfaces 5. **Documentation**: Clear migration guides and examples This plan transforms Conduit into a scalable, library-first solution while maintaining full backward compatibility and providing a smooth migration path for existing users.