diff --git a/src/i18n/locales/ca/embeddings.json b/src/i18n/locales/ca/embeddings.json index 5deed252bf..07535ca92d 100644 --- a/src/i18n/locales/ca/embeddings.json +++ b/src/i18n/locales/ca/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "El model d'Ollama no és capaç de fer incrustacions: {{modelId}}", "hostNotFound": "No s'ha trobat l'amfitrió d'Ollama: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "El servidor de LM Studio no s'està executant a {{baseUrl}}. Assegureu-vos que LM Studio s'estigui executant amb el servidor local activat.", + "modelNotFound": "No s'ha trobat el model \"{{modelId}}\" a LM Studio. Assegureu-vos que el model estigui carregat a LM Studio.", + "hostNotFound": "No es pot connectar a LM Studio a {{baseUrl}}. Comproveu la configuració de l'URL base." + }, "scanner": { "unknownErrorProcessingFile": "Error desconegut en processar el fitxer {{filePath}}", "unknownErrorDeletingPoints": "Error desconegut en eliminar els punts per a {{filePath}}", diff --git a/src/i18n/locales/de/embeddings.json b/src/i18n/locales/de/embeddings.json index 74381747e1..d7cb72cf3e 100644 --- a/src/i18n/locales/de/embeddings.json +++ b/src/i18n/locales/de/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Ollama-Modell ist nicht für Einbettungen geeignet: {{modelId}}", "hostNotFound": "Ollama-Host nicht gefunden: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "Der LM Studio-Server wird unter {{baseUrl}} nicht ausgeführt. Bitte stelle sicher, dass LM Studio mit aktiviertem lokalen Server läuft.", + "modelNotFound": "Modell \"{{modelId}}\" in LM Studio nicht gefunden. Bitte stelle sicher, dass das Modell in LM Studio geladen ist.", + "hostNotFound": "Verbindung zu LM Studio unter {{baseUrl}} nicht möglich. Bitte überprüfe die Konfiguration der Basis-URL." + }, "scanner": { "unknownErrorProcessingFile": "Unbekannter Fehler beim Verarbeiten der Datei {{filePath}}", "unknownErrorDeletingPoints": "Unbekannter Fehler beim Löschen der Punkte für {{filePath}}", diff --git a/src/i18n/locales/en/embeddings.json b/src/i18n/locales/en/embeddings.json index 96b3b2dbea..4def64d454 100644 --- a/src/i18n/locales/en/embeddings.json +++ b/src/i18n/locales/en/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Ollama model is not embedding capable: {{modelId}}", "hostNotFound": "Ollama host not found: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "LM Studio server is not running at {{baseUrl}}. Please ensure LM Studio is running with the local server enabled.", + "modelNotFound": "Model \"{{modelId}}\" not found in LM Studio. Please ensure the model is loaded in LM Studio.", + "hostNotFound": "Cannot connect to LM Studio at {{baseUrl}}. Please check the base URL configuration." + }, "scanner": { "unknownErrorProcessingFile": "Unknown error processing file {{filePath}}", "unknownErrorDeletingPoints": "Unknown error deleting points for {{filePath}}", diff --git a/src/i18n/locales/es/embeddings.json b/src/i18n/locales/es/embeddings.json index e47db420eb..acfd43b95b 100644 --- a/src/i18n/locales/es/embeddings.json +++ b/src/i18n/locales/es/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "El modelo Ollama no es capaz de realizar incrustaciones: {{modelId}}", "hostNotFound": "No se encuentra el host de Ollama: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "El servidor de LM Studio no se está ejecutando en {{baseUrl}}. Asegúrate de que LM Studio se esté ejecutando con el servidor local habilitado.", + "modelNotFound": "No se encontró el modelo \"{{modelId}}\" en LM Studio. Asegúrate de que el modelo esté cargado en LM Studio.", + "hostNotFound": "No se puede conectar a LM Studio en {{baseUrl}}. Comprueba la configuración de la URL base." + }, "scanner": { "unknownErrorProcessingFile": "Error desconocido procesando archivo {{filePath}}", "unknownErrorDeletingPoints": "Error desconocido eliminando puntos para {{filePath}}", diff --git a/src/i18n/locales/fr/embeddings.json b/src/i18n/locales/fr/embeddings.json index c63d3a7fbc..eb836706a1 100644 --- a/src/i18n/locales/fr/embeddings.json +++ b/src/i18n/locales/fr/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Le modèle Ollama n'est pas capable d'intégrer : {{modelId}}", "hostNotFound": "Hôte Ollama introuvable : {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "Le serveur LM Studio n'est pas en cours d'exécution à {{baseUrl}}. Veuillez vous assurer que LM Studio est en cours d'exécution avec le serveur local activé.", + "modelNotFound": "Le modèle \"{{modelId}}\" n'a pas été trouvé dans LM Studio. Veuillez vous assurer que le modèle est chargé dans LM Studio.", + "hostNotFound": "Impossible de se connecter à LM Studio à {{baseUrl}}. Veuillez vérifier la configuration de l'URL de base." + }, "scanner": { "unknownErrorProcessingFile": "Erreur inconnue lors du traitement du fichier {{filePath}}", "unknownErrorDeletingPoints": "Erreur inconnue lors de la suppression des points pour {{filePath}}", diff --git a/src/i18n/locales/hi/embeddings.json b/src/i18n/locales/hi/embeddings.json index 15709fd700..e44db49231 100644 --- a/src/i18n/locales/hi/embeddings.json +++ b/src/i18n/locales/hi/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "ओलामा मॉडल एम्बेडिंग में सक्षम नहीं है: {{modelId}}", "hostNotFound": "ओलामा होस्ट नहीं मिला: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "एलएम स्टूडियो सर्वर {{baseUrl}} पर नहीं चल रहा है। कृपया सुनिश्चित करें कि एलएम स्टूडियो स्थानीय सर्वर सक्षम के साथ चल रहा है।", + "modelNotFound": "एलएम स्टूडियो में मॉडल \"{{modelId}}\" नहीं मिला। कृपया सुनिश्चित करें कि मॉडल एलएम स्टूडियो में लोड किया गया है।", + "hostNotFound": "{{baseUrl}} पर एलएम स्टूडियो से कनेक्ट नहीं हो सकता। कृपया आधार यूआरएल कॉन्फ़िगरेशन की जांच करें।" + }, "scanner": { "unknownErrorProcessingFile": "फ़ाइल {{filePath}} प्रसंस्करण में अज्ञात त्रुटि", "unknownErrorDeletingPoints": "{{filePath}} के लिए बिंदु हटाने में अज्ञात त्रुटि", diff --git a/src/i18n/locales/id/embeddings.json b/src/i18n/locales/id/embeddings.json index e78d39d1ab..be49925b46 100644 --- a/src/i18n/locales/id/embeddings.json +++ b/src/i18n/locales/id/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Model Ollama tidak mampu melakukan embedding: {{modelId}}", "hostNotFound": "Host Ollama tidak ditemukan: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "Server LM Studio tidak berjalan di {{baseUrl}}. Pastikan LM Studio berjalan dengan server lokal diaktifkan.", + "modelNotFound": "Model \"{{modelId}}\" tidak ditemukan di LM Studio. Pastikan model dimuat di LM Studio.", + "hostNotFound": "Tidak dapat terhubung ke LM Studio di {{baseUrl}}. Silakan periksa konfigurasi URL dasar." + }, "scanner": { "unknownErrorProcessingFile": "Error tidak dikenal saat memproses file {{filePath}}", "unknownErrorDeletingPoints": "Error tidak dikenal saat menghapus points untuk {{filePath}}", diff --git a/src/i18n/locales/it/embeddings.json b/src/i18n/locales/it/embeddings.json index 679b17a25e..16e7e30640 100644 --- a/src/i18n/locales/it/embeddings.json +++ b/src/i18n/locales/it/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Il modello Ollama non è in grado di eseguire l'embedding: {{modelId}}", "hostNotFound": "Host Ollama non trovato: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "Il server di LM Studio non è in esecuzione su {{baseUrl}}. Assicurati che LM Studio sia in esecuzione con il server locale abilitato.", + "modelNotFound": "Modello \"{{modelId}}\" non trovato in LM Studio. Assicurati che il modello sia caricato in LM Studio.", + "hostNotFound": "Impossibile connettersi a LM Studio su {{baseUrl}}. Controlla la configurazione dell'URL di base." + }, "scanner": { "unknownErrorProcessingFile": "Errore sconosciuto nell'elaborazione del file {{filePath}}", "unknownErrorDeletingPoints": "Errore sconosciuto nell'eliminazione dei punti per {{filePath}}", diff --git a/src/i18n/locales/ja/embeddings.json b/src/i18n/locales/ja/embeddings.json index 89136eb1cc..dea8087395 100644 --- a/src/i18n/locales/ja/embeddings.json +++ b/src/i18n/locales/ja/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Ollamaモデルは埋め込みに対応していません:{{modelId}}", "hostNotFound": "Ollamaホストが見つかりません:{{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "LM Studioサーバーが{{baseUrl}}で実行されていません。LM Studioがローカルサーバーを有効にして実行されていることを確認してください。", + "modelNotFound": "モデル「{{modelId}}」がLM Studioで見つかりません。モデルがLM Studioにロードされていることを確認してください。", + "hostNotFound": "{{baseUrl}}のLM Studioに接続できません。ベースURLの構成を確認してください。" + }, "scanner": { "unknownErrorProcessingFile": "ファイル{{filePath}}の処理中に不明なエラーが発生しました", "unknownErrorDeletingPoints": "{{filePath}}のポイント削除中に不明なエラーが発生しました", diff --git a/src/i18n/locales/ko/embeddings.json b/src/i18n/locales/ko/embeddings.json index 7129883ad7..1bbb7cc5dd 100644 --- a/src/i18n/locales/ko/embeddings.json +++ b/src/i18n/locales/ko/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Ollama 모델은 임베딩이 불가능합니다: {{modelId}}", "hostNotFound": "Ollama 호스트를 찾을 수 없습니다: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "LM Studio 서버가 {{baseUrl}}에서 실행되고 있지 않습니다. 로컬 서버가 활성화된 상태로 LM Studio가 실행 중인지 확인하세요.", + "modelNotFound": "LM Studio에서 \"{{modelId}}\" 모델을 찾을 수 없습니다. 모델이 LM Studio에 로드되었는지 확인하세요.", + "hostNotFound": "{{baseUrl}}에서 LM Studio에 연결할 수 없습니다. 기본 URL 구성을 확인하세요." + }, "scanner": { "unknownErrorProcessingFile": "파일 {{filePath}} 처리 중 알 수 없는 오류", "unknownErrorDeletingPoints": "{{filePath}}의 포인트 삭제 중 알 수 없는 오류", diff --git a/src/i18n/locales/nl/embeddings.json b/src/i18n/locales/nl/embeddings.json index ede20774ac..71b450e5a4 100644 --- a/src/i18n/locales/nl/embeddings.json +++ b/src/i18n/locales/nl/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Ollama-model is niet in staat tot insluiten: {{modelId}}", "hostNotFound": "Ollama-host niet gevonden: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "LM Studio-server draait niet op {{baseUrl}}. Zorg ervoor dat LM Studio draait met de lokale server ingeschakeld.", + "modelNotFound": "Model \"{{modelId}}\" niet gevonden in LM Studio. Zorg ervoor dat het model in LM Studio is geladen.", + "hostNotFound": "Kan geen verbinding maken met LM Studio op {{baseUrl}}. Controleer de basis-URL-configuratie." + }, "scanner": { "unknownErrorProcessingFile": "Onbekende fout bij verwerken van bestand {{filePath}}", "unknownErrorDeletingPoints": "Onbekende fout bij verwijderen van punten voor {{filePath}}", diff --git a/src/i18n/locales/pl/embeddings.json b/src/i18n/locales/pl/embeddings.json index 70279021bd..cfa777bc64 100644 --- a/src/i18n/locales/pl/embeddings.json +++ b/src/i18n/locales/pl/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Model Ollama nie jest zdolny do osadzania: {{modelId}}", "hostNotFound": "Nie znaleziono hosta Ollama: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "Serwer LM Studio nie działa pod adresem {{baseUrl}}. Upewnij się, że LM Studio jest uruchomione z włączonym serwerem lokalnym.", + "modelNotFound": "Nie znaleziono modelu \"{{modelId}}\" w LM Studio. Upewnij się, że model jest załadowany w LM Studio.", + "hostNotFound": "Nie można połączyć się z LM Studio pod adresem {{baseUrl}}. Sprawdź konfigurację podstawowego adresu URL." + }, "scanner": { "unknownErrorProcessingFile": "Nieznany błąd podczas przetwarzania pliku {{filePath}}", "unknownErrorDeletingPoints": "Nieznany błąd podczas usuwania punktów dla {{filePath}}", diff --git a/src/i18n/locales/pt-BR/embeddings.json b/src/i18n/locales/pt-BR/embeddings.json index aea1bb5007..4089ac8c48 100644 --- a/src/i18n/locales/pt-BR/embeddings.json +++ b/src/i18n/locales/pt-BR/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "O modelo Ollama não é capaz de embedding: {{modelId}}", "hostNotFound": "Host Ollama não encontrado: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "O servidor do LM Studio não está em execução em {{baseUrl}}. Certifique-se de que o LM Studio esteja em execução com o servidor local ativado.", + "modelNotFound": "Modelo \"{{modelId}}\" não encontrado no LM Studio. Certifique-se de que o modelo esteja carregado no LM Studio.", + "hostNotFound": "Não é possível conectar-se ao LM Studio em {{baseUrl}}. Verifique a configuração do URL base." + }, "scanner": { "unknownErrorProcessingFile": "Erro desconhecido ao processar arquivo {{filePath}}", "unknownErrorDeletingPoints": "Erro desconhecido ao deletar pontos para {{filePath}}", diff --git a/src/i18n/locales/ru/embeddings.json b/src/i18n/locales/ru/embeddings.json index a724539b76..abae7892a4 100644 --- a/src/i18n/locales/ru/embeddings.json +++ b/src/i18n/locales/ru/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Модель Ollama не способна к вложению: {{modelId}}", "hostNotFound": "Хост Ollama не найден: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "Сервер LM Studio не запущен по адресу {{baseUrl}}. Убедитесь, что LM Studio запущен с включенным локальным сервером.", + "modelNotFound": "Модель \"{{modelId}}\" не найдена в LM Studio. Убедитесь, что модель загружена в LM Studio.", + "hostNotFound": "Не удается подключиться к LM Studio по адресу {{baseUrl}}. Проверьте конфигурацию базового URL-адреса." + }, "scanner": { "unknownErrorProcessingFile": "Неизвестная ошибка при обработке файла {{filePath}}", "unknownErrorDeletingPoints": "Неизвестная ошибка при удалении точек для {{filePath}}", diff --git a/src/i18n/locales/tr/embeddings.json b/src/i18n/locales/tr/embeddings.json index 3e115ce103..a09d2f12e1 100644 --- a/src/i18n/locales/tr/embeddings.json +++ b/src/i18n/locales/tr/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Ollama modeli gömme yeteneğine sahip değil: {{modelId}}", "hostNotFound": "Ollama ana bilgisayarı bulunamadı: {{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "LM Studio sunucusu {{baseUrl}} adresinde çalışmıyor. Lütfen LM Studio'nun yerel sunucu etkinken çalıştığından emin olun.", + "modelNotFound": "LM Studio'da \"{{modelId}}\" modeli bulunamadı. Lütfen modelin LM Studio'da yüklü olduğundan emin olun.", + "hostNotFound": "LM Studio'ya {{baseUrl}} adresinden bağlanılamıyor. Lütfen temel URL yapılandırmasını kontrol edin." + }, "scanner": { "unknownErrorProcessingFile": "{{filePath}} dosyası işlenirken bilinmeyen hata", "unknownErrorDeletingPoints": "{{filePath}} için noktalar silinirken bilinmeyen hata", diff --git a/src/i18n/locales/vi/embeddings.json b/src/i18n/locales/vi/embeddings.json index 9ef61105fa..509f4034d9 100644 --- a/src/i18n/locales/vi/embeddings.json +++ b/src/i18n/locales/vi/embeddings.json @@ -17,6 +17,11 @@ "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}}" }, + "lmstudio": { + "serviceNotRunning": "Máy chủ LM Studio không chạy tại {{baseUrl}}. Vui lòng đảm bảo LM Studio đang chạy với máy chủ cục bộ được bật.", + "modelNotFound": "Không tìm thấy mô hình \"{{modelId}}\" trong LM Studio. Vui lòng đảm bảo mô hình đã được tải trong LM Studio.", + "hostNotFound": "Không thể kết nối với LM Studio tại {{baseUrl}}. Vui lòng kiểm tra cấu hình URL cơ sở." + }, "scanner": { "unknownErrorProcessingFile": "Lỗi không xác định khi xử lý tệp {{filePath}}", "unknownErrorDeletingPoints": "Lỗi không xác định khi xóa điểm cho {{filePath}}", diff --git a/src/i18n/locales/zh-CN/embeddings.json b/src/i18n/locales/zh-CN/embeddings.json index d3ded6e5a2..ab296c2efb 100644 --- a/src/i18n/locales/zh-CN/embeddings.json +++ b/src/i18n/locales/zh-CN/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Ollama 模型不具备嵌入能力:{{modelId}}", "hostNotFound": "未找到 Ollama 主机:{{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "LM Studio 服务器未在 {{baseUrl}} 运行。请确保 LM Studio 已启用本地服务器并正在运行。", + "modelNotFound": "在 LM Studio 中未找到模型“{{modelId}}”。请确保该模型已在 LM Studio 中加载。", + "hostNotFound": "无法连接到 {{baseUrl}} 上的 LM Studio。请检查基本 URL 配置。" + }, "scanner": { "unknownErrorProcessingFile": "处理文件 {{filePath}} 时出现未知错误", "unknownErrorDeletingPoints": "删除 {{filePath}} 的数据点时出现未知错误", diff --git a/src/i18n/locales/zh-TW/embeddings.json b/src/i18n/locales/zh-TW/embeddings.json index 5ab5dcb292..a33790592c 100644 --- a/src/i18n/locales/zh-TW/embeddings.json +++ b/src/i18n/locales/zh-TW/embeddings.json @@ -17,6 +17,11 @@ "modelNotEmbeddingCapable": "Ollama 模型不具備內嵌能力:{{modelId}}", "hostNotFound": "找不到 Ollama 主機:{{baseUrl}}" }, + "lmstudio": { + "serviceNotRunning": "LM Studio 伺服器未在 {{baseUrl}} 執行。請確保 LM Studio 已啟用本機伺服器並正在執行。", + "modelNotFound": "在 LM Studio 中找不到模型「{{modelId}}」。請確保該模型已在 LM Studio 中載入。", + "hostNotFound": "無法連線至 {{baseUrl}} 上的 LM Studio。請檢查基礎 URL 設定。" + }, "scanner": { "unknownErrorProcessingFile": "處理檔案 {{filePath}} 時發生未知錯誤", "unknownErrorDeletingPoints": "刪除 {{filePath}} 的資料點時發生未知錯誤", diff --git a/src/services/code-index/embedders/__tests__/lmstudio.spec.ts b/src/services/code-index/embedders/__tests__/lmstudio.spec.ts new file mode 100644 index 0000000000..8c69c5e104 --- /dev/null +++ b/src/services/code-index/embedders/__tests__/lmstudio.spec.ts @@ -0,0 +1,210 @@ +import { vitest, describe, it, expect, beforeEach, afterEach } from "vitest" +import type { MockedFunction } from "vitest" +import { CodeIndexLmStudioEmbedder } from "../lmstudio" +import { OpenAI } from "openai" + +vitest.mock("openai", () => { + const mockEmbeddingsCreate = vitest.fn() + return { + OpenAI: vitest.fn().mockImplementation(() => ({ + embeddings: { + create: mockEmbeddingsCreate, + }, + })), + } +}) + +const consoleMocks = { + error: vitest.spyOn(console, "error").mockImplementation(() => {}), + warn: vitest.spyOn(console, "warn").mockImplementation(() => {}), +} + +describe("CodeIndexLmStudioEmbedder", () => { + let embedder: CodeIndexLmStudioEmbedder + let mockEmbeddingsCreate: MockedFunction + + beforeEach(() => { + vitest.clearAllMocks() + consoleMocks.error.mockClear() + consoleMocks.warn.mockClear() + + const MockedOpenAI = OpenAI as any + mockEmbeddingsCreate = vitest.fn() + MockedOpenAI.mockImplementation(() => ({ + embeddings: { + create: mockEmbeddingsCreate, + }, + })) + + embedder = new CodeIndexLmStudioEmbedder({ + lmStudioBaseUrl: "http://localhost:1234", + embeddingModelId: "text-embedding-nomic-embed-text-v1.5@f16", + }) + }) + + afterEach(() => { + vitest.clearAllMocks() + }) + + describe("constructor", () => { + it("should initialize with provided options", () => { + expect(embedder.embedderInfo.name).toBe("lmstudio") + }) + + it("should use default values when not provided", () => { + const embedderWithDefaults = new CodeIndexLmStudioEmbedder({}) + expect(embedderWithDefaults.embedderInfo.name).toBe("lmstudio") + }) + + it("should normalize base URL to include /v1", () => { + const embedderWithoutV1 = new CodeIndexLmStudioEmbedder({ + lmStudioBaseUrl: "http://localhost:1234", + }) + expect(embedderWithoutV1.embedderInfo.name).toBe("lmstudio") + + const embedderWithV1 = new CodeIndexLmStudioEmbedder({ + lmStudioBaseUrl: "http://localhost:1234/v1", + }) + expect(embedderWithV1.embedderInfo.name).toBe("lmstudio") + }) + }) + + 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-nomic-embed-text-v1.5@f16", + encoding_format: "float", + }) + }) + + it("should fail validation when response has no data", async () => { + const mockResponse = { + data: [], + usage: { prompt_tokens: 0, total_tokens: 0 }, + } + mockEmbeddingsCreate.mockResolvedValue(mockResponse) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("embeddings:validation.invalidResponse") + }) + + it("should fail validation when LM Studio is not running (ECONNREFUSED)", async () => { + const error = new Error("ECONNREFUSED") + ;(error as any).code = "ECONNREFUSED" + mockEmbeddingsCreate.mockRejectedValue(error) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("lmstudio.serviceNotRunning") + }) + + it("should fail validation when model is not found (404)", async () => { + const error = new Error("HTTP 404: Not Found") + ;(error as any).status = 404 + mockEmbeddingsCreate.mockRejectedValue(error) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("lmstudio.modelNotFound") + }) + + it("should fail validation when host is not found (ENOTFOUND)", async () => { + const error = new Error("ENOTFOUND") + ;(error as any).code = "ENOTFOUND" + mockEmbeddingsCreate.mockRejectedValue(error) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("lmstudio.hostNotFound") + }) + + it("should handle generic errors with standard error handling", async () => { + const error = new Error("Unknown error") + mockEmbeddingsCreate.mockRejectedValue(error) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + + expect(result.error).toBe("embeddings:validation.configurationError") + }) + + it("should handle fetch failed errors", async () => { + const error = new Error("fetch failed") + mockEmbeddingsCreate.mockRejectedValue(error) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("lmstudio.serviceNotRunning") + }) + }) + + describe("createEmbeddings", () => { + it("should create embeddings successfully", async () => { + const mockResponse = { + data: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }], + usage: { prompt_tokens: 10, total_tokens: 10 }, + } + mockEmbeddingsCreate.mockResolvedValue(mockResponse) + + const result = await embedder.createEmbeddings(["test1", "test2"]) + + expect(result.embeddings).toEqual([ + [0.1, 0.2, 0.3], + [0.4, 0.5, 0.6], + ]) + expect(result.usage).toEqual({ promptTokens: 10, totalTokens: 10 }) + }) + + it("should handle rate limit errors with retry", async () => { + const error = new Error("Rate limit exceeded") + ;(error as any).status = 429 + + mockEmbeddingsCreate.mockRejectedValueOnce(error) + + const mockResponse = { + data: [{ embedding: [0.1, 0.2, 0.3] }], + usage: { prompt_tokens: 5, total_tokens: 5 }, + } + mockEmbeddingsCreate.mockResolvedValueOnce(mockResponse) + + const result = await embedder.createEmbeddings(["test"]) + + expect(result.embeddings).toEqual([[0.1, 0.2, 0.3]]) + expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(2) + }) + + it("should skip texts that exceed token limit", async () => { + const longText = "a".repeat(100000) + const shortText = "short text" + + const mockResponse = { + data: [{ embedding: [0.1, 0.2, 0.3] }], + usage: { prompt_tokens: 3, total_tokens: 3 }, + } + mockEmbeddingsCreate.mockResolvedValue(mockResponse) + + const result = await embedder.createEmbeddings([longText, shortText]) + + expect(result.embeddings).toEqual([[0.1, 0.2, 0.3]]) + expect(consoleMocks.warn).toHaveBeenCalledWith(expect.stringContaining("exceeds maximum token limit")) + }) + }) +}) diff --git a/src/services/code-index/embedders/lmstudio.ts b/src/services/code-index/embedders/lmstudio.ts index bbcdd56835..d9ebb81bb8 100644 --- a/src/services/code-index/embedders/lmstudio.ts +++ b/src/services/code-index/embedders/lmstudio.ts @@ -7,6 +7,8 @@ import { MAX_BATCH_RETRIES as MAX_RETRIES, INITIAL_RETRY_DELAY_MS as INITIAL_DELAY_MS, } from "../constants" +import { withValidationErrorHandling, formatEmbeddingError, HttpError } from "../shared/validation-helpers" +import { t } from "../../../i18n" /** * LM Studio implementation of the embedder interface with batching and rate limiting. @@ -81,19 +83,10 @@ export class CodeIndexLmStudioEmbedder implements IEmbedder { } if (currentBatch.length > 0) { - try { - const batchResult = await this._embedBatchWithRetries(currentBatch, modelToUse) - - allEmbeddings.push(...batchResult.embeddings) - usage.promptTokens += batchResult.usage.promptTokens - usage.totalTokens += batchResult.usage.totalTokens - } catch (error) { - const batchInfo = `batch of ${currentBatch.length} documents (indices: ${processedIndices.join(", ")})` - console.error(`Failed to process ${batchInfo}:`, error) - throw new Error( - `Failed to create embeddings for ${batchInfo}: ${error instanceof Error ? error.message : "batch processing error"}`, - ) - } + const batchResult = await this._embedBatchWithRetries(currentBatch, modelToUse) + allEmbeddings.push(...batchResult.embeddings) + usage.promptTokens += batchResult.usage.promptTokens + usage.totalTokens += batchResult.usage.totalTokens } } @@ -115,7 +108,6 @@ export class CodeIndexLmStudioEmbedder implements IEmbedder { const response = await this.embeddingsClient.embeddings.create({ input: batchTexts, model: model, - encoding_format: "float", }) return { @@ -126,10 +118,11 @@ export class CodeIndexLmStudioEmbedder 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) await new Promise((resolve) => setTimeout(resolve, delayMs)) continue @@ -139,7 +132,86 @@ export class CodeIndexLmStudioEmbedder implements IEmbedder { } } - throw new Error(`Failed to create embeddings after ${MAX_RETRIES} attempts`) + throw new Error(t("embeddings:failedMaxAttempts", { attempts: MAX_RETRIES })) + } + + /** + * Validates the LM Studio embedder configuration by testing connectivity and model availability + * @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 + + try { + const response = await this.embeddingsClient.embeddings.create({ + input: testTexts, + model: modelToUse, + }) + + // Check if we got a valid response + if (!response.data || response.data.length === 0) { + return { + valid: false, + error: t("embeddings:validation.invalidResponse"), + } + } + + return { valid: true } + } catch (error: any) { + // Handle LM Studio specific errors + if (error?.message?.includes("ECONNREFUSED") || error?.code === "ECONNREFUSED") { + return { + valid: false, + error: t("embeddings:lmstudio.serviceNotRunning", { + baseUrl: this.options.lmStudioBaseUrl, + }), + } + } + + if (error?.status === 404 || error?.message?.includes("404")) { + return { + valid: false, + error: t("embeddings:lmstudio.modelNotFound", { modelId: modelToUse }), + } + } + + // Re-throw to let standard error handling take over + throw error + } + }, + "lmstudio", + { + beforeStandardHandling: (error: any) => { + // Handle LM Studio-specific connection errors + if ( + error?.message?.includes("fetch failed") || + error?.code === "ECONNREFUSED" || + error?.message?.includes("ECONNREFUSED") + ) { + return { + valid: false, + error: t("embeddings:lmstudio.serviceNotRunning", { + baseUrl: this.options.lmStudioBaseUrl, + }), + } + } + + if (error?.code === "ENOTFOUND" || error?.message?.includes("ENOTFOUND")) { + return { + valid: false, + error: t("embeddings:lmstudio.hostNotFound", { baseUrl: this.options.lmStudioBaseUrl }), + } + } + + // Let standard handling take over + return undefined + }, + }, + ) } get embedderInfo(): EmbedderInfo {