mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	feat(llm): change from using precedence list to using a sing specified provider for either chat and/or embeddings
This commit is contained in:
		| @@ -65,7 +65,7 @@ export default class AiSettingsWidget extends OptionsWidget { | |||||||
|  |  | ||||||
|         // Core AI options |         // Core AI options | ||||||
|         this.setupChangeHandler('.ai-enabled', 'aiEnabled', true, true); |         this.setupChangeHandler('.ai-enabled', 'aiEnabled', true, true); | ||||||
|         this.setupChangeHandler('.ai-provider-precedence', 'aiProviderPrecedence', true); |         this.setupChangeHandler('.ai-selected-provider', 'aiSelectedProvider', true); | ||||||
|         this.setupChangeHandler('.ai-temperature', 'aiTemperature'); |         this.setupChangeHandler('.ai-temperature', 'aiTemperature'); | ||||||
|         this.setupChangeHandler('.ai-system-prompt', 'aiSystemPrompt'); |         this.setupChangeHandler('.ai-system-prompt', 'aiSystemPrompt'); | ||||||
|  |  | ||||||
| @@ -132,11 +132,28 @@ export default class AiSettingsWidget extends OptionsWidget { | |||||||
|         this.setupChangeHandler('.enable-automatic-indexing', 'enableAutomaticIndexing', false, true); |         this.setupChangeHandler('.enable-automatic-indexing', 'enableAutomaticIndexing', false, true); | ||||||
|         this.setupChangeHandler('.embedding-similarity-threshold', 'embeddingSimilarityThreshold'); |         this.setupChangeHandler('.embedding-similarity-threshold', 'embeddingSimilarityThreshold'); | ||||||
|         this.setupChangeHandler('.max-notes-per-llm-query', 'maxNotesPerLlmQuery'); |         this.setupChangeHandler('.max-notes-per-llm-query', 'maxNotesPerLlmQuery'); | ||||||
|         this.setupChangeHandler('.embedding-provider-precedence', 'embeddingProviderPrecedence', true); |         this.setupChangeHandler('.embedding-selected-provider', 'embeddingSelectedProvider', true); | ||||||
|         this.setupChangeHandler('.embedding-dimension-strategy', 'embeddingDimensionStrategy'); |         this.setupChangeHandler('.embedding-dimension-strategy', 'embeddingDimensionStrategy'); | ||||||
|         this.setupChangeHandler('.embedding-batch-size', 'embeddingBatchSize'); |         this.setupChangeHandler('.embedding-batch-size', 'embeddingBatchSize'); | ||||||
|         this.setupChangeHandler('.embedding-update-interval', 'embeddingUpdateInterval'); |         this.setupChangeHandler('.embedding-update-interval', 'embeddingUpdateInterval'); | ||||||
|  |  | ||||||
|  |         // Add provider selection change handlers for dynamic settings visibility | ||||||
|  |         this.$widget.find('.ai-selected-provider').on('change', () => { | ||||||
|  |             const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string; | ||||||
|  |             this.$widget.find('.provider-settings').hide(); | ||||||
|  |             if (selectedProvider) { | ||||||
|  |                 this.$widget.find(`.${selectedProvider}-provider-settings`).show(); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         this.$widget.find('.embedding-selected-provider').on('change', () => { | ||||||
|  |             const selectedProvider = this.$widget.find('.embedding-selected-provider').val() as string; | ||||||
|  |             this.$widget.find('.embedding-provider-settings').hide(); | ||||||
|  |             if (selectedProvider) { | ||||||
|  |                 this.$widget.find(`.${selectedProvider}-embedding-provider-settings`).show(); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|         // No sortable behavior needed anymore |         // No sortable behavior needed anymore | ||||||
|  |  | ||||||
|         // Embedding stats refresh button |         // Embedding stats refresh button | ||||||
| @@ -194,42 +211,25 @@ export default class AiSettingsWidget extends OptionsWidget { | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Get provider precedence |         // Get selected provider | ||||||
|         const providerPrecedence = (this.$widget.find('.ai-provider-precedence').val() as string || '').split(','); |         const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string; | ||||||
|  |  | ||||||
|         // Check for OpenAI configuration if it's in the precedence list |         // Check for selected provider configuration | ||||||
|         const openaiWarnings: string[] = []; |         const providerWarnings: string[] = []; | ||||||
|         if (providerPrecedence.includes('openai')) { |         if (selectedProvider === 'openai') { | ||||||
|             const openaiApiKey = this.$widget.find('.openai-api-key').val(); |             const openaiApiKey = this.$widget.find('.openai-api-key').val(); | ||||||
|             if (!openaiApiKey) { |             if (!openaiApiKey) { | ||||||
|                 openaiWarnings.push(t("ai_llm.empty_key_warning.openai")); |                 providerWarnings.push(t("ai_llm.empty_key_warning.openai")); | ||||||
|             } |             } | ||||||
|         } |         } else if (selectedProvider === 'anthropic') { | ||||||
|  |  | ||||||
|         // Check for Anthropic configuration if it's in the precedence list |  | ||||||
|         const anthropicWarnings: string[] = []; |  | ||||||
|         if (providerPrecedence.includes('anthropic')) { |  | ||||||
|             const anthropicApiKey = this.$widget.find('.anthropic-api-key').val(); |             const anthropicApiKey = this.$widget.find('.anthropic-api-key').val(); | ||||||
|             if (!anthropicApiKey) { |             if (!anthropicApiKey) { | ||||||
|                 anthropicWarnings.push(t("ai_llm.empty_key_warning.anthropic")); |                 providerWarnings.push(t("ai_llm.empty_key_warning.anthropic")); | ||||||
|             } |             } | ||||||
|         } |         } else if (selectedProvider === 'ollama') { | ||||||
|  |  | ||||||
|         // Check for Voyage configuration if it's in the precedence list |  | ||||||
|         const voyageWarnings: string[] = []; |  | ||||||
|         if (providerPrecedence.includes('voyage')) { |  | ||||||
|             const voyageApiKey = this.$widget.find('.voyage-api-key').val(); |  | ||||||
|             if (!voyageApiKey) { |  | ||||||
|                 voyageWarnings.push(t("ai_llm.empty_key_warning.voyage")); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Check for Ollama configuration if it's in the precedence list |  | ||||||
|         const ollamaWarnings: string[] = []; |  | ||||||
|         if (providerPrecedence.includes('ollama')) { |  | ||||||
|             const ollamaBaseUrl = this.$widget.find('.ollama-base-url').val(); |             const ollamaBaseUrl = this.$widget.find('.ollama-base-url').val(); | ||||||
|             if (!ollamaBaseUrl) { |             if (!ollamaBaseUrl) { | ||||||
|                 ollamaWarnings.push(t("ai_llm.ollama_no_url")); |                 providerWarnings.push(t("ai_llm.ollama_no_url")); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -238,27 +238,24 @@ export default class AiSettingsWidget extends OptionsWidget { | |||||||
|         const embeddingsEnabled = this.$widget.find('.enable-automatic-indexing').prop('checked'); |         const embeddingsEnabled = this.$widget.find('.enable-automatic-indexing').prop('checked'); | ||||||
|  |  | ||||||
|         if (embeddingsEnabled) { |         if (embeddingsEnabled) { | ||||||
|             const embeddingProviderPrecedence = (this.$widget.find('.embedding-provider-precedence').val() as string || '').split(','); |             const selectedEmbeddingProvider = this.$widget.find('.embedding-selected-provider').val() as string; | ||||||
|  |  | ||||||
|             if (embeddingProviderPrecedence.includes('openai') && !this.$widget.find('.openai-api-key').val()) { |             if (selectedEmbeddingProvider === 'openai' && !this.$widget.find('.openai-api-key').val()) { | ||||||
|                 embeddingWarnings.push(t("ai_llm.empty_key_warning.openai")); |                 embeddingWarnings.push(t("ai_llm.empty_key_warning.openai")); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (embeddingProviderPrecedence.includes('voyage') && !this.$widget.find('.voyage-api-key').val()) { |             if (selectedEmbeddingProvider === 'voyage' && !this.$widget.find('.voyage-api-key').val()) { | ||||||
|                 embeddingWarnings.push(t("ai_llm.empty_key_warning.voyage")); |                 embeddingWarnings.push(t("ai_llm.empty_key_warning.voyage")); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (embeddingProviderPrecedence.includes('ollama') && !this.$widget.find('.ollama-base-url').val()) { |             if (selectedEmbeddingProvider === 'ollama' && !this.$widget.find('.ollama-base-url').val()) { | ||||||
|                 embeddingWarnings.push(t("ai_llm.empty_key_warning.ollama")); |                 embeddingWarnings.push(t("ai_llm.empty_key_warning.ollama")); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Combine all warnings |         // Combine all warnings | ||||||
|         const allWarnings = [ |         const allWarnings = [ | ||||||
|             ...openaiWarnings, |             ...providerWarnings, | ||||||
|             ...anthropicWarnings, |  | ||||||
|             ...voyageWarnings, |  | ||||||
|             ...ollamaWarnings, |  | ||||||
|             ...embeddingWarnings |             ...embeddingWarnings | ||||||
|         ]; |         ]; | ||||||
|  |  | ||||||
| @@ -449,6 +446,27 @@ export default class AiSettingsWidget extends OptionsWidget { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Update provider settings visibility based on selected providers | ||||||
|  |      */ | ||||||
|  |     updateProviderSettingsVisibility() { | ||||||
|  |         if (!this.$widget) return; | ||||||
|  |  | ||||||
|  |         // Update AI provider settings visibility | ||||||
|  |         const selectedAiProvider = this.$widget.find('.ai-selected-provider').val() as string; | ||||||
|  |         this.$widget.find('.provider-settings').hide(); | ||||||
|  |         if (selectedAiProvider) { | ||||||
|  |             this.$widget.find(`.${selectedAiProvider}-provider-settings`).show(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Update embedding provider settings visibility | ||||||
|  |         const selectedEmbeddingProvider = this.$widget.find('.embedding-selected-provider').val() as string; | ||||||
|  |         this.$widget.find('.embedding-provider-settings').hide(); | ||||||
|  |         if (selectedEmbeddingProvider) { | ||||||
|  |             this.$widget.find(`.${selectedEmbeddingProvider}-embedding-provider-settings`).show(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Called when the options have been loaded from the server |      * Called when the options have been loaded from the server | ||||||
|      */ |      */ | ||||||
| @@ -459,30 +477,30 @@ export default class AiSettingsWidget extends OptionsWidget { | |||||||
|         this.$widget.find('.ai-enabled').prop('checked', options.aiEnabled !== 'false'); |         this.$widget.find('.ai-enabled').prop('checked', options.aiEnabled !== 'false'); | ||||||
|         this.$widget.find('.ai-temperature').val(options.aiTemperature || '0.7'); |         this.$widget.find('.ai-temperature').val(options.aiTemperature || '0.7'); | ||||||
|         this.$widget.find('.ai-system-prompt').val(options.aiSystemPrompt || ''); |         this.$widget.find('.ai-system-prompt').val(options.aiSystemPrompt || ''); | ||||||
|         this.$widget.find('.ai-provider-precedence').val(options.aiProviderPrecedence || 'openai,anthropic,ollama'); |         this.$widget.find('.ai-selected-provider').val(options.aiSelectedProvider || 'openai'); | ||||||
|  |  | ||||||
|         // OpenAI Section |         // OpenAI Section | ||||||
|         this.$widget.find('.openai-api-key').val(options.openaiApiKey || ''); |         this.$widget.find('.openai-api-key').val(options.openaiApiKey || ''); | ||||||
|         this.$widget.find('.openai-base-url').val(options.openaiBaseUrl || 'https://api.openai_llm.com/v1'); |         this.$widget.find('.openai-base-url').val(options.openaiBaseUrl || 'https://api.openai.com/v1'); | ||||||
|         this.$widget.find('.openai-default-model').val(options.openaiDefaultModel || 'gpt-4o'); |         this.$widget.find('.openai-default-model').val(options.openaiDefaultModel || ''); | ||||||
|         this.$widget.find('.openai-embedding-model').val(options.openaiEmbeddingModel || 'text-embedding-3-small'); |         this.$widget.find('.openai-embedding-model').val(options.openaiEmbeddingModel || ''); | ||||||
|  |  | ||||||
|         // Anthropic Section |         // Anthropic Section | ||||||
|         this.$widget.find('.anthropic-api-key').val(options.anthropicApiKey || ''); |         this.$widget.find('.anthropic-api-key').val(options.anthropicApiKey || ''); | ||||||
|         this.$widget.find('.anthropic-base-url').val(options.anthropicBaseUrl || 'https://api.anthropic.com'); |         this.$widget.find('.anthropic-base-url').val(options.anthropicBaseUrl || 'https://api.anthropic.com'); | ||||||
|         this.$widget.find('.anthropic-default-model').val(options.anthropicDefaultModel || 'claude-3-opus-20240229'); |         this.$widget.find('.anthropic-default-model').val(options.anthropicDefaultModel || ''); | ||||||
|  |  | ||||||
|         // Voyage Section |         // Voyage Section | ||||||
|         this.$widget.find('.voyage-api-key').val(options.voyageApiKey || ''); |         this.$widget.find('.voyage-api-key').val(options.voyageApiKey || ''); | ||||||
|         this.$widget.find('.voyage-embedding-model').val(options.voyageEmbeddingModel || 'voyage-2'); |         this.$widget.find('.voyage-embedding-model').val(options.voyageEmbeddingModel || ''); | ||||||
|  |  | ||||||
|         // Ollama Section |         // Ollama Section | ||||||
|         this.$widget.find('.ollama-base-url').val(options.ollamaBaseUrl || 'http://localhost:11434'); |         this.$widget.find('.ollama-base-url').val(options.ollamaBaseUrl || 'http://localhost:11434'); | ||||||
|         this.$widget.find('.ollama-default-model').val(options.ollamaDefaultModel || 'llama3'); |         this.$widget.find('.ollama-default-model').val(options.ollamaDefaultModel || ''); | ||||||
|         this.$widget.find('.ollama-embedding-model').val(options.ollamaEmbeddingModel || 'nomic-embed-text'); |         this.$widget.find('.ollama-embedding-model').val(options.ollamaEmbeddingModel || ''); | ||||||
|  |  | ||||||
|         // Embedding Options |         // Embedding Options | ||||||
|         this.$widget.find('.embedding-provider-precedence').val(options.embeddingProviderPrecedence || 'openai,voyage,ollama,local'); |         this.$widget.find('.embedding-selected-provider').val(options.embeddingSelectedProvider || 'openai'); | ||||||
|         this.$widget.find('.embedding-auto-update-enabled').prop('checked', options.embeddingAutoUpdateEnabled !== 'false'); |         this.$widget.find('.embedding-auto-update-enabled').prop('checked', options.embeddingAutoUpdateEnabled !== 'false'); | ||||||
|         this.$widget.find('.enable-automatic-indexing').prop('checked', options.enableAutomaticIndexing !== 'false'); |         this.$widget.find('.enable-automatic-indexing').prop('checked', options.enableAutomaticIndexing !== 'false'); | ||||||
|         this.$widget.find('.embedding-similarity-threshold').val(options.embeddingSimilarityThreshold || '0.75'); |         this.$widget.find('.embedding-similarity-threshold').val(options.embeddingSimilarityThreshold || '0.75'); | ||||||
| @@ -491,6 +509,9 @@ export default class AiSettingsWidget extends OptionsWidget { | |||||||
|         this.$widget.find('.embedding-batch-size').val(options.embeddingBatchSize || '10'); |         this.$widget.find('.embedding-batch-size').val(options.embeddingBatchSize || '10'); | ||||||
|         this.$widget.find('.embedding-update-interval').val(options.embeddingUpdateInterval || '5000'); |         this.$widget.find('.embedding-update-interval').val(options.embeddingUpdateInterval || '5000'); | ||||||
|  |  | ||||||
|  |         // Show/hide provider settings based on selected providers | ||||||
|  |         this.updateProviderSettingsVisibility(); | ||||||
|  |  | ||||||
|         // Display validation warnings |         // Display validation warnings | ||||||
|         this.displayValidationWarnings(); |         this.displayValidationWarnings(); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -61,9 +61,125 @@ export const TPL = ` | |||||||
|     <h4>${t("ai_llm.provider_configuration")}</h4> |     <h4>${t("ai_llm.provider_configuration")}</h4> | ||||||
|  |  | ||||||
|     <div class="form-group"> |     <div class="form-group"> | ||||||
|         <label>${t("ai_llm.provider_precedence")}</label> |         <label>${t("ai_llm.selected_provider")}</label> | ||||||
|         <input type="text" class="ai-provider-precedence form-control" placeholder="openai,anthropic,ollama"> |         <select class="ai-selected-provider form-control"> | ||||||
|         <div class="form-text">${t("ai_llm.provider_precedence_description")}</div> |             <option value="">${t("ai_llm.select_provider")}</option> | ||||||
|  |             <option value="openai">OpenAI</option> | ||||||
|  |             <option value="anthropic">Anthropic</option> | ||||||
|  |             <option value="ollama">Ollama</option> | ||||||
|  |         </select> | ||||||
|  |         <div class="form-text">${t("ai_llm.selected_provider_description")}</div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <!-- OpenAI Provider Settings --> | ||||||
|  |     <div class="provider-settings openai-provider-settings" style="display: none;"> | ||||||
|  |         <div class="card mt-3"> | ||||||
|  |             <div class="card-header"> | ||||||
|  |                 <h5>${t("ai_llm.openai_settings")}</h5> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-body"> | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.api_key")}</label> | ||||||
|  |                     <input type="password" class="openai-api-key form-control" autocomplete="off" /> | ||||||
|  |                     <div class="form-text">${t("ai_llm.openai_api_key_description")}</div> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.url")}</label> | ||||||
|  |                     <input type="text" class="openai-base-url form-control" /> | ||||||
|  |                     <div class="form-text">${t("ai_llm.openai_url_description")}</div> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.model")}</label> | ||||||
|  |                     <select class="openai-default-model form-control"> | ||||||
|  |                         <option value="gpt-4o">GPT-4o (recommended)</option> | ||||||
|  |                         <option value="gpt-4">GPT-4</option> | ||||||
|  |                         <option value="gpt-3.5-turbo">GPT-3.5 Turbo</option> | ||||||
|  |                     </select> | ||||||
|  |                     <div class="form-text">${t("ai_llm.openai_model_description")}</div> | ||||||
|  |                     <button class="btn btn-sm btn-outline-secondary refresh-openai-models">${t("ai_llm.refresh_models")}</button> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.embedding_model")}</label> | ||||||
|  |                     <select class="openai-embedding-model form-control"> | ||||||
|  |                         <option value="text-embedding-3-small">text-embedding-3-small (recommended)</option> | ||||||
|  |                         <option value="text-embedding-3-large">text-embedding-3-large</option> | ||||||
|  |                     </select> | ||||||
|  |                     <div class="form-text">${t("ai_llm.openai_embedding_model_description")}</div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <!-- Anthropic Provider Settings --> | ||||||
|  |     <div class="provider-settings anthropic-provider-settings" style="display: none;"> | ||||||
|  |         <div class="card mt-3"> | ||||||
|  |             <div class="card-header"> | ||||||
|  |                 <h5>${t("ai_llm.anthropic_settings")}</h5> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-body"> | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.api_key")}</label> | ||||||
|  |                     <input type="password" class="anthropic-api-key form-control" autocomplete="off" /> | ||||||
|  |                     <div class="form-text">${t("ai_llm.anthropic_api_key_description")}</div> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.url")}</label> | ||||||
|  |                     <input type="text" class="anthropic-base-url form-control" /> | ||||||
|  |                     <div class="form-text">${t("ai_llm.anthropic_url_description")}</div> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.model")}</label> | ||||||
|  |                     <select class="anthropic-default-model form-control"> | ||||||
|  |                         <option value="claude-3-opus-20240229">Claude 3 Opus (recommended)</option> | ||||||
|  |                         <option value="claude-3-sonnet-20240229">Claude 3 Sonnet</option> | ||||||
|  |                         <option value="claude-3-haiku-20240307">Claude 3 Haiku</option> | ||||||
|  |                     </select> | ||||||
|  |                     <div class="form-text">${t("ai_llm.anthropic_model_description")}</div> | ||||||
|  |                     <button class="btn btn-sm btn-outline-secondary refresh-anthropic-models">${t("ai_llm.refresh_models")}</button> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <!-- Ollama Provider Settings --> | ||||||
|  |     <div class="provider-settings ollama-provider-settings" style="display: none;"> | ||||||
|  |         <div class="card mt-3"> | ||||||
|  |             <div class="card-header"> | ||||||
|  |                 <h5>${t("ai_llm.ollama_settings")}</h5> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-body"> | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.url")}</label> | ||||||
|  |                     <input type="text" class="ollama-base-url form-control" /> | ||||||
|  |                     <div class="form-text">${t("ai_llm.ollama_url_description")}</div> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.model")}</label> | ||||||
|  |                     <select class="ollama-default-model form-control"> | ||||||
|  |                         <option value="llama3">llama3 (recommended)</option> | ||||||
|  |                         <option value="mistral">mistral</option> | ||||||
|  |                         <option value="phi3">phi3</option> | ||||||
|  |                     </select> | ||||||
|  |                     <div class="form-text">${t("ai_llm.ollama_model_description")}</div> | ||||||
|  |                     <button class="btn btn-sm btn-outline-secondary refresh-models"><span class="bx bx-refresh"></span></button> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.embedding_model")}</label> | ||||||
|  |                     <select class="ollama-embedding-model form-control"> | ||||||
|  |                         <option value="nomic-embed-text">nomic-embed-text (recommended)</option> | ||||||
|  |                         <option value="all-MiniLM-L6-v2">all-MiniLM-L6-v2</option> | ||||||
|  |                     </select> | ||||||
|  |                     <div class="form-text">${t("ai_llm.ollama_embedding_model_description")}</div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="form-group"> |     <div class="form-group"> | ||||||
| @@ -79,155 +195,98 @@ export const TPL = ` | |||||||
|     </div> |     </div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <nav class="options-section-tabs"> |  | ||||||
|     <div class="nav nav-tabs" id="nav-tab" role="tablist"> |  | ||||||
|         <button class="nav-link active" id="nav-openai-tab" data-bs-toggle="tab" data-bs-target="#nav-openai" type="button" role="tab" aria-controls="nav-openai" aria-selected="true">${t("ai_llm.openai_tab")}</button> |  | ||||||
|         <button class="nav-link" id="nav-anthropic-tab" data-bs-toggle="tab" data-bs-target="#nav-anthropic" type="button" role="tab" aria-controls="nav-anthropic" aria-selected="false">${t("ai_llm.anthropic_tab")}</button> |  | ||||||
|         <button class="nav-link" id="nav-voyage-tab" data-bs-toggle="tab" data-bs-target="#nav-voyage" type="button" role="tab" aria-controls="nav-voyage" aria-selected="false">${t("ai_llm.voyage_tab")}</button> |  | ||||||
|         <button class="nav-link" id="nav-ollama-tab" data-bs-toggle="tab" data-bs-target="#nav-ollama" type="button" role="tab" aria-controls="nav-ollama" aria-selected="false">${t("ai_llm.ollama_tab")}</button> |  | ||||||
|     </div> |  | ||||||
| </nav> |  | ||||||
| <div class="options-section"> |  | ||||||
|     <div class="tab-content" id="nav-tabContent"> |  | ||||||
|         <div class="tab-pane fade show active" id="nav-openai" role="tabpanel" aria-labelledby="nav-openai-tab"> |  | ||||||
|             <div class="card"> |  | ||||||
|                 <div class="card-header"> |  | ||||||
|                     <h5>${t("ai_llm.openai_settings")}</h5> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="card-body"> |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label>${t("ai_llm.api_key")}</label> |  | ||||||
|                         <input type="password" class="openai-api-key form-control" autocomplete="off" /> |  | ||||||
|                         <div class="form-text">${t("ai_llm.openai_api_key_description")}</div> |  | ||||||
|                     </div> |  | ||||||
|  |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label>${t("ai_llm.url")}</label> |  | ||||||
|                         <input type="text" class="openai-base-url form-control" /> |  | ||||||
|                         <div class="form-text">${t("ai_llm.openai_url_description")}</div> |  | ||||||
|                     </div> |  | ||||||
|  |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label>${t("ai_llm.model")}</label> |  | ||||||
|                         <select class="openai-default-model form-control"> |  | ||||||
|                             <option value="gpt-4o">GPT-4o (recommended)</option> |  | ||||||
|                             <option value="gpt-4">GPT-4</option> |  | ||||||
|                             <option value="gpt-3.5-turbo">GPT-3.5 Turbo</option> |  | ||||||
|                         </select> |  | ||||||
|                         <div class="form-text">${t("ai_llm.openai_model_description")}</div> |  | ||||||
|                         <button class="btn btn-sm btn-outline-secondary refresh-openai-models">${t("ai_llm.refresh_models")}</button> |  | ||||||
|                     </div> |  | ||||||
|  |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label>${t("ai_llm.embedding_model")}</label> |  | ||||||
|                         <select class="openai-embedding-model form-control"> |  | ||||||
|                             <option value="text-embedding-3-small">text-embedding-3-small (recommended)</option> |  | ||||||
|                             <option value="text-embedding-3-large">text-embedding-3-large</option> |  | ||||||
|                         </select> |  | ||||||
|                         <div class="form-text">${t("ai_llm.openai_embedding_model_description")}</div> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div class="tab-pane fade" id="nav-anthropic" role="tabpanel" aria-labelledby="nav-anthropic-tab"> |  | ||||||
|             <div class="card"> |  | ||||||
|                 <div class="card-header"> |  | ||||||
|                     <h5>${t("ai_llm.anthropic_settings")}</h5> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="card-body"> |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label>${t("ai_llm.api_key")}</label> |  | ||||||
|                         <input type="password" class="anthropic-api-key form-control" autocomplete="off" /> |  | ||||||
|                         <div class="form-text">${t("ai_llm.anthropic_api_key_description")}</div> |  | ||||||
|                     </div> |  | ||||||
|  |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label>${t("ai_llm.url")}</label> |  | ||||||
|                         <input type="text" class="anthropic-base-url form-control" /> |  | ||||||
|                         <div class="form-text">${t("ai_llm.anthropic_url_description")}</div> |  | ||||||
|                     </div> |  | ||||||
|  |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label>${t("ai_llm.model")}</label> |  | ||||||
|                         <select class="anthropic-default-model form-control"> |  | ||||||
|                             <option value="claude-3-opus-20240229">Claude 3 Opus (recommended)</option> |  | ||||||
|                             <option value="claude-3-sonnet-20240229">Claude 3 Sonnet</option> |  | ||||||
|                             <option value="claude-3-haiku-20240307">Claude 3 Haiku</option> |  | ||||||
|                         </select> |  | ||||||
|                         <div class="form-text">${t("ai_llm.anthropic_model_description")}</div> |  | ||||||
|                         <button class="btn btn-sm btn-outline-secondary refresh-anthropic-models">${t("ai_llm.refresh_models")}</button> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div class="tab-pane fade" id="nav-voyage" role="tabpanel" aria-labelledby="nav-voyage-tab"> |  | ||||||
|             <div class="card"> |  | ||||||
|                 <div class="card-header"> |  | ||||||
|                     <h5>${t("ai_llm.voyage_settings")}</h5> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="card-body"> |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label>${t("ai_llm.api_key")}</label> |  | ||||||
|                         <input type="password" class="voyage-api-key form-control" autocomplete="off" /> |  | ||||||
|                         <div class="form-text">${t("ai_llm.voyage_api_key_description")}</div> |  | ||||||
|                     </div> |  | ||||||
|  |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label>${t("ai_llm.embedding_model")}</label> |  | ||||||
|                         <select class="voyage-embedding-model form-control"> |  | ||||||
|                             <option value="voyage-2">Voyage-2 (recommended)</option> |  | ||||||
|                             <option value="voyage-2-code">Voyage-2-Code</option> |  | ||||||
|                             <option value="voyage-large-2">Voyage-Large-2</option> |  | ||||||
|                         </select> |  | ||||||
|                         <div class="form-text">${t("ai_llm.voyage_embedding_model_description")}</div> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div class="tab-pane fade" id="nav-ollama" role="tabpanel" aria-labelledby="nav-ollama-tab"> |  | ||||||
|             <div class="card"> |  | ||||||
|                 <div class="card-header"> |  | ||||||
|                     <h5>${t("ai_llm.ollama_settings")}</h5> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="card-body"> |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label>${t("ai_llm.url")}</label> |  | ||||||
|                         <input type="text" class="ollama-base-url form-control" /> |  | ||||||
|                         <div class="form-text">${t("ai_llm.ollama_url_description")}</div> |  | ||||||
|                     </div> |  | ||||||
|  |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label>${t("ai_llm.model")}</label> |  | ||||||
|                         <select class="ollama-default-model form-control"> |  | ||||||
|                             <option value="llama3">llama3 (recommended)</option> |  | ||||||
|                             <option value="mistral">mistral</option> |  | ||||||
|                             <option value="phi3">phi3</option> |  | ||||||
|                         </select> |  | ||||||
|                         <div class="form-text">${t("ai_llm.ollama_model_description")}</div> |  | ||||||
|                         <button class="btn btn-sm btn-outline-secondary refresh-models"><span class="bx bx-refresh"></span></button> |  | ||||||
|                     </div> |  | ||||||
|  |  | ||||||
|                     <div class="form-group"> |  | ||||||
|                         <label>${t("ai_llm.embedding_model")}</label> |  | ||||||
|                         <select class="ollama-embedding-model form-control"> |  | ||||||
|                             <option value="nomic-embed-text">nomic-embed-text (recommended)</option> |  | ||||||
|                             <option value="all-MiniLM-L6-v2">all-MiniLM-L6-v2</option> |  | ||||||
|                         </select> |  | ||||||
|                         <div class="form-text">${t("ai_llm.ollama_embedding_model_description")}</div> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
|  |  | ||||||
| <div class="options-section"> | <div class="options-section"> | ||||||
|     <h4>${t("ai_llm.embeddings_configuration")}</h4> |     <h4>${t("ai_llm.embeddings_configuration")}</h4> | ||||||
|  |  | ||||||
|     <div class="form-group"> |     <div class="form-group"> | ||||||
|         <label class="embedding-provider-label">${t("ai_llm.embedding_provider_precedence")}</label> |         <label class="embedding-provider-label">${t("ai_llm.selected_embedding_provider")}</label> | ||||||
|         <input type="text" class="embedding-provider-precedence form-control" placeholder="openai,voyage,ollama,local"> |         <select class="embedding-selected-provider form-control"> | ||||||
|         <div class="form-text">${t("ai_llm.embedding_provider_precedence_description")}</div> |             <option value="">${t("ai_llm.select_embedding_provider")}</option> | ||||||
|  |             <option value="openai">OpenAI</option> | ||||||
|  |             <option value="voyage">Voyage AI</option> | ||||||
|  |             <option value="ollama">Ollama</option> | ||||||
|  |             <option value="local">Local</option> | ||||||
|  |         </select> | ||||||
|  |         <div class="form-text">${t("ai_llm.selected_embedding_provider_description")}</div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <!-- OpenAI Embedding Provider Settings --> | ||||||
|  |     <div class="embedding-provider-settings openai-embedding-provider-settings" style="display: none;"> | ||||||
|  |         <div class="card mt-3"> | ||||||
|  |             <div class="card-header"> | ||||||
|  |                 <h5>${t("ai_llm.openai_embedding_settings")}</h5> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-body"> | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.embedding_model")}</label> | ||||||
|  |                     <select class="openai-embedding-model form-control"> | ||||||
|  |                         <option value="text-embedding-3-small">text-embedding-3-small (recommended)</option> | ||||||
|  |                         <option value="text-embedding-3-large">text-embedding-3-large</option> | ||||||
|  |                     </select> | ||||||
|  |                     <div class="form-text">${t("ai_llm.openai_embedding_model_description")}</div> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="form-text text-muted">${t("ai_llm.openai_embedding_shared_settings")}</div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <!-- Voyage Embedding Provider Settings --> | ||||||
|  |     <div class="embedding-provider-settings voyage-embedding-provider-settings" style="display: none;"> | ||||||
|  |         <div class="card mt-3"> | ||||||
|  |             <div class="card-header"> | ||||||
|  |                 <h5>${t("ai_llm.voyage_settings")}</h5> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-body"> | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.api_key")}</label> | ||||||
|  |                     <input type="password" class="voyage-api-key form-control" autocomplete="off" /> | ||||||
|  |                     <div class="form-text">${t("ai_llm.voyage_api_key_description")}</div> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.embedding_model")}</label> | ||||||
|  |                     <select class="voyage-embedding-model form-control"> | ||||||
|  |                         <option value="voyage-2">Voyage-2 (recommended)</option> | ||||||
|  |                         <option value="voyage-2-code">Voyage-2-Code</option> | ||||||
|  |                         <option value="voyage-large-2">Voyage-Large-2</option> | ||||||
|  |                     </select> | ||||||
|  |                     <div class="form-text">${t("ai_llm.voyage_embedding_model_description")}</div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <!-- Ollama Embedding Provider Settings --> | ||||||
|  |     <div class="embedding-provider-settings ollama-embedding-provider-settings" style="display: none;"> | ||||||
|  |         <div class="card mt-3"> | ||||||
|  |             <div class="card-header"> | ||||||
|  |                 <h5>${t("ai_llm.ollama_embedding_settings")}</h5> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-body"> | ||||||
|  |                 <div class="form-group"> | ||||||
|  |                     <label>${t("ai_llm.embedding_model")}</label> | ||||||
|  |                     <select class="ollama-embedding-model form-control"> | ||||||
|  |                         <option value="nomic-embed-text">nomic-embed-text (recommended)</option> | ||||||
|  |                         <option value="all-MiniLM-L6-v2">all-MiniLM-L6-v2</option> | ||||||
|  |                     </select> | ||||||
|  |                     <div class="form-text">${t("ai_llm.ollama_embedding_model_description")}</div> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="form-text text-muted">${t("ai_llm.ollama_embedding_shared_settings")}</div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <!-- Local Embedding Provider Settings --> | ||||||
|  |     <div class="embedding-provider-settings local-embedding-provider-settings" style="display: none;"> | ||||||
|  |         <div class="card mt-3"> | ||||||
|  |             <div class="card-header"> | ||||||
|  |                 <h5>${t("ai_llm.local_embedding_settings")}</h5> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-body"> | ||||||
|  |                 <div class="form-text">${t("ai_llm.local_embedding_description")}</div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="form-group"> |     <div class="form-group"> | ||||||
|   | |||||||
| @@ -95,7 +95,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([ | |||||||
|     "aiEnabled", |     "aiEnabled", | ||||||
|     "aiTemperature", |     "aiTemperature", | ||||||
|     "aiSystemPrompt", |     "aiSystemPrompt", | ||||||
|     "aiProviderPrecedence", |     "aiSelectedProvider", | ||||||
|     "openaiApiKey", |     "openaiApiKey", | ||||||
|     "openaiBaseUrl", |     "openaiBaseUrl", | ||||||
|     "openaiDefaultModel", |     "openaiDefaultModel", | ||||||
| @@ -110,7 +110,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([ | |||||||
|     "ollamaEmbeddingModel", |     "ollamaEmbeddingModel", | ||||||
|     "embeddingAutoUpdateEnabled", |     "embeddingAutoUpdateEnabled", | ||||||
|     "embeddingDimensionStrategy", |     "embeddingDimensionStrategy", | ||||||
|     "embeddingProviderPrecedence", |     "embeddingSelectedProvider", | ||||||
|     "embeddingSimilarityThreshold", |     "embeddingSimilarityThreshold", | ||||||
|     "embeddingBatchSize", |     "embeddingBatchSize", | ||||||
|     "embeddingUpdateInterval", |     "embeddingUpdateInterval", | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import options from '../options.js'; | import options from '../options.js'; | ||||||
|  | import eventService from '../events.js'; | ||||||
| import type { AIService, ChatCompletionOptions, ChatResponse, Message } from './ai_interface.js'; | import type { AIService, ChatCompletionOptions, ChatResponse, Message } from './ai_interface.js'; | ||||||
| import { AnthropicService } from './providers/anthropic_service.js'; | import { AnthropicService } from './providers/anthropic_service.js'; | ||||||
| import { ContextExtractor } from './context/index.js'; | import { ContextExtractor } from './context/index.js'; | ||||||
| @@ -20,9 +21,8 @@ import type { NoteSearchResult } from './interfaces/context_interfaces.js'; | |||||||
|  |  | ||||||
| // Import new configuration system | // Import new configuration system | ||||||
| import { | import { | ||||||
|     getProviderPrecedence, |     getSelectedProvider, | ||||||
|     getPreferredProvider, |     getSelectedEmbeddingProvider, | ||||||
|     getEmbeddingProviderPrecedence, |  | ||||||
|     parseModelIdentifier, |     parseModelIdentifier, | ||||||
|     isAIEnabled, |     isAIEnabled, | ||||||
|     getDefaultModelForProvider, |     getDefaultModelForProvider, | ||||||
| @@ -60,6 +60,9 @@ export class AIServiceManager implements IAIServiceManager { | |||||||
|         this.initializeTools().catch(error => { |         this.initializeTools().catch(error => { | ||||||
|             log.error(`Error initializing LLM tools during AIServiceManager construction: ${error.message || String(error)}`); |             log.error(`Error initializing LLM tools during AIServiceManager construction: ${error.message || String(error)}`); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  |         // Set up event listener for provider changes | ||||||
|  |         this.setupProviderChangeListener(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -84,16 +87,21 @@ export class AIServiceManager implements IAIServiceManager { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Update the provider precedence order using the new configuration system |      * Update the provider order using the new configuration system (single provider) | ||||||
|      */ |      */ | ||||||
|     async updateProviderOrderAsync(): Promise<void> { |     async updateProviderOrderAsync(): Promise<void> { | ||||||
|         try { |         try { | ||||||
|             const providers = await getProviderPrecedence(); |             const selectedProvider = await getSelectedProvider(); | ||||||
|             this.providerOrder = providers as ServiceProviders[]; |             if (selectedProvider) { | ||||||
|  |                 this.providerOrder = [selectedProvider as ServiceProviders]; | ||||||
|  |                 log.info(`Updated provider order: ${selectedProvider}`); | ||||||
|  |             } else { | ||||||
|  |                 this.providerOrder = []; | ||||||
|  |                 log.info('No provider selected'); | ||||||
|  |             } | ||||||
|             this.initialized = true; |             this.initialized = true; | ||||||
|             log.info(`Updated provider order: ${providers.join(', ')}`); |  | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             log.error(`Failed to get provider precedence: ${error}`); |             log.error(`Failed to get selected provider: ${error}`); | ||||||
|             // Keep empty order, will be handled gracefully by other methods |             // Keep empty order, will be handled gracefully by other methods | ||||||
|             this.providerOrder = []; |             this.providerOrder = []; | ||||||
|             this.initialized = true; |             this.initialized = true; | ||||||
| @@ -521,13 +529,13 @@ export class AIServiceManager implements IAIServiceManager { | |||||||
|      */ |      */ | ||||||
|     async getPreferredProviderAsync(): Promise<string> { |     async getPreferredProviderAsync(): Promise<string> { | ||||||
|         try { |         try { | ||||||
|             const preferredProvider = await getPreferredProvider(); |             const selectedProvider = await getSelectedProvider(); | ||||||
|             if (preferredProvider === null) { |             if (selectedProvider === null) { | ||||||
|                 // No providers configured, fallback to first available |                 // No provider selected, fallback to first available | ||||||
|                 log.info('No providers configured in precedence, using first available provider'); |                 log.info('No provider selected, using first available provider'); | ||||||
|                 return this.providerOrder[0]; |                 return this.providerOrder[0]; | ||||||
|             } |             } | ||||||
|             return preferredProvider; |             return selectedProvider; | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             log.error(`Error getting preferred provider: ${error}`); |             log.error(`Error getting preferred provider: ${error}`); | ||||||
|             return this.providerOrder[0]; |             return this.providerOrder[0]; | ||||||
| @@ -580,6 +588,7 @@ export class AIServiceManager implements IAIServiceManager { | |||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Error handler that properly types the error object |      * Error handler that properly types the error object | ||||||
|      */ |      */ | ||||||
| @@ -589,6 +598,75 @@ export class AIServiceManager implements IAIServiceManager { | |||||||
|         } |         } | ||||||
|         return String(error); |         return String(error); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Set up event listener for provider changes | ||||||
|  |      */ | ||||||
|  |     private setupProviderChangeListener(): void { | ||||||
|  |         // List of AI-related options that should trigger service recreation | ||||||
|  |         const aiRelatedOptions = [ | ||||||
|  |             'aiSelectedProvider', | ||||||
|  |             'embeddingSelectedProvider', | ||||||
|  |             'openaiApiKey', | ||||||
|  |             'openaiBaseUrl',  | ||||||
|  |             'openaiDefaultModel', | ||||||
|  |             'anthropicApiKey', | ||||||
|  |             'anthropicBaseUrl', | ||||||
|  |             'anthropicDefaultModel', | ||||||
|  |             'ollamaBaseUrl', | ||||||
|  |             'ollamaDefaultModel', | ||||||
|  |             'voyageApiKey' | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         eventService.subscribe(['entityChanged'], ({ entityName, entity }) => { | ||||||
|  |             if (entityName === 'options' && entity && aiRelatedOptions.includes(entity.name)) { | ||||||
|  |                 log.info(`AI-related option '${entity.name}' changed, recreating LLM services`); | ||||||
|  |                 this.recreateServices(); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Recreate LLM services when provider settings change | ||||||
|  |      */ | ||||||
|  |     private async recreateServices(): Promise<void> { | ||||||
|  |         try { | ||||||
|  |             log.info('Recreating LLM services due to configuration change'); | ||||||
|  |  | ||||||
|  |             // Clear configuration cache first | ||||||
|  |             clearConfigurationCache(); | ||||||
|  |  | ||||||
|  |             // Recreate all service instances to pick up new configuration | ||||||
|  |             this.recreateServiceInstances(); | ||||||
|  |  | ||||||
|  |             // Update provider order with new configuration | ||||||
|  |             await this.updateProviderOrderAsync(); | ||||||
|  |  | ||||||
|  |             log.info('LLM services recreated successfully'); | ||||||
|  |         } catch (error) { | ||||||
|  |             log.error(`Error recreating LLM services: ${this.handleError(error)}`); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Recreate service instances to pick up new configuration | ||||||
|  |      */ | ||||||
|  |     private recreateServiceInstances(): void { | ||||||
|  |         try { | ||||||
|  |             log.info('Recreating service instances'); | ||||||
|  |  | ||||||
|  |             // Recreate service instances | ||||||
|  |             this.services = { | ||||||
|  |                 openai: new OpenAIService(), | ||||||
|  |                 anthropic: new AnthropicService(), | ||||||
|  |                 ollama: new OllamaService() | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             log.info('Service instances recreated successfully'); | ||||||
|  |         } catch (error) { | ||||||
|  |             log.error(`Error recreating service instances: ${this.handleError(error)}`); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| // Don't create singleton immediately, use a lazy-loading pattern | // Don't create singleton immediately, use a lazy-loading pattern | ||||||
|   | |||||||
| @@ -1,10 +1,9 @@ | |||||||
| import configurationManager from './configuration_manager.js'; | import configurationManager from './configuration_manager.js'; | ||||||
|  | import optionService from '../../options.js'; | ||||||
| import type { | import type { | ||||||
|     ProviderType, |     ProviderType, | ||||||
|     ModelIdentifier, |     ModelIdentifier, | ||||||
|     ModelConfig, |     ModelConfig, | ||||||
|     ProviderPrecedenceConfig, |  | ||||||
|     EmbeddingProviderPrecedenceConfig |  | ||||||
| } from '../interfaces/configuration_interfaces.js'; | } from '../interfaces/configuration_interfaces.js'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -13,41 +12,19 @@ import type { | |||||||
|  */ |  */ | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Get the ordered list of AI providers |  * Get the selected AI provider | ||||||
|  */ |  */ | ||||||
| export async function getProviderPrecedence(): Promise<ProviderType[]> { | export async function getSelectedProvider(): Promise<ProviderType | null> { | ||||||
|     const config = await configurationManager.getProviderPrecedence(); |     const providerOption = optionService.getOption('aiSelectedProvider'); | ||||||
|     return config.providers; |     return providerOption as ProviderType || null; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Get the default/preferred AI provider |  * Get the selected embedding provider | ||||||
|  */ |  */ | ||||||
| export async function getPreferredProvider(): Promise<ProviderType | null> { | export async function getSelectedEmbeddingProvider(): Promise<string | null> { | ||||||
|     const config = await configurationManager.getProviderPrecedence(); |     const providerOption = optionService.getOption('embeddingSelectedProvider'); | ||||||
|     if (config.providers.length === 0) { |     return providerOption || null; | ||||||
|         return null; // No providers configured |  | ||||||
|     } |  | ||||||
|     return config.defaultProvider || config.providers[0]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Get the ordered list of embedding providers |  | ||||||
|  */ |  | ||||||
| export async function getEmbeddingProviderPrecedence(): Promise<string[]> { |  | ||||||
|     const config = await configurationManager.getEmbeddingProviderPrecedence(); |  | ||||||
|     return config.providers; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Get the default embedding provider |  | ||||||
|  */ |  | ||||||
| export async function getPreferredEmbeddingProvider(): Promise<string | null> { |  | ||||||
|     const config = await configurationManager.getEmbeddingProviderPrecedence(); |  | ||||||
|     if (config.providers.length === 0) { |  | ||||||
|         return null; // No providers configured |  | ||||||
|     } |  | ||||||
|     return config.defaultProvider || config.providers[0]; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -107,22 +84,20 @@ export async function isProviderConfigured(provider: ProviderType): Promise<bool | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Get the first available (configured) provider from the precedence list |  * Get the currently selected provider if it's available and configured | ||||||
|  */ |  */ | ||||||
| export async function getFirstAvailableProvider(): Promise<ProviderType | null> { | export async function getAvailableSelectedProvider(): Promise<ProviderType | null> { | ||||||
|     const providers = await getProviderPrecedence(); |     const selectedProvider = await getSelectedProvider(); | ||||||
|      |      | ||||||
|     if (providers.length === 0) { |     if (!selectedProvider) { | ||||||
|         return null; // No providers configured |         return null; // No provider selected | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     for (const provider of providers) { |     if (await isProviderConfigured(selectedProvider)) { | ||||||
|         if (await isProviderConfigured(provider)) { |         return selectedProvider; | ||||||
|             return provider; |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return null; // No providers are properly configured |     return null; // Selected provider is not properly configured | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -163,17 +138,59 @@ export async function getValidModelConfig(provider: ProviderType): Promise<{ mod | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Get the first valid model configuration from the provider precedence list |  * Get the model configuration for the currently selected provider | ||||||
|  */ |  */ | ||||||
| export async function getFirstValidModelConfig(): Promise<{ model: string; provider: ProviderType } | null> { | export async function getSelectedModelConfig(): Promise<{ model: string; provider: ProviderType } | null> { | ||||||
|     const providers = await getProviderPrecedence(); |     const selectedProvider = await getSelectedProvider(); | ||||||
|      |      | ||||||
|     for (const provider of providers) { |     if (!selectedProvider) { | ||||||
|         const config = await getValidModelConfig(provider); |         return null; // No provider selected | ||||||
|         if (config) { |  | ||||||
|             return config; |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return null; // No valid model configuration found |     return await getValidModelConfig(selectedProvider); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Legacy support functions - these maintain backwards compatibility but now use single provider logic | ||||||
|  | /** | ||||||
|  |  * @deprecated Use getSelectedProvider() instead | ||||||
|  |  */ | ||||||
|  | export async function getProviderPrecedence(): Promise<ProviderType[]> { | ||||||
|  |     const selected = await getSelectedProvider(); | ||||||
|  |     return selected ? [selected] : []; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @deprecated Use getSelectedProvider() instead   | ||||||
|  |  */ | ||||||
|  | export async function getPreferredProvider(): Promise<ProviderType | null> { | ||||||
|  |     return await getSelectedProvider(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @deprecated Use getSelectedEmbeddingProvider() instead | ||||||
|  |  */ | ||||||
|  | export async function getEmbeddingProviderPrecedence(): Promise<string[]> { | ||||||
|  |     const selected = await getSelectedEmbeddingProvider(); | ||||||
|  |     return selected ? [selected] : []; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @deprecated Use getSelectedEmbeddingProvider() instead | ||||||
|  |  */ | ||||||
|  | export async function getPreferredEmbeddingProvider(): Promise<string | null> { | ||||||
|  |     return await getSelectedEmbeddingProvider(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @deprecated Use getAvailableSelectedProvider() instead | ||||||
|  |  */ | ||||||
|  | export async function getFirstAvailableProvider(): Promise<ProviderType | null> { | ||||||
|  |     return await getAvailableSelectedProvider(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @deprecated Use getSelectedModelConfig() instead | ||||||
|  |  */ | ||||||
|  | export async function getFirstValidModelConfig(): Promise<{ model: string; provider: ProviderType } | null> { | ||||||
|  |     return await getSelectedModelConfig(); | ||||||
| } | } | ||||||
| @@ -50,8 +50,8 @@ export class ConfigurationManager { | |||||||
|         try { |         try { | ||||||
|             const config: AIConfig = { |             const config: AIConfig = { | ||||||
|                 enabled: await this.getAIEnabled(), |                 enabled: await this.getAIEnabled(), | ||||||
|                 providerPrecedence: await this.getProviderPrecedence(), |                 selectedProvider: await this.getSelectedProvider(), | ||||||
|                 embeddingProviderPrecedence: await this.getEmbeddingProviderPrecedence(), |                 selectedEmbeddingProvider: await this.getSelectedEmbeddingProvider(), | ||||||
|                 defaultModels: await this.getDefaultModels(), |                 defaultModels: await this.getDefaultModels(), | ||||||
|                 providerSettings: await this.getProviderSettings() |                 providerSettings: await this.getProviderSettings() | ||||||
|             }; |             }; | ||||||
| @@ -66,46 +66,28 @@ export class ConfigurationManager { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Parse provider precedence from string option |      * Get the selected AI provider | ||||||
|      */ |      */ | ||||||
|     public async getProviderPrecedence(): Promise<ProviderPrecedenceConfig> { |     public async getSelectedProvider(): Promise<ProviderType | null> { | ||||||
|         try { |         try { | ||||||
|             const precedenceOption = await options.getOption('aiProviderPrecedence'); |             const selectedProvider = await options.getOption('aiSelectedProvider'); | ||||||
|             const providers = this.parseProviderList(precedenceOption); |             return selectedProvider as ProviderType || null; | ||||||
|  |  | ||||||
|             return { |  | ||||||
|                 providers: providers as ProviderType[], |  | ||||||
|                 defaultProvider: providers.length > 0 ? providers[0] as ProviderType : undefined |  | ||||||
|             }; |  | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             log.error(`Error parsing provider precedence: ${error}`); |             log.error(`Error getting selected provider: ${error}`); | ||||||
|             // Only return known providers if they exist, don't assume defaults |             return null; | ||||||
|             return { |  | ||||||
|                 providers: [], |  | ||||||
|                 defaultProvider: undefined |  | ||||||
|             }; |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Parse embedding provider precedence from string option |      * Get the selected embedding provider | ||||||
|      */ |      */ | ||||||
|     public async getEmbeddingProviderPrecedence(): Promise<EmbeddingProviderPrecedenceConfig> { |     public async getSelectedEmbeddingProvider(): Promise<EmbeddingProviderType | null> { | ||||||
|         try { |         try { | ||||||
|             const precedenceOption = await options.getOption('embeddingProviderPrecedence'); |             const selectedProvider = await options.getOption('embeddingSelectedProvider'); | ||||||
|             const providers = this.parseProviderList(precedenceOption); |             return selectedProvider as EmbeddingProviderType || null; | ||||||
|  |  | ||||||
|             return { |  | ||||||
|                 providers: providers as EmbeddingProviderType[], |  | ||||||
|                 defaultProvider: providers.length > 0 ? providers[0] as EmbeddingProviderType : undefined |  | ||||||
|             }; |  | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             log.error(`Error parsing embedding provider precedence: ${error}`); |             log.error(`Error getting selected embedding provider: ${error}`); | ||||||
|             // Don't assume defaults, return empty configuration |             return null; | ||||||
|             return { |  | ||||||
|                 providers: [], |  | ||||||
|                 defaultProvider: undefined |  | ||||||
|             }; |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -265,31 +247,29 @@ export class ConfigurationManager { | |||||||
|                 return result; |                 return result; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Validate provider precedence |             // Validate selected provider | ||||||
|             if (config.providerPrecedence.providers.length === 0) { |             if (!config.selectedProvider) { | ||||||
|                 result.errors.push('No providers configured in precedence list'); |                 result.errors.push('No AI provider selected'); | ||||||
|                 result.isValid = false; |                 result.isValid = false; | ||||||
|             } |             } else { | ||||||
|  |                 // Validate selected provider settings | ||||||
|  |                 const providerConfig = config.providerSettings[config.selectedProvider]; | ||||||
|  |  | ||||||
|             // Validate provider settings |                 if (config.selectedProvider === 'openai') { | ||||||
|             for (const provider of config.providerPrecedence.providers) { |  | ||||||
|                 const providerConfig = config.providerSettings[provider]; |  | ||||||
|  |  | ||||||
|                 if (provider === 'openai') { |  | ||||||
|                     const openaiConfig = providerConfig as OpenAISettings | undefined; |                     const openaiConfig = providerConfig as OpenAISettings | undefined; | ||||||
|                     if (!openaiConfig?.apiKey) { |                     if (!openaiConfig?.apiKey) { | ||||||
|                         result.warnings.push('OpenAI API key is not configured'); |                         result.warnings.push('OpenAI API key is not configured'); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 if (provider === 'anthropic') { |                 if (config.selectedProvider === 'anthropic') { | ||||||
|                     const anthropicConfig = providerConfig as AnthropicSettings | undefined; |                     const anthropicConfig = providerConfig as AnthropicSettings | undefined; | ||||||
|                     if (!anthropicConfig?.apiKey) { |                     if (!anthropicConfig?.apiKey) { | ||||||
|                         result.warnings.push('Anthropic API key is not configured'); |                         result.warnings.push('Anthropic API key is not configured'); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 if (provider === 'ollama') { |                 if (config.selectedProvider === 'ollama') { | ||||||
|                     const ollamaConfig = providerConfig as OllamaSettings | undefined; |                     const ollamaConfig = providerConfig as OllamaSettings | undefined; | ||||||
|                     if (!ollamaConfig?.baseUrl) { |                     if (!ollamaConfig?.baseUrl) { | ||||||
|                         result.warnings.push('Ollama base URL is not configured'); |                         result.warnings.push('Ollama base URL is not configured'); | ||||||
| @@ -297,6 +277,11 @@ export class ConfigurationManager { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             // Validate selected embedding provider | ||||||
|  |             if (!config.selectedEmbeddingProvider) { | ||||||
|  |                 result.warnings.push('No embedding provider selected'); | ||||||
|  |             } | ||||||
|  |  | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             result.errors.push(`Configuration validation error: ${error}`); |             result.errors.push(`Configuration validation error: ${error}`); | ||||||
|             result.isValid = false; |             result.isValid = false; | ||||||
| @@ -356,14 +341,8 @@ export class ConfigurationManager { | |||||||
|     private getDefaultConfig(): AIConfig { |     private getDefaultConfig(): AIConfig { | ||||||
|         return { |         return { | ||||||
|             enabled: false, |             enabled: false, | ||||||
|             providerPrecedence: { |             selectedProvider: null, | ||||||
|                 providers: [], |             selectedEmbeddingProvider: null, | ||||||
|                 defaultProvider: undefined |  | ||||||
|             }, |  | ||||||
|             embeddingProviderPrecedence: { |  | ||||||
|                 providers: [], |  | ||||||
|                 defaultProvider: undefined |  | ||||||
|             }, |  | ||||||
|             defaultModels: { |             defaultModels: { | ||||||
|                 openai: undefined, |                 openai: undefined, | ||||||
|                 anthropic: undefined, |                 anthropic: undefined, | ||||||
|   | |||||||
| @@ -1,51 +1,32 @@ | |||||||
| import options from '../../../options.js'; |  | ||||||
| import log from '../../../log.js'; | import log from '../../../log.js'; | ||||||
| import { getEmbeddingProvider, getEnabledEmbeddingProviders } from '../../providers/providers.js'; | import { getEmbeddingProvider, getEnabledEmbeddingProviders } from '../../providers/providers.js'; | ||||||
|  | import { getSelectedEmbeddingProvider } from '../../config/configuration_helpers.js'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Manages embedding providers for context services |  * Manages embedding providers for context services | ||||||
|  */ |  */ | ||||||
| export class ProviderManager { | export class ProviderManager { | ||||||
|     /** |     /** | ||||||
|      * Get the preferred embedding provider based on user settings |      * Get the selected embedding provider based on user settings | ||||||
|      * Tries to use the most appropriate provider in this order: |      * Uses the single provider selection approach | ||||||
|      * 1. User's configured default provider |  | ||||||
|      * 2. OpenAI if API key is set |  | ||||||
|      * 3. Anthropic if API key is set |  | ||||||
|      * 4. Ollama if configured |  | ||||||
|      * 5. Any available provider |  | ||||||
|      * 6. Local provider as fallback |  | ||||||
|      * |      * | ||||||
|      * @returns The preferred embedding provider or null if none available |      * @returns The selected embedding provider or null if none available | ||||||
|      */ |      */ | ||||||
|     async getPreferredEmbeddingProvider(): Promise<any> { |     async getPreferredEmbeddingProvider(): Promise<any> { | ||||||
|         try { |         try { | ||||||
|             // Try to get providers based on precedence list |             // Get the selected embedding provider | ||||||
|             const precedenceOption = await options.getOption('embeddingProviderPrecedence'); |             const selectedProvider = await getSelectedEmbeddingProvider(); | ||||||
|             let precedenceList: string[] = []; |  | ||||||
|              |              | ||||||
|             if (precedenceOption) { |             if (selectedProvider) { | ||||||
|                 if (precedenceOption.startsWith('[') && precedenceOption.endsWith(']')) { |                 const provider = await getEmbeddingProvider(selectedProvider); | ||||||
|                     precedenceList = JSON.parse(precedenceOption); |  | ||||||
|                 } else if (typeof precedenceOption === 'string') { |  | ||||||
|                     if (precedenceOption.includes(',')) { |  | ||||||
|                         precedenceList = precedenceOption.split(',').map(p => p.trim()); |  | ||||||
|                     } else { |  | ||||||
|                         precedenceList = [precedenceOption]; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Try each provider in the precedence list |  | ||||||
|             for (const providerId of precedenceList) { |  | ||||||
|                 const provider = await getEmbeddingProvider(providerId); |  | ||||||
|                 if (provider) { |                 if (provider) { | ||||||
|                     log.info(`Using embedding provider from precedence list: ${providerId}`); |                     log.info(`Using selected embedding provider: ${selectedProvider}`); | ||||||
|                     return provider; |                     return provider; | ||||||
|                 } |                 } | ||||||
|  |                 log.info(`Selected embedding provider ${selectedProvider} is not available`); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // If no provider from precedence list is available, try any enabled provider |             // If no provider is selected or available, try any enabled provider | ||||||
|             const providers = await getEnabledEmbeddingProviders(); |             const providers = await getEnabledEmbeddingProviders(); | ||||||
|             if (providers.length > 0) { |             if (providers.length > 0) { | ||||||
|                 log.info(`Using available embedding provider: ${providers[0].name}`); |                 log.info(`Using available embedding provider: ${providers[0].name}`); | ||||||
|   | |||||||
| @@ -497,40 +497,24 @@ export class IndexService { | |||||||
|                 throw new Error("No embedding providers available"); |                 throw new Error("No embedding providers available"); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Get the embedding provider precedence |             // Get the selected embedding provider | ||||||
|             const options = (await import('../options.js')).default; |             const options = (await import('../options.js')).default; | ||||||
|             let preferredProviders: string[] = []; |             const selectedEmbeddingProvider = await options.getOption('embeddingSelectedProvider'); | ||||||
|  |  | ||||||
|             const embeddingPrecedence = await options.getOption('embeddingProviderPrecedence'); |  | ||||||
|             let provider; |             let provider; | ||||||
|  |  | ||||||
|             if (embeddingPrecedence) { |             if (selectedEmbeddingProvider) { | ||||||
|                 // Parse the precedence string |                 // Try to use the selected provider | ||||||
|                 if (embeddingPrecedence.startsWith('[') && embeddingPrecedence.endsWith(']')) { |                 const enabledProviders = await providerManager.getEnabledEmbeddingProviders(); | ||||||
|                     preferredProviders = JSON.parse(embeddingPrecedence); |                 provider = enabledProviders.find(p => p.name === selectedEmbeddingProvider); | ||||||
|                 } else if (typeof embeddingPrecedence === 'string') { |  | ||||||
|                     if (embeddingPrecedence.includes(',')) { |  | ||||||
|                         preferredProviders = embeddingPrecedence.split(',').map(p => p.trim()); |  | ||||||
|                     } else { |  | ||||||
|                         preferredProviders = [embeddingPrecedence]; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                  |                  | ||||||
|                 // Find first enabled provider by precedence order |                 if (!provider) { | ||||||
|                 for (const providerName of preferredProviders) { |                     log.info(`Selected embedding provider ${selectedEmbeddingProvider} is not available, using first enabled provider`); | ||||||
|                     const matchedProvider = providers.find(p => p.name === providerName); |                     // Fall back to first enabled provider | ||||||
|                     if (matchedProvider) { |  | ||||||
|                         provider = matchedProvider; |  | ||||||
|                         break; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 // If no match found, use first available |  | ||||||
|                 if (!provider && providers.length > 0) { |  | ||||||
|                     provider = providers[0]; |                     provider = providers[0]; | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|                 // Default to first available provider |                 // No provider selected, use first available provider | ||||||
|  |                 log.info('No embedding provider selected, using first available provider'); | ||||||
|                 provider = providers[0]; |                 provider = providers[0]; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -46,8 +46,8 @@ export interface ModelCapabilities { | |||||||
|  */ |  */ | ||||||
| export interface AIConfig { | export interface AIConfig { | ||||||
|     enabled: boolean; |     enabled: boolean; | ||||||
|     providerPrecedence: ProviderPrecedenceConfig; |     selectedProvider: ProviderType | null; | ||||||
|     embeddingProviderPrecedence: EmbeddingProviderPrecedenceConfig; |     selectedEmbeddingProvider: EmbeddingProviderType | null; | ||||||
|     defaultModels: Record<ProviderType, string | undefined>; |     defaultModels: Record<ProviderType, string | undefined>; | ||||||
|     providerSettings: ProviderSettings; |     providerSettings: ProviderSettings; | ||||||
| } | } | ||||||
| @@ -87,7 +87,7 @@ export type ProviderType = 'openai' | 'anthropic' | 'ollama'; | |||||||
| /** | /** | ||||||
|  * Valid embedding provider types |  * Valid embedding provider types | ||||||
|  */ |  */ | ||||||
| export type EmbeddingProviderType = 'openai' | 'ollama' | 'local'; | export type EmbeddingProviderType = 'openai' | 'voyage' | 'ollama' | 'local'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Model identifier with provider prefix (e.g., "openai:gpt-4" or "ollama:llama2") |  * Model identifier with provider prefix (e.g., "openai:gpt-4" or "ollama:llama2") | ||||||
|   | |||||||
| @@ -11,8 +11,7 @@ import type { ServiceProviders } from '../../interfaces/ai_service_interfaces.js | |||||||
|  |  | ||||||
| // Import new configuration system | // Import new configuration system | ||||||
| import { | import { | ||||||
|     getProviderPrecedence, |     getSelectedProvider, | ||||||
|     getPreferredProvider, |  | ||||||
|     parseModelIdentifier, |     parseModelIdentifier, | ||||||
|     getDefaultModelForProvider, |     getDefaultModelForProvider, | ||||||
|     createModelConfig |     createModelConfig | ||||||
| @@ -99,22 +98,30 @@ export class ModelSelectionStage extends BasePipelineStage<ModelSelectionInput, | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Get default provider and model using the new configuration system |         // Get selected provider and model using the new configuration system | ||||||
|         try { |         try { | ||||||
|             // Use the new configuration helpers - no string parsing! |             // Use the new configuration helpers - no string parsing! | ||||||
|             const preferredProvider = await getPreferredProvider(); |             const selectedProvider = await getSelectedProvider(); | ||||||
|  |  | ||||||
|             if (!preferredProvider) { |             if (!selectedProvider) { | ||||||
|                 throw new Error('No AI providers are configured. Please check your AI settings.'); |                 throw new Error('No AI provider is selected. Please select a provider in your AI settings.'); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             const modelName = await getDefaultModelForProvider(preferredProvider); |             const modelName = await getDefaultModelForProvider(selectedProvider); | ||||||
|  |  | ||||||
|             if (!modelName) { |             if (!modelName) { | ||||||
|                 throw new Error(`No default model configured for provider ${preferredProvider}. Please set a default model in your AI settings.`); |                 // Try to fetch and set a default model from the provider | ||||||
|  |                 const fetchedModel = await this.fetchAndSetDefaultModel(selectedProvider); | ||||||
|  |                 if (!fetchedModel) { | ||||||
|  |                     throw new Error(`No default model configured for provider ${selectedProvider}. Please set a default model in your AI settings.`); | ||||||
|  |                 } | ||||||
|  |                 // Use the fetched model | ||||||
|  |                 updatedOptions.model = fetchedModel; | ||||||
|  |             } else { | ||||||
|  |                 updatedOptions.model = modelName; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             log.info(`Selected provider: ${preferredProvider}, model: ${modelName}`); |             log.info(`Selected provider: ${selectedProvider}, model: ${updatedOptions.model}`); | ||||||
|  |  | ||||||
|             // Determine query complexity |             // Determine query complexity | ||||||
|             let queryComplexity = 'low'; |             let queryComplexity = 'low'; | ||||||
| @@ -142,15 +149,14 @@ export class ModelSelectionStage extends BasePipelineStage<ModelSelectionInput, | |||||||
|                 queryComplexity = contentLength > SEARCH_CONSTANTS.CONTEXT.CONTENT_LENGTH.HIGH_THRESHOLD ? 'high' : 'medium'; |                 queryComplexity = contentLength > SEARCH_CONSTANTS.CONTEXT.CONTENT_LENGTH.HIGH_THRESHOLD ? 'high' : 'medium'; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Set the model and add provider metadata |             // Add provider metadata (model is already set above) | ||||||
|             updatedOptions.model = modelName; |             this.addProviderMetadata(updatedOptions, selectedProvider as ServiceProviders, updatedOptions.model); | ||||||
|             this.addProviderMetadata(updatedOptions, preferredProvider as ServiceProviders, modelName); |  | ||||||
|  |  | ||||||
|             log.info(`Selected model: ${modelName} from provider: ${preferredProvider} for query complexity: ${queryComplexity}`); |             log.info(`Selected model: ${updatedOptions.model} from provider: ${selectedProvider} for query complexity: ${queryComplexity}`); | ||||||
|             log.info(`[ModelSelectionStage] Final options: ${JSON.stringify({ |             log.info(`[ModelSelectionStage] Final options: ${JSON.stringify({ | ||||||
|                 model: updatedOptions.model, |                 model: updatedOptions.model, | ||||||
|                 stream: updatedOptions.stream, |                 stream: updatedOptions.stream, | ||||||
|                 provider: preferredProvider, |                 provider: selectedProvider, | ||||||
|                 enableTools: updatedOptions.enableTools |                 enableTools: updatedOptions.enableTools | ||||||
|             })}`); |             })}`); | ||||||
|  |  | ||||||
| @@ -210,38 +216,38 @@ export class ModelSelectionStage extends BasePipelineStage<ModelSelectionInput, | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Determine model based on provider precedence using the new configuration system |      * Determine model based on selected provider using the new configuration system | ||||||
|      */ |      */ | ||||||
|     private async determineDefaultModel(input: ModelSelectionInput): Promise<string> { |     private async determineDefaultModel(input: ModelSelectionInput): Promise<string> { | ||||||
|         try { |         try { | ||||||
|             // Use the new configuration system |             // Use the new single provider configuration system | ||||||
|             const providers = await getProviderPrecedence(); |             const selectedProvider = await getSelectedProvider(); | ||||||
|  |  | ||||||
|             // Use only providers that are available |             if (!selectedProvider) { | ||||||
|             const availableProviders = providers.filter(provider => |                 throw new Error('No AI provider is selected. Please select a provider in your AI settings.'); | ||||||
|                 aiServiceManager.isProviderAvailable(provider)); |  | ||||||
|  |  | ||||||
|             if (availableProviders.length === 0) { |  | ||||||
|                 throw new Error('No AI providers are available'); |  | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Get the first available provider and its default model |             // Check if the provider is available | ||||||
|             const defaultProvider = availableProviders[0]; |             if (!aiServiceManager.isProviderAvailable(selectedProvider)) { | ||||||
|             const defaultModel = await getDefaultModelForProvider(defaultProvider); |                 throw new Error(`Selected provider ${selectedProvider} is not available`); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Get the default model for the selected provider | ||||||
|  |             const defaultModel = await getDefaultModelForProvider(selectedProvider); | ||||||
|  |  | ||||||
|             if (!defaultModel) { |             if (!defaultModel) { | ||||||
|                 throw new Error(`No default model configured for provider ${defaultProvider}. Please configure a default model in your AI settings.`); |                 throw new Error(`No default model configured for provider ${selectedProvider}. Please configure a default model in your AI settings.`); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Set provider metadata |             // Set provider metadata | ||||||
|             if (!input.options.providerMetadata) { |             if (!input.options.providerMetadata) { | ||||||
|                 input.options.providerMetadata = { |                 input.options.providerMetadata = { | ||||||
|                     provider: defaultProvider as 'openai' | 'anthropic' | 'ollama' | 'local', |                     provider: selectedProvider as 'openai' | 'anthropic' | 'ollama' | 'local', | ||||||
|                     modelId: defaultModel |                     modelId: defaultModel | ||||||
|                 }; |                 }; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             log.info(`Selected default model ${defaultModel} from provider ${defaultProvider}`); |             log.info(`Selected default model ${defaultModel} from provider ${selectedProvider}`); | ||||||
|             return defaultModel; |             return defaultModel; | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             log.error(`Error determining default model: ${error}`); |             log.error(`Error determining default model: ${error}`); | ||||||
| @@ -271,4 +277,126 @@ export class ModelSelectionStage extends BasePipelineStage<ModelSelectionInput, | |||||||
|             return MODEL_CAPABILITIES['default'].contextWindowTokens; |             return MODEL_CAPABILITIES['default'].contextWindowTokens; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Fetch available models from provider and set a default model | ||||||
|  |      */ | ||||||
|  |     private async fetchAndSetDefaultModel(provider: ProviderType): Promise<string | null> { | ||||||
|  |         try { | ||||||
|  |             log.info(`Fetching available models for provider ${provider}`); | ||||||
|  |              | ||||||
|  |             // Import server-side options to update the default model | ||||||
|  |             const optionService = (await import('../../../options.js')).default; | ||||||
|  |              | ||||||
|  |             switch (provider) { | ||||||
|  |                 case 'openai': | ||||||
|  |                     const openaiModels = await this.fetchOpenAIModels(); | ||||||
|  |                     if (openaiModels.length > 0) { | ||||||
|  |                         // Use the first available model without any preferences | ||||||
|  |                         const selectedModel = openaiModels[0]; | ||||||
|  |                          | ||||||
|  |                         await optionService.setOption('openaiDefaultModel', selectedModel); | ||||||
|  |                         log.info(`Set default OpenAI model to: ${selectedModel}`); | ||||||
|  |                         return selectedModel; | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                      | ||||||
|  |                 case 'anthropic': | ||||||
|  |                     const anthropicModels = await this.fetchAnthropicModels(); | ||||||
|  |                     if (anthropicModels.length > 0) { | ||||||
|  |                         // Use the first available model without any preferences | ||||||
|  |                         const selectedModel = anthropicModels[0]; | ||||||
|  |                          | ||||||
|  |                         await optionService.setOption('anthropicDefaultModel', selectedModel); | ||||||
|  |                         log.info(`Set default Anthropic model to: ${selectedModel}`); | ||||||
|  |                         return selectedModel; | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                      | ||||||
|  |                 case 'ollama': | ||||||
|  |                     const ollamaModels = await this.fetchOllamaModels(); | ||||||
|  |                     if (ollamaModels.length > 0) { | ||||||
|  |                         // Use the first available model without any preferences | ||||||
|  |                         const selectedModel = ollamaModels[0]; | ||||||
|  |                          | ||||||
|  |                         await optionService.setOption('ollamaDefaultModel', selectedModel); | ||||||
|  |                         log.info(`Set default Ollama model to: ${selectedModel}`); | ||||||
|  |                         return selectedModel; | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             log.info(`No models available for provider ${provider}`); | ||||||
|  |             return null; | ||||||
|  |         } catch (error) { | ||||||
|  |             log.error(`Error fetching models for provider ${provider}: ${error}`); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Fetch available OpenAI models | ||||||
|  |      */ | ||||||
|  |     private async fetchOpenAIModels(): Promise<string[]> { | ||||||
|  |         try { | ||||||
|  |             // Use the provider service to get available models | ||||||
|  |             const aiServiceManager = (await import('../../ai_service_manager.js')).default; | ||||||
|  |             const service = aiServiceManager.getInstance().getService('openai'); | ||||||
|  |              | ||||||
|  |             if (service && typeof (service as any).getAvailableModels === 'function') { | ||||||
|  |                 return await (service as any).getAvailableModels(); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // No fallback - return empty array if models can't be fetched | ||||||
|  |             log.info('OpenAI service does not support getAvailableModels method'); | ||||||
|  |             return []; | ||||||
|  |         } catch (error) { | ||||||
|  |             log.error(`Error fetching OpenAI models: ${error}`); | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Fetch available Anthropic models | ||||||
|  |      */ | ||||||
|  |     private async fetchAnthropicModels(): Promise<string[]> { | ||||||
|  |         try { | ||||||
|  |             // Use the provider service to get available models | ||||||
|  |             const aiServiceManager = (await import('../../ai_service_manager.js')).default; | ||||||
|  |             const service = aiServiceManager.getInstance().getService('anthropic'); | ||||||
|  |              | ||||||
|  |             if (service && typeof (service as any).getAvailableModels === 'function') { | ||||||
|  |                 return await (service as any).getAvailableModels(); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // No fallback - return empty array if models can't be fetched | ||||||
|  |             log.info('Anthropic service does not support getAvailableModels method'); | ||||||
|  |             return []; | ||||||
|  |         } catch (error) { | ||||||
|  |             log.error(`Error fetching Anthropic models: ${error}`); | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Fetch available Ollama models | ||||||
|  |      */ | ||||||
|  |     private async fetchOllamaModels(): Promise<string[]> { | ||||||
|  |         try { | ||||||
|  |             // Use the provider service to get available models | ||||||
|  |             const aiServiceManager = (await import('../../ai_service_manager.js')).default; | ||||||
|  |             const service = aiServiceManager.getInstance().getService('ollama'); | ||||||
|  |              | ||||||
|  |             if (service && typeof (service as any).getAvailableModels === 'function') { | ||||||
|  |                 return await (service as any).getAvailableModels(); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // No fallback - return empty array if models can't be fetched | ||||||
|  |             log.info('Ollama service does not support getAvailableModels method'); | ||||||
|  |             return []; | ||||||
|  |         } catch (error) { | ||||||
|  |             log.error(`Error fetching Ollama models: ${error}`); | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -606,4 +606,12 @@ export class AnthropicService extends BaseAIService { | |||||||
|  |  | ||||||
|         return convertedTools; |         return convertedTools; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Clear cached Anthropic client to force recreation with new settings | ||||||
|  |      */ | ||||||
|  |     clearCache(): void { | ||||||
|  |         this.client = null; | ||||||
|  |         log.info('Anthropic client cache cleared'); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -526,4 +526,13 @@ export class OllamaService extends BaseAIService { | |||||||
|         log.info(`Added tool execution feedback: ${toolExecutionStatus.length} statuses`); |         log.info(`Added tool execution feedback: ${toolExecutionStatus.length} statuses`); | ||||||
|         return updatedMessages; |         return updatedMessages; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Clear cached Ollama client to force recreation with new settings | ||||||
|  |      */ | ||||||
|  |     clearCache(): void { | ||||||
|  |         // Ollama service doesn't maintain a persistent client like OpenAI/Anthropic | ||||||
|  |         // but we can clear any future cached state here if needed | ||||||
|  |         log.info('Ollama client cache cleared (no persistent client to clear)'); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -257,4 +257,12 @@ export class OpenAIService extends BaseAIService { | |||||||
|             throw error; |             throw error; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Clear cached OpenAI client to force recreation with new settings | ||||||
|  |      */ | ||||||
|  |     clearCache(): void { | ||||||
|  |         this.openai = null; | ||||||
|  |         log.info('OpenAI client cache cleared'); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -195,26 +195,26 @@ const defaultOptions: DefaultOption[] = [ | |||||||
|     // AI Options |     // AI Options | ||||||
|     { name: "aiEnabled", value: "false", isSynced: true }, |     { name: "aiEnabled", value: "false", isSynced: true }, | ||||||
|     { name: "openaiApiKey", value: "", isSynced: false }, |     { name: "openaiApiKey", value: "", isSynced: false }, | ||||||
|     { name: "openaiDefaultModel", value: "gpt-4o", isSynced: true }, |     { name: "openaiDefaultModel", value: "", isSynced: true }, | ||||||
|     { name: "openaiEmbeddingModel", value: "text-embedding-3-small", isSynced: true }, |     { name: "openaiEmbeddingModel", value: "", isSynced: true }, | ||||||
|     { name: "openaiBaseUrl", value: "https://api.openai.com/v1", isSynced: true }, |     { name: "openaiBaseUrl", value: "https://api.openai.com/v1", isSynced: true }, | ||||||
|     { name: "anthropicApiKey", value: "", isSynced: false }, |     { name: "anthropicApiKey", value: "", isSynced: false }, | ||||||
|     { name: "anthropicDefaultModel", value: "claude-3-opus-20240229", isSynced: true }, |     { name: "anthropicDefaultModel", value: "", isSynced: true }, | ||||||
|     { name: "voyageEmbeddingModel", value: "voyage-2", isSynced: true }, |     { name: "voyageEmbeddingModel", value: "", isSynced: true }, | ||||||
|     { name: "voyageApiKey", value: "", isSynced: false }, |     { name: "voyageApiKey", value: "", isSynced: false }, | ||||||
|     { name: "anthropicBaseUrl", value: "https://api.anthropic.com/v1", isSynced: true }, |     { name: "anthropicBaseUrl", value: "https://api.anthropic.com/v1", isSynced: true }, | ||||||
|     { name: "ollamaEnabled", value: "false", isSynced: true }, |     { name: "ollamaEnabled", value: "false", isSynced: true }, | ||||||
|     { name: "ollamaDefaultModel", value: "llama3", isSynced: true }, |     { name: "ollamaDefaultModel", value: "", isSynced: true }, | ||||||
|     { name: "ollamaBaseUrl", value: "http://localhost:11434", isSynced: true }, |     { name: "ollamaBaseUrl", value: "http://localhost:11434", isSynced: true }, | ||||||
|     { name: "ollamaEmbeddingModel", value: "nomic-embed-text", isSynced: true }, |     { name: "ollamaEmbeddingModel", value: "", isSynced: true }, | ||||||
|     { name: "embeddingAutoUpdateEnabled", value: "true", isSynced: true }, |     { name: "embeddingAutoUpdateEnabled", value: "true", isSynced: true }, | ||||||
|  |  | ||||||
|     // Adding missing AI options |     // Adding missing AI options | ||||||
|     { name: "aiTemperature", value: "0.7", isSynced: true }, |     { name: "aiTemperature", value: "0.7", isSynced: true }, | ||||||
|     { name: "aiSystemPrompt", value: "", isSynced: true }, |     { name: "aiSystemPrompt", value: "", isSynced: true }, | ||||||
|     { name: "aiProviderPrecedence", value: "openai,anthropic,ollama", isSynced: true }, |     { name: "aiSelectedProvider", value: "openai", isSynced: true }, | ||||||
|     { name: "embeddingDimensionStrategy", value: "auto", isSynced: true }, |     { name: "embeddingDimensionStrategy", value: "auto", isSynced: true }, | ||||||
|     { name: "embeddingProviderPrecedence", value: "openai,voyage,ollama,local", isSynced: true }, |     { name: "embeddingSelectedProvider", value: "openai", isSynced: true }, | ||||||
|     { name: "embeddingSimilarityThreshold", value: "0.75", isSynced: true }, |     { name: "embeddingSimilarityThreshold", value: "0.75", isSynced: true }, | ||||||
|     { name: "enableAutomaticIndexing", value: "true", isSynced: true }, |     { name: "enableAutomaticIndexing", value: "true", isSynced: true }, | ||||||
|     { name: "maxNotesPerLlmQuery", value: "3", isSynced: true }, |     { name: "maxNotesPerLlmQuery", value: "3", isSynced: true }, | ||||||
|   | |||||||
| @@ -142,15 +142,14 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi | |||||||
|     ollamaDefaultModel: string; |     ollamaDefaultModel: string; | ||||||
|     ollamaEmbeddingModel: string; |     ollamaEmbeddingModel: string; | ||||||
|     codeOpenAiModel: string; |     codeOpenAiModel: string; | ||||||
|     aiProviderPrecedence: string; |     aiSelectedProvider: string; | ||||||
|  |  | ||||||
|     // Embedding-related options |     // Embedding-related options | ||||||
|     embeddingAutoUpdateEnabled: boolean; |     embeddingAutoUpdateEnabled: boolean; | ||||||
|     embeddingUpdateInterval: number; |     embeddingUpdateInterval: number; | ||||||
|     embeddingBatchSize: number; |     embeddingBatchSize: number; | ||||||
|     embeddingDefaultDimension: number; |     embeddingDefaultDimension: number; | ||||||
|     embeddingsDefaultProvider: string; |     embeddingSelectedProvider: string; | ||||||
|     embeddingProviderPrecedence: string; |  | ||||||
|     enableAutomaticIndexing: boolean; |     enableAutomaticIndexing: boolean; | ||||||
|     embeddingGenerationLocation: string; |     embeddingGenerationLocation: string; | ||||||
|     embeddingDimensionStrategy: string; |     embeddingDimensionStrategy: string; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user