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
This commit is contained in:
Roo Code 2025-09-06 17:37:14 +00:00
parent 079b37a85d
commit 9263590c76
5 changed files with 219 additions and 29 deletions

View file

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

View file

@ -162,12 +162,68 @@ 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
const result = await embedder.validateConfiguration()
// Assert
expect(mockValidateConfiguration).toHaveBeenCalled()
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: "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 enhance connection error messages with API endpoint info", async () => {
// Arrange
embedder = new GeminiEmbedder("test-api-key")
mockValidateConfiguration.mockResolvedValue({
valid: false,
error: "connection refused",
})
// 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
@ -177,17 +233,38 @@ describe("GeminiEmbedder", () => {
expect(mockValidateConfiguration).toHaveBeenCalled()
expect(result).toEqual({
valid: false,
error: "embeddings:validation.authenticationFailed",
error: "Some other error",
})
})
it("should handle validation exceptions", async () => {
it("should handle validation exceptions with detailed error message", async () => {
// Arrange
embedder = new GeminiEmbedder("test-api-key")
mockValidateConfiguration.mockRejectedValue(new Error("Validation failed"))
embedder = new GeminiEmbedder("test-api-key", "test-model")
mockValidateConfiguration.mockRejectedValue(new Error("Network error"))
// 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("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")
})
})
})

View file

@ -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}).`,
}
}
}

View file

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

View file

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