diff --git a/apps/server/src/services/llm/ai_service_manager.ts b/apps/server/src/services/llm/ai_service_manager.ts index 805034072..222f91dfd 100644 --- a/apps/server/src/services/llm/ai_service_manager.ts +++ b/apps/server/src/services/llm/ai_service_manager.ts @@ -267,12 +267,23 @@ export class AIServiceManager implements IAIServiceManager { // If not a provider prefix, treat the entire string as a model name and continue with normal provider selection } - // Try each provider in order until one succeeds + // If user has a specific provider selected, try only that one and fail fast + if (this.providerOrder.length === 1 && sortedProviders.length === 1) { + const selectedProvider = sortedProviders[0]; + const service = await this.getOrCreateChatProvider(selectedProvider); + if (!service) { + throw new Error(`Failed to create selected chat provider: ${selectedProvider}. Please check your configuration.`); + } + log.info(`[AIServiceManager] Using selected provider ${selectedProvider} with options.stream: ${options.stream}`); + return await service.generateChatCompletion(messages, options); + } + + // If no specific provider selected, try each provider in order until one succeeds let lastError: Error | null = null; for (const provider of sortedProviders) { try { - const service = this.services[provider]; + const service = await this.getOrCreateChatProvider(provider); if (service) { log.info(`[AIServiceManager] Trying provider ${provider} with options.stream: ${options.stream}`); return await service.generateChatCompletion(messages, options); @@ -383,7 +394,7 @@ export class AIServiceManager implements IAIServiceManager { } /** - * Get or create a chat provider on-demand + * Get or create a chat provider on-demand with inline validation */ private async getOrCreateChatProvider(providerName: ServiceProviders): Promise { // Return existing provider if already created @@ -391,38 +402,54 @@ export class AIServiceManager implements IAIServiceManager { return this.services[providerName]; } - // Create provider on-demand based on configuration + // Create and validate provider on-demand try { + let service: AIService | null = null; + switch (providerName) { - case 'openai': - const openaiApiKey = await options.getOption('openaiApiKey'); - if (openaiApiKey) { - this.services.openai = new OpenAIService(); - log.info('Created OpenAI chat provider on-demand'); - return this.services.openai; + case 'openai': { + const apiKey = await options.getOption('openaiApiKey'); + const baseUrl = await options.getOption('openaiBaseUrl'); + if (!apiKey && !baseUrl) return null; + + service = new OpenAIService(); + // Validate by checking if it's available + if (!service.isAvailable()) { + throw new Error('OpenAI service not available'); } break; + } - case 'anthropic': - const anthropicApiKey = await options.getOption('anthropicApiKey'); - if (anthropicApiKey) { - this.services.anthropic = new AnthropicService(); - log.info('Created Anthropic chat provider on-demand'); - return this.services.anthropic; + case 'anthropic': { + const apiKey = await options.getOption('anthropicApiKey'); + if (!apiKey) return null; + + service = new AnthropicService(); + if (!service.isAvailable()) { + throw new Error('Anthropic service not available'); } break; + } - case 'ollama': - const ollamaBaseUrl = await options.getOption('ollamaBaseUrl'); - if (ollamaBaseUrl) { - this.services.ollama = new OllamaService(); - log.info('Created Ollama chat provider on-demand'); - return this.services.ollama; + case 'ollama': { + const baseUrl = await options.getOption('ollamaBaseUrl'); + if (!baseUrl) return null; + + service = new OllamaService(); + if (!service.isAvailable()) { + throw new Error('Ollama service not available'); } break; + } + } + + if (service) { + this.services[providerName] = service; + log.info(`Created and validated ${providerName} chat provider`); + return service; } } catch (error: any) { - log.error(`Error creating ${providerName} provider on-demand: ${error.message || 'Unknown error'}`); + log.error(`Failed to create ${providerName} chat provider: ${error.message || 'Unknown error'}`); } return null; diff --git a/apps/server/src/services/llm/index_service.ts b/apps/server/src/services/llm/index_service.ts index 7786a2cc7..6887fbc36 100644 --- a/apps/server/src/services/llm/index_service.ts +++ b/apps/server/src/services/llm/index_service.ts @@ -48,53 +48,16 @@ export class IndexService { async initialize() { if (this.initialized) return; - try { - // Check if database is initialized before proceeding - if (!sqlInit.isDbInitialized()) { - log.info("Index service: Database not initialized yet, skipping initialization"); - return; - } + // Setup event listeners for note changes + this.setupEventListeners(); - const aiEnabled = options.getOptionOrNull('aiEnabled') === "true"; - if (!aiEnabled) { - log.info("Index service: AI features disabled, skipping initialization"); - return; - } - - // Check if embedding system is ready - if (!(await hasWorkingEmbeddingProviders())) { - log.info("Index service: No working embedding providers available, skipping initialization"); - return; - } - - const providers = await providerManager.getEnabledEmbeddingProviders(); - if (!providers || providers.length === 0) { - log.info("Index service: No enabled embedding providers, skipping initialization"); - return; - } - - // Check if this instance should process embeddings - const embeddingLocation = await options.getOption('embeddingGenerationLocation') || 'client'; - const isSyncServer = await this.isSyncServerForEmbeddings(); - const shouldProcessEmbeddings = embeddingLocation === 'client' || isSyncServer; - - // Setup automatic indexing if enabled and this instance should process embeddings - if (await options.getOptionBool('embeddingAutoUpdateEnabled') && shouldProcessEmbeddings) { - this.setupAutomaticIndexing(); - log.info(`Index service: Automatic indexing enabled, processing embeddings ${isSyncServer ? 'as sync server' : 'as client'}`); - } else if (await options.getOptionBool('embeddingAutoUpdateEnabled')) { - log.info("Index service: Automatic indexing enabled, but this instance is not configured to process embeddings"); - } - - // Listen for note changes to update index - this.setupEventListeners(); - - this.initialized = true; - log.info("Index service initialized successfully"); - } catch (error: any) { - log.error(`Error initializing index service: ${error.message || "Unknown error"}`); - throw error; + // Setup automatic indexing if enabled + if (await options.getOptionBool('embeddingAutoUpdateEnabled')) { + this.setupAutomaticIndexing(); } + + this.initialized = true; + log.info("Index service initialized"); } /** @@ -147,23 +110,7 @@ export class IndexService { this.automaticIndexingInterval = setInterval(async () => { try { if (!this.indexingInProgress) { - // Check if this instance should process embeddings - const embeddingLocation = await options.getOption('embeddingGenerationLocation') || 'client'; - const isSyncServer = await this.isSyncServerForEmbeddings(); - const shouldProcessEmbeddings = embeddingLocation === 'client' || isSyncServer; - - if (!shouldProcessEmbeddings) { - // This instance is not configured to process embeddings - return; - } - - const stats = await vectorStore.getEmbeddingStats(); - - // Only run automatic indexing if we're below 95% completion - if (stats.percentComplete < 95) { - log.info(`Starting automatic indexing (current completion: ${stats.percentComplete}%)`); - await this.runBatchIndexing(50); // Process 50 notes at a time - } + await this.runBatchIndexing(50); // Processing logic handles sync server checks } } catch (error: any) { log.error(`Error in automatic indexing: ${error.message || "Unknown error"}`); @@ -498,35 +445,14 @@ export class IndexService { } try { - // Get all enabled embedding providers - const providers = await providerManager.getEnabledEmbeddingProviders(); - if (!providers || providers.length === 0) { - throw new Error("No embedding providers available"); - } - - // Get the selected embedding provider - const options = (await import('../options.js')).default; + // Get the selected embedding provider on-demand const selectedEmbeddingProvider = await options.getOption('embeddingSelectedProvider'); - let provider; - - if (selectedEmbeddingProvider) { - // Try to use the selected provider - const enabledProviders = await providerManager.getEnabledEmbeddingProviders(); - provider = enabledProviders.find(p => p.name === selectedEmbeddingProvider); - - if (!provider) { - log.info(`Selected embedding provider ${selectedEmbeddingProvider} is not available, using first enabled provider`); - // Fall back to first enabled provider - provider = providers[0]; - } - } else { - // No provider selected, use first available provider - log.info('No embedding provider selected, using first available provider'); - provider = providers[0]; - } + const provider = selectedEmbeddingProvider + ? await providerManager.getOrCreateEmbeddingProvider(selectedEmbeddingProvider) + : (await providerManager.getEnabledEmbeddingProviders())[0]; if (!provider) { - throw new Error("No suitable embedding provider found"); + throw new Error("No embedding provider available"); } log.info(`Searching with embedding provider: ${provider.name}, model: ${provider.getConfig().model}`); @@ -684,6 +610,12 @@ export class IndexService { } try { + // Get embedding providers on-demand + const providers = await providerManager.getEnabledEmbeddingProviders(); + if (providers.length === 0) { + return "I don't have access to your note embeddings. Please configure an embedding provider in your AI settings."; + } + // Find similar notes to the query const similarNotes = await this.findSimilarNotes( query, @@ -819,9 +751,13 @@ export class IndexService { // Get complete note context for indexing const context = await vectorStore.getNoteEmbeddingContext(noteId); - // Queue note for embedding with all available providers - const providers = await providerManager.getEnabledEmbeddingProviders(); - for (const provider of providers) { + // Generate embedding with the selected provider + const selectedEmbeddingProvider = await options.getOption('embeddingSelectedProvider'); + const provider = selectedEmbeddingProvider + ? await providerManager.getOrCreateEmbeddingProvider(selectedEmbeddingProvider) + : (await providerManager.getEnabledEmbeddingProviders())[0]; + + if (provider) { try { const embedding = await provider.generateNoteEmbeddings(context); if (embedding) { @@ -851,7 +787,7 @@ export class IndexService { async startEmbeddingGeneration() { try { log.info("Starting embedding generation system"); - + const aiEnabled = options.getOptionOrNull('aiEnabled') === "true"; if (!aiEnabled) { log.error("Cannot start embedding generation - AI features are disabled"); @@ -873,16 +809,13 @@ export class IndexService { return; } - // Verify providers are available - if (!(await hasWorkingEmbeddingProviders())) { - throw new Error("No working embedding providers available"); - } - + // Get embedding providers (will be created on-demand when needed) const providers = await providerManager.getEnabledEmbeddingProviders(); if (providers.length === 0) { - throw new Error("No embedding providers available"); + log.info("No embedding providers configured, but continuing initialization"); + } else { + log.info(`Found ${providers.length} embedding providers: ${providers.map(p => p.name).join(', ')}`); } - log.info(`Found ${providers.length} embedding providers: ${providers.map(p => p.name).join(', ')}`); // Setup automatic indexing if enabled if (await options.getOptionBool('embeddingAutoUpdateEnabled')) { @@ -899,10 +832,10 @@ export class IndexService { // Queue notes that don't have embeddings for current providers await this.queueNotesForMissingEmbeddings(); - + // Start processing the queue immediately await this.runBatchIndexing(20); - + log.info("Embedding generation started successfully"); } catch (error: any) { log.error(`Error starting embedding generation: ${error.message || "Unknown error"}`); @@ -919,24 +852,24 @@ export class IndexService { try { // Wait for becca to be fully loaded before accessing notes await beccaLoader.beccaLoaded; - + // Get all non-deleted notes const allNotes = Object.values(becca.notes).filter(note => !note.isDeleted); - + // Get enabled providers const providers = await providerManager.getEnabledEmbeddingProviders(); if (providers.length === 0) { return; } - + let queuedCount = 0; let excludedCount = 0; - + // Process notes in batches to avoid overwhelming the system const batchSize = 100; for (let i = 0; i < allNotes.length; i += batchSize) { const batch = allNotes.slice(i, i + batchSize); - + for (const note of batch) { try { // Skip notes excluded from AI @@ -944,10 +877,10 @@ export class IndexService { excludedCount++; continue; } - + // Check if note needs embeddings for any enabled provider let needsEmbedding = false; - + for (const provider of providers) { const config = provider.getConfig(); const existingEmbedding = await vectorStore.getEmbeddingForNote( @@ -955,13 +888,13 @@ export class IndexService { provider.name, config.model ); - + if (!existingEmbedding) { needsEmbedding = true; break; } } - + if (needsEmbedding) { await vectorStore.queueNoteForEmbedding(note.noteId, 'UPDATE'); queuedCount++; @@ -970,7 +903,7 @@ export class IndexService { log.error(`Error checking embeddings for note ${note.noteId}: ${error.message || 'Unknown error'}`); } } - + } } catch (error: any) { log.error(`Error queuing notes for missing embeddings: ${error.message || 'Unknown error'}`); @@ -1005,7 +938,7 @@ export class IndexService { async stopEmbeddingGeneration() { try { log.info("Stopping embedding generation system"); - + // Clear automatic indexing interval if (this.automaticIndexingInterval) { clearInterval(this.automaticIndexingInterval); @@ -1023,7 +956,7 @@ export class IndexService { // Mark as not indexing this.indexingInProgress = false; this.indexRebuildInProgress = false; - + log.info("Embedding generation stopped successfully"); } catch (error: any) { log.error(`Error stopping embedding generation: ${error.message || "Unknown error"}`); diff --git a/apps/server/src/services/llm/provider_validation.ts b/apps/server/src/services/llm/provider_validation.ts index 1cab7bb21..6a94153e8 100644 --- a/apps/server/src/services/llm/provider_validation.ts +++ b/apps/server/src/services/llm/provider_validation.ts @@ -18,7 +18,7 @@ export interface ProviderValidationResult { } /** - * Validate all available providers without throwing errors + * Simplified provider validation - just checks configuration without creating providers */ export async function validateProviders(): Promise { const result: ProviderValidationResult = { @@ -37,14 +37,12 @@ export async function validateProviders(): Promise { return result; } - // Validate embedding providers - await validateEmbeddingProviders(result); - - // Validate chat providers - await validateChatProviders(result); + // Check configuration only - don't create providers + await checkEmbeddingProviderConfigs(result); + await checkChatProviderConfigs(result); - // Determine if we have any valid providers - result.hasValidProviders = result.validEmbeddingProviders.length > 0 || result.validChatProviders.length > 0; + // Determine if we have any valid providers based on configuration + result.hasValidProviders = result.validChatProviders.length > 0; if (!result.hasValidProviders) { result.errors.push("No valid AI providers are configured"); @@ -58,241 +56,80 @@ export async function validateProviders(): Promise { } /** - * Validate embedding providers + * Check embedding provider configurations without creating providers */ -async function validateEmbeddingProviders(result: ProviderValidationResult): Promise { +async function checkEmbeddingProviderConfigs(result: ProviderValidationResult): Promise { try { - // Import provider classes and check configurations - const { OpenAIEmbeddingProvider } = await import("./embeddings/providers/openai.js"); - const { OllamaEmbeddingProvider } = await import("./embeddings/providers/ollama.js"); - const { VoyageEmbeddingProvider } = await import("./embeddings/providers/voyage.js"); + // Check OpenAI embedding configuration + const openaiApiKey = await options.getOption('openaiApiKey'); + const openaiBaseUrl = await options.getOption('openaiBaseUrl'); + if (openaiApiKey || openaiBaseUrl) { + if (!openaiApiKey) { + result.warnings.push("OpenAI embedding: No API key (may work with compatible endpoints)"); + } + log.info("OpenAI embedding provider configuration available"); + } - // Check OpenAI embedding provider - await validateOpenAIEmbeddingProvider(result, OpenAIEmbeddingProvider); - - // Check Ollama embedding provider - await validateOllamaEmbeddingProvider(result, OllamaEmbeddingProvider); - - // Check Voyage embedding provider - await validateVoyageEmbeddingProvider(result, VoyageEmbeddingProvider); + // Check Ollama embedding configuration + const ollamaEmbeddingBaseUrl = await options.getOption('ollamaEmbeddingBaseUrl'); + if (ollamaEmbeddingBaseUrl) { + log.info("Ollama embedding provider configuration available"); + } - // Local provider is always available as fallback - await validateLocalEmbeddingProvider(result); + // Check Voyage embedding configuration + const voyageApiKey = await options.getOption('voyageApiKey' as any); + if (voyageApiKey) { + log.info("Voyage embedding provider configuration available"); + } + + // Local provider is always available + log.info("Local embedding provider available as fallback"); } catch (error: any) { - result.errors.push(`Error validating embedding providers: ${error.message || 'Unknown error'}`); + result.errors.push(`Error checking embedding provider configs: ${error.message || 'Unknown error'}`); } } /** - * Validate chat providers + * Check chat provider configurations without creating providers */ -async function validateChatProviders(result: ProviderValidationResult): Promise { +async function checkChatProviderConfigs(result: ProviderValidationResult): Promise { try { // Check OpenAI chat provider const openaiApiKey = await options.getOption('openaiApiKey'); const openaiBaseUrl = await options.getOption('openaiBaseUrl'); if (openaiApiKey || openaiBaseUrl) { - if (!openaiApiKey && !openaiBaseUrl) { - result.warnings.push("OpenAI chat provider: No API key or base URL configured"); - } else if (!openaiApiKey) { - result.warnings.push("OpenAI chat provider: No API key configured (may work with compatible endpoints)"); - result.validChatProviders.push('openai'); - } else { - result.validChatProviders.push('openai'); + if (!openaiApiKey) { + result.warnings.push("OpenAI chat: No API key (may work with compatible endpoints)"); } + result.validChatProviders.push('openai'); } // Check Anthropic chat provider const anthropicApiKey = await options.getOption('anthropicApiKey'); if (anthropicApiKey) { result.validChatProviders.push('anthropic'); - } else { - result.warnings.push("Anthropic chat provider: No API key configured"); } // Check Ollama chat provider const ollamaBaseUrl = await options.getOption('ollamaBaseUrl'); if (ollamaBaseUrl) { result.validChatProviders.push('ollama'); - } else { - result.warnings.push("Ollama chat provider: No base URL configured"); + } + + if (result.validChatProviders.length === 0) { + result.warnings.push("No chat providers configured. Please configure at least one provider."); } } catch (error: any) { - result.errors.push(`Error validating chat providers: ${error.message || 'Unknown error'}`); + result.errors.push(`Error checking chat provider configs: ${error.message || 'Unknown error'}`); } } -/** - * Validate OpenAI embedding provider - */ -async function validateOpenAIEmbeddingProvider( - result: ProviderValidationResult, - OpenAIEmbeddingProvider: any -): Promise { - try { - const openaiApiKey = await options.getOption('openaiApiKey'); - const openaiBaseUrl = await options.getOption('openaiBaseUrl'); - - if (openaiApiKey || openaiBaseUrl) { - const openaiModel = await options.getOption('openaiEmbeddingModel'); - const finalBaseUrl = openaiBaseUrl || 'https://api.openai.com/v1'; - - if (!openaiApiKey) { - result.warnings.push("OpenAI embedding provider: No API key configured (may work with compatible endpoints)"); - } - - const provider = new OpenAIEmbeddingProvider({ - model: openaiModel, - dimension: 1536, - type: 'float32', - apiKey: openaiApiKey || '', - baseUrl: finalBaseUrl - }); - - result.validEmbeddingProviders.push(provider); - log.info(`Validated OpenAI embedding provider: ${openaiModel} at ${finalBaseUrl}`); - } else { - result.warnings.push("OpenAI embedding provider: No API key or base URL configured"); - } - } catch (error: any) { - result.errors.push(`OpenAI embedding provider validation failed: ${error.message || 'Unknown error'}`); - } -} /** - * Validate Ollama embedding provider - */ -async function validateOllamaEmbeddingProvider( - result: ProviderValidationResult, - OllamaEmbeddingProvider: any -): Promise { - try { - const ollamaEmbeddingBaseUrl = await options.getOption('ollamaEmbeddingBaseUrl'); - - if (ollamaEmbeddingBaseUrl) { - const embeddingModel = await options.getOption('ollamaEmbeddingModel'); - - try { - const provider = new OllamaEmbeddingProvider({ - model: embeddingModel, - dimension: 768, - type: 'float32', - baseUrl: ollamaEmbeddingBaseUrl - }); - - // Try to initialize to validate connection - await provider.initialize(); - result.validEmbeddingProviders.push(provider); - log.info(`Validated Ollama embedding provider: ${embeddingModel} at ${ollamaEmbeddingBaseUrl}`); - } catch (error: any) { - result.warnings.push(`Ollama embedding provider initialization failed: ${error.message || 'Unknown error'}`); - } - } else { - result.warnings.push("Ollama embedding provider: No base URL configured"); - } - } catch (error: any) { - result.errors.push(`Ollama embedding provider validation failed: ${error.message || 'Unknown error'}`); - } -} - -/** - * Validate Voyage embedding provider - */ -async function validateVoyageEmbeddingProvider( - result: ProviderValidationResult, - VoyageEmbeddingProvider: any -): Promise { - try { - const voyageApiKey = await options.getOption('voyageApiKey' as any); - - if (voyageApiKey) { - const voyageModel = await options.getOption('voyageEmbeddingModel') || 'voyage-2'; - - const provider = new VoyageEmbeddingProvider({ - model: voyageModel, - dimension: 1024, - type: 'float32', - apiKey: voyageApiKey, - baseUrl: 'https://api.voyageai.com/v1' - }); - - result.validEmbeddingProviders.push(provider); - log.info(`Validated Voyage embedding provider: ${voyageModel}`); - } else { - result.warnings.push("Voyage embedding provider: No API key configured"); - } - } catch (error: any) { - result.errors.push(`Voyage embedding provider validation failed: ${error.message || 'Unknown error'}`); - } -} - -/** - * Validate local embedding provider (always available as fallback) - */ -async function validateLocalEmbeddingProvider(result: ProviderValidationResult): Promise { - try { - // Simple local embedding provider implementation - class SimpleLocalEmbeddingProvider { - name = "local"; - config = { - model: 'local', - dimension: 384, - type: 'float32' as const - }; - - getConfig() { - return this.config; - } - - getNormalizationStatus() { - return 0; // NormalizationStatus.NEVER - } - - async generateEmbeddings(text: string): Promise { - const result = new Float32Array(this.config.dimension); - for (let i = 0; i < result.length; i++) { - const charSum = Array.from(text).reduce((sum, char, idx) => - sum + char.charCodeAt(0) * Math.sin(idx * 0.1), 0); - result[i] = Math.sin(i * 0.1 + charSum * 0.01); - } - return result; - } - - async generateBatchEmbeddings(texts: string[]): Promise { - return Promise.all(texts.map(text => this.generateEmbeddings(text))); - } - - async generateNoteEmbeddings(context: any): Promise { - const text = (context.title || "") + " " + (context.content || ""); - return this.generateEmbeddings(text); - } - - async generateBatchNoteEmbeddings(contexts: any[]): Promise { - return Promise.all(contexts.map(context => this.generateNoteEmbeddings(context))); - } - } - - const localProvider = new SimpleLocalEmbeddingProvider(); - result.validEmbeddingProviders.push(localProvider as any); - log.info("Validated local embedding provider as fallback"); - } catch (error: any) { - result.errors.push(`Local embedding provider validation failed: ${error.message || 'Unknown error'}`); - } -} - -/** - * Check if any working providers are available for embeddings - */ -export async function hasWorkingEmbeddingProviders(): Promise { - const validation = await validateProviders(); - return validation.validEmbeddingProviders.length > 0; -} - -/** - * Check if any working providers are available for chat + * Check if any chat providers are configured */ export async function hasWorkingChatProviders(): Promise { const validation = await validateProviders(); @@ -300,11 +137,21 @@ export async function hasWorkingChatProviders(): Promise { } /** - * Get only the working embedding providers + * Check if any embedding providers are configured (simplified) */ -export async function getWorkingEmbeddingProviders(): Promise { - const validation = await validateProviders(); - return validation.validEmbeddingProviders; +export async function hasWorkingEmbeddingProviders(): Promise { + if (!(await options.getOptionBool('aiEnabled'))) { + return false; + } + + // Check if any embedding provider is configured + const openaiKey = await options.getOption('openaiApiKey'); + const openaiBaseUrl = await options.getOption('openaiBaseUrl'); + const ollamaUrl = await options.getOption('ollamaEmbeddingBaseUrl'); + const voyageKey = await options.getOption('voyageApiKey' as any); + + // Local provider is always available as fallback + return !!(openaiKey || openaiBaseUrl || ollamaUrl || voyageKey) || true; } /** diff --git a/apps/server/src/services/llm/providers/providers.ts b/apps/server/src/services/llm/providers/providers.ts index fd7c603fd..dae8b34a0 100644 --- a/apps/server/src/services/llm/providers/providers.ts +++ b/apps/server/src/services/llm/providers/providers.ts @@ -124,118 +124,129 @@ export function getEmbeddingProvider(name: string): EmbeddingProvider | undefine } /** - * Create providers on-demand based on current options + * Get or create a specific embedding provider with inline validation */ -export async function createProvidersFromCurrentOptions(): Promise { - const result: EmbeddingProvider[] = []; - - try { - // Create Ollama provider if embedding base URL is configured - const ollamaEmbeddingBaseUrl = await options.getOption('ollamaEmbeddingBaseUrl'); - if (ollamaEmbeddingBaseUrl) { - const embeddingModel = await options.getOption('ollamaEmbeddingModel'); - - try { - const ollamaProvider = new OllamaEmbeddingProvider({ - model: embeddingModel, - dimension: 768, // Initial value, will be updated during initialization - type: 'float32', - baseUrl: ollamaEmbeddingBaseUrl - }); - - await ollamaProvider.initialize(); - registerEmbeddingProvider(ollamaProvider); - result.push(ollamaProvider); - log.info(`Created Ollama provider on-demand: ${embeddingModel} at ${ollamaEmbeddingBaseUrl}`); - } catch (error: any) { - log.error(`Error creating Ollama embedding provider on-demand: ${error.message || 'Unknown error'}`); - } - } - - // Create OpenAI provider even without API key (for OpenAI-compatible endpoints) - const openaiApiKey = await options.getOption('openaiApiKey'); - const openaiBaseUrl = await options.getOption('openaiBaseUrl'); - - // Only create OpenAI provider if base URL is set or API key is provided - if (openaiApiKey || openaiBaseUrl) { - const openaiModel = await options.getOption('openaiEmbeddingModel') - const finalBaseUrl = openaiBaseUrl || 'https://api.openai.com/v1'; - - if (!openaiApiKey) { - log.info('Creating OpenAI embedding provider without API key. This may cause issues with official OpenAI endpoints.'); - } - - const openaiProvider = new OpenAIEmbeddingProvider({ - model: openaiModel, - dimension: 1536, - type: 'float32', - apiKey: openaiApiKey || '', // Default to empty string - baseUrl: finalBaseUrl - }); - - registerEmbeddingProvider(openaiProvider); - result.push(openaiProvider); - log.info(`Created OpenAI provider on-demand: ${openaiModel} at ${finalBaseUrl}`); - } - - // Create Voyage provider if API key is configured - const voyageApiKey = await options.getOption('voyageApiKey' as any); - if (voyageApiKey) { - const voyageModel = await options.getOption('voyageEmbeddingModel') || 'voyage-2'; - const voyageBaseUrl = 'https://api.voyageai.com/v1'; - - const voyageProvider = new VoyageEmbeddingProvider({ - model: voyageModel, - dimension: 1024, - type: 'float32', - apiKey: voyageApiKey, - baseUrl: voyageBaseUrl - }); - - registerEmbeddingProvider(voyageProvider); - result.push(voyageProvider); - log.info(`Created Voyage provider on-demand: ${voyageModel}`); - } - - // Always include local provider as fallback - if (!providers.has('local')) { - const localProvider = new SimpleLocalEmbeddingProvider({ - model: 'local', - dimension: 384, - type: 'float32' - }); - registerEmbeddingProvider(localProvider); - result.push(localProvider); - log.info(`Created local provider on-demand as fallback`); - } else { - result.push(providers.get('local')!); - } - - } catch (error: any) { - log.error(`Error creating providers from current options: ${error.message || 'Unknown error'}`); +export async function getOrCreateEmbeddingProvider(providerName: string): Promise { + // Return existing provider if already created and valid + const existing = providers.get(providerName); + if (existing) { + return existing; } - return result; + // Create and validate provider on-demand + try { + let provider: EmbeddingProvider | null = null; + + switch (providerName) { + case 'ollama': { + const baseUrl = await options.getOption('ollamaEmbeddingBaseUrl'); + if (!baseUrl) return null; + + const model = await options.getOption('ollamaEmbeddingModel'); + provider = new OllamaEmbeddingProvider({ + model, + dimension: 768, + type: 'float32', + baseUrl + }); + + // Validate by initializing (if provider supports it) + if ('initialize' in provider && typeof provider.initialize === 'function') { + await provider.initialize(); + } + break; + } + + case 'openai': { + const apiKey = await options.getOption('openaiApiKey'); + const baseUrl = await options.getOption('openaiBaseUrl'); + if (!apiKey && !baseUrl) return null; + + const model = await options.getOption('openaiEmbeddingModel'); + provider = new OpenAIEmbeddingProvider({ + model, + dimension: 1536, + type: 'float32', + apiKey: apiKey || '', + baseUrl: baseUrl || 'https://api.openai.com/v1' + }); + + if (!apiKey) { + log.info('OpenAI embedding provider created without API key for compatible endpoints'); + } + break; + } + + case 'voyage': { + const apiKey = await options.getOption('voyageApiKey' as any); + if (!apiKey) return null; + + const model = await options.getOption('voyageEmbeddingModel') || 'voyage-2'; + provider = new VoyageEmbeddingProvider({ + model, + dimension: 1024, + type: 'float32', + apiKey, + baseUrl: 'https://api.voyageai.com/v1' + }); + break; + } + + case 'local': { + provider = new SimpleLocalEmbeddingProvider({ + model: 'local', + dimension: 384, + type: 'float32' + }); + break; + } + + default: + return null; + } + + if (provider) { + registerEmbeddingProvider(provider); + log.info(`Created and validated ${providerName} embedding provider`); + return provider; + } + } catch (error: any) { + log.error(`Failed to create ${providerName} embedding provider: ${error.message || 'Unknown error'}`); + } + + return null; } /** - * Get all enabled embedding providers + * Get all enabled embedding providers for the specified feature */ -export async function getEnabledEmbeddingProviders(): Promise { +export async function getEnabledEmbeddingProviders(feature: 'embeddings' | 'chat' = 'embeddings'): Promise { if (!(await options.getOptionBool('aiEnabled'))) { return []; } - // First try to get existing registered providers - const existingProviders = Array.from(providers.values()); + const result: EmbeddingProvider[] = []; - // If no providers are registered, create them on-demand from current options - if (existingProviders.length === 0) { - log.info('No providers registered, creating from current options'); - return await createProvidersFromCurrentOptions(); + // Get the selected provider for the feature + const selectedProvider = feature === 'embeddings' + ? await options.getOption('embeddingSelectedProvider') + : await options.getOption('aiSelectedProvider'); + + // Try to get or create the specific selected provider + const provider = await getOrCreateEmbeddingProvider(selectedProvider); + if (!provider) { + throw new Error(`Failed to create selected embedding provider: ${selectedProvider}. Please check your configuration.`); + } + result.push(provider); + + + // Always ensure local provider as fallback + const localProvider = await getOrCreateEmbeddingProvider('local'); + if (localProvider && !result.some(p => p.name === 'local')) { + result.push(localProvider); } - return existingProviders; + return result; } /** @@ -342,7 +353,7 @@ export default { getEmbeddingProviders, getEmbeddingProvider, getEnabledEmbeddingProviders, - createProvidersFromCurrentOptions, + getOrCreateEmbeddingProvider, createEmbeddingProviderConfig, updateEmbeddingProviderConfig, deleteEmbeddingProviderConfig,