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 <ricciodaniel98@gmail.com>
This commit is contained in:
Hannes Rudolph 2025-07-07 16:46:35 -06:00 committed by GitHub
parent 7645aad435
commit d4abe73875
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1801 additions and 119 deletions

View file

@ -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({

View file

@ -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ó."
}
}

View file

@ -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."
}
}

View file

@ -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."
}
}

View file

@ -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."
}
}

View file

@ -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."
}
}

View file

@ -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": "एम्बेडर सेवा से अमान्य प्रतिक्रिया। कृपया अपनी कॉन्फ़िगरेशन जांचें।"
}
}

View file

@ -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."
}
}

View file

@ -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."
}
}

View file

@ -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": "エンベッダーサービスからの無効な応答です。設定を確認してください。"
}
}

View file

@ -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": "임베더 서비스에서 잘못된 응답이 왔습니다. 구성을 확인하세요."
}
}

View file

@ -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."
}
}

View file

@ -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ę."
}
}

View file

@ -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."
}
}

View file

@ -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. Проверьте вашу конфигурацию."
}
}

View file

@ -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."
}
}

View file

@ -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."
}
}

View file

@ -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": "嵌入服务响应无效。请检查您的配置。"
}
}

View file

@ -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": "內嵌服務回應無效。請檢查您的組態。"
}
}

View file

@ -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<typeof CodeIndexServiceFactory>
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()
})
})
})

View file

@ -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")
})
})
})

View file

@ -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")
})
})
})

View file

@ -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<typeof fetch>
// Mock i18n
vitest.mock("../../../../i18n", () => ({
t: (key: string, params?: Record<string, any>) => {
const translations: Record<string, string> = {
"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<typeof fetch>
beforeEach(() => {
vitest.clearAllMocks()
consoleMocks.error.mockClear()
mockFetch = global.fetch as MockedFunction<typeof fetch>
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")
})
})
})

View file

@ -882,4 +882,133 @@ describe("OpenAICompatibleEmbedder", () => {
expect(mockCreate).toHaveBeenCalled()
})
})
describe("validateConfiguration", () => {
let embedder: OpenAICompatibleEmbedder
let mockFetch: MockedFunction<typeof fetch>
beforeEach(() => {
vitest.clearAllMocks()
// Reset and re-assign the global fetch mock
global.fetch = vitest.fn()
mockFetch = global.fetch as MockedFunction<typeof fetch>
})
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")
})
})
})

View file

@ -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")
})
})
})

View file

@ -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
*/

View file

@ -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",

View file

@ -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
*/

View file

@ -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",

View file

@ -10,6 +10,13 @@ export interface IEmbedder {
* @returns Promise resolving to an EmbeddingResponse
*/
createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse>
/**
* 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
}

View file

@ -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
}
}
}
}

View file

@ -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.")

View file

@ -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.
*/

View file

@ -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<T extends { valid: boolean; error?: string }>(
validationFn: () => Promise<T>,
embedderType: string,
customHandlers?: Parameters<typeof handleValidationError>[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 }))
}
}