UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

539 lines (538 loc) 22.7 kB
import path from 'path'; import { readFile } from 'fs/promises'; import { getProjectRoot } from '../tools/code-map-generator/utils/pathUtils.enhanced.js'; import { ConfigurationError, ValidationError, createErrorContext } from '../tools/vibe-task-manager/utils/enhanced-errors.js'; import logger from '../logger.js'; export class OpenRouterConfigManager { static instance = null; config = null; llmConfig = null; configCache = new Map(); cacheTTL = 300000; llmConfigPath; initializationPromise = null; constructor() { const projectRoot = getProjectRoot(); this.llmConfigPath = path.join(projectRoot, 'llm_config.json'); logger.debug('OpenRouterConfigManager initialized'); } static getInstance() { if (!OpenRouterConfigManager.instance) { OpenRouterConfigManager.instance = new OpenRouterConfigManager(); } return OpenRouterConfigManager.instance; } async initialize() { if (this.initializationPromise) { return this.initializationPromise; } this.initializationPromise = this._performInitialization(); return this.initializationPromise; } async _performInitialization() { const context = createErrorContext('OpenRouterConfigManager', '_performInitialization') .metadata({ timestamp: new Date() }) .build(); try { await this.ensureEnvironmentLoaded(); const envValidation = this.validateEnvironmentVariables(); if (!envValidation.valid) { throw new ConfigurationError(`Environment validation failed: ${envValidation.missing.join(', ')}`, context, { configKey: 'environment_variables', expectedValue: 'OPENROUTER_API_KEY is required', userFriendly: true }); } await this.loadLLMConfig(); this.config = { baseUrl: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1', apiKey: process.env.OPENROUTER_API_KEY || '', geminiModel: process.env.GEMINI_MODEL || 'google/gemini-2.5-flash-preview-05-20', perplexityModel: process.env.PERPLEXITY_MODEL || 'perplexity/sonar', llm_mapping: this.llmConfig?.llm_mapping || {}, env: { VIBE_TASK_MANAGER_READ_DIR: process.env.VIBE_TASK_MANAGER_READ_DIR, VIBE_CODER_OUTPUT_DIR: process.env.VIBE_CODER_OUTPUT_DIR, CODE_MAP_ALLOWED_DIR: process.env.CODE_MAP_ALLOWED_DIR, VIBE_TASK_MANAGER_SECURITY_MODE: process.env.VIBE_TASK_MANAGER_SECURITY_MODE, LOG_LEVEL: process.env.LOG_LEVEL, NODE_ENV: process.env.NODE_ENV, LLM_CONFIG_PATH: process.env.LLM_CONFIG_PATH } }; const configValidation = this.validateConfiguration(); if (!configValidation.valid) { throw new ValidationError(`Configuration validation failed: ${configValidation.errors.join(', ')}`, context, { userFriendly: true }); } if (configValidation.warnings.length > 0) { logger.warn({ warnings: configValidation.warnings, suggestions: configValidation.suggestions }, 'OpenRouter configuration has warnings'); } const mappingCount = Object.keys(this.config.llm_mapping || {}).length; const hasDefaultGeneration = Boolean(this.config.llm_mapping?.['default_generation']); logger.info({ hasApiKey: Boolean(this.config.apiKey), baseUrl: this.config.baseUrl, geminiModel: this.config.geminiModel, perplexityModel: this.config.perplexityModel, mappingCount, hasDefaultGeneration, configPath: this.llmConfigPath }, 'OpenRouterConfigManager initialized successfully'); if (mappingCount > 0) { const sampleMappings = Object.entries(this.config.llm_mapping || {}) .slice(0, 3) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); logger.debug({ sampleMappings, totalMappings: mappingCount }, 'LLM mapping sample (first 3 entries)'); } } catch (error) { if (error instanceof ConfigurationError || error instanceof ValidationError) { throw error; } throw new ConfigurationError(`Failed to initialize OpenRouter configuration: ${error instanceof Error ? error.message : String(error)}`, context, { cause: error instanceof Error ? error : undefined, userFriendly: true }); } } async loadLLMConfig() { const context = createErrorContext('OpenRouterConfigManager', 'loadLLMConfig') .metadata({ configPath: this.llmConfigPath }) .build(); try { const configContent = await this.readFileWithRetry(this.llmConfigPath, 3); if (configContent === undefined || configContent === null) { throw new Error('Configuration file returned undefined/null content'); } if (typeof configContent !== 'string') { throw new Error(`Configuration file returned unexpected type: ${typeof configContent}`); } if (configContent.trim().length === 0) { throw new Error('Configuration file is empty'); } logger.debug({ configPath: this.llmConfigPath, contentLength: configContent.length }, 'Configuration file read successfully'); let parsedConfig; try { parsedConfig = JSON.parse(configContent); } catch (parseError) { throw new ValidationError(`Invalid JSON in LLM configuration file: ${parseError instanceof Error ? parseError.message : String(parseError)}`, context, { userFriendly: true }); } if (!parsedConfig || typeof parsedConfig !== 'object') { throw new ValidationError('LLM configuration must be a valid JSON object', context, { userFriendly: true }); } if (!parsedConfig.llm_mapping || typeof parsedConfig.llm_mapping !== 'object') { throw new ValidationError('LLM configuration must contain llm_mapping object', context, { userFriendly: true }); } this.llmConfig = parsedConfig; const requiredMappings = ['default_generation']; const missing = requiredMappings.filter(mapping => !this.llmConfig.llm_mapping[mapping]); if (missing.length > 0) { logger.warn({ missing, configPath: this.llmConfigPath }, 'LLM configuration missing required mappings'); } logger.debug({ configPath: this.llmConfigPath, mappingCount: Object.keys(this.llmConfig.llm_mapping).length, hasDefaultGeneration: Boolean(this.llmConfig.llm_mapping['default_generation']) }, 'LLM configuration loaded successfully'); } catch (error) { if (error instanceof ValidationError) { throw error; } logger.error({ err: error, configPath: this.llmConfigPath, errorType: error instanceof Error ? error.constructor.name : typeof error, errorMessage: error instanceof Error ? error.message : String(error) }, 'Critical: Failed to load LLM configuration'); throw new ConfigurationError(`Failed to load LLM configuration: ${error instanceof Error ? error.message : String(error)}`, context, { cause: error instanceof Error ? error : undefined, userFriendly: true }); } } isInitialized() { return Boolean(this.config && this.llmConfig); } isInitializing() { return Boolean(this.initializationPromise); } async getOpenRouterConfig() { if (!this.config) { if (this.initializationPromise) { try { await this.initializationPromise; } catch (error) { logger.error({ err: error }, 'Initialization failed while waiting for configuration'); throw new Error(`Configuration initialization failed: ${error instanceof Error ? error.message : String(error)}`); } } else { try { await this.initialize(); } catch (error) { logger.error({ err: error }, 'Failed to initialize configuration'); throw new Error(`Failed to initialize OpenRouter configuration: ${error instanceof Error ? error.message : String(error)}`); } } } if (!this.config) { throw new Error('Configuration is null after successful initialization - this should not happen'); } if (!this.llmConfig) { logger.warn('LLM configuration is missing, using empty mapping'); } return { baseUrl: this.config.baseUrl, apiKey: this.config.apiKey, geminiModel: this.config.geminiModel, perplexityModel: this.config.perplexityModel, llm_mapping: { ...this.config.llm_mapping }, tools: this.config.tools ? { ...this.config.tools } : undefined, config: this.config.config ? { ...this.config.config } : undefined, env: this.config.env ? { ...this.config.env } : undefined }; } getModelForTask(taskName) { if (!this.config) { logger.warn({ taskName, initialized: Boolean(this.config), initializationInProgress: Boolean(this.initializationPromise) }, 'Configuration not initialized when getting model for task, using fallback'); return process.env.GEMINI_MODEL || 'google/gemini-2.5-flash-preview-05-20'; } if (this.config.llm_mapping && this.config.llm_mapping[taskName]) { return this.config.llm_mapping[taskName]; } if (this.config.llm_mapping && this.config.llm_mapping['default_generation']) { return this.config.llm_mapping['default_generation']; } return this.config.geminiModel; } async getLLMModel(operation) { if (!this.config) { try { await this.initialize(); } catch (error) { logger.warn({ err: error, operation, fallbackModel: this.getDefaultModel() }, 'Failed to initialize configuration for LLM model lookup, using fallback'); return this.getDefaultModel(); } } if (!this.config || !this.llmConfig) { logger.warn({ operation, hasConfig: Boolean(this.config), hasLlmConfig: Boolean(this.llmConfig), fallbackModel: this.getDefaultModel() }, 'Configuration incomplete after initialization, using fallback model'); return this.getDefaultModel(); } const mappedModel = this.llmConfig.llm_mapping[operation]; if (mappedModel) { return mappedModel; } const defaultGeneration = this.llmConfig.llm_mapping['default_generation']; if (defaultGeneration) { return defaultGeneration; } return this.getDefaultModel(); } getDefaultModel() { return process.env.GEMINI_MODEL || 'google/gemini-2.5-flash-preview-05-20'; } async readFileWithRetry(filePath, maxRetries = 3) { let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const content = await readFile(filePath, 'utf-8'); if (content === undefined || content === null) { throw new Error(`File read returned ${content}`); } if (typeof content !== 'string') { throw new Error(`File read returned non-string type: ${typeof content}`); } logger.debug({ filePath, attempt, contentLength: content.length, contentType: typeof content }, 'File read successful'); return content; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); logger.debug({ filePath, attempt, maxRetries, error: lastError.message, errorType: lastError.constructor.name }, `File read attempt ${attempt} failed`); if (attempt < maxRetries) { const backoffMs = Math.min(100 * Math.pow(2, attempt - 1), 1000); await new Promise(resolve => setTimeout(resolve, backoffMs)); } } } throw new Error(`Failed to read file after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`); } async reloadConfig() { this.config = null; this.llmConfig = null; this.initializationPromise = null; this.configCache.clear(); await this.initialize(); logger.info('OpenRouter configuration reloaded'); } async ensureEnvironmentLoaded() { await new Promise(resolve => setTimeout(resolve, 10)); if (!process.env.OPENROUTER_API_KEY) { try { const dotenv = await import('dotenv'); const path = await import('path'); const { fileURLToPath } = await import('url'); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const envPath = path.resolve(__dirname, '../../.env'); const result = dotenv.config({ path: envPath }); if (result.error) { logger.debug({ err: result.error, envPath }, 'Could not reload .env file during environment validation'); } else { logger.debug({ envPath, reloaded: result.parsed ? Object.keys(result.parsed) : [] }, 'Reloaded environment variables during initialization'); } } catch (error) { logger.debug({ err: error }, 'Failed to reload environment variables'); } } } validateEnvironmentVariables() { const missing = []; const invalid = []; const warnings = []; if (!process.env.OPENROUTER_API_KEY) { missing.push('OPENROUTER_API_KEY'); } if (!process.env.OPENROUTER_BASE_URL) { warnings.push('OPENROUTER_BASE_URL not set, using default'); } if (!process.env.GEMINI_MODEL) { warnings.push('GEMINI_MODEL not set, using default'); } if (!process.env.PERPLEXITY_MODEL) { warnings.push('PERPLEXITY_MODEL not set, using default'); } if (process.env.OPENROUTER_BASE_URL) { try { new URL(process.env.OPENROUTER_BASE_URL); } catch { invalid.push('OPENROUTER_BASE_URL must be a valid URL'); } } return { valid: missing.length === 0 && invalid.length === 0, missing, invalid, warnings }; } validateConfiguration() { const errors = []; const warnings = []; const suggestions = []; if (!this.config) { errors.push('Configuration not initialized'); return { valid: false, errors, warnings, suggestions }; } if (!this.config.apiKey) { errors.push('Missing OPENROUTER_API_KEY'); suggestions.push('Set OPENROUTER_API_KEY environment variable'); } if (!this.config.baseUrl) { errors.push('Missing OPENROUTER_BASE_URL'); suggestions.push('Set OPENROUTER_BASE_URL environment variable'); } if (!this.config.geminiModel) { errors.push('Missing GEMINI_MODEL'); suggestions.push('Set GEMINI_MODEL environment variable'); } if (!this.config.perplexityModel) { errors.push('Missing PERPLEXITY_MODEL'); suggestions.push('Set PERPLEXITY_MODEL environment variable'); } if (this.config.baseUrl) { try { const url = new URL(this.config.baseUrl); if (!url.protocol.startsWith('http')) { errors.push('OPENROUTER_BASE_URL must use HTTP or HTTPS protocol'); } } catch { errors.push('OPENROUTER_BASE_URL must be a valid URL'); } } if (this.config.geminiModel && !this.config.geminiModel.includes('gemini')) { warnings.push('GEMINI_MODEL does not appear to be a Gemini model'); } if (this.config.perplexityModel && !this.config.perplexityModel.includes('perplexity')) { warnings.push('PERPLEXITY_MODEL does not appear to be a Perplexity model'); } const mappingCount = Object.keys(this.config.llm_mapping || {}).length; if (!this.config.llm_mapping || mappingCount === 0) { warnings.push('No LLM mappings configured, using defaults'); suggestions.push('Configure llm_config.json with task-specific model mappings'); suggestions.push('Run: echo \'{"llm_mapping": {"default_generation": "google/gemini-2.5-flash-preview-05-20"}}\' > llm_config.json'); } else { logger.debug({ mappingCount, hasDefaultGeneration: Boolean(this.config.llm_mapping['default_generation']), configPath: this.llmConfigPath }, 'LLM mappings loaded successfully'); } if (this.config.llm_mapping && !this.config.llm_mapping['default_generation']) { warnings.push('No default_generation mapping configured'); suggestions.push('Add default_generation mapping to llm_config.json'); suggestions.push('This mapping is used as fallback when specific task mappings are not found'); } if (mappingCount > 0 && mappingCount < 10) { suggestions.push('Consider adding more task-specific mappings for better performance'); } if (this.config.llm_mapping && Object.keys(this.config.llm_mapping).length > 50) { suggestions.push('Consider optimizing LLM mappings for better performance'); } return { valid: errors.length === 0, errors, warnings, suggestions }; } getStatus() { return { initialized: Boolean(this.config), hasApiKey: Boolean(this.config?.apiKey), mappingCount: Object.keys(this.config?.llm_mapping || {}).length, cacheSize: this.configCache.size }; } getCachedConfig(key) { const entry = this.configCache.get(key); if (!entry) { return null; } const now = Date.now(); if (now - entry.timestamp > entry.ttl) { this.configCache.delete(key); return null; } return entry.config; } setCachedConfig(key, config, ttl) { this.configCache.set(key, { config: { ...config }, timestamp: Date.now(), ttl: ttl || this.cacheTTL }); } validateLLMMappings() { const missing = []; const recommendations = []; if (!this.llmConfig) { return { valid: false, missing: ['LLM configuration not loaded'], recommendations: ['Load llm_config.json file'] }; } const coreRequiredMappings = [ 'default_generation', 'task_decomposition', 'intent_recognition' ]; const recommendedMappings = [ 'research_query', 'sequential_thought_generation', 'context_curator_intent_analysis', 'context_curator_relevance_ranking', 'agent_coordination' ]; for (const mapping of coreRequiredMappings) { if (!this.llmConfig.llm_mapping[mapping]) { missing.push(mapping); } } for (const mapping of recommendedMappings) { if (!this.llmConfig.llm_mapping[mapping]) { recommendations.push(`Consider adding ${mapping} mapping for optimized performance`); } } return { valid: missing.length === 0, missing, recommendations }; } clearCache() { this.configCache.clear(); logger.debug('OpenRouter configuration cache cleared'); } getCacheStats() { const entries = Array.from(this.configCache.keys()); return { size: this.configCache.size, hitRate: 0, entries }; } static resetInstance() { OpenRouterConfigManager.instance = null; } } export async function getOpenRouterConfig() { const manager = OpenRouterConfigManager.getInstance(); return await manager.getOpenRouterConfig(); } export async function getLLMModelForOperation(operation) { const manager = OpenRouterConfigManager.getInstance(); return await manager.getLLMModel(operation); }