@tehreet/conduit
Version:
LLM API gateway with intelligent routing, robust process management, and health monitoring
819 lines (676 loc) • 22.2 kB
Markdown
# 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.