From 9263590c7601739dd9a572f577509420815ec60c Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 6 Sep 2025 17:37:14 +0000 Subject: [PATCH] fix: improve Code Index error messages with detailed diagnostics - Enhanced validation-helpers.ts to preserve original error details - Added specific error messages for common configuration issues - Improved Gemini embedder error handling with provider-specific guidance - Added new i18n translations for detailed error messages - Updated tests to cover new error handling scenarios Fixes #7743 --- src/i18n/locales/en/embeddings.json | 6 +- .../embedders/__tests__/gemini.spec.ts | 93 +++++++++++++++++-- src/services/code-index/embedders/gemini.ts | 30 +++++- src/services/code-index/service-factory.ts | 39 +++++++- .../code-index/shared/validation-helpers.ts | 80 +++++++++++++--- 5 files changed, 219 insertions(+), 29 deletions(-) diff --git a/src/i18n/locales/en/embeddings.json b/src/i18n/locales/en/embeddings.json index fc902cadc1..aa16103c72 100644 --- a/src/i18n/locales/en/embeddings.json +++ b/src/i18n/locales/en/embeddings.json @@ -39,7 +39,11 @@ "invalidModel": "Invalid model. Please check your model configuration.", "invalidResponse": "Invalid response from embedder service. Please check your configuration.", "apiKeyRequired": "API key is required for this embedder", - "baseUrlRequired": "Base URL is required for this embedder" + "baseUrlRequired": "Base URL is required for this embedder", + "hostNotFound": "Cannot resolve host: {{url}}. Please verify the URL is correct and accessible.", + "connectionRefused": "Connection refused to {{url}}. Please ensure the service is running and accessible.", + "connectionTimeout": "Connection timed out. Please check your network connection and try again.", + "geminiAuthDetails": "For Gemini, ensure you have a valid API key from Google AI Studio (makersuite.google.com/app/apikey)." }, "serviceFactory": { "openAiConfigMissing": "OpenAI configuration missing for embedder creation", diff --git a/src/services/code-index/embedders/__tests__/gemini.spec.ts b/src/services/code-index/embedders/__tests__/gemini.spec.ts index d41a4dc1e9..9cc624f962 100644 --- a/src/services/code-index/embedders/__tests__/gemini.spec.ts +++ b/src/services/code-index/embedders/__tests__/gemini.spec.ts @@ -162,12 +162,12 @@ describe("GeminiEmbedder", () => { expect(result).toEqual({ valid: true }) }) - it("should pass through validation errors from OpenAICompatibleEmbedder", async () => { + it("should enhance authentication error messages with Gemini-specific guidance", async () => { // Arrange embedder = new GeminiEmbedder("test-api-key") mockValidateConfiguration.mockResolvedValue({ valid: false, - error: "embeddings:validation.authenticationFailed", + error: "Authentication failed (HTTP 401)", }) // Act @@ -175,19 +175,96 @@ describe("GeminiEmbedder", () => { // Assert expect(mockValidateConfiguration).toHaveBeenCalled() - expect(result).toEqual({ + expect(result.valid).toBe(false) + expect(result.error).toContain("Authentication failed (HTTP 401)") + expect(result.error).toContain("Google AI Studio") + expect(result.error).toContain("makersuite.google.com/app/apikey") + }) + + it("should enhance model error messages with supported models list", async () => { + // Arrange + embedder = new GeminiEmbedder("test-api-key", "invalid-model") + mockValidateConfiguration.mockResolvedValue({ valid: false, - error: "embeddings:validation.authenticationFailed", + error: "Model not found (HTTP 404)", }) + + // Act + const result = await embedder.validateConfiguration() + + // Assert + expect(result.valid).toBe(false) + expect(result.error).toContain("Model not found") + expect(result.error).toContain("text-embedding-004") + expect(result.error).toContain("gemini-embedding-001") + expect(result.error).toContain("dimension: 768") + expect(result.error).toContain("dimension: 2048") }) - it("should handle validation exceptions", async () => { + it("should enhance connection error messages with API endpoint info", async () => { // Arrange embedder = new GeminiEmbedder("test-api-key") - mockValidateConfiguration.mockRejectedValue(new Error("Validation failed")) + mockValidateConfiguration.mockResolvedValue({ + valid: false, + error: "connection refused", + }) - // Act & Assert - await expect(embedder.validateConfiguration()).rejects.toThrow("Validation failed") + // Act + const result = await embedder.validateConfiguration() + + // Assert + expect(result.valid).toBe(false) + expect(result.error).toContain("connection refused") + expect(result.error).toContain("https://generativelanguage.googleapis.com/v1beta/openai/") + }) + + it("should pass through validation errors without enhancement for non-specific errors", async () => { + // Arrange + embedder = new GeminiEmbedder("test-api-key") + mockValidateConfiguration.mockResolvedValue({ + valid: false, + error: "Some other error", + }) + + // Act + const result = await embedder.validateConfiguration() + + // Assert + expect(mockValidateConfiguration).toHaveBeenCalled() + expect(result).toEqual({ + valid: false, + error: "Some other error", + }) + }) + + it("should handle validation exceptions with detailed error message", async () => { + // Arrange + embedder = new GeminiEmbedder("test-api-key", "test-model") + mockValidateConfiguration.mockRejectedValue(new Error("Network error")) + + // Act + const result = await embedder.validateConfiguration() + + // Assert + expect(result.valid).toBe(false) + expect(result.error).toContain("Gemini embedder validation failed") + expect(result.error).toContain("Network error") + expect(result.error).toContain("test-model") + }) + + it("should handle non-Error exceptions gracefully", async () => { + // Arrange + embedder = new GeminiEmbedder("test-api-key", "test-model") + mockValidateConfiguration.mockRejectedValue("String error") + + // Act + const result = await embedder.validateConfiguration() + + // Assert + expect(result.valid).toBe(false) + expect(result.error).toContain("Gemini embedder validation failed") + expect(result.error).toContain("String error") + expect(result.error).toContain("test-model") }) }) }) diff --git a/src/services/code-index/embedders/gemini.ts b/src/services/code-index/embedders/gemini.ts index 7e795875c9..b83d2e5930 100644 --- a/src/services/code-index/embedders/gemini.ts +++ b/src/services/code-index/embedders/gemini.ts @@ -69,15 +69,39 @@ export class GeminiEmbedder implements IEmbedder { async validateConfiguration(): Promise<{ valid: boolean; error?: string }> { try { // Delegate validation to the OpenAI-compatible embedder - // The error messages will be specific to Gemini since we're using Gemini's base URL - return await this.openAICompatibleEmbedder.validateConfiguration() + const result = await this.openAICompatibleEmbedder.validateConfiguration() + + // If validation failed, enhance the error message with Gemini-specific guidance + if (!result.valid && result.error) { + // Check for common Gemini-specific issues + if ( + result.error.includes("401") || + result.error.includes("403") || + result.error.includes("Authentication") + ) { + result.error = `${result.error}. For Gemini, ensure you have a valid API key from Google AI Studio (makersuite.google.com/app/apikey) and that it's correctly configured.` + } else if (result.error.includes("404") || result.error.includes("model")) { + result.error = `${result.error}. Supported Gemini models: text-embedding-004 (dimension: 768), gemini-embedding-001 (dimension: 2048).` + } else if (result.error.includes("connection") || result.error.includes("host")) { + result.error = `${result.error}. Gemini API endpoint: ${GeminiEmbedder.GEMINI_BASE_URL}` + } + } + + return result } catch (error) { TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, location: "GeminiEmbedder:validateConfiguration", + modelId: this.modelId, }) - throw error + + // Provide a more informative error message + const errorMessage = error instanceof Error ? error.message : String(error) + return { + valid: false, + error: `Gemini embedder validation failed: ${errorMessage}. Please check your API key and model configuration (current model: ${this.modelId}).`, + } } } diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index 6d69e1f0b6..00b34d0ae8 100644 --- a/src/services/code-index/service-factory.ts +++ b/src/services/code-index/service-factory.ts @@ -93,19 +93,50 @@ export class CodeIndexServiceFactory { */ public async validateEmbedder(embedder: IEmbedder): Promise<{ valid: boolean; error?: string }> { try { - return await embedder.validateConfiguration() + const result = await embedder.validateConfiguration() + + // If validation failed but no specific error was provided, add context + if (!result.valid && !result.error) { + const config = this.configManager.getConfig() + result.error = + t("embeddings:validation.configurationError") + + ` (Provider: ${config.embedderProvider}, Model: ${config.modelId || "default"})` + } + + return result } catch (error) { - // Capture telemetry for the error + // Capture telemetry for the error with additional context + const config = this.configManager.getConfig() TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, location: "validateEmbedder", + provider: config.embedderProvider, + model: config.modelId, }) - // If validation throws an exception, preserve the original error message + // Provide detailed error message with context + let errorMessage = error instanceof Error ? error.message : String(error) + + // Add provider-specific guidance + if (config.embedderProvider === "gemini") { + if (errorMessage.includes("401") || errorMessage.includes("403") || errorMessage.includes("API key")) { + errorMessage += + ". Please ensure your Gemini API key is valid and correctly configured in the settings." + } + } else if (config.embedderProvider === "openai") { + if (errorMessage.includes("401") || errorMessage.includes("403")) { + errorMessage += ". Please check your OpenAI API key in the settings." + } + } else if (config.embedderProvider === "ollama") { + if (errorMessage.includes("connection") || errorMessage.includes("ECONNREFUSED")) { + errorMessage += ". Please ensure Ollama is running locally and accessible." + } + } + return { valid: false, - error: error instanceof Error ? error.message : "embeddings:validation.configurationError", + error: errorMessage, } } } diff --git a/src/services/code-index/shared/validation-helpers.ts b/src/services/code-index/shared/validation-helpers.ts index 6b043d44d3..42b2e9487d 100644 --- a/src/services/code-index/shared/validation-helpers.ts +++ b/src/services/code-index/shared/validation-helpers.ts @@ -167,34 +167,88 @@ export function handleValidationError( // Check for status-based errors first const statusError = getErrorMessageForStatus(statusCode, embedderType) if (statusError) { - return { valid: false, error: statusError } + // Include additional context for authentication errors + if (statusCode === 401 || statusCode === 403) { + const details = embedderType === "gemini" ? t("embeddings:validation.geminiAuthDetails") : "" + return { valid: false, error: details ? `${statusError} ${details}` : statusError } + } + // Include status code in error for better debugging + return { valid: false, error: `${statusError} (HTTP ${statusCode})` } } - // Check for connection errors + // Check for connection errors with more specific messages if (errorMessage) { - if ( - errorMessage.includes("ENOTFOUND") || - errorMessage.includes("ECONNREFUSED") || - errorMessage.includes("ETIMEDOUT") || - errorMessage === "AbortError" || - errorMessage.includes("HTTP 0:") || - errorMessage === "No response" - ) { + if (errorMessage.includes("ENOTFOUND")) { + const url = extractUrlFromError(errorMessage) + return { + valid: false, + error: t("embeddings:validation.hostNotFound", { url: url || "configured endpoint" }), + } + } + + if (errorMessage.includes("ECONNREFUSED")) { + const url = extractUrlFromError(errorMessage) + return { + valid: false, + error: t("embeddings:validation.connectionRefused", { url: url || "configured endpoint" }), + } + } + + if (errorMessage.includes("ETIMEDOUT")) { + return { + valid: false, + error: t("embeddings:validation.connectionTimeout"), + } + } + + if (errorMessage === "AbortError" || errorMessage.includes("HTTP 0:") || errorMessage === "No response") { return { valid: false, error: t("embeddings:validation.connectionFailed") } } if (errorMessage.includes("Failed to parse response JSON")) { return { valid: false, error: t("embeddings:validation.invalidResponse") } } + + // Check for API key related errors + if (errorMessage.includes("API key") || errorMessage.includes("api key") || errorMessage.includes("api-key")) { + return { valid: false, error: t("embeddings:validation.invalidApiKey") } + } + + // Check for model-related errors + if ( + errorMessage.includes("model") && + (errorMessage.includes("not found") || errorMessage.includes("does not exist")) + ) { + return { valid: false, error: t("embeddings:validation.modelNotAvailable") } + } } // 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 } + // Provide more context with the error + return { + valid: false, + error: `${t("embeddings:validation.configurationError")}: ${errorMessage}`, + } } - // Fallback to generic error - return { valid: false, error: t("embeddings:validation.configurationError") } + // Fallback to generic error with embedder type for context + return { + valid: false, + error: `${t("embeddings:validation.configurationError")} (${embedderType})`, + } +} + +/** + * Extracts URL from error message if present + */ +function extractUrlFromError(errorMessage: string): string | undefined { + // Try to extract URL from common error patterns + const urlMatch = errorMessage.match(/(?:https?:\/\/[^\s]+)|(?:getaddrinfo\s+\w+\s+([^\s]+))/i) + if (urlMatch) { + return urlMatch[1] || urlMatch[0] + } + return undefined } /**