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
JavaScript
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);
}