import options from '../../options.js'; import log from '../../log.js'; import type { AIConfig, ProviderPrecedenceConfig, ModelIdentifier, ModelConfig, ProviderType, ConfigValidationResult, ProviderSettings, OpenAISettings, AnthropicSettings, OllamaSettings } from '../interfaces/configuration_interfaces.js'; /** * Configuration manager that handles conversion from string-based options * to proper typed configuration objects. * * This is the ONLY place where string parsing should happen for LLM configurations. */ export class ConfigurationManager { private static instance: ConfigurationManager | null = null; private constructor() {} public static getInstance(): ConfigurationManager { if (!ConfigurationManager.instance) { ConfigurationManager.instance = new ConfigurationManager(); } return ConfigurationManager.instance; } /** * Get the complete AI configuration - always fresh, no caching */ public async getAIConfig(): Promise { try { const config: AIConfig = { enabled: await this.getAIEnabled(), selectedProvider: await this.getSelectedProvider(), defaultModels: await this.getDefaultModels(), providerSettings: await this.getProviderSettings() }; return config; } catch (error) { log.error(`Error loading AI configuration: ${error}`); return this.getDefaultConfig(); } } /** * Get the selected AI provider */ public async getSelectedProvider(): Promise { try { const selectedProvider = options.getOption('aiSelectedProvider'); return selectedProvider as ProviderType || null; } catch (error) { log.error(`Error getting selected provider: ${error}`); return null; } } /** * Parse model identifier with optional provider prefix * Handles formats like "gpt-4", "openai:gpt-4", "ollama:llama2:7b" */ public parseModelIdentifier(modelString: string): ModelIdentifier { if (!modelString) { return { modelId: '', fullIdentifier: '' }; } const parts = modelString.split(':'); if (parts.length === 1) { // No provider prefix, just model name return { modelId: modelString, fullIdentifier: modelString }; } // Check if first part is a known provider const potentialProvider = parts[0].toLowerCase(); const knownProviders: ProviderType[] = ['openai', 'anthropic', 'ollama']; if (knownProviders.includes(potentialProvider as ProviderType)) { // Provider prefix format const provider = potentialProvider as ProviderType; const modelId = parts.slice(1).join(':'); // Rejoin in case model has colons return { provider, modelId, fullIdentifier: modelString }; } // Not a provider prefix, treat whole string as model name return { modelId: modelString, fullIdentifier: modelString }; } /** * Create model configuration from string */ public createModelConfig(modelString: string, defaultProvider?: ProviderType): ModelConfig { const identifier = this.parseModelIdentifier(modelString); const provider = identifier.provider || defaultProvider || 'openai'; return { provider, modelId: identifier.modelId, displayName: identifier.fullIdentifier }; } /** * Get default models for each provider - ONLY from user configuration */ public async getDefaultModels(): Promise> { try { const openaiModel = options.getOption('openaiDefaultModel'); const anthropicModel = options.getOption('anthropicDefaultModel'); const ollamaModel = options.getOption('ollamaDefaultModel'); return { openai: openaiModel || undefined, anthropic: anthropicModel || undefined, ollama: ollamaModel || undefined }; } catch (error) { log.error(`Error loading default models: ${error}`); // Return undefined for all providers if we can't load config return { openai: undefined, anthropic: undefined, ollama: undefined }; } } /** * Get provider-specific settings */ public async getProviderSettings(): Promise { try { const openaiApiKey = options.getOption('openaiApiKey'); const openaiBaseUrl = options.getOption('openaiBaseUrl'); const openaiDefaultModel = options.getOption('openaiDefaultModel'); const anthropicApiKey = options.getOption('anthropicApiKey'); const anthropicBaseUrl = options.getOption('anthropicBaseUrl'); const anthropicDefaultModel = options.getOption('anthropicDefaultModel'); const ollamaBaseUrl = options.getOption('ollamaBaseUrl'); const ollamaDefaultModel = options.getOption('ollamaDefaultModel'); const settings: ProviderSettings = {}; if (openaiApiKey || openaiBaseUrl || openaiDefaultModel) { settings.openai = { apiKey: openaiApiKey, baseUrl: openaiBaseUrl, defaultModel: openaiDefaultModel }; } if (anthropicApiKey || anthropicBaseUrl || anthropicDefaultModel) { settings.anthropic = { apiKey: anthropicApiKey, baseUrl: anthropicBaseUrl, defaultModel: anthropicDefaultModel }; } if (ollamaBaseUrl || ollamaDefaultModel) { settings.ollama = { baseUrl: ollamaBaseUrl, defaultModel: ollamaDefaultModel }; } return settings; } catch (error) { log.error(`Error loading provider settings: ${error}`); return {}; } } /** * Validate configuration */ public async validateConfig(): Promise { const result: ConfigValidationResult = { isValid: true, errors: [], warnings: [] }; try { const config = await this.getAIConfig(); if (!config.enabled) { result.warnings.push('AI features are disabled'); return result; } // Validate selected provider if (!config.selectedProvider) { result.errors.push('No AI provider selected'); result.isValid = false; } else { // Validate selected provider settings const providerConfig = config.providerSettings[config.selectedProvider]; if (config.selectedProvider === 'openai') { const openaiConfig = providerConfig as OpenAISettings | undefined; if (!openaiConfig?.apiKey) { result.warnings.push('OpenAI API key is not configured'); } } if (config.selectedProvider === 'anthropic') { const anthropicConfig = providerConfig as AnthropicSettings | undefined; if (!anthropicConfig?.apiKey) { result.warnings.push('Anthropic API key is not configured'); } } if (config.selectedProvider === 'ollama') { const ollamaConfig = providerConfig as OllamaSettings | undefined; if (!ollamaConfig?.baseUrl) { result.warnings.push('Ollama base URL is not configured'); } } } } catch (error) { result.errors.push(`Configuration validation error: ${error}`); result.isValid = false; } return result; } // Private helper methods private async getAIEnabled(): Promise { try { return options.getOptionBool('aiEnabled'); } catch { return false; } } private parseProviderList(precedenceOption: string | null): string[] { if (!precedenceOption) { // Don't assume any defaults - return empty array return []; } try { // Handle JSON array format if (precedenceOption.startsWith('[') && precedenceOption.endsWith(']')) { const parsed = JSON.parse(precedenceOption); if (Array.isArray(parsed)) { return parsed.map(p => String(p).trim()); } } // Handle comma-separated format if (precedenceOption.includes(',')) { return precedenceOption.split(',').map(p => p.trim()); } // Handle single provider return [precedenceOption.trim()]; } catch (error) { log.error(`Error parsing provider list "${precedenceOption}": ${error}`); // Don't assume defaults on parse error return []; } } private getDefaultConfig(): AIConfig { return { enabled: false, selectedProvider: null, defaultModels: { openai: undefined, anthropic: undefined, ollama: undefined }, providerSettings: {} }; } } // Export singleton instance export default ConfigurationManager.getInstance();