From d4abe738752ffec443a32d1eefcc2454f88b8674 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 7 Jul 2025 16:46:35 -0600 Subject: [PATCH] fix: add embedder validation to prevent misleading status indicators (#4398) (#5404) * fix: add embedder validation to prevent misleading status indicators (#4398) * fix: address PR feedback and fix critical issues - Fixed settings-save flow to save before validation - Fixed Error constructor usage in scanner.ts - Fixed segment identification in file-watcher.ts - Added missing translation keys for embedder validation errors * fix: add missing Ollama translation keys - Added missing ollama.title, description, and settings keys - Fixed translation check failure in CI/CD pipeline - Synchronized all 17 non-English locale files * feat: add proactive embedder validation on provider switch - Validate embedder connection when switching providers - Prevent misleading 'Indexed' status when embedder is unavailable - Show immediate error feedback for invalid configurations - Add comprehensive test coverage for validation flow This ensures users get immediate feedback when configuring embedders, preventing confusion when providers like Ollama are not accessible. * fix: improve error handling and validation in code indexing process * refactor: extract common embedder validation and error handling logic - Created shared/validation-helpers.ts with centralized error handling utilities - Refactored OpenAI, OpenAI-Compatible, and Ollama embedders to use shared helpers - Eliminated duplicate error handling code across embedders - Improved maintainability and consistency of error handling - Fixed test compatibility in manager.spec.ts - All 2721 tests passing * refactor: simplify validation helpers by removing unnecessary wrapper functions - Removed getErrorMessageForConnectionError and inlined logic into handleValidationError - Removed isRateLimitError, logRateLimitRetry, and logEmbeddingError wrapper functions - Updated openai.ts and openai-compatible.ts to inline rate limit checking and logging - Reduced code complexity while maintaining all functionality - All 311 tests continue to pass * fix: add missing invalidResponse i18n key and fix French translation - Added missing 'invalidResponse' key to all locale files - Fixed French translation: changed 'and accessible' to 'et accessible' - Ensures proper error messages are displayed when embedder returns invalid responses * fix: restore removed score settings in webviewMessageHandler - Restored codebaseIndexSearchMaxResults and codebaseIndexSearchMinScore settings that were unintentionally removed - Keep embedder validation related changes * fix: revert unintended changes to file-watcher and scanner - Reverted point ID generation back to using line numbers instead of segmentHash - Restored { cause: deleteError } parameter in scanner error handling - These changes were unrelated to the embedder validation feature --------- Co-authored-by: Daniel Riccio --- src/core/webview/webviewMessageHandler.ts | 79 ++++-- src/i18n/locales/ca/embeddings.json | 20 +- src/i18n/locales/de/embeddings.json | 20 +- src/i18n/locales/en/embeddings.json | 20 +- src/i18n/locales/es/embeddings.json | 20 +- src/i18n/locales/fr/embeddings.json | 20 +- src/i18n/locales/hi/embeddings.json | 20 +- src/i18n/locales/id/embeddings.json | 20 +- src/i18n/locales/it/embeddings.json | 20 +- src/i18n/locales/ja/embeddings.json | 20 +- src/i18n/locales/ko/embeddings.json | 20 +- src/i18n/locales/nl/embeddings.json | 20 +- src/i18n/locales/pl/embeddings.json | 20 +- src/i18n/locales/pt-BR/embeddings.json | 20 +- src/i18n/locales/ru/embeddings.json | 20 +- src/i18n/locales/tr/embeddings.json | 20 +- src/i18n/locales/vi/embeddings.json | 20 +- src/i18n/locales/zh-CN/embeddings.json | 20 +- src/i18n/locales/zh-TW/embeddings.json | 20 +- .../code-index/__tests__/manager.spec.ts | 239 +++++++++++++++++- .../__tests__/service-factory.spec.ts | 183 ++++++++++++++ .../embedders/__tests__/gemini.spec.ts | 50 ++++ .../embedders/__tests__/ollama.spec.ts | 238 +++++++++++++++++ .../__tests__/openai-compatible.spec.ts | 129 ++++++++++ .../embedders/__tests__/openai.spec.ts | 62 +++++ src/services/code-index/embedders/gemini.ts | 10 + src/services/code-index/embedders/ollama.ts | 122 +++++++++ .../code-index/embedders/openai-compatible.ts | 103 +++++--- src/services/code-index/embedders/openai.ts | 57 +++-- .../code-index/interfaces/embedder.ts | 7 + src/services/code-index/manager.ts | 52 +++- src/services/code-index/orchestrator.ts | 25 ++ src/services/code-index/service-factory.ts | 17 ++ .../code-index/shared/validation-helpers.ts | 187 ++++++++++++++ 34 files changed, 1801 insertions(+), 119 deletions(-) create mode 100644 src/services/code-index/embedders/__tests__/ollama.spec.ts create mode 100644 src/services/code-index/shared/validation-helpers.ts diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index b8e21e6040..cb1f2fd6b7 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1843,8 +1843,12 @@ export const webviewMessageHandler = async ( const settings = message.codeIndexSettings try { - // Save global state settings atomically (without codebaseIndexEnabled which is now in global settings) + // Check if embedder provider has changed const currentConfig = getGlobalState("codebaseIndexConfig") || {} + const embedderProviderChanged = + currentConfig.codebaseIndexEmbedderProvider !== settings.codebaseIndexEmbedderProvider + + // Save global state settings atomically (without codebaseIndexEnabled which is now in global settings) const globalStateConfig = { ...currentConfig, codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl, @@ -1880,23 +1884,7 @@ export const webviewMessageHandler = async ( ) } - // Verify secrets are actually stored - const storedOpenAiKey = provider.contextProxy.getSecret("codeIndexOpenAiKey") - - // Notify code index manager of changes - if (provider.codeIndexManager) { - await provider.codeIndexManager.handleSettingsChange() - - // Auto-start indexing if now enabled and configured - if (provider.codeIndexManager.isFeatureEnabled && provider.codeIndexManager.isFeatureConfigured) { - if (!provider.codeIndexManager.isInitialized) { - await provider.codeIndexManager.initialize(provider.contextProxy) - } - provider.codeIndexManager.startIndexing() - } - } - - // Send success response + // Send success response first - settings are saved regardless of validation await provider.postMessageToWebview({ type: "codeIndexSettingsSaved", success: true, @@ -1905,6 +1893,61 @@ export const webviewMessageHandler = async ( // Update webview state await provider.postStateToWebview() + + // Then handle validation and initialization + if (provider.codeIndexManager) { + // If embedder provider changed, perform proactive validation + if (embedderProviderChanged) { + try { + // Force handleSettingsChange which will trigger validation + await provider.codeIndexManager.handleSettingsChange() + } catch (error) { + // Validation failed - the error state is already set by handleSettingsChange + provider.log( + `Embedder validation failed after provider change: ${error instanceof Error ? error.message : String(error)}`, + ) + // Send validation error to webview + await provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: provider.codeIndexManager.getCurrentStatus(), + }) + // Exit early - don't try to start indexing with invalid configuration + break + } + } else { + // No provider change, just handle settings normally + try { + await provider.codeIndexManager.handleSettingsChange() + } catch (error) { + // Log but don't fail - settings are saved + provider.log( + `Settings change handling error: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } + + // Wait a bit more to ensure everything is ready + await new Promise((resolve) => setTimeout(resolve, 200)) + + // Auto-start indexing if now enabled and configured + if (provider.codeIndexManager.isFeatureEnabled && provider.codeIndexManager.isFeatureConfigured) { + if (!provider.codeIndexManager.isInitialized) { + try { + await provider.codeIndexManager.initialize(provider.contextProxy) + provider.log(`Code index manager initialized after settings save`) + } catch (error) { + provider.log( + `Code index initialization failed: ${error instanceof Error ? error.message : String(error)}`, + ) + // Send error status to webview + await provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: provider.codeIndexManager.getCurrentStatus(), + }) + } + } + } + } } catch (error) { provider.log(`Error saving code index settings: ${error.message || error}`) await provider.postMessageToWebview({ diff --git a/src/i18n/locales/ca/embeddings.json b/src/i18n/locales/ca/embeddings.json index 3302ff7acd..35be4089d4 100644 --- a/src/i18n/locales/ca/embeddings.json +++ b/src/i18n/locales/ca/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "No s'ha pogut llegir el cos de l'error", "requestFailed": "La sol·licitud de l'API d'Ollama ha fallat amb l'estat {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Estructura de resposta no vàlida de l'API d'Ollama: no s'ha trobat la matriu \"embeddings\" o no és una matriu.", - "embeddingFailed": "La incrustació d'Ollama ha fallat: {{message}}" + "embeddingFailed": "La incrustació d'Ollama ha fallat: {{message}}", + "serviceNotRunning": "El servei d'Ollama no s'està executant a {{baseUrl}}", + "serviceUnavailable": "El servei d'Ollama no està disponible (estat: {{status}})", + "modelNotFound": "No s'ha trobat el model d'Ollama: {{modelId}}", + "modelNotEmbeddingCapable": "El model d'Ollama no és capaç de fer incrustacions: {{modelId}}", + "hostNotFound": "No s'ha trobat l'amfitrió d'Ollama: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "Error desconegut en processar el fitxer {{filePath}}", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "No s'ha pogut connectar a la base de dades vectorial Qdrant. Assegura't que Qdrant estigui funcionant i sigui accessible a {{qdrantUrl}}. Error: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Ha fallat l'autenticació. Comproveu la vostra clau d'API a la configuració.", + "connectionFailed": "No s'ha pogut connectar al servei d'incrustació. Comproveu la vostra configuració de connexió i assegureu-vos que el servei estigui funcionant.", + "modelNotAvailable": "El model especificat no està disponible. Comproveu la vostra configuració de model.", + "configurationError": "Configuració d'incrustació no vàlida. Reviseu la vostra configuració.", + "serviceUnavailable": "El servei d'incrustació no està disponible. Assegureu-vos que estigui funcionant i sigui accessible.", + "invalidEndpoint": "Punt final d'API no vàlid. Comproveu la vostra configuració d'URL.", + "invalidEmbedderConfig": "Configuració d'incrustació no vàlida. Comproveu la vostra configuració.", + "invalidApiKey": "Clau d'API no vàlida. Comproveu la vostra configuració de clau d'API.", + "invalidBaseUrl": "URL base no vàlida. Comproveu la vostra configuració d'URL.", + "invalidModel": "Model no vàlid. Comproveu la vostra configuració de model.", + "invalidResponse": "Resposta no vàlida del servei d'incrustació. Comproveu la vostra configuració." } } diff --git a/src/i18n/locales/de/embeddings.json b/src/i18n/locales/de/embeddings.json index 300899fd1b..7d96ddb511 100644 --- a/src/i18n/locales/de/embeddings.json +++ b/src/i18n/locales/de/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "Fehlerinhalt konnte nicht gelesen werden", "requestFailed": "Ollama API-Anfrage fehlgeschlagen mit Status {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Ungültige Antwortstruktur von Ollama API: \"embeddings\" Array nicht gefunden oder kein Array.", - "embeddingFailed": "Ollama Einbettung fehlgeschlagen: {{message}}" + "embeddingFailed": "Ollama Einbettung fehlgeschlagen: {{message}}", + "serviceNotRunning": "Ollama-Dienst wird unter {{baseUrl}} nicht ausgeführt", + "serviceUnavailable": "Ollama-Dienst ist nicht verfügbar (Status: {{status}})", + "modelNotFound": "Ollama-Modell nicht gefunden: {{modelId}}", + "modelNotEmbeddingCapable": "Ollama-Modell ist nicht für Einbettungen geeignet: {{modelId}}", + "hostNotFound": "Ollama-Host nicht gefunden: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "Unbekannter Fehler beim Verarbeiten der Datei {{filePath}}", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Verbindung zur Qdrant-Vektordatenbank fehlgeschlagen. Stelle sicher, dass Qdrant läuft und unter {{qdrantUrl}} erreichbar ist. Fehler: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Authentifizierung fehlgeschlagen. Bitte überprüfe deinen API-Schlüssel in den Einstellungen.", + "connectionFailed": "Verbindung zum Embedder-Dienst fehlgeschlagen. Bitte überprüfe deine Verbindungseinstellungen und stelle sicher, dass der Dienst läuft.", + "modelNotAvailable": "Das angegebene Modell ist nicht verfügbar. Bitte überprüfe deine Modellkonfiguration.", + "configurationError": "Ungültige Embedder-Konfiguration. Bitte überprüfe deine Einstellungen.", + "serviceUnavailable": "Der Embedder-Dienst ist nicht verfügbar. Bitte stelle sicher, dass er läuft und erreichbar ist.", + "invalidEndpoint": "Ungültiger API-Endpunkt. Bitte überprüfe deine URL-Konfiguration.", + "invalidEmbedderConfig": "Ungültige Embedder-Konfiguration. Bitte überprüfe deine Einstellungen.", + "invalidApiKey": "Ungültiger API-Schlüssel. Bitte überprüfe deine API-Schlüssel-Konfiguration.", + "invalidBaseUrl": "Ungültige Basis-URL. Bitte überprüfe deine URL-Konfiguration.", + "invalidModel": "Ungültiges Modell. Bitte überprüfe deine Modellkonfiguration.", + "invalidResponse": "Ungültige Antwort vom Embedder-Dienst. Bitte überprüfe deine Konfiguration." } } diff --git a/src/i18n/locales/en/embeddings.json b/src/i18n/locales/en/embeddings.json index e57f3de0e8..012b2323cf 100644 --- a/src/i18n/locales/en/embeddings.json +++ b/src/i18n/locales/en/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "Could not read error body", "requestFailed": "Ollama API request failed with status {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Invalid response structure from Ollama API: \"embeddings\" array not found or not an array.", - "embeddingFailed": "Ollama embedding failed: {{message}}" + "embeddingFailed": "Ollama embedding failed: {{message}}", + "serviceNotRunning": "Ollama service is not running at {{baseUrl}}", + "serviceUnavailable": "Ollama service is unavailable (status: {{status}})", + "modelNotFound": "Ollama model not found: {{modelId}}", + "modelNotEmbeddingCapable": "Ollama model is not embedding capable: {{modelId}}", + "hostNotFound": "Ollama host not found: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "Unknown error processing file {{filePath}}", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Failed to connect to Qdrant vector database. Please ensure Qdrant is running and accessible at {{qdrantUrl}}. Error: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Authentication failed. Please check your API key in the settings.", + "connectionFailed": "Failed to connect to the embedder service. Please check your connection settings and ensure the service is running.", + "modelNotAvailable": "The specified model is not available. Please check your model configuration.", + "configurationError": "Invalid embedder configuration. Please review your settings.", + "serviceUnavailable": "The embedder service is not available. Please ensure it is running and accessible.", + "invalidEndpoint": "Invalid API endpoint. Please check your URL configuration.", + "invalidEmbedderConfig": "Invalid embedder configuration. Please check your settings.", + "invalidApiKey": "Invalid API key. Please check your API key configuration.", + "invalidBaseUrl": "Invalid base URL. Please check your URL configuration.", + "invalidModel": "Invalid model. Please check your model configuration.", + "invalidResponse": "Invalid response from embedder service. Please check your configuration." } } diff --git a/src/i18n/locales/es/embeddings.json b/src/i18n/locales/es/embeddings.json index c2d7795362..5fa46f9c45 100644 --- a/src/i18n/locales/es/embeddings.json +++ b/src/i18n/locales/es/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "No se pudo leer el cuerpo del error", "requestFailed": "La solicitud de la API de Ollama falló con estado {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Estructura de respuesta inválida de la API de Ollama: array \"embeddings\" no encontrado o no es un array.", - "embeddingFailed": "Incrustación de Ollama falló: {{message}}" + "embeddingFailed": "Incrustación de Ollama falló: {{message}}", + "serviceNotRunning": "El servicio Ollama no se está ejecutando en {{baseUrl}}", + "serviceUnavailable": "El servicio Ollama no está disponible (estado: {{status}})", + "modelNotFound": "No se encuentra el modelo Ollama: {{modelId}}", + "modelNotEmbeddingCapable": "El modelo Ollama no es capaz de realizar incrustaciones: {{modelId}}", + "hostNotFound": "No se encuentra el host de Ollama: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "Error desconocido procesando archivo {{filePath}}", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Error al conectar con la base de datos vectorial Qdrant. Asegúrate de que Qdrant esté funcionando y sea accesible en {{qdrantUrl}}. Error: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Error de autenticación. Comprueba tu clave de API en los ajustes.", + "connectionFailed": "Error al conectar con el servicio de embedder. Comprueba los ajustes de conexión y asegúrate de que el servicio esté funcionando.", + "modelNotAvailable": "El modelo especificado no está disponible. Comprueba la configuración de tu modelo.", + "configurationError": "Configuración de embedder no válida. Revisa tus ajustes.", + "serviceUnavailable": "El servicio de embedder no está disponible. Asegúrate de que esté funcionando y sea accesible.", + "invalidEndpoint": "Punto de conexión de API no válido. Comprueba la configuración de tu URL.", + "invalidEmbedderConfig": "Configuración de embedder no válida. Comprueba tus ajustes.", + "invalidApiKey": "Clave de API no válida. Comprueba la configuración de tu clave de API.", + "invalidBaseUrl": "URL base no válida. Comprueba la configuración de tu URL.", + "invalidModel": "Modelo no válido. Comprueba la configuración de tu modelo.", + "invalidResponse": "Respuesta no válida del servicio de embedder. Comprueba tu configuración." } } diff --git a/src/i18n/locales/fr/embeddings.json b/src/i18n/locales/fr/embeddings.json index 4dbbe6218b..b6ef0d8786 100644 --- a/src/i18n/locales/fr/embeddings.json +++ b/src/i18n/locales/fr/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "Impossible de lire le corps de l'erreur", "requestFailed": "Échec de la requête API Ollama avec le statut {{status}} {{statusText}} : {{errorBody}}", "invalidResponseStructure": "Structure de réponse invalide de l'API Ollama : tableau \"embeddings\" non trouvé ou n'est pas un tableau.", - "embeddingFailed": "Échec de l'embedding Ollama : {{message}}" + "embeddingFailed": "Échec de l'embedding Ollama : {{message}}", + "serviceNotRunning": "Le service Ollama n'est pas en cours d'exécution sur {{baseUrl}}", + "serviceUnavailable": "Le service Ollama est indisponible (statut : {{status}})", + "modelNotFound": "Modèle Ollama introuvable : {{modelId}}", + "modelNotEmbeddingCapable": "Le modèle Ollama n'est pas capable d'intégrer : {{modelId}}", + "hostNotFound": "Hôte Ollama introuvable : {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "Erreur inconnue lors du traitement du fichier {{filePath}}", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Échec de la connexion à la base de données vectorielle Qdrant. Veuillez vous assurer que Qdrant fonctionne et est accessible à {{qdrantUrl}}. Erreur : {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Échec de l'authentification. Veuillez vérifier votre clé API dans les paramètres.", + "connectionFailed": "Échec de la connexion au service d'embedding. Veuillez vérifier vos paramètres de connexion et vous assurer que le service est en cours d'exécution.", + "modelNotAvailable": "Le modèle spécifié n'est pas disponible. Veuillez vérifier la configuration de votre modèle.", + "configurationError": "Configuration de l'embedder invalide. Veuillez vérifier vos paramètres.", + "serviceUnavailable": "Le service d'embedding n'est pas disponible. Veuillez vous assurer qu'il est en cours d'exécution et accessible.", + "invalidEndpoint": "Point de terminaison d'API invalide. Veuillez vérifier votre configuration d'URL.", + "invalidEmbedderConfig": "Configuration de l'embedder invalide. Veuillez vérifier vos paramètres.", + "invalidApiKey": "Clé API invalide. Veuillez vérifier votre configuration de clé API.", + "invalidBaseUrl": "URL de base invalide. Veuillez vérifier votre configuration d'URL.", + "invalidModel": "Modèle invalide. Veuillez vérifier votre configuration de modèle.", + "invalidResponse": "Réponse invalide du service d'embedder. Veuillez vérifier votre configuration." } } diff --git a/src/i18n/locales/hi/embeddings.json b/src/i18n/locales/hi/embeddings.json index 312d42e69c..5ec34e5624 100644 --- a/src/i18n/locales/hi/embeddings.json +++ b/src/i18n/locales/hi/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "त्रुटि सामग्री पढ़ नहीं सका", "requestFailed": "Ollama API अनुरोध स्थिति {{status}} {{statusText}} के साथ विफल: {{errorBody}}", "invalidResponseStructure": "Ollama API से अमान्य प्रतिक्रिया संरचना: \"embeddings\" सरणी नहीं मिली या सरणी नहीं है।", - "embeddingFailed": "Ollama एम्बेडिंग विफल: {{message}}" + "embeddingFailed": "Ollama एम्बेडिंग विफल: {{message}}", + "serviceNotRunning": "ओलामा सेवा {{baseUrl}} पर नहीं चल रही है", + "serviceUnavailable": "ओलामा सेवा अनुपलब्ध है (स्थिति: {{status}})", + "modelNotFound": "ओलामा मॉडल नहीं मिला: {{modelId}}", + "modelNotEmbeddingCapable": "ओलामा मॉडल एम्बेडिंग में सक्षम नहीं है: {{modelId}}", + "hostNotFound": "ओलामा होस्ट नहीं मिला: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "फ़ाइल {{filePath}} प्रसंस्करण में अज्ञात त्रुटि", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Qdrant वेक्टर डेटाबेस से कनेक्ट करने में विफल। कृपया सुनिश्चित करें कि Qdrant चल रहा है और {{qdrantUrl}} पर पहुंच योग्य है। त्रुटि: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "प्रमाणीकरण विफल। कृपया सेटिंग्स में अपनी एपीआई कुंजी जांचें।", + "connectionFailed": "एम्बेडर सेवा से कनेक्ट करने में विफल। कृपया अपनी कनेक्शन सेटिंग्स जांचें और सुनिश्चित करें कि सेवा चल रही है।", + "modelNotAvailable": "निर्दिष्ट मॉडल उपलब्ध नहीं है। कृपया अपनी मॉडल कॉन्फ़िगरेशन जांचें।", + "configurationError": "अमान्य एम्बेडर कॉन्फ़िगरेशन। कृपया अपनी सेटिंग्स की समीक्षा करें।", + "serviceUnavailable": "एम्बेडर सेवा उपलब्ध नहीं है। कृपया सुनिश्चित करें कि यह चल रहा है और पहुंच योग्य है।", + "invalidEndpoint": "अमान्य एपीआई एंडपॉइंट। कृपया अपनी यूआरएल कॉन्फ़िगरेशन जांचें।", + "invalidEmbedderConfig": "अमान्य एम्बेडर कॉन्फ़िगरेशन। कृपया अपनी सेटिंग्स जांचें।", + "invalidApiKey": "अमान्य एपीआई कुंजी। कृपया अपनी एपीआई कुंजी कॉन्फ़िगरेशन जांचें।", + "invalidBaseUrl": "अमान्य बेस यूआरएल। कृपया अपनी यूआरएल कॉन्फ़िगरेशन जांचें।", + "invalidModel": "अमान्य मॉडल। कृपया अपनी मॉडल कॉन्फ़िगरेशन जांचें।", + "invalidResponse": "एम्बेडर सेवा से अमान्य प्रतिक्रिया। कृपया अपनी कॉन्फ़िगरेशन जांचें।" } } diff --git a/src/i18n/locales/id/embeddings.json b/src/i18n/locales/id/embeddings.json index abfa9cb354..0082ec8dcf 100644 --- a/src/i18n/locales/id/embeddings.json +++ b/src/i18n/locales/id/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "Tidak dapat membaca body error", "requestFailed": "Permintaan API Ollama gagal dengan status {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Struktur respons tidak valid dari API Ollama: array \"embeddings\" tidak ditemukan atau bukan array.", - "embeddingFailed": "Embedding Ollama gagal: {{message}}" + "embeddingFailed": "Embedding Ollama gagal: {{message}}", + "serviceNotRunning": "Layanan Ollama tidak berjalan di {{baseUrl}}", + "serviceUnavailable": "Layanan Ollama tidak tersedia (status: {{status}})", + "modelNotFound": "Model Ollama tidak ditemukan: {{modelId}}", + "modelNotEmbeddingCapable": "Model Ollama tidak mampu melakukan embedding: {{modelId}}", + "hostNotFound": "Host Ollama tidak ditemukan: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "Error tidak dikenal saat memproses file {{filePath}}", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Gagal terhubung ke database vektor Qdrant. Pastikan Qdrant berjalan dan dapat diakses di {{qdrantUrl}}. Error: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Autentikasi gagal. Silakan periksa kunci API Anda di pengaturan.", + "connectionFailed": "Gagal terhubung ke layanan embedder. Silakan periksa pengaturan koneksi Anda dan pastikan layanan berjalan.", + "modelNotAvailable": "Model yang ditentukan tidak tersedia. Silakan periksa konfigurasi model Anda.", + "configurationError": "Konfigurasi embedder tidak valid. Harap tinjau pengaturan Anda.", + "serviceUnavailable": "Layanan embedder tidak tersedia. Pastikan layanan tersebut berjalan dan dapat diakses.", + "invalidEndpoint": "Endpoint API tidak valid. Silakan periksa konfigurasi URL Anda.", + "invalidEmbedderConfig": "Konfigurasi embedder tidak valid. Silakan periksa pengaturan Anda.", + "invalidApiKey": "Kunci API tidak valid. Silakan periksa konfigurasi kunci API Anda.", + "invalidBaseUrl": "URL dasar tidak valid. Silakan periksa konfigurasi URL Anda.", + "invalidModel": "Model tidak valid. Silakan periksa konfigurasi model Anda.", + "invalidResponse": "Respons tidak valid dari layanan embedder. Silakan periksa konfigurasi Anda." } } diff --git a/src/i18n/locales/it/embeddings.json b/src/i18n/locales/it/embeddings.json index 5bd7164886..19e6af332f 100644 --- a/src/i18n/locales/it/embeddings.json +++ b/src/i18n/locales/it/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "Impossibile leggere il corpo dell'errore", "requestFailed": "Richiesta API Ollama fallita con stato {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Struttura di risposta non valida dall'API Ollama: array \"embeddings\" non trovato o non è un array.", - "embeddingFailed": "Embedding Ollama fallito: {{message}}" + "embeddingFailed": "Embedding Ollama fallito: {{message}}", + "serviceNotRunning": "Il servizio Ollama non è in esecuzione su {{baseUrl}}", + "serviceUnavailable": "Il servizio Ollama non è disponibile (stato: {{status}})", + "modelNotFound": "Modello Ollama non trovato: {{modelId}}", + "modelNotEmbeddingCapable": "Il modello Ollama non è in grado di eseguire l'embedding: {{modelId}}", + "hostNotFound": "Host Ollama non trovato: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "Errore sconosciuto nell'elaborazione del file {{filePath}}", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Impossibile connettersi al database vettoriale Qdrant. Assicurati che Qdrant sia in esecuzione e accessibile su {{qdrantUrl}}. Errore: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Autenticazione fallita. Controlla la tua chiave API nelle impostazioni.", + "connectionFailed": "Connessione al servizio di embedder fallita. Controlla le impostazioni di connessione e assicurati che il servizio sia in esecuzione.", + "modelNotAvailable": "Il modello specificato non è disponibile. Controlla la configurazione del tuo modello.", + "configurationError": "Configurazione dell'embedder non valida. Rivedi le tue impostazioni.", + "serviceUnavailable": "Il servizio di embedder non è disponibile. Assicurati che sia in esecuzione e accessibile.", + "invalidEndpoint": "Endpoint API non valido. Controlla la configurazione del tuo URL.", + "invalidEmbedderConfig": "Configurazione dell'embedder non valida. Controlla le tue impostazioni.", + "invalidApiKey": "Chiave API non valida. Controlla la configurazione della tua chiave API.", + "invalidBaseUrl": "URL di base non valido. Controlla la configurazione del tuo URL.", + "invalidModel": "Modello non valido. Controlla la configurazione del tuo modello.", + "invalidResponse": "Risposta non valida dal servizio embedder. Controlla la tua configurazione." } } diff --git a/src/i18n/locales/ja/embeddings.json b/src/i18n/locales/ja/embeddings.json index 862270a364..fcf426a14c 100644 --- a/src/i18n/locales/ja/embeddings.json +++ b/src/i18n/locales/ja/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "エラー本文を読み取れませんでした", "requestFailed": "Ollama APIリクエストが失敗しました。ステータス {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Ollama APIからの無効な応答構造:\"embeddings\"配列が見つからないか、配列ではありません。", - "embeddingFailed": "Ollama埋め込みが失敗しました:{{message}}" + "embeddingFailed": "Ollama埋め込みが失敗しました:{{message}}", + "serviceNotRunning": "Ollamaサービスは{{baseUrl}}で実行されていません", + "serviceUnavailable": "Ollamaサービスは利用できません(ステータス:{{status}})", + "modelNotFound": "Ollamaモデルが見つかりません:{{modelId}}", + "modelNotEmbeddingCapable": "Ollamaモデルは埋め込みに対応していません:{{modelId}}", + "hostNotFound": "Ollamaホストが見つかりません:{{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "ファイル{{filePath}}の処理中に不明なエラーが発生しました", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Qdrantベクターデータベースへの接続に失敗しました。Qdrantが実行中で{{qdrantUrl}}でアクセス可能であることを確認してください。エラー:{{errorMessage}}" + }, + "validation": { + "authenticationFailed": "認証に失敗しました。設定でAPIキーを確認してください。", + "connectionFailed": "エンベッダーサービスへの接続に失敗しました。接続設定を確認し、サービスが実行されていることを確認してください。", + "modelNotAvailable": "指定されたモデルは利用できません。モデル構成を確認してください。", + "configurationError": "無効なエンベッダー構成です。設定を確認してください。", + "serviceUnavailable": "エンベッダーサービスは利用できません。実行中でアクセス可能であることを確認してください。", + "invalidEndpoint": "無効なAPIエンドポイントです。URL構成を確認してください。", + "invalidEmbedderConfig": "無効なエンベッダー構成です。設定を確認してください。", + "invalidApiKey": "無効なAPIキーです。APIキー構成を確認してください。", + "invalidBaseUrl": "無効なベースURLです。URL構成を確認してください。", + "invalidModel": "無効なモデルです。モデル構成を確認してください。", + "invalidResponse": "エンベッダーサービスからの無効な応答です。設定を確認してください。" } } diff --git a/src/i18n/locales/ko/embeddings.json b/src/i18n/locales/ko/embeddings.json index 37877bfa97..16d119c959 100644 --- a/src/i18n/locales/ko/embeddings.json +++ b/src/i18n/locales/ko/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "오류 본문을 읽을 수 없습니다", "requestFailed": "Ollama API 요청이 실패했습니다. 상태 {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Ollama API에서 잘못된 응답 구조: \"embeddings\" 배열을 찾을 수 없거나 배열이 아닙니다.", - "embeddingFailed": "Ollama 임베딩 실패: {{message}}" + "embeddingFailed": "Ollama 임베딩 실패: {{message}}", + "serviceNotRunning": "Ollama 서비스가 {{baseUrl}}에서 실행되고 있지 않습니다", + "serviceUnavailable": "Ollama 서비스를 사용할 수 없습니다 (상태: {{status}})", + "modelNotFound": "Ollama 모델을 찾을 수 없습니다: {{modelId}}", + "modelNotEmbeddingCapable": "Ollama 모델은 임베딩이 불가능합니다: {{modelId}}", + "hostNotFound": "Ollama 호스트를 찾을 수 없습니다: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "파일 {{filePath}} 처리 중 알 수 없는 오류", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Qdrant 벡터 데이터베이스에 연결하지 못했습니다. Qdrant가 실행 중이고 {{qdrantUrl}}에서 접근 가능한지 확인하세요. 오류: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "인증에 실패했습니다. 설정에서 API 키를 확인하세요.", + "connectionFailed": "임베더 서비스에 연결하지 못했습니다. 연결 설정을 확인하고 서비스가 실행 중인지 확인하세요.", + "modelNotAvailable": "지정된 모델을 사용할 수 없습니다. 모델 구성을 확인하세요.", + "configurationError": "잘못된 임베더 구성입니다. 설정을 검토하세요.", + "serviceUnavailable": "임베더 서비스를 사용할 수 없습니다. 실행 중이고 액세스 가능한지 확인하세요.", + "invalidEndpoint": "잘못된 API 엔드포인트입니다. URL 구성을 확인하세요.", + "invalidEmbedderConfig": "잘못된 임베더 구성입니다. 설정을 확인하세요.", + "invalidApiKey": "잘못된 API 키입니다. API 키 구성을 확인하세요.", + "invalidBaseUrl": "잘못된 기본 URL입니다. URL 구성을 확인하세요.", + "invalidModel": "잘못된 모델입니다. 모델 구성을 확인하세요.", + "invalidResponse": "임베더 서비스에서 잘못된 응답이 왔습니다. 구성을 확인하세요." } } diff --git a/src/i18n/locales/nl/embeddings.json b/src/i18n/locales/nl/embeddings.json index 7256b0973b..9eeb5a04ea 100644 --- a/src/i18n/locales/nl/embeddings.json +++ b/src/i18n/locales/nl/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "Kon foutinhoud niet lezen", "requestFailed": "Ollama API-verzoek mislukt met status {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Ongeldige responsstructuur van Ollama API: \"embeddings\" array niet gevonden of is geen array.", - "embeddingFailed": "Ollama insluiting mislukt: {{message}}" + "embeddingFailed": "Ollama insluiting mislukt: {{message}}", + "serviceNotRunning": "Ollama-service draait niet op {{baseUrl}}", + "serviceUnavailable": "Ollama-service is niet beschikbaar (status: {{status}})", + "modelNotFound": "Ollama-model niet gevonden: {{modelId}}", + "modelNotEmbeddingCapable": "Ollama-model is niet in staat tot insluiten: {{modelId}}", + "hostNotFound": "Ollama-host niet gevonden: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "Onbekende fout bij verwerken van bestand {{filePath}}", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Kan geen verbinding maken met Qdrant vectordatabase. Zorg ervoor dat Qdrant draait en toegankelijk is op {{qdrantUrl}}. Fout: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Authenticatie mislukt. Controleer je API-sleutel in de instellingen.", + "connectionFailed": "Verbinding met de embedder-service mislukt. Controleer je verbindingsinstellingen en zorg ervoor dat de service draait.", + "modelNotAvailable": "Het opgegeven model is niet beschikbaar. Controleer je modelconfiguratie.", + "configurationError": "Ongeldige embedder-configuratie. Controleer je instellingen.", + "serviceUnavailable": "De embedder-service is niet beschikbaar. Zorg ervoor dat deze draait en toegankelijk is.", + "invalidEndpoint": "Ongeldig API-eindpunt. Controleer je URL-configuratie.", + "invalidEmbedderConfig": "Ongeldige embedder-configuratie. Controleer je instellingen.", + "invalidApiKey": "Ongeldige API-sleutel. Controleer je API-sleutelconfiguratie.", + "invalidBaseUrl": "Ongeldige basis-URL. Controleer je URL-configuratie.", + "invalidModel": "Ongeldig model. Controleer je modelconfiguratie.", + "invalidResponse": "Ongeldige reactie van embedder-service. Controleer je configuratie." } } diff --git a/src/i18n/locales/pl/embeddings.json b/src/i18n/locales/pl/embeddings.json index c3e160869b..dd10c1ec4c 100644 --- a/src/i18n/locales/pl/embeddings.json +++ b/src/i18n/locales/pl/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "Nie można odczytać treści błędu", "requestFailed": "Żądanie API Ollama nie powiodło się ze statusem {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Nieprawidłowa struktura odpowiedzi z API Ollama: tablica \"embeddings\" nie została znaleziona lub nie jest tablicą.", - "embeddingFailed": "Osadzenie Ollama nie powiodło się: {{message}}" + "embeddingFailed": "Osadzenie Ollama nie powiodło się: {{message}}", + "serviceNotRunning": "Usługa Ollama nie jest uruchomiona pod adresem {{baseUrl}}", + "serviceUnavailable": "Usługa Ollama jest niedostępna (status: {{status}})", + "modelNotFound": "Nie znaleziono modelu Ollama: {{modelId}}", + "modelNotEmbeddingCapable": "Model Ollama nie jest zdolny do osadzania: {{modelId}}", + "hostNotFound": "Nie znaleziono hosta Ollama: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "Nieznany błąd podczas przetwarzania pliku {{filePath}}", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Nie udało się połączyć z bazą danych wektorowych Qdrant. Upewnij się, że Qdrant jest uruchomiony i dostępny pod adresem {{qdrantUrl}}. Błąd: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Uwierzytelnianie nie powiodło się. Sprawdź swój klucz API w ustawieniach.", + "connectionFailed": "Nie udało się połączyć z usługą embeddera. Sprawdź ustawienia połączenia i upewnij się, że usługa jest uruchomiona.", + "modelNotAvailable": "Określony model jest niedostępny. Sprawdź konfigurację modelu.", + "configurationError": "Nieprawidłowa konfiguracja embeddera. Sprawdź swoje ustawienia.", + "serviceUnavailable": "Usługa embeddera jest niedostępna. Upewnij się, że jest uruchomiona i dostępna.", + "invalidEndpoint": "Nieprawidłowy punkt końcowy API. Sprawdź konfigurację adresu URL.", + "invalidEmbedderConfig": "Nieprawidłowa konfiguracja embeddera. Sprawdź swoje ustawienia.", + "invalidApiKey": "Nieprawidłowy klucz API. Sprawdź konfigurację klucza API.", + "invalidBaseUrl": "Nieprawidłowy podstawowy adres URL. Sprawdź konfigurację adresu URL.", + "invalidModel": "Nieprawidłowy model. Sprawdź konfigurację modelu.", + "invalidResponse": "Nieprawidłowa odpowiedź z usługi embedder. Sprawdź swoją konfigurację." } } diff --git a/src/i18n/locales/pt-BR/embeddings.json b/src/i18n/locales/pt-BR/embeddings.json index 6b97475265..ec1db07113 100644 --- a/src/i18n/locales/pt-BR/embeddings.json +++ b/src/i18n/locales/pt-BR/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "Não foi possível ler o corpo do erro", "requestFailed": "Solicitação da API Ollama falhou com status {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Estrutura de resposta inválida da API Ollama: array \"embeddings\" não encontrado ou não é um array.", - "embeddingFailed": "Embedding Ollama falhou: {{message}}" + "embeddingFailed": "Embedding Ollama falhou: {{message}}", + "serviceNotRunning": "O serviço Ollama não está em execução em {{baseUrl}}", + "serviceUnavailable": "O serviço Ollama não está disponível (status: {{status}})", + "modelNotFound": "Modelo Ollama não encontrado: {{modelId}}", + "modelNotEmbeddingCapable": "O modelo Ollama não é capaz de embedding: {{modelId}}", + "hostNotFound": "Host Ollama não encontrado: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "Erro desconhecido ao processar arquivo {{filePath}}", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Falha ao conectar com o banco de dados vetorial Qdrant. Certifique-se de que o Qdrant esteja rodando e acessível em {{qdrantUrl}}. Erro: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Falha na autenticação. Verifique sua chave de API nas configurações.", + "connectionFailed": "Falha ao conectar ao serviço do embedder. Verifique suas configurações de conexão e garanta que o serviço esteja em execução.", + "modelNotAvailable": "O modelo especificado não está disponível. Verifique a configuração do seu modelo.", + "configurationError": "Configuração do embedder inválida. Revise suas configurações.", + "serviceUnavailable": "O serviço do embedder não está disponível. Garanta que ele esteja em execução e acessível.", + "invalidEndpoint": "Endpoint de API inválido. Verifique sua configuração de URL.", + "invalidEmbedderConfig": "Configuração do embedder inválida. Verifique suas configurações.", + "invalidApiKey": "Chave de API inválida. Verifique sua configuração de chave de API.", + "invalidBaseUrl": "URL base inválida. Verifique sua configuração de URL.", + "invalidModel": "Modelo inválido. Verifique a configuração do seu modelo.", + "invalidResponse": "Resposta inválida do serviço de embedder. Verifique sua configuração." } } diff --git a/src/i18n/locales/ru/embeddings.json b/src/i18n/locales/ru/embeddings.json index c6143816e8..931dca5bbd 100644 --- a/src/i18n/locales/ru/embeddings.json +++ b/src/i18n/locales/ru/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "Не удалось прочитать тело ошибки", "requestFailed": "Запрос к API Ollama не удался со статусом {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Неверная структура ответа от API Ollama: массив \"embeddings\" не найден или не является массивом.", - "embeddingFailed": "Вложение Ollama не удалось: {{message}}" + "embeddingFailed": "Вложение Ollama не удалось: {{message}}", + "serviceNotRunning": "Сервис Ollama не запущен по адресу {{baseUrl}}", + "serviceUnavailable": "Сервис Ollama недоступен (статус: {{status}})", + "modelNotFound": "Модель Ollama не найдена: {{modelId}}", + "modelNotEmbeddingCapable": "Модель Ollama не способна к вложению: {{modelId}}", + "hostNotFound": "Хост Ollama не найден: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "Неизвестная ошибка при обработке файла {{filePath}}", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Не удалось подключиться к векторной базе данных Qdrant. Убедитесь, что Qdrant запущен и доступен по адресу {{qdrantUrl}}. Ошибка: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Ошибка аутентификации. Проверьте свой ключ API в настройках.", + "connectionFailed": "Не удалось подключиться к службе эмбеддера. Проверьте настройки подключения и убедитесь, что служба запущена.", + "modelNotAvailable": "Указанная модель недоступна. Проверьте конфигурацию модели.", + "configurationError": "Неверная конфигурация эмбеддера. Проверьте свои настройки.", + "serviceUnavailable": "Служба эмбеддера недоступна. Убедитесь, что она запущена и доступна.", + "invalidEndpoint": "Неверная конечная точка API. Проверьте конфигурацию URL.", + "invalidEmbedderConfig": "Неверная конфигурация эмбеддера. Проверьте свои настройки.", + "invalidApiKey": "Неверный ключ API. Проверьте конфигурацию ключа API.", + "invalidBaseUrl": "Неверный базовый URL. Проверьте конфигурацию URL.", + "invalidModel": "Неверная модель. Проверьте конфигурацию модели.", + "invalidResponse": "Неверный ответ от службы embedder. Проверьте вашу конфигурацию." } } diff --git a/src/i18n/locales/tr/embeddings.json b/src/i18n/locales/tr/embeddings.json index 10ad965f0f..8ff94b0dae 100644 --- a/src/i18n/locales/tr/embeddings.json +++ b/src/i18n/locales/tr/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "Hata gövdesi okunamadı", "requestFailed": "Ollama API isteği {{status}} {{statusText}} durumuyla başarısız oldu: {{errorBody}}", "invalidResponseStructure": "Ollama API'den geçersiz yanıt yapısı: \"embeddings\" dizisi bulunamadı veya dizi değil.", - "embeddingFailed": "Ollama gömülmesi başarısız oldu: {{message}}" + "embeddingFailed": "Ollama gömülmesi başarısız oldu: {{message}}", + "serviceNotRunning": "Ollama hizmeti {{baseUrl}} adresinde çalışmıyor", + "serviceUnavailable": "Ollama hizmeti kullanılamıyor (durum: {{status}})", + "modelNotFound": "Ollama modeli bulunamadı: {{modelId}}", + "modelNotEmbeddingCapable": "Ollama modeli gömme yeteneğine sahip değil: {{modelId}}", + "hostNotFound": "Ollama ana bilgisayarı bulunamadı: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "{{filePath}} dosyası işlenirken bilinmeyen hata", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Qdrant vektör veritabanına bağlanılamadı. Qdrant'ın çalıştığından ve {{qdrantUrl}} adresinde erişilebilir olduğundan emin olun. Hata: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Kimlik doğrulama başarısız oldu. Lütfen ayarlardan API anahtarınızı kontrol edin.", + "connectionFailed": "Gömücü hizmetine bağlanılamadı. Lütfen bağlantı ayarlarınızı kontrol edin ve hizmetin çalıştığından emin olun.", + "modelNotAvailable": "Belirtilen model mevcut değil. Lütfen model yapılandırmanızı kontrol edin.", + "configurationError": "Geçersiz gömücü yapılandırması. Lütfen ayarlarınızı gözden geçirin.", + "serviceUnavailable": "Gömücü hizmeti mevcut değil. Lütfen çalıştığından ve erişilebilir olduğundan emin olun.", + "invalidEndpoint": "Geçersiz API uç noktası. Lütfen URL yapılandırmanızı kontrol edin.", + "invalidEmbedderConfig": "Geçersiz gömücü yapılandırması. Lütfen ayarlarınızı kontrol edin.", + "invalidApiKey": "Geçersiz API anahtarı. Lütfen API anahtarı yapılandırmanızı kontrol edin.", + "invalidBaseUrl": "Geçersiz temel URL. Lütfen URL yapılandırmanızı kontrol edin.", + "invalidModel": "Geçersiz model. Lütfen model yapılandırmanızı kontrol edin.", + "invalidResponse": "Embedder hizmetinden geçersiz yanıt. Lütfen yapılandırmanızı kontrol edin." } } diff --git a/src/i18n/locales/vi/embeddings.json b/src/i18n/locales/vi/embeddings.json index a533aaac07..5988219aed 100644 --- a/src/i18n/locales/vi/embeddings.json +++ b/src/i18n/locales/vi/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "Không thể đọc nội dung lỗi", "requestFailed": "Yêu cầu API Ollama thất bại với trạng thái {{status}} {{statusText}}: {{errorBody}}", "invalidResponseStructure": "Cấu trúc phản hồi không hợp lệ từ API Ollama: không tìm thấy mảng \"embeddings\" hoặc không phải là mảng.", - "embeddingFailed": "Nhúng Ollama thất bại: {{message}}" + "embeddingFailed": "Nhúng Ollama thất bại: {{message}}", + "serviceNotRunning": "Dịch vụ Ollama không chạy tại {{baseUrl}}", + "serviceUnavailable": "Dịch vụ Ollama không khả dụng (trạng thái: {{status}})", + "modelNotFound": "Không tìm thấy mô hình Ollama: {{modelId}}", + "modelNotEmbeddingCapable": "Mô hình Ollama không có khả năng nhúng: {{modelId}}", + "hostNotFound": "Không tìm thấy máy chủ Ollama: {{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "Lỗi không xác định khi xử lý tệp {{filePath}}", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "Không thể kết nối với cơ sở dữ liệu vector Qdrant. Vui lòng đảm bảo Qdrant đang chạy và có thể truy cập tại {{qdrantUrl}}. Lỗi: {{errorMessage}}" + }, + "validation": { + "authenticationFailed": "Xác thực không thành công. Vui lòng kiểm tra khóa API của bạn trong cài đặt.", + "connectionFailed": "Không thể kết nối với dịch vụ nhúng. Vui lòng kiểm tra cài đặt kết nối của bạn và đảm bảo dịch vụ đang chạy.", + "modelNotAvailable": "Mô hình được chỉ định không có sẵn. Vui lòng kiểm tra cấu hình mô hình của bạn.", + "configurationError": "Cấu hình nhúng không hợp lệ. Vui lòng xem lại cài đặt của bạn.", + "serviceUnavailable": "Dịch vụ nhúng không có sẵn. Vui lòng đảm bảo nó đang chạy và có thể truy cập được.", + "invalidEndpoint": "Điểm cuối API không hợp lệ. Vui lòng kiểm tra cấu hình URL của bạn.", + "invalidEmbedderConfig": "Cấu hình nhúng không hợp lệ. Vui lòng kiểm tra cài đặt của bạn.", + "invalidApiKey": "Khóa API không hợp lệ. Vui lòng kiểm tra cấu hình khóa API của bạn.", + "invalidBaseUrl": "URL cơ sở không hợp lệ. Vui lòng kiểm tra cấu hình URL của bạn.", + "invalidModel": "Mô hình không hợp lệ. Vui lòng kiểm tra cấu hình mô hình của bạn.", + "invalidResponse": "Phản hồi không hợp lệ từ dịch vụ embedder. Vui lòng kiểm tra cấu hình của bạn." } } diff --git a/src/i18n/locales/zh-CN/embeddings.json b/src/i18n/locales/zh-CN/embeddings.json index dba5282844..68d41a2f4c 100644 --- a/src/i18n/locales/zh-CN/embeddings.json +++ b/src/i18n/locales/zh-CN/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "无法读取错误内容", "requestFailed": "Ollama API 请求失败,状态码 {{status}} {{statusText}}:{{errorBody}}", "invalidResponseStructure": "Ollama API 响应结构无效:未找到 \"embeddings\" 数组或不是数组。", - "embeddingFailed": "Ollama 嵌入失败:{{message}}" + "embeddingFailed": "Ollama 嵌入失败:{{message}}", + "serviceNotRunning": "Ollama 服务未在 {{baseUrl}} 运行", + "serviceUnavailable": "Ollama 服务不可用(状态:{{status}})", + "modelNotFound": "未找到 Ollama 模型:{{modelId}}", + "modelNotEmbeddingCapable": "Ollama 模型不具备嵌入能力:{{modelId}}", + "hostNotFound": "未找到 Ollama 主机:{{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "处理文件 {{filePath}} 时出现未知错误", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "连接 Qdrant 向量数据库失败。请确保 Qdrant 正在运行并可在 {{qdrantUrl}} 访问。错误:{{errorMessage}}" + }, + "validation": { + "authenticationFailed": "身份验证失败。请在设置中检查您的 API 密钥。", + "connectionFailed": "连接嵌入器服务失败。请检查您的连接设置并确保服务正在运行。", + "modelNotAvailable": "指定的模型不可用。请检查您的模型配置。", + "configurationError": "嵌入器配置无效。请查看您的设置。", + "serviceUnavailable": "嵌入器服务不可用。请确保它正在运行且可访问。", + "invalidEndpoint": "API 端点无效。请检查您的 URL 配置。", + "invalidEmbedderConfig": "嵌入器配置无效。请检查您的设置。", + "invalidApiKey": "API 密钥无效。请检查您的 API 密钥配置。", + "invalidBaseUrl": "基础 URL 无效。请检查您的 URL 配置。", + "invalidModel": "模型无效。请检查您的模型配置。", + "invalidResponse": "嵌入服务响应无效。请检查您的配置。" } } diff --git a/src/i18n/locales/zh-TW/embeddings.json b/src/i18n/locales/zh-TW/embeddings.json index 71a5a482f2..2b9967a930 100644 --- a/src/i18n/locales/zh-TW/embeddings.json +++ b/src/i18n/locales/zh-TW/embeddings.json @@ -10,7 +10,12 @@ "couldNotReadErrorBody": "無法讀取錯誤內容", "requestFailed": "Ollama API 請求失敗,狀態碼 {{status}} {{statusText}}:{{errorBody}}", "invalidResponseStructure": "Ollama API 回應結構無效:未找到 \"embeddings\" 陣列或不是陣列。", - "embeddingFailed": "Ollama 內嵌失敗:{{message}}" + "embeddingFailed": "Ollama 內嵌失敗:{{message}}", + "serviceNotRunning": "Ollama 服務未在 {{baseUrl}} 執行", + "serviceUnavailable": "Ollama 服務不可用(狀態:{{status}})", + "modelNotFound": "找不到 Ollama 模型:{{modelId}}", + "modelNotEmbeddingCapable": "Ollama 模型不具備內嵌能力:{{modelId}}", + "hostNotFound": "找不到 Ollama 主機:{{baseUrl}}" }, "scanner": { "unknownErrorProcessingFile": "處理檔案 {{filePath}} 時發生未知錯誤", @@ -19,5 +24,18 @@ }, "vectorStore": { "qdrantConnectionFailed": "連接 Qdrant 向量資料庫失敗。請確保 Qdrant 正在執行並可在 {{qdrantUrl}} 存取。錯誤:{{errorMessage}}" + }, + "validation": { + "authenticationFailed": "驗證失敗。請在設定中檢查您的 API 金鑰。", + "connectionFailed": "連線至內嵌服務失敗。請檢查您的連線設定並確保服務正在執行。", + "modelNotAvailable": "指定的模型不可用。請檢查您的模型組態。", + "configurationError": "無效的內嵌程式組態。請檢閱您的設定。", + "serviceUnavailable": "內嵌服務不可用。請確保它正在執行且可存取。", + "invalidEndpoint": "無效的 API 端點。請檢查您的 URL 組態。", + "invalidEmbedderConfig": "無效的內嵌程式組態。請檢查您的設定。", + "invalidApiKey": "無效的 API 金鑰。請檢查您的 API 金鑰組態。", + "invalidBaseUrl": "無效的基礎 URL。請檢查您的 URL 組態。", + "invalidModel": "無效的模型。請檢查您的模型組態。", + "invalidResponse": "內嵌服務回應無效。請檢查您的組態。" } } diff --git a/src/services/code-index/__tests__/manager.spec.ts b/src/services/code-index/__tests__/manager.spec.ts index b52f92f8ff..ae473e3870 100644 --- a/src/services/code-index/__tests__/manager.spec.ts +++ b/src/services/code-index/__tests__/manager.spec.ts @@ -1,3 +1,7 @@ +import { CodeIndexManager } from "../manager" +import { CodeIndexServiceFactory } from "../service-factory" +import type { MockedClass } from "vitest" + // Mock vscode module vi.mock("vscode", () => ({ workspace: { @@ -21,10 +25,12 @@ vi.mock("../state-manager", () => ({ onProgressUpdate: vi.fn(), getCurrentStatus: vi.fn(), dispose: vi.fn(), + setSystemState: vi.fn(), })), })) -import { CodeIndexManager } from "../manager" +vi.mock("../service-factory") +const MockedCodeIndexServiceFactory = CodeIndexServiceFactory as MockedClass describe("CodeIndexManager - handleSettingsChange regression", () => { let mockContext: any @@ -72,13 +78,63 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { // Mock a minimal config manager that simulates first-time configuration const mockConfigManager = { loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: true }), + isFeatureConfigured: true, + isFeatureEnabled: true, + getConfig: vi.fn().mockReturnValue({ + isEnabled: true, + isConfigured: true, + embedderProvider: "openai", + modelId: "text-embedding-3-small", + openAiOptions: { openAiNativeApiKey: "test-key" }, + qdrantUrl: "http://localhost:6333", + qdrantApiKey: "test-key", + searchMinScore: 0.4, + }), } ;(manager as any)._configManager = mockConfigManager + // Mock cache manager + const mockCacheManager = { + initialize: vi.fn(), + clearCacheFile: vi.fn(), + } + ;(manager as any)._cacheManager = mockCacheManager + // Mock the feature state to simulate valid configuration that would normally trigger restart vi.spyOn(manager, "isFeatureEnabled", "get").mockReturnValue(true) vi.spyOn(manager, "isFeatureConfigured", "get").mockReturnValue(true) + // Mock service factory to handle _recreateServices call + const mockServiceFactoryInstance = { + configManager: mockConfigManager, + workspacePath: "/test/workspace", + cacheManager: mockCacheManager, + createEmbedder: vi.fn().mockReturnValue({ embedderInfo: { name: "openai" } }), + createVectorStore: vi.fn().mockReturnValue({}), + createDirectoryScanner: vi.fn().mockReturnValue({}), + createFileWatcher: vi.fn().mockReturnValue({ + onDidStartBatchProcessing: vi.fn(), + onBatchProgressUpdate: vi.fn(), + watch: vi.fn(), + stopWatcher: vi.fn(), + dispose: vi.fn(), + }), + createServices: vi.fn().mockReturnValue({ + embedder: { embedderInfo: { name: "openai" } }, + vectorStore: {}, + scanner: {}, + fileWatcher: { + onDidStartBatchProcessing: vi.fn(), + onBatchProgressUpdate: vi.fn(), + watch: vi.fn(), + stopWatcher: vi.fn(), + dispose: vi.fn(), + }, + }), + validateEmbedder: vi.fn().mockResolvedValue({ valid: true }), + } + MockedCodeIndexServiceFactory.mockImplementation(() => mockServiceFactoryInstance as any) + // The key test: this should NOT throw "CodeIndexManager not initialized" error await expect(manager.handleSettingsChange()).resolves.not.toThrow() @@ -105,29 +161,65 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { } ;(manager as any)._configManager = mockConfigManager + // Mock cache manager + const mockCacheManager = { + initialize: vi.fn(), + clearCacheFile: vi.fn(), + } + ;(manager as any)._cacheManager = mockCacheManager + // Simulate an initialized manager by setting the required properties ;(manager as any)._orchestrator = { stopWatcher: vi.fn() } ;(manager as any)._searchService = {} - ;(manager as any)._cacheManager = {} // Verify manager is considered initialized expect(manager.isInitialized).toBe(true) - // Mock the methods that would be called during restart - const recreateServicesSpy = vi.spyOn(manager as any, "_recreateServices").mockImplementation(() => {}) - const startIndexingSpy = vi.spyOn(manager, "startIndexing").mockResolvedValue() - // Mock the feature state vi.spyOn(manager, "isFeatureEnabled", "get").mockReturnValue(true) vi.spyOn(manager, "isFeatureConfigured", "get").mockReturnValue(true) + // Mock service factory to handle _recreateServices call + const mockServiceFactoryInstance = { + configManager: mockConfigManager, + workspacePath: "/test/workspace", + cacheManager: mockCacheManager, + createEmbedder: vi.fn().mockReturnValue({ embedderInfo: { name: "openai" } }), + createVectorStore: vi.fn().mockReturnValue({}), + createDirectoryScanner: vi.fn().mockReturnValue({}), + createFileWatcher: vi.fn().mockReturnValue({ + onDidStartBatchProcessing: vi.fn(), + onBatchProgressUpdate: vi.fn(), + watch: vi.fn(), + stopWatcher: vi.fn(), + dispose: vi.fn(), + }), + createServices: vi.fn().mockReturnValue({ + embedder: { embedderInfo: { name: "openai" } }, + vectorStore: {}, + scanner: {}, + fileWatcher: { + onDidStartBatchProcessing: vi.fn(), + onBatchProgressUpdate: vi.fn(), + watch: vi.fn(), + stopWatcher: vi.fn(), + dispose: vi.fn(), + }, + }), + validateEmbedder: vi.fn().mockResolvedValue({ valid: true }), + } + MockedCodeIndexServiceFactory.mockImplementation(() => mockServiceFactoryInstance as any) + + // Mock the methods that would be called during restart + const recreateServicesSpy = vi.spyOn(manager as any, "_recreateServices") + await manager.handleSettingsChange() // Verify that the restart sequence was called expect(mockConfigManager.loadConfiguration).toHaveBeenCalled() - // stopWatcher is called inside _recreateServices, which we mocked + // _recreateServices should be called when requiresRestart is true expect(recreateServicesSpy).toHaveBeenCalled() - expect(startIndexingSpy).toHaveBeenCalled() + // Note: startIndexing is NOT called by handleSettingsChange - it's only called by initialize() }) it("should handle case when config manager is not set", async () => { @@ -138,4 +230,135 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { await expect(manager.handleSettingsChange()).resolves.not.toThrow() }) }) + + describe("embedder validation integration", () => { + let mockServiceFactoryInstance: any + let mockStateManager: any + let mockEmbedder: any + let mockVectorStore: any + let mockScanner: any + let mockFileWatcher: any + + beforeEach(() => { + // Mock service factory objects + mockEmbedder = { embedderInfo: { name: "openai" } } + mockVectorStore = {} + mockScanner = {} + mockFileWatcher = { + onDidStartBatchProcessing: vi.fn(), + onBatchProgressUpdate: vi.fn(), + watch: vi.fn(), + stopWatcher: vi.fn(), + dispose: vi.fn(), + } + + // Mock service factory instance + mockServiceFactoryInstance = { + createServices: vi.fn().mockReturnValue({ + embedder: mockEmbedder, + vectorStore: mockVectorStore, + scanner: mockScanner, + fileWatcher: mockFileWatcher, + }), + validateEmbedder: vi.fn(), + } + + // Mock the ServiceFactory constructor + MockedCodeIndexServiceFactory.mockImplementation(() => mockServiceFactoryInstance) + + // Mock state manager methods directly on the existing instance + mockStateManager = (manager as any)._stateManager + mockStateManager.setSystemState = vi.fn() + + // Mock config manager + const mockConfigManager = { + loadConfiguration: vitest.fn().mockResolvedValue({ requiresRestart: false }), + isFeatureConfigured: true, + isFeatureEnabled: true, + getConfig: vitest.fn().mockReturnValue({ + isEnabled: true, + isConfigured: true, + embedderProvider: "openai", + modelId: "text-embedding-3-small", + openAiOptions: { openAiNativeApiKey: "test-key" }, + qdrantUrl: "http://localhost:6333", + qdrantApiKey: "test-key", + searchMinScore: 0.4, + }), + } + ;(manager as any)._configManager = mockConfigManager + }) + + it("should validate embedder during _recreateServices when validation succeeds", async () => { + // Arrange + mockServiceFactoryInstance.validateEmbedder.mockResolvedValue({ valid: true }) + + // Act - directly call the private method for testing + await (manager as any)._recreateServices() + + // Assert + expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled() + const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder + expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder) + expect(mockStateManager.setSystemState).not.toHaveBeenCalledWith("Error", expect.any(String)) + }) + + it("should set error state when embedder validation fails", async () => { + // Arrange + mockServiceFactoryInstance.validateEmbedder.mockResolvedValue({ + valid: false, + error: "embeddings:validation.authenticationFailed", + }) + + // Act & Assert + await expect((manager as any)._recreateServices()).rejects.toThrow( + "embeddings:validation.authenticationFailed", + ) + + // Assert other expectations + expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled() + const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder + expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder) + expect(mockStateManager.setSystemState).toHaveBeenCalledWith( + "Error", + "embeddings:validation.authenticationFailed", + ) + }) + + it("should set generic error state when embedder validation throws", async () => { + // Arrange + // Since the real service factory catches exceptions, we should mock it to resolve with an error + mockServiceFactoryInstance.validateEmbedder.mockResolvedValue({ + valid: false, + error: "embeddings:validation.configurationError", + }) + + // Act & Assert + await expect((manager as any)._recreateServices()).rejects.toThrow( + "embeddings:validation.configurationError", + ) + + // Assert other expectations + expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled() + const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder + expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder) + expect(mockStateManager.setSystemState).toHaveBeenCalledWith( + "Error", + "embeddings:validation.configurationError", + ) + }) + + it("should handle embedder creation failure", async () => { + // Arrange + mockServiceFactoryInstance.createServices.mockImplementation(() => { + throw new Error("Invalid configuration") + }) + + // Act & Assert - should throw the error + await expect((manager as any)._recreateServices()).rejects.toThrow("Invalid configuration") + + // Should not attempt validation if embedder creation fails + expect(mockServiceFactoryInstance.validateEmbedder).not.toHaveBeenCalled() + }) + }) }) diff --git a/src/services/code-index/__tests__/service-factory.spec.ts b/src/services/code-index/__tests__/service-factory.spec.ts index 5e2d878ffb..c2b56a0463 100644 --- a/src/services/code-index/__tests__/service-factory.spec.ts +++ b/src/services/code-index/__tests__/service-factory.spec.ts @@ -580,4 +580,187 @@ describe("CodeIndexServiceFactory", () => { expect(() => factory.createVectorStore()).toThrow("Qdrant URL missing for vector store creation") }) }) + + describe("validateEmbedder", () => { + let mockEmbedderInstance: any + + beforeEach(() => { + mockEmbedderInstance = { + validateConfiguration: vitest.fn(), + } + }) + + it("should validate OpenAI embedder successfully", async () => { + // Arrange + const testConfig = { + embedderProvider: "openai", + modelId: "text-embedding-3-small", + openAiOptions: { + openAiNativeApiKey: "test-api-key", + }, + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + MockedOpenAiEmbedder.mockImplementation(() => mockEmbedderInstance) + mockEmbedderInstance.validateConfiguration.mockResolvedValue({ valid: true }) + + // Act + const embedder = factory.createEmbedder() + const result = await factory.validateEmbedder(embedder) + + // Assert + expect(result).toEqual({ valid: true }) + expect(mockEmbedderInstance.validateConfiguration).toHaveBeenCalled() + }) + + it("should return validation error from OpenAI embedder", async () => { + // Arrange + const testConfig = { + embedderProvider: "openai", + modelId: "text-embedding-3-small", + openAiOptions: { + openAiNativeApiKey: "invalid-key", + }, + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + MockedOpenAiEmbedder.mockImplementation(() => mockEmbedderInstance) + mockEmbedderInstance.validateConfiguration.mockResolvedValue({ + valid: false, + error: "embeddings:validation.authenticationFailed", + }) + + // Act + const embedder = factory.createEmbedder() + const result = await factory.validateEmbedder(embedder) + + // Assert + expect(result).toEqual({ + valid: false, + error: "embeddings:validation.authenticationFailed", + }) + }) + + it("should validate Ollama embedder successfully", async () => { + // Arrange + const testConfig = { + embedderProvider: "ollama", + modelId: "nomic-embed-text", + ollamaOptions: { + ollamaBaseUrl: "http://localhost:11434", + }, + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + MockedCodeIndexOllamaEmbedder.mockImplementation(() => mockEmbedderInstance) + mockEmbedderInstance.validateConfiguration.mockResolvedValue({ valid: true }) + + // Act + const embedder = factory.createEmbedder() + const result = await factory.validateEmbedder(embedder) + + // Assert + expect(result).toEqual({ valid: true }) + expect(mockEmbedderInstance.validateConfiguration).toHaveBeenCalled() + }) + + it("should validate OpenAI Compatible embedder successfully", async () => { + // Arrange + const testConfig = { + embedderProvider: "openai-compatible", + modelId: "custom-model", + openAiCompatibleOptions: { + baseUrl: "https://api.example.com/v1", + apiKey: "test-api-key", + }, + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + MockedOpenAICompatibleEmbedder.mockImplementation(() => mockEmbedderInstance) + mockEmbedderInstance.validateConfiguration.mockResolvedValue({ valid: true }) + + // Act + const embedder = factory.createEmbedder() + const result = await factory.validateEmbedder(embedder) + + // Assert + expect(result).toEqual({ valid: true }) + expect(mockEmbedderInstance.validateConfiguration).toHaveBeenCalled() + }) + + it("should validate Gemini embedder successfully", async () => { + // Arrange + const testConfig = { + embedderProvider: "gemini", + geminiOptions: { + apiKey: "test-gemini-api-key", + }, + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + MockedGeminiEmbedder.mockImplementation(() => mockEmbedderInstance) + mockEmbedderInstance.validateConfiguration.mockResolvedValue({ valid: true }) + + // Act + const embedder = factory.createEmbedder() + const result = await factory.validateEmbedder(embedder) + + // Assert + expect(result).toEqual({ valid: true }) + expect(mockEmbedderInstance.validateConfiguration).toHaveBeenCalled() + }) + + it("should handle validation exceptions", async () => { + // Arrange + const testConfig = { + embedderProvider: "openai", + modelId: "text-embedding-3-small", + openAiOptions: { + openAiNativeApiKey: "test-api-key", + }, + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + MockedOpenAiEmbedder.mockImplementation(() => mockEmbedderInstance) + const networkError = new Error("Network error") + mockEmbedderInstance.validateConfiguration.mockRejectedValue(networkError) + + // Act + const embedder = factory.createEmbedder() + const result = await factory.validateEmbedder(embedder) + + // Assert + expect(result).toEqual({ + valid: false, + error: "Network error", + }) + expect(mockEmbedderInstance.validateConfiguration).toHaveBeenCalled() + }) + + it("should return error for invalid embedder configuration", async () => { + // Arrange + const testConfig = { + embedderProvider: "openai", + modelId: "text-embedding-3-small", + openAiOptions: { + openAiNativeApiKey: undefined, // Missing API key + }, + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + + // Act & Assert + // This should throw when trying to create the embedder + await expect(async () => { + const embedder = factory.createEmbedder() + await factory.validateEmbedder(embedder) + }).rejects.toThrow("OpenAI configuration missing for embedder creation") + }) + + it("should return error for unknown embedder provider", async () => { + // Arrange + const testConfig = { + embedderProvider: "unknown-provider", + modelId: "some-model", + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + + // Act & Assert + // This should throw when trying to create the embedder + expect(() => factory.createEmbedder()).toThrow("Invalid embedder type configured: unknown-provider") + }) + }) }) diff --git a/src/services/code-index/embedders/__tests__/gemini.spec.ts b/src/services/code-index/embedders/__tests__/gemini.spec.ts index 6d518f65aa..856f5bf7c6 100644 --- a/src/services/code-index/embedders/__tests__/gemini.spec.ts +++ b/src/services/code-index/embedders/__tests__/gemini.spec.ts @@ -55,4 +55,54 @@ describe("GeminiEmbedder", () => { expect(GeminiEmbedder.dimension).toBe(768) }) }) + + describe("validateConfiguration", () => { + let mockValidateConfiguration: any + + beforeEach(() => { + mockValidateConfiguration = vitest.fn() + MockedOpenAICompatibleEmbedder.prototype.validateConfiguration = mockValidateConfiguration + }) + + it("should delegate validation to OpenAICompatibleEmbedder", async () => { + // Arrange + embedder = new GeminiEmbedder("test-api-key") + mockValidateConfiguration.mockResolvedValue({ valid: true }) + + // Act + const result = await embedder.validateConfiguration() + + // Assert + expect(mockValidateConfiguration).toHaveBeenCalled() + expect(result).toEqual({ valid: true }) + }) + + it("should pass through validation errors from OpenAICompatibleEmbedder", async () => { + // Arrange + embedder = new GeminiEmbedder("test-api-key") + mockValidateConfiguration.mockResolvedValue({ + valid: false, + error: "embeddings:validation.authenticationFailed", + }) + + // Act + const result = await embedder.validateConfiguration() + + // Assert + expect(mockValidateConfiguration).toHaveBeenCalled() + expect(result).toEqual({ + valid: false, + error: "embeddings:validation.authenticationFailed", + }) + }) + + it("should handle validation exceptions", async () => { + // Arrange + embedder = new GeminiEmbedder("test-api-key") + mockValidateConfiguration.mockRejectedValue(new Error("Validation failed")) + + // Act & Assert + await expect(embedder.validateConfiguration()).rejects.toThrow("Validation failed") + }) + }) }) diff --git a/src/services/code-index/embedders/__tests__/ollama.spec.ts b/src/services/code-index/embedders/__tests__/ollama.spec.ts new file mode 100644 index 0000000000..30e6057388 --- /dev/null +++ b/src/services/code-index/embedders/__tests__/ollama.spec.ts @@ -0,0 +1,238 @@ +import { vitest, describe, it, expect, beforeEach, afterEach } from "vitest" +import type { MockedFunction } from "vitest" +import { CodeIndexOllamaEmbedder } from "../ollama" + +// Mock fetch +global.fetch = vitest.fn() as MockedFunction + +// Mock i18n +vitest.mock("../../../../i18n", () => ({ + t: (key: string, params?: Record) => { + const translations: Record = { + "embeddings:validation.serviceUnavailable": + "The embedder service is not available. Please ensure it is running and accessible.", + "embeddings:validation.modelNotAvailable": + "The specified model is not available. Please check your model configuration.", + "embeddings:validation.connectionFailed": + "Failed to connect to the embedder service. Please check your connection settings and ensure the service is running.", + "embeddings:validation.configurationError": "Invalid embedder configuration. Please review your settings.", + "embeddings:errors.ollama.serviceNotRunning": + "Ollama service is not running at {{baseUrl}}. Please start Ollama first.", + "embeddings:errors.ollama.serviceUnavailable": + "Ollama service is unavailable at {{baseUrl}}. HTTP status: {{status}}", + "embeddings:errors.ollama.modelNotFound": + "Model '{{model}}' not found. Available models: {{availableModels}}", + "embeddings:errors.ollama.modelNotEmbedding": "Model '{{model}}' is not embedding capable", + "embeddings:errors.ollama.hostNotFound": "Ollama host not found: {{baseUrl}}", + "embeddings:errors.ollama.connectionTimeout": "Connection to Ollama timed out at {{baseUrl}}", + } + // Handle parameter substitution + let result = translations[key] || key + if (params) { + Object.entries(params).forEach(([param, value]) => { + result = result.replace(new RegExp(`{{${param}}}`, "g"), String(value)) + }) + } + return result + }, +})) + +// Mock console methods +const consoleMocks = { + error: vitest.spyOn(console, "error").mockImplementation(() => {}), +} + +describe("CodeIndexOllamaEmbedder", () => { + let embedder: CodeIndexOllamaEmbedder + let mockFetch: MockedFunction + + beforeEach(() => { + vitest.clearAllMocks() + consoleMocks.error.mockClear() + + mockFetch = global.fetch as MockedFunction + + embedder = new CodeIndexOllamaEmbedder({ + ollamaModelId: "nomic-embed-text", + ollamaBaseUrl: "http://localhost:11434", + }) + }) + + afterEach(() => { + vitest.clearAllMocks() + }) + + describe("constructor", () => { + it("should initialize with provided options", () => { + expect(embedder.embedderInfo.name).toBe("ollama") + }) + + it("should use default values when not provided", () => { + const embedderWithDefaults = new CodeIndexOllamaEmbedder({}) + expect(embedderWithDefaults.embedderInfo.name).toBe("ollama") + }) + }) + + describe("validateConfiguration", () => { + it("should validate successfully when service is available and model exists", async () => { + // Mock successful /api/tags call + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + status: 200, + json: () => + Promise.resolve({ + models: [{ name: "nomic-embed-text:latest" }, { name: "llama2:latest" }], + }), + } as Response), + ) + + // Mock successful /api/embed test call + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + status: 200, + json: () => + Promise.resolve({ + embeddings: [[0.1, 0.2, 0.3]], + }), + } as Response), + ) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(true) + expect(result.error).toBeUndefined() + expect(mockFetch).toHaveBeenCalledTimes(2) + + // Check first call (GET /api/tags) + const firstCall = mockFetch.mock.calls[0] + expect(firstCall[0]).toBe("http://localhost:11434/api/tags") + expect(firstCall[1]?.method).toBe("GET") + expect(firstCall[1]?.headers).toEqual({ "Content-Type": "application/json" }) + expect(firstCall[1]?.signal).toBeDefined() // AbortSignal for timeout + + // Check second call (POST /api/embed) + const secondCall = mockFetch.mock.calls[1] + expect(secondCall[0]).toBe("http://localhost:11434/api/embed") + expect(secondCall[1]?.method).toBe("POST") + expect(secondCall[1]?.headers).toEqual({ "Content-Type": "application/json" }) + expect(secondCall[1]?.body).toBe(JSON.stringify({ model: "nomic-embed-text", input: ["test"] })) + expect(secondCall[1]?.signal).toBeDefined() // AbortSignal for timeout + }) + + it("should fail validation when service is not available", async () => { + mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED")) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("Connection to Ollama timed out at http://localhost:11434") + }) + + it("should fail validation when tags endpoint returns 404", async () => { + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + ok: false, + status: 404, + } as Response), + ) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe( + "Ollama service is not running at http://localhost:11434. Please start Ollama first.", + ) + }) + + it("should fail validation when tags endpoint returns other error", async () => { + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + ok: false, + status: 500, + } as Response), + ) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("Ollama service is unavailable at http://localhost:11434. HTTP status: 500") + }) + + it("should fail validation when model does not exist", async () => { + // Mock successful /api/tags call with different models + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + status: 200, + json: () => + Promise.resolve({ + models: [{ name: "llama2:latest" }, { name: "mistral:latest" }], + }), + } as Response), + ) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe( + "Model 'nomic-embed-text' not found. Available models: llama2:latest, mistral:latest", + ) + }) + + it("should fail validation when model exists but doesn't support embeddings", async () => { + // Mock successful /api/tags call + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + status: 200, + json: () => + Promise.resolve({ + models: [{ name: "nomic-embed-text" }], + }), + } as Response), + ) + + // Mock failed /api/embed test call + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + ok: false, + status: 400, + } as Response), + ) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("Model 'nomic-embed-text' is not embedding capable") + }) + + it("should handle ECONNREFUSED errors", async () => { + mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED")) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("Connection to Ollama timed out at http://localhost:11434") + }) + + it("should handle ENOTFOUND errors", async () => { + mockFetch.mockRejectedValueOnce(new Error("ENOTFOUND")) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("Ollama host not found: http://localhost:11434") + }) + + it("should handle generic network errors", async () => { + mockFetch.mockRejectedValueOnce(new Error("Network timeout")) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("Network timeout") + }) + }) +}) diff --git a/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts b/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts index 107f3af24d..d1f45d75ca 100644 --- a/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts +++ b/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts @@ -882,4 +882,133 @@ describe("OpenAICompatibleEmbedder", () => { expect(mockCreate).toHaveBeenCalled() }) }) + + describe("validateConfiguration", () => { + let embedder: OpenAICompatibleEmbedder + let mockFetch: MockedFunction + + beforeEach(() => { + vitest.clearAllMocks() + // Reset and re-assign the global fetch mock + global.fetch = vitest.fn() + mockFetch = global.fetch as MockedFunction + }) + + it("should validate successfully with valid configuration and base URL", async () => { + embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId) + + const mockResponse = { + data: [{ embedding: [0.1, 0.2, 0.3] }], + usage: { prompt_tokens: 2, total_tokens: 2 }, + } + mockEmbeddingsCreate.mockResolvedValue(mockResponse) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(true) + expect(result.error).toBeUndefined() + expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ + input: ["test"], + model: testModelId, + encoding_format: "base64", + }) + }) + + it("should validate successfully with full endpoint URL", async () => { + const fullUrl = "https://api.example.com/v1/embeddings" + embedder = new OpenAICompatibleEmbedder(fullUrl, testApiKey, testModelId) + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ + data: [{ embedding: [0.1, 0.2, 0.3] }], + usage: { prompt_tokens: 2, total_tokens: 2 }, + }), + text: async () => "", + } as any) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(true) + expect(result.error).toBeUndefined() + expect(mockFetch).toHaveBeenCalledWith( + fullUrl, + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + Authorization: `Bearer ${testApiKey}`, + }), + }), + ) + }) + + it("should fail validation with authentication error", async () => { + embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId) + + const authError = new Error("Invalid API key") + ;(authError as any).status = 401 + mockEmbeddingsCreate.mockRejectedValue(authError) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("embeddings:validation.authenticationFailed") + }) + + it("should fail validation with connection error", async () => { + embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId) + + const connectionError = new Error("ECONNREFUSED") + mockEmbeddingsCreate.mockRejectedValue(connectionError) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("embeddings:validation.connectionFailed") + }) + + it("should fail validation with invalid endpoint for full URL", async () => { + const fullUrl = "https://api.example.com/v1/embeddings" + embedder = new OpenAICompatibleEmbedder(fullUrl, testApiKey, testModelId) + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + json: async () => ({ error: "Not found" }), + text: async () => "Not found", + } as any) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("embeddings:validation.invalidEndpoint") + }) + + it("should fail validation with rate limit error", async () => { + embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId) + + const rateLimitError = new Error("Rate limit exceeded") + ;(rateLimitError as any).status = 429 + mockEmbeddingsCreate.mockRejectedValue(rateLimitError) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("embeddings:validation.serviceUnavailable") + }) + + it("should fail validation with generic error", async () => { + embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId) + + const genericError = new Error("Unknown error") + ;(genericError as any).status = 500 + mockEmbeddingsCreate.mockRejectedValue(genericError) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("embeddings:validation.configurationError") + }) + }) }) diff --git a/src/services/code-index/embedders/__tests__/openai.spec.ts b/src/services/code-index/embedders/__tests__/openai.spec.ts index c93c049844..3f46fc248b 100644 --- a/src/services/code-index/embedders/__tests__/openai.spec.ts +++ b/src/services/code-index/embedders/__tests__/openai.spec.ts @@ -464,4 +464,66 @@ describe("OpenAiEmbedder", () => { }) }) }) + + describe("validateConfiguration", () => { + it("should validate successfully with valid configuration", async () => { + const mockResponse = { + data: [{ embedding: [0.1, 0.2, 0.3] }], + usage: { prompt_tokens: 2, total_tokens: 2 }, + } + mockEmbeddingsCreate.mockResolvedValue(mockResponse) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(true) + expect(result.error).toBeUndefined() + expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ + input: ["test"], + model: "text-embedding-3-small", + }) + }) + + it("should fail validation with authentication error", async () => { + const authError = new Error("Invalid API key") + ;(authError as any).status = 401 + mockEmbeddingsCreate.mockRejectedValue(authError) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("embeddings:validation.authenticationFailed") + }) + + it("should fail validation with rate limit error", async () => { + const rateLimitError = new Error("Rate limit exceeded") + ;(rateLimitError as any).status = 429 + mockEmbeddingsCreate.mockRejectedValue(rateLimitError) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("embeddings:validation.serviceUnavailable") + }) + + it("should fail validation with connection error", async () => { + const connectionError = new Error("ECONNREFUSED") + mockEmbeddingsCreate.mockRejectedValue(connectionError) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("embeddings:validation.connectionFailed") + }) + + it("should fail validation with generic error", async () => { + const genericError = new Error("Unknown error") + ;(genericError as any).status = 500 + mockEmbeddingsCreate.mockRejectedValue(genericError) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("embeddings:validation.configurationError") + }) + }) }) diff --git a/src/services/code-index/embedders/gemini.ts b/src/services/code-index/embedders/gemini.ts index 38bb132df7..f99ae4c1d7 100644 --- a/src/services/code-index/embedders/gemini.ts +++ b/src/services/code-index/embedders/gemini.ts @@ -46,6 +46,16 @@ export class GeminiEmbedder implements IEmbedder { return this.openAICompatibleEmbedder.createEmbeddings(texts, GeminiEmbedder.GEMINI_MODEL) } + /** + * Validates the Gemini embedder configuration by delegating to the underlying OpenAI-compatible embedder + * @returns Promise resolving to validation result with success status and optional error message + */ + async validateConfiguration(): Promise<{ valid: boolean; error?: string }> { + // Delegate validation to the OpenAI-compatible embedder + // The error messages will be specific to Gemini since we're using Gemini's base URL + return this.openAICompatibleEmbedder.validateConfiguration() + } + /** * Returns information about this embedder */ diff --git a/src/services/code-index/embedders/ollama.ts b/src/services/code-index/embedders/ollama.ts index 2f212c7745..f9001a743e 100644 --- a/src/services/code-index/embedders/ollama.ts +++ b/src/services/code-index/embedders/ollama.ts @@ -3,6 +3,7 @@ import { EmbedderInfo, EmbeddingResponse, IEmbedder } from "../interfaces" import { getModelQueryPrefix } from "../../../shared/embeddingModels" import { MAX_ITEM_TOKENS } from "../constants" import { t } from "../../../i18n" +import { withValidationErrorHandling } from "../shared/validation-helpers" /** * Implements the IEmbedder interface using a local Ollama instance. @@ -101,6 +102,127 @@ export class CodeIndexOllamaEmbedder implements IEmbedder { } } + /** + * Validates the Ollama embedder configuration by checking service availability and model existence + * @returns Promise resolving to validation result with success status and optional error message + */ + async validateConfiguration(): Promise<{ valid: boolean; error?: string }> { + return withValidationErrorHandling( + async () => { + // First check if Ollama service is running by trying to list models + const modelsUrl = `${this.baseUrl}/api/tags` + + // Add timeout to prevent indefinite hanging + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 5000) // 5 second timeout + + const modelsResponse = await fetch(modelsUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + signal: controller.signal, + }) + clearTimeout(timeoutId) + + if (!modelsResponse.ok) { + if (modelsResponse.status === 404) { + return { + valid: false, + error: t("embeddings:errors.ollama.serviceNotRunning", { baseUrl: this.baseUrl }), + } + } + return { + valid: false, + error: t("embeddings:errors.ollama.serviceUnavailable", { + baseUrl: this.baseUrl, + status: modelsResponse.status, + }), + } + } + + // Check if the specific model exists + const modelsData = await modelsResponse.json() + const models = modelsData.models || [] + + // Check both with and without :latest suffix + const modelExists = models.some((m: any) => { + const modelName = m.name || "" + return ( + modelName === this.defaultModelId || + modelName === `${this.defaultModelId}:latest` || + modelName === this.defaultModelId.replace(":latest", "") + ) + }) + + if (!modelExists) { + const availableModels = models.map((m: any) => m.name).join(", ") + return { + valid: false, + error: t("embeddings:errors.ollama.modelNotFound", { + model: this.defaultModelId, + availableModels, + }), + } + } + + // Try a test embedding to ensure the model works for embeddings + const testUrl = `${this.baseUrl}/api/embed` + + // Add timeout for test request too + const testController = new AbortController() + const testTimeoutId = setTimeout(() => testController.abort(), 5000) + + const testResponse = await fetch(testUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: this.defaultModelId, + input: ["test"], + }), + signal: testController.signal, + }) + clearTimeout(testTimeoutId) + + if (!testResponse.ok) { + return { + valid: false, + error: t("embeddings:errors.ollama.modelNotEmbedding", { model: this.defaultModelId }), + } + } + + return { valid: true } + }, + "ollama", + { + beforeStandardHandling: (error: any) => { + // Handle Ollama-specific connection errors + if (error?.message === "ECONNREFUSED") { + return { + valid: false, + error: t("embeddings:errors.ollama.connectionTimeout", { baseUrl: this.baseUrl }), + } + } else if (error?.message === "ENOTFOUND") { + return { + valid: false, + error: t("embeddings:errors.ollama.hostNotFound", { baseUrl: this.baseUrl }), + } + } else if (error?.name === "AbortError") { + // Handle timeout + return { + valid: false, + error: t("embeddings:errors.ollama.connectionTimeout", { baseUrl: this.baseUrl }), + } + } + // Let standard handling take over + return undefined + }, + }, + ) + } + get embedderInfo(): EmbedderInfo { return { name: "ollama", diff --git a/src/services/code-index/embedders/openai-compatible.ts b/src/services/code-index/embedders/openai-compatible.ts index 88eced8a0a..b378bbe7ac 100644 --- a/src/services/code-index/embedders/openai-compatible.ts +++ b/src/services/code-index/embedders/openai-compatible.ts @@ -8,6 +8,7 @@ import { } from "../constants" import { getDefaultModelId, getModelQueryPrefix } from "../../../shared/embeddingModels" import { t } from "../../../i18n" +import { withValidationErrorHandling, HttpError, formatEmbeddingError } from "../shared/validation-helpers" interface EmbeddingItem { embedding: string | number[] @@ -26,12 +27,6 @@ interface OpenAIEmbeddingResponse { * OpenAI Compatible implementation of the embedder interface with batching and rate limiting. * This embedder allows using any OpenAI-compatible API endpoint by specifying a custom baseURL. */ -interface HttpError extends Error { - status?: number - response?: { - status?: number - } -} export class OpenAICompatibleEmbedder implements IEmbedder { private embeddingsClient: OpenAI @@ -201,14 +196,31 @@ export class OpenAICompatibleEmbedder implements IEmbedder { }), }) - if (!response.ok) { - const errorText = await response.text() - const error = new Error(`HTTP ${response.status}: ${errorText}`) as HttpError - error.status = response.status + if (!response || !response.ok) { + const status = response?.status || 0 + let errorText = "No response" + try { + if (response && typeof response.text === "function") { + errorText = await response.text() + } else if (response) { + errorText = `Error ${status}` + } + } catch { + // Ignore text parsing errors + errorText = `Error ${status}` + } + const error = new Error(`HTTP ${status}: ${errorText}`) as HttpError + error.status = status || response?.status || 0 throw error } - return await response.json() + try { + return await response.json() + } catch (e) { + const error = new Error(`Failed to parse response JSON`) as HttpError + error.status = response.status + throw error + } } /** @@ -272,11 +284,11 @@ export class OpenAICompatibleEmbedder implements IEmbedder { }, } } catch (error) { - const httpError = error as HttpError - const isRateLimitError = httpError?.status === 429 const hasMoreAttempts = attempts < MAX_RETRIES - 1 - if (isRateLimitError && hasMoreAttempts) { + // Check if it's a rate limit error + const httpError = error as HttpError + if (httpError?.status === 429 && hasMoreAttempts) { const delayMs = INITIAL_DELAY_MS * Math.pow(2, attempts) console.warn( t("embeddings:rateLimitRetry", { @@ -292,37 +304,50 @@ export class OpenAICompatibleEmbedder implements IEmbedder { // Log the error for debugging console.error(`OpenAI Compatible embedder error (attempt ${attempts + 1}/${MAX_RETRIES}):`, error) - // Provide more context in the error message using robust error extraction - let errorMessage = t("embeddings:unknownError") - if (httpError?.message) { - errorMessage = httpError.message - } else if (typeof error === "string") { - errorMessage = error - } else if (error && typeof error === "object" && "toString" in error) { - try { - errorMessage = String(error) - } catch { - errorMessage = t("embeddings:unknownError") - } - } - - const statusCode = httpError?.status || httpError?.response?.status - - if (statusCode === 401) { - throw new Error(t("embeddings:authenticationFailed")) - } else if (statusCode) { - throw new Error( - t("embeddings:failedWithStatus", { attempts: MAX_RETRIES, statusCode, errorMessage }), - ) - } else { - throw new Error(t("embeddings:failedWithError", { attempts: MAX_RETRIES, errorMessage })) - } + // Format and throw the error + throw formatEmbeddingError(error, MAX_RETRIES) } } throw new Error(t("embeddings:failedMaxAttempts", { attempts: MAX_RETRIES })) } + /** + * Validates the OpenAI-compatible embedder configuration by testing endpoint connectivity and API key + * @returns Promise resolving to validation result with success status and optional error message + */ + async validateConfiguration(): Promise<{ valid: boolean; error?: string }> { + return withValidationErrorHandling(async () => { + // Test with a minimal embedding request + const testTexts = ["test"] + const modelToUse = this.defaultModelId + + let response: OpenAIEmbeddingResponse + + if (this.isFullUrl) { + // Test direct HTTP request for full endpoint URLs + response = await this.makeDirectEmbeddingRequest(this.baseUrl, testTexts, modelToUse) + } else { + // Test using OpenAI SDK for base URLs + response = (await this.embeddingsClient.embeddings.create({ + input: testTexts, + model: modelToUse, + encoding_format: "base64", + })) as OpenAIEmbeddingResponse + } + + // Check if we got a valid response + if (!response?.data || response.data.length === 0) { + return { + valid: false, + error: "embeddings:validation.invalidResponse", + } + } + + return { valid: true } + }, "openai-compatible") + } + /** * Returns information about this embedder */ diff --git a/src/services/code-index/embedders/openai.ts b/src/services/code-index/embedders/openai.ts index 667c2f46d4..a620edc307 100644 --- a/src/services/code-index/embedders/openai.ts +++ b/src/services/code-index/embedders/openai.ts @@ -10,6 +10,7 @@ import { } from "../constants" import { getModelQueryPrefix } from "../../../shared/embeddingModels" import { t } from "../../../i18n" +import { withValidationErrorHandling, formatEmbeddingError, HttpError } from "../shared/validation-helpers" /** * OpenAI implementation of the embedder interface with batching and rate limiting @@ -138,10 +139,11 @@ export class OpenAiEmbedder extends OpenAiNativeHandler implements IEmbedder { }, } } catch (error: any) { - const isRateLimitError = error?.status === 429 const hasMoreAttempts = attempts < MAX_RETRIES - 1 - if (isRateLimitError && hasMoreAttempts) { + // Check if it's a rate limit error + const httpError = error as HttpError + if (httpError?.status === 429 && hasMoreAttempts) { const delayMs = INITIAL_DELAY_MS * Math.pow(2, attempts) console.warn( t("embeddings:rateLimitRetry", { @@ -157,37 +159,38 @@ export class OpenAiEmbedder extends OpenAiNativeHandler implements IEmbedder { // Log the error for debugging console.error(`OpenAI embedder error (attempt ${attempts + 1}/${MAX_RETRIES}):`, error) - // Provide more context in the error message using robust error extraction - let errorMessage = "Unknown error" - if (error?.message) { - errorMessage = error.message - } else if (typeof error === "string") { - errorMessage = error - } else if (error && typeof error.toString === "function") { - try { - errorMessage = error.toString() - } catch { - errorMessage = "Unknown error" - } - } - - const statusCode = error?.status || error?.response?.status - - if (statusCode === 401) { - throw new Error(t("embeddings:authenticationFailed")) - } else if (statusCode) { - throw new Error( - t("embeddings:failedWithStatus", { attempts: MAX_RETRIES, statusCode, errorMessage }), - ) - } else { - throw new Error(t("embeddings:failedWithError", { attempts: MAX_RETRIES, errorMessage })) - } + // Format and throw the error + throw formatEmbeddingError(error, MAX_RETRIES) } } throw new Error(t("embeddings:failedMaxAttempts", { attempts: MAX_RETRIES })) } + /** + * Validates the OpenAI embedder configuration by attempting a minimal embedding request + * @returns Promise resolving to validation result with success status and optional error message + */ + async validateConfiguration(): Promise<{ valid: boolean; error?: string }> { + return withValidationErrorHandling(async () => { + // Test with a minimal embedding request + const response = await this.embeddingsClient.embeddings.create({ + input: ["test"], + model: this.defaultModelId, + }) + + // Check if we got a valid response + if (!response.data || response.data.length === 0) { + return { + valid: false, + error: t("embeddings:openai.invalidResponseFormat"), + } + } + + return { valid: true } + }, "openai") + } + get embedderInfo(): EmbedderInfo { return { name: "openai", diff --git a/src/services/code-index/interfaces/embedder.ts b/src/services/code-index/interfaces/embedder.ts index 3ea6293aa5..0a74446d5e 100644 --- a/src/services/code-index/interfaces/embedder.ts +++ b/src/services/code-index/interfaces/embedder.ts @@ -10,6 +10,13 @@ export interface IEmbedder { * @returns Promise resolving to an EmbeddingResponse */ createEmbeddings(texts: string[], model?: string): Promise + + /** + * Validates the embedder configuration by testing connectivity and credentials. + * @returns Promise resolving to validation result with success status and optional error message + */ + validateConfiguration(): Promise<{ valid: boolean; error?: string }> + get embedderInfo(): EmbedderInfo } diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index aa58469077..7002283226 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -118,7 +118,14 @@ export class CodeIndexManager { return { requiresRestart } } - // 3. CacheManager Initialization + // 3. Check if workspace is available + const workspacePath = getWorkspacePath() + if (!workspacePath) { + this._stateManager.setSystemState("Standby", "No workspace folder open") + return { requiresRestart } + } + + // 4. CacheManager Initialization if (!this._cacheManager) { this._cacheManager = new CacheManager(this.context, this.workspacePath) await this._cacheManager.initialize() @@ -215,6 +222,9 @@ export class CodeIndexManager { if (this._orchestrator) { this.stopWatcher() } + // Clear existing services to ensure clean state + this._orchestrator = undefined + this._searchService = undefined // (Re)Initialize service factory this._serviceFactory = new CodeIndexServiceFactory( @@ -224,7 +234,14 @@ export class CodeIndexManager { ) const ignoreInstance = ignore() - const ignorePath = path.join(getWorkspacePath(), ".gitignore") + const workspacePath = getWorkspacePath() + + if (!workspacePath) { + this._stateManager.setSystemState("Standby", "") + return + } + + const ignorePath = path.join(workspacePath, ".gitignore") try { const content = await fs.readFile(ignorePath, "utf8") ignoreInstance.add(content) @@ -241,6 +258,17 @@ export class CodeIndexManager { ignoreInstance, ) + // Validate embedder configuration before proceeding + const validationResult = await this._serviceFactory.validateEmbedder(embedder) + if (!validationResult.valid) { + // Set error state with clear message + this._stateManager.setSystemState( + "Error", + validationResult.error || "Embedder configuration validation failed", + ) + throw new Error(validationResult.error || "Invalid embedder configuration") + } + // (Re)Initialize orchestrator this._orchestrator = new CodeIndexOrchestrator( this._configManager!, @@ -259,6 +287,9 @@ export class CodeIndexManager { embedder, vectorStore, ) + + // Clear any error state after successful recreation + this._stateManager.setSystemState("Standby", "") } /** @@ -274,13 +305,16 @@ export class CodeIndexManager { const isFeatureEnabled = this.isFeatureEnabled const isFeatureConfigured = this.isFeatureConfigured - // If configuration changes require a restart and the manager is initialized, restart the service - if (requiresRestart && isFeatureEnabled && isFeatureConfigured && this.isInitialized) { - // Recreate services with new configuration - await this._recreateServices() - - // Start indexing with new services - this.startIndexing() + if (requiresRestart && isFeatureEnabled && isFeatureConfigured) { + try { + // Recreate services with new configuration + await this._recreateServices() + } catch (error) { + // Error state already set in _recreateServices + console.error("Failed to recreate services:", error) + // Re-throw the error so the caller knows validation failed + throw error + } } } } diff --git a/src/services/code-index/orchestrator.ts b/src/services/code-index/orchestrator.ts index 0d8151b5e1..948a86fae5 100644 --- a/src/services/code-index/orchestrator.ts +++ b/src/services/code-index/orchestrator.ts @@ -164,6 +164,31 @@ export class CodeIndexOrchestrator { } } + // Check for partial failures - if a significant portion of blocks failed + const failureRate = (cumulativeBlocksFoundSoFar - cumulativeBlocksIndexed) / cumulativeBlocksFoundSoFar + if (batchErrors.length > 0 && failureRate > 0.1) { + // More than 10% of blocks failed to index + const firstError = batchErrors[0] + throw new Error( + `Indexing partially failed: Only ${cumulativeBlocksIndexed} of ${cumulativeBlocksFoundSoFar} blocks were indexed. ${firstError.message}`, + ) + } + + // CRITICAL: If there were ANY batch errors and NO blocks were successfully indexed, + // this is a complete failure regardless of the failure rate calculation + if (batchErrors.length > 0 && cumulativeBlocksIndexed === 0) { + const firstError = batchErrors[0] + throw new Error(`Indexing failed completely: ${firstError.message}`) + } + + // Final sanity check: If we found blocks but indexed none and somehow no errors were reported, + // this is still a failure + if (cumulativeBlocksFoundSoFar > 0 && cumulativeBlocksIndexed === 0) { + throw new Error( + "Indexing failed: No code blocks were successfully indexed despite finding files to process. This indicates a critical embedder failure.", + ) + } + await this._startWatcher() this.stateManager.setSystemState("Indexed", "File watcher started.") diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index 2c00d96525..a9c84481a6 100644 --- a/src/services/code-index/service-factory.ts +++ b/src/services/code-index/service-factory.ts @@ -66,6 +66,23 @@ export class CodeIndexServiceFactory { throw new Error(`Invalid embedder type configured: ${config.embedderProvider}`) } + /** + * Validates an embedder instance to ensure it's properly configured. + * @param embedder The embedder instance to validate + * @returns Promise resolving to validation result + */ + public async validateEmbedder(embedder: IEmbedder): Promise<{ valid: boolean; error?: string }> { + try { + return await embedder.validateConfiguration() + } catch (error) { + // If validation throws an exception, preserve the original error message + return { + valid: false, + error: error instanceof Error ? error.message : "embeddings:validation.configurationError", + } + } + } + /** * Creates a vector store instance using the current configuration. */ diff --git a/src/services/code-index/shared/validation-helpers.ts b/src/services/code-index/shared/validation-helpers.ts new file mode 100644 index 0000000000..c210c8ec17 --- /dev/null +++ b/src/services/code-index/shared/validation-helpers.ts @@ -0,0 +1,187 @@ +import { t } from "../../../i18n" +import { serializeError } from "serialize-error" + +/** + * HTTP error interface for embedder errors + */ +export interface HttpError extends Error { + status?: number + response?: { + status?: number + } +} + +/** + * Common error types that can occur during embedder validation + */ +export interface ValidationError { + status?: number + message?: string + name?: string + code?: string +} + +/** + * Maps HTTP status codes to appropriate error messages + */ +export function getErrorMessageForStatus(status: number | undefined, embedderType: string): string | undefined { + switch (status) { + case 401: + case 403: + return "embeddings:validation.authenticationFailed" + case 404: + return embedderType === "openai" + ? "embeddings:validation.modelNotAvailable" + : "embeddings:validation.invalidEndpoint" + case 429: + return "embeddings:validation.serviceUnavailable" + default: + if (status && status >= 400 && status < 600) { + return "embeddings:validation.configurationError" + } + return undefined + } +} + +/** + * Extracts status code from various error formats + */ +export function extractStatusCode(error: any): number | undefined { + // Direct status property + if (error?.status) return error.status + + // Response status property + if (error?.response?.status) return error.response.status + + // Extract from error message (e.g., "HTTP 404: Not Found") + if (error?.message) { + const match = error.message.match(/HTTP (\d+):/) + if (match) { + return parseInt(match[1], 10) + } + } + + // Use serialize-error as fallback for complex objects + const serialized = serializeError(error) + if (serialized?.status) return serialized.status + if (serialized?.response?.status) return serialized.response.status + + return undefined +} + +/** + * Extracts error message from various error formats + */ +export function extractErrorMessage(error: any): string { + if (error?.message) { + return error.message + } + + if (typeof error === "string") { + return error + } + + if (error && typeof error === "object" && "toString" in error) { + try { + return String(error) + } catch { + return "Unknown error" + } + } + + // Use serialize-error as fallback for complex objects + const serialized = serializeError(error) + if (serialized?.message) { + return serialized.message + } + + return "Unknown error" +} + +/** + * Standard validation error handler for embedder configuration validation + * Returns a consistent error response based on the error type + */ +export function handleValidationError( + error: any, + embedderType: string, + customHandlers?: { + beforeStandardHandling?: (error: any) => { valid: boolean; error: string } | undefined + }, +): { valid: boolean; error: string } { + // Serialize the error to ensure we have access to all properties + const serializedError = serializeError(error) + + // Allow custom handling first (pass original error for backward compatibility) + if (customHandlers?.beforeStandardHandling) { + const customResult = customHandlers.beforeStandardHandling(error) + if (customResult) return customResult + } + + // Extract status code and error message from serialized error + const statusCode = extractStatusCode(serializedError) + const errorMessage = extractErrorMessage(serializedError) + + // Check for status-based errors first + const statusError = getErrorMessageForStatus(statusCode, embedderType) + if (statusError) { + return { valid: false, error: statusError } + } + + // Check for connection errors + if (errorMessage) { + if ( + errorMessage.includes("ENOTFOUND") || + errorMessage.includes("ECONNREFUSED") || + errorMessage.includes("ETIMEDOUT") || + errorMessage === "AbortError" || + errorMessage.includes("HTTP 0:") || + errorMessage === "No response" + ) { + return { valid: false, error: "embeddings:validation.connectionFailed" } + } + + if (errorMessage.includes("Failed to parse response JSON")) { + return { valid: false, error: "embeddings:validation.invalidResponse" } + } + } + + // For generic errors, preserve the original error message if it's not a standard one + if (errorMessage && errorMessage !== "Unknown error") { + return { valid: false, error: errorMessage } + } + + // Fallback to generic error + return { valid: false, error: "embeddings:validation.configurationError" } +} + +/** + * Wraps an async validation function with standard error handling + */ +export async function withValidationErrorHandling( + validationFn: () => Promise, + embedderType: string, + customHandlers?: Parameters[2], +): Promise<{ valid: boolean; error?: string }> { + try { + return await validationFn() + } catch (error) { + return handleValidationError(error, embedderType, customHandlers) + } +} + +/** + * Formats an embedding error message based on the error type and context + */ +export function formatEmbeddingError(error: any, maxRetries: number): Error { + const errorMessage = extractErrorMessage(error) + const statusCode = extractStatusCode(error) + + if (statusCode === 401) { + return new Error(t("embeddings:authenticationFailed")) + } else if (statusCode) { + return new Error(t("embeddings:failedWithStatus", { attempts: maxRetries, statusCode, errorMessage })) + } else { + return new Error(t("embeddings:failedWithError", { attempts: maxRetries, errorMessage })) + } +}