UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

592 lines 21.4 kB
import * as fs from 'fs/promises'; import * as path from 'path'; import { logger } from '../logger.js'; /** * Model Bridge Manager for cross-provider model synchronization and management * Inspired by LM Studio Ollama Bridge patterns with intelligent routing */ export class ModelBridgeManager { providers; modelCache; symlinkDir; configPath; syncInterval; constructor(workspaceRoot) { this.providers = new Map(); this.modelCache = new Map(); this.symlinkDir = path.join(workspaceRoot, '.codecrucible', 'models'); this.configPath = path.join(workspaceRoot, 'config', 'model-bridge.yaml'); logger.info('Model bridge manager initialized', { symlinkDir: this.symlinkDir, configPath: this.configPath, }); } /** * Initialize model bridge with providers */ async initialize() { await this.ensureDirectoryExists(this.symlinkDir); await this.loadConfiguration(); await this.initializeProviders(); await this.startPeriodicSync(); logger.info('Model bridge manager fully initialized'); } /** * Register a model provider */ registerProvider(provider) { this.providers.set(provider.name, provider); logger.info(`Registered model provider: ${provider.name}`); } /** * Synchronize models across all providers */ async synchronizeModels() { const startTime = Date.now(); const allModels = new Map(); const errors = []; let symlinksCreated = 0; logger.info('Starting model synchronization across providers...'); // Discover models from all providers for (const [name, provider] of this.providers) { if (!provider.enabled) { logger.debug(`Skipping disabled provider: ${name}`); continue; } try { const isHealthy = await provider.isHealthy(); if (!isHealthy) { errors.push(`Provider ${name} is not healthy`); continue; } const models = await provider.discoverModels(); logger.info(`Discovered ${models.length} models from ${name}`); models.forEach(model => { const key = this.generateModelKey(model); if (!allModels.has(key) || this.isModelBetter(model, allModels.get(key))) { allModels.set(key, { ...model, provider: model.provider }); } }); } catch (error) { const errorMsg = `Failed to discover models from ${name}: ${error instanceof Error ? error.message : 'Unknown error'}`; errors.push(errorMsg); logger.error(errorMsg, error); } } // Create cross-provider symlinks try { symlinksCreated = await this.createModelSymlinks(allModels); } catch (error) { const errorMsg = `Failed to create symlinks: ${error instanceof Error ? error.message : 'Unknown error'}`; errors.push(errorMsg); logger.error(errorMsg, error); } // Update cache this.modelCache.clear(); allModels.forEach((model, key) => { this.modelCache.set(key, model); }); // Generate recommendations const recommendations = this.generateRecommendations(allModels, errors); const result = { synchronized: allModels.size, providers: Array.from(this.providers.values()).filter(p => p.enabled).length, symlinks: symlinksCreated, errors, recommendations, }; const duration = Date.now() - startTime; logger.info(`Model synchronization completed in ${duration}ms`, result); return result; } /** * Get all available models */ async getAvailableModels() { if (this.modelCache.size === 0) { await this.synchronizeModels(); } return Array.from(this.modelCache.values()); } /** * Find best model for a specific task */ async findBestModel(requirements) { const models = await this.getAvailableModels(); let candidates = models.filter(model => { // Filter by provider if specified if (requirements.provider && model.provider !== requirements.provider) { return false; } // Filter by capabilities switch (requirements.task) { case 'code-generation': return model.capabilities.codeGeneration; case 'reasoning': return model.capabilities.reasoning; case 'chat': return true; // All models can chat case 'analysis': return model.capabilities.reasoning || model.capabilities.codeGeneration; default: return true; } }); // Filter by language if specified if (requirements.language) { candidates = candidates.filter(model => model.capabilities.languages.includes(requirements.language) || model.capabilities.languages.includes('*') // Universal language support ); } // Score models based on requirements const scored = candidates.map(model => ({ model, score: this.calculateModelScore(model, requirements), })); // Sort by score and return best scored.sort((a, b) => b.score - a.score); return scored.length > 0 ? scored[0].model : null; } /** * Get provider health status */ async getProviderHealth() { const healthChecks = Array.from(this.providers.entries()).map(async ([name, provider]) => { const startTime = Date.now(); try { const healthy = await provider.isHealthy(); const latency = Date.now() - startTime; let modelsAvailable = 0; if (healthy) { try { const models = await provider.discoverModels(); modelsAvailable = models.length; } catch (error) { logger.warn(`Failed to count models for ${name}:`, error); } } return { provider: name, healthy, latency, modelsAvailable, }; } catch (error) { return { provider: name, healthy: false, latency: Date.now() - startTime, modelsAvailable: 0, error: error instanceof Error ? error.message : 'Unknown error', }; } }); return Promise.all(healthChecks); } /** * Create symbolic links for cross-provider model access */ async createModelSymlinks(models) { let created = 0; // Clean up existing symlinks first await this.cleanupOldSymlinks(); for (const [key, model] of models) { try { const symlinkName = this.generateSymlinkName(model); const symlinkPath = path.join(this.symlinkDir, symlinkName); // Create descriptive symlink pointing to model file if (await this.isValidModelPath(model.path)) { await fs.symlink(model.path, symlinkPath); created++; logger.debug(`Created symlink: ${symlinkName} -> ${model.path}`); // Create metadata file alongside symlink await this.createModelMetadataFile(symlinkPath, model); } } catch (error) { logger.warn(`Failed to create symlink for ${model.name}:`, error); } } return created; } /** * Generate descriptive symlink name */ generateSymlinkName(model) { const safeName = model.name .toLowerCase() .replace(/[^a-z0-9.-]/g, '_') .replace(/_+/g, '_'); const provider = model.provider; const family = model.metadata.family?.toLowerCase().replace(/[^a-z0-9]/g, '') || 'unknown'; const params = model.metadata.parameters?.toLowerCase().replace(/[^a-z0-9]/g, '') || ''; return `${provider}_${family}_${params}_${safeName}`; } /** * Create metadata file for model */ async createModelMetadataFile(symlinkPath, model) { const metadataPath = symlinkPath + '.metadata.json'; const metadata = { model: { id: model.id, name: model.name, provider: model.provider, size: model.size, capabilities: model.capabilities, metadata: model.metadata, }, bridge: { created: new Date().toISOString(), symlinkTarget: model.path, bridgeVersion: '1.0.0', }, }; await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); } /** * Clean up old symlinks and metadata files */ async cleanupOldSymlinks() { try { const files = await fs.readdir(this.symlinkDir); for (const file of files) { const filePath = path.join(this.symlinkDir, file); const stats = await fs.lstat(filePath); if (stats.isSymbolicLink() || file.endsWith('.metadata.json')) { await fs.unlink(filePath); } } logger.debug('Cleaned up old symlinks and metadata files'); } catch (error) { logger.warn('Failed to cleanup old symlinks:', error); } } /** * Generate unique key for model */ generateModelKey(model) { return `${model.provider}:${model.id}`; } /** * Check if one model is better than another */ isModelBetter(newModel, existingModel) { // Prefer models with better capabilities const newScore = this.calculateModelCapabilityScore(newModel); const existingScore = this.calculateModelCapabilityScore(existingModel); return newScore > existingScore; } /** * Calculate model capability score */ calculateModelCapabilityScore(model) { let score = 0; // Context window size score += model.capabilities.contextWindow / 1000; // Capabilities if (model.capabilities.codeGeneration) score += 10; if (model.capabilities.reasoning) score += 10; if (model.capabilities.streaming) score += 5; if (model.capabilities.multimodal) score += 8; // Language support score += model.capabilities.languages.length; // Provider preference (can be configured) const providerPreference = { ollama: 1.2, lmstudio: 1.1, openai: 1.0, anthropic: 1.0 }; score *= providerPreference[model.provider] || 1.0; return score; } /** * Calculate model score for specific requirements */ calculateModelScore(model, requirements) { let score = this.calculateModelCapabilityScore(model); // Task-specific scoring switch (requirements.task) { case 'code-generation': if (model.capabilities.codeGeneration) score *= 1.5; if (model.name.toLowerCase().includes('code')) score *= 1.2; break; case 'reasoning': if (model.capabilities.reasoning) score *= 1.5; if (model.capabilities.contextWindow > 8000) score *= 1.2; break; case 'analysis': if (model.capabilities.reasoning) score *= 1.3; if (model.capabilities.contextWindow > 4000) score *= 1.1; break; } // Language preference if (requirements.language && model.capabilities.languages.includes(requirements.language)) { score *= 1.3; } // Size considerations (smaller can be faster) if (model.size < 4 * 1024 * 1024 * 1024) { // < 4GB score *= 1.1; } return score; } /** * Validate model path exists and is accessible */ async isValidModelPath(modelPath) { try { await fs.access(modelPath); return true; } catch { return false; } } /** * Generate recommendations based on sync results */ generateRecommendations(models, errors) { const recommendations = []; // Check for missing providers if (models.size === 0) { recommendations.push('No models found. Check that LM Studio and Ollama are running with models loaded.'); } // Check for capability gaps const hasCodeGeneration = Array.from(models.values()).some(m => m.capabilities.codeGeneration); const hasReasoning = Array.from(models.values()).some(m => m.capabilities.reasoning); if (!hasCodeGeneration) { recommendations.push('Consider loading a code-generation model for better programming assistance.'); } if (!hasReasoning) { recommendations.push('Consider loading a reasoning-capable model for complex analysis tasks.'); } // Check for size variety const sizes = Array.from(models.values()).map(m => m.size); const avgSize = sizes.reduce((sum, size) => sum + size, 0) / sizes.length; if (sizes.every(size => size > 4 * 1024 * 1024 * 1024)) { recommendations.push('Consider adding smaller models for faster responses to simple queries.'); } // Error-based recommendations if (errors.length > 0) { recommendations.push('Some providers had errors. Check logs and provider configurations.'); } return recommendations; } /** * Load configuration from file */ async loadConfiguration() { try { const configContent = await fs.readFile(this.configPath, 'utf-8'); const yaml = await import('js-yaml'); const config = yaml.load(configContent); // Apply configuration settings logger.debug('Loaded model bridge configuration', { config }); } catch (error) { logger.debug('No model bridge configuration found, using defaults'); } } /** * Initialize providers (Ollama, LM Studio, etc.) */ async initializeProviders() { // Register Ollama provider const ollamaProvider = new OllamaModelProvider(); this.registerProvider(ollamaProvider); // Register LM Studio provider const lmStudioProvider = new LMStudioModelProvider(); this.registerProvider(lmStudioProvider); logger.info('Initialized model providers'); } /** * Start periodic synchronization */ async startPeriodicSync() { // Sync every 5 minutes this.syncInterval = setInterval( // TODO: Store interval ID and call clearInterval in cleanup async () => { try { await this.synchronizeModels(); } catch (error) { logger.error('Periodic sync failed:', error); } }, 5 * 60 * 1000); logger.debug('Started periodic model synchronization'); } /** * Ensure directory exists */ async ensureDirectoryExists(dir) { try { await fs.access(dir); } catch { await fs.mkdir(dir, { recursive: true }); } } /** * Cleanup resources */ async dispose() { if (this.syncInterval) { clearInterval(this.syncInterval); } this.providers.clear(); this.modelCache.clear(); logger.info('Model bridge manager disposed'); } } /** * Ollama provider implementation */ class OllamaModelProvider { name = 'ollama'; endpoint = 'http://localhost:11434'; enabled = true; async discoverModels() { try { const response = await fetch(`${this.endpoint}/api/tags`); const data = await response.json(); return data.models?.map((model) => this.mapOllamaModel(model)) || []; } catch (error) { logger.warn('Failed to discover Ollama models:', error); return []; } } async isHealthy() { try { const response = await fetch(`${this.endpoint}/api/tags`, { method: 'GET', signal: AbortSignal.timeout(5000), }); return response.ok; } catch { return false; } } async getModelDetails(modelId) { try { const response = await fetch(`${this.endpoint}/api/show`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: modelId }), }); if (!response.ok) return null; const data = await response.json(); return this.mapOllamaModel(data); } catch { return null; } } mapOllamaModel(model) { return { id: model.name, name: model.name, provider: 'ollama', path: model.digest || model.name, // Ollama uses digest/name as path size: model.size || 0, capabilities: { maxTokens: 4096, // Default for Ollama contextWindow: 8192, streaming: true, codeGeneration: model.name.toLowerCase().includes('code'), reasoning: model.name.toLowerCase().includes('qwq') || model.name.toLowerCase().includes('reasoning'), multimodal: model.name.toLowerCase().includes('vision') || model.name.toLowerCase().includes('llava'), languages: ['*'], // Assume universal language support }, status: 'available', metadata: { family: model.details?.family || 'unknown', version: model.details?.version || '1.0', quantization: model.details?.quantization_level, parameters: model.details?.parameter_size || 'unknown', license: 'unknown', description: model.details?.description || '', tags: model.name.split(':'), }, }; } } /** * LM Studio provider implementation */ class LMStudioModelProvider { name = 'lmstudio'; endpoint = 'http://localhost:1234'; enabled = true; async discoverModels() { try { const response = await fetch(`${this.endpoint}/v1/models`); const data = await response.json(); return data.data?.map((model) => this.mapLMStudioModel(model)) || []; } catch (error) { logger.warn('Failed to discover LM Studio models:', error); return []; } } async isHealthy() { try { const response = await fetch(`${this.endpoint}/v1/models`, { method: 'GET', signal: AbortSignal.timeout(5000), }); return response.ok; } catch { return false; } } async getModelDetails(modelId) { const models = await this.discoverModels(); return models.find(model => model.id === modelId) || null; } mapLMStudioModel(model) { return { id: model.id, name: model.id, provider: 'lmstudio', path: model.id, // LM Studio uses model ID as path size: 0, // LM Studio doesn't provide size in API capabilities: { maxTokens: 2048, // Default for LM Studio contextWindow: 4096, streaming: true, codeGeneration: model.id.toLowerCase().includes('code'), reasoning: true, // Assume reasoning capability multimodal: false, // Most LM Studio models are text-only languages: ['*'], // Assume universal language support }, status: 'available', metadata: { family: 'unknown', version: '1.0', parameters: 'unknown', license: 'unknown', description: `LM Studio model: ${model.id}`, tags: model.id.split('-'), }, }; } } //# sourceMappingURL=model-bridge-manager.js.map