Skip to content

Commit 9263590

Browse files
committed
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
1 parent 079b37a commit 9263590

File tree

5 files changed

+219
-29
lines changed

5 files changed

+219
-29
lines changed

src/i18n/locales/en/embeddings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@
3939
"invalidModel": "Invalid model. Please check your model configuration.",
4040
"invalidResponse": "Invalid response from embedder service. Please check your configuration.",
4141
"apiKeyRequired": "API key is required for this embedder",
42-
"baseUrlRequired": "Base URL is required for this embedder"
42+
"baseUrlRequired": "Base URL is required for this embedder",
43+
"hostNotFound": "Cannot resolve host: {{url}}. Please verify the URL is correct and accessible.",
44+
"connectionRefused": "Connection refused to {{url}}. Please ensure the service is running and accessible.",
45+
"connectionTimeout": "Connection timed out. Please check your network connection and try again.",
46+
"geminiAuthDetails": "For Gemini, ensure you have a valid API key from Google AI Studio (makersuite.google.com/app/apikey)."
4347
},
4448
"serviceFactory": {
4549
"openAiConfigMissing": "OpenAI configuration missing for embedder creation",

src/services/code-index/embedders/__tests__/gemini.spec.ts

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,32 +162,109 @@ describe("GeminiEmbedder", () => {
162162
expect(result).toEqual({ valid: true })
163163
})
164164

165-
it("should pass through validation errors from OpenAICompatibleEmbedder", async () => {
165+
it("should enhance authentication error messages with Gemini-specific guidance", async () => {
166166
// Arrange
167167
embedder = new GeminiEmbedder("test-api-key")
168168
mockValidateConfiguration.mockResolvedValue({
169169
valid: false,
170-
error: "embeddings:validation.authenticationFailed",
170+
error: "Authentication failed (HTTP 401)",
171171
})
172172

173173
// Act
174174
const result = await embedder.validateConfiguration()
175175

176176
// Assert
177177
expect(mockValidateConfiguration).toHaveBeenCalled()
178-
expect(result).toEqual({
178+
expect(result.valid).toBe(false)
179+
expect(result.error).toContain("Authentication failed (HTTP 401)")
180+
expect(result.error).toContain("Google AI Studio")
181+
expect(result.error).toContain("makersuite.google.com/app/apikey")
182+
})
183+
184+
it("should enhance model error messages with supported models list", async () => {
185+
// Arrange
186+
embedder = new GeminiEmbedder("test-api-key", "invalid-model")
187+
mockValidateConfiguration.mockResolvedValue({
179188
valid: false,
180-
error: "embeddings:validation.authenticationFailed",
189+
error: "Model not found (HTTP 404)",
181190
})
191+
192+
// Act
193+
const result = await embedder.validateConfiguration()
194+
195+
// Assert
196+
expect(result.valid).toBe(false)
197+
expect(result.error).toContain("Model not found")
198+
expect(result.error).toContain("text-embedding-004")
199+
expect(result.error).toContain("gemini-embedding-001")
200+
expect(result.error).toContain("dimension: 768")
201+
expect(result.error).toContain("dimension: 2048")
182202
})
183203

184-
it("should handle validation exceptions", async () => {
204+
it("should enhance connection error messages with API endpoint info", async () => {
185205
// Arrange
186206
embedder = new GeminiEmbedder("test-api-key")
187-
mockValidateConfiguration.mockRejectedValue(new Error("Validation failed"))
207+
mockValidateConfiguration.mockResolvedValue({
208+
valid: false,
209+
error: "connection refused",
210+
})
188211

189-
// Act & Assert
190-
await expect(embedder.validateConfiguration()).rejects.toThrow("Validation failed")
212+
// Act
213+
const result = await embedder.validateConfiguration()
214+
215+
// Assert
216+
expect(result.valid).toBe(false)
217+
expect(result.error).toContain("connection refused")
218+
expect(result.error).toContain("https://generativelanguage.googleapis.com/v1beta/openai/")
219+
})
220+
221+
it("should pass through validation errors without enhancement for non-specific errors", async () => {
222+
// Arrange
223+
embedder = new GeminiEmbedder("test-api-key")
224+
mockValidateConfiguration.mockResolvedValue({
225+
valid: false,
226+
error: "Some other error",
227+
})
228+
229+
// Act
230+
const result = await embedder.validateConfiguration()
231+
232+
// Assert
233+
expect(mockValidateConfiguration).toHaveBeenCalled()
234+
expect(result).toEqual({
235+
valid: false,
236+
error: "Some other error",
237+
})
238+
})
239+
240+
it("should handle validation exceptions with detailed error message", async () => {
241+
// Arrange
242+
embedder = new GeminiEmbedder("test-api-key", "test-model")
243+
mockValidateConfiguration.mockRejectedValue(new Error("Network error"))
244+
245+
// Act
246+
const result = await embedder.validateConfiguration()
247+
248+
// Assert
249+
expect(result.valid).toBe(false)
250+
expect(result.error).toContain("Gemini embedder validation failed")
251+
expect(result.error).toContain("Network error")
252+
expect(result.error).toContain("test-model")
253+
})
254+
255+
it("should handle non-Error exceptions gracefully", async () => {
256+
// Arrange
257+
embedder = new GeminiEmbedder("test-api-key", "test-model")
258+
mockValidateConfiguration.mockRejectedValue("String error")
259+
260+
// Act
261+
const result = await embedder.validateConfiguration()
262+
263+
// Assert
264+
expect(result.valid).toBe(false)
265+
expect(result.error).toContain("Gemini embedder validation failed")
266+
expect(result.error).toContain("String error")
267+
expect(result.error).toContain("test-model")
191268
})
192269
})
193270
})

src/services/code-index/embedders/gemini.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,39 @@ export class GeminiEmbedder implements IEmbedder {
6969
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
7070
try {
7171
// Delegate validation to the OpenAI-compatible embedder
72-
// The error messages will be specific to Gemini since we're using Gemini's base URL
73-
return await this.openAICompatibleEmbedder.validateConfiguration()
72+
const result = await this.openAICompatibleEmbedder.validateConfiguration()
73+
74+
// If validation failed, enhance the error message with Gemini-specific guidance
75+
if (!result.valid && result.error) {
76+
// Check for common Gemini-specific issues
77+
if (
78+
result.error.includes("401") ||
79+
result.error.includes("403") ||
80+
result.error.includes("Authentication")
81+
) {
82+
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.`
83+
} else if (result.error.includes("404") || result.error.includes("model")) {
84+
result.error = `${result.error}. Supported Gemini models: text-embedding-004 (dimension: 768), gemini-embedding-001 (dimension: 2048).`
85+
} else if (result.error.includes("connection") || result.error.includes("host")) {
86+
result.error = `${result.error}. Gemini API endpoint: ${GeminiEmbedder.GEMINI_BASE_URL}`
87+
}
88+
}
89+
90+
return result
7491
} catch (error) {
7592
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
7693
error: error instanceof Error ? error.message : String(error),
7794
stack: error instanceof Error ? error.stack : undefined,
7895
location: "GeminiEmbedder:validateConfiguration",
96+
modelId: this.modelId,
7997
})
80-
throw error
98+
99+
// Provide a more informative error message
100+
const errorMessage = error instanceof Error ? error.message : String(error)
101+
return {
102+
valid: false,
103+
error: `Gemini embedder validation failed: ${errorMessage}. Please check your API key and model configuration (current model: ${this.modelId}).`,
104+
}
81105
}
82106
}
83107

src/services/code-index/service-factory.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,19 +93,50 @@ export class CodeIndexServiceFactory {
9393
*/
9494
public async validateEmbedder(embedder: IEmbedder): Promise<{ valid: boolean; error?: string }> {
9595
try {
96-
return await embedder.validateConfiguration()
96+
const result = await embedder.validateConfiguration()
97+
98+
// If validation failed but no specific error was provided, add context
99+
if (!result.valid && !result.error) {
100+
const config = this.configManager.getConfig()
101+
result.error =
102+
t("embeddings:validation.configurationError") +
103+
` (Provider: ${config.embedderProvider}, Model: ${config.modelId || "default"})`
104+
}
105+
106+
return result
97107
} catch (error) {
98-
// Capture telemetry for the error
108+
// Capture telemetry for the error with additional context
109+
const config = this.configManager.getConfig()
99110
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
100111
error: error instanceof Error ? error.message : String(error),
101112
stack: error instanceof Error ? error.stack : undefined,
102113
location: "validateEmbedder",
114+
provider: config.embedderProvider,
115+
model: config.modelId,
103116
})
104117

105-
// If validation throws an exception, preserve the original error message
118+
// Provide detailed error message with context
119+
let errorMessage = error instanceof Error ? error.message : String(error)
120+
121+
// Add provider-specific guidance
122+
if (config.embedderProvider === "gemini") {
123+
if (errorMessage.includes("401") || errorMessage.includes("403") || errorMessage.includes("API key")) {
124+
errorMessage +=
125+
". Please ensure your Gemini API key is valid and correctly configured in the settings."
126+
}
127+
} else if (config.embedderProvider === "openai") {
128+
if (errorMessage.includes("401") || errorMessage.includes("403")) {
129+
errorMessage += ". Please check your OpenAI API key in the settings."
130+
}
131+
} else if (config.embedderProvider === "ollama") {
132+
if (errorMessage.includes("connection") || errorMessage.includes("ECONNREFUSED")) {
133+
errorMessage += ". Please ensure Ollama is running locally and accessible."
134+
}
135+
}
136+
106137
return {
107138
valid: false,
108-
error: error instanceof Error ? error.message : "embeddings:validation.configurationError",
139+
error: errorMessage,
109140
}
110141
}
111142
}

src/services/code-index/shared/validation-helpers.ts

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -167,34 +167,88 @@ export function handleValidationError(
167167
// Check for status-based errors first
168168
const statusError = getErrorMessageForStatus(statusCode, embedderType)
169169
if (statusError) {
170-
return { valid: false, error: statusError }
170+
// Include additional context for authentication errors
171+
if (statusCode === 401 || statusCode === 403) {
172+
const details = embedderType === "gemini" ? t("embeddings:validation.geminiAuthDetails") : ""
173+
return { valid: false, error: details ? `${statusError} ${details}` : statusError }
174+
}
175+
// Include status code in error for better debugging
176+
return { valid: false, error: `${statusError} (HTTP ${statusCode})` }
171177
}
172178

173-
// Check for connection errors
179+
// Check for connection errors with more specific messages
174180
if (errorMessage) {
175-
if (
176-
errorMessage.includes("ENOTFOUND") ||
177-
errorMessage.includes("ECONNREFUSED") ||
178-
errorMessage.includes("ETIMEDOUT") ||
179-
errorMessage === "AbortError" ||
180-
errorMessage.includes("HTTP 0:") ||
181-
errorMessage === "No response"
182-
) {
181+
if (errorMessage.includes("ENOTFOUND")) {
182+
const url = extractUrlFromError(errorMessage)
183+
return {
184+
valid: false,
185+
error: t("embeddings:validation.hostNotFound", { url: url || "configured endpoint" }),
186+
}
187+
}
188+
189+
if (errorMessage.includes("ECONNREFUSED")) {
190+
const url = extractUrlFromError(errorMessage)
191+
return {
192+
valid: false,
193+
error: t("embeddings:validation.connectionRefused", { url: url || "configured endpoint" }),
194+
}
195+
}
196+
197+
if (errorMessage.includes("ETIMEDOUT")) {
198+
return {
199+
valid: false,
200+
error: t("embeddings:validation.connectionTimeout"),
201+
}
202+
}
203+
204+
if (errorMessage === "AbortError" || errorMessage.includes("HTTP 0:") || errorMessage === "No response") {
183205
return { valid: false, error: t("embeddings:validation.connectionFailed") }
184206
}
185207

186208
if (errorMessage.includes("Failed to parse response JSON")) {
187209
return { valid: false, error: t("embeddings:validation.invalidResponse") }
188210
}
211+
212+
// Check for API key related errors
213+
if (errorMessage.includes("API key") || errorMessage.includes("api key") || errorMessage.includes("api-key")) {
214+
return { valid: false, error: t("embeddings:validation.invalidApiKey") }
215+
}
216+
217+
// Check for model-related errors
218+
if (
219+
errorMessage.includes("model") &&
220+
(errorMessage.includes("not found") || errorMessage.includes("does not exist"))
221+
) {
222+
return { valid: false, error: t("embeddings:validation.modelNotAvailable") }
223+
}
189224
}
190225

191226
// For generic errors, preserve the original error message if it's not a standard one
192227
if (errorMessage && errorMessage !== "Unknown error") {
193-
return { valid: false, error: errorMessage }
228+
// Provide more context with the error
229+
return {
230+
valid: false,
231+
error: `${t("embeddings:validation.configurationError")}: ${errorMessage}`,
232+
}
194233
}
195234

196-
// Fallback to generic error
197-
return { valid: false, error: t("embeddings:validation.configurationError") }
235+
// Fallback to generic error with embedder type for context
236+
return {
237+
valid: false,
238+
error: `${t("embeddings:validation.configurationError")} (${embedderType})`,
239+
}
240+
}
241+
242+
/**
243+
* Extracts URL from error message if present
244+
*/
245+
function extractUrlFromError(errorMessage: string): string | undefined {
246+
// Try to extract URL from common error patterns
247+
const urlMatch = errorMessage.match(/(?:https?:\/\/[^\s]+)|(?:getaddrinfo\s+\w+\s+([^\s]+))/i)
248+
if (urlMatch) {
249+
return urlMatch[1] || urlMatch[0]
250+
}
251+
return undefined
198252
}
199253

200254
/**

0 commit comments

Comments
 (0)