diff --git a/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts b/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts index 0353771f601..ecde7691515 100644 --- a/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts +++ b/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts @@ -30,11 +30,26 @@ vitest.mock("../../../../i18n", () => ({ "embeddings:textExceedsTokenLimit": `Text at index ${params?.index} exceeds maximum token limit (${params?.itemTokens} > ${params?.maxTokens}). Skipping.`, "embeddings:rateLimitRetry": `Rate limit hit, retrying in ${params?.delayMs}ms (attempt ${params?.attempt}/${params?.maxRetries})`, "embeddings:unknownError": "Unknown error", + "common:errors.api.invalidKeyInvalidChars": + "API key contains invalid characters. Please check your API key for special characters.", } return translations[key] || key }, })) +// Mock i18n/setup module used by the error handler +vitest.mock("../../../../i18n/setup", () => ({ + default: { + t: (key: string) => { + const translations: Record = { + "common:errors.api.invalidKeyInvalidChars": + "API key contains invalid characters. Please check your API key for special characters.", + } + return translations[key] || key + }, + }, +})) + const MockedOpenAI = OpenAI as MockedClass describe("OpenAICompatibleEmbedder", () => { @@ -114,6 +129,22 @@ describe("OpenAICompatibleEmbedder", () => { "embeddings:validation.baseUrlRequired", ) }) + + it("should handle API key with invalid characters (ByteString conversion error)", () => { + // API key with special characters that cause ByteString conversion error + const invalidApiKey = "sk-test•invalid" // Contains bullet character (U+2022) + + // Mock the OpenAI constructor to throw ByteString error + MockedOpenAI.mockImplementationOnce(() => { + throw new Error( + "Cannot convert argument to a ByteString because the character at index 7 has a value of 8226 which is greater than 255.", + ) + }) + + expect(() => new OpenAICompatibleEmbedder(testBaseUrl, invalidApiKey, testModelId)).toThrow( + "API key contains invalid characters", + ) + }) }) describe("embedderInfo", () => { diff --git a/src/services/code-index/embedders/openai-compatible.ts b/src/services/code-index/embedders/openai-compatible.ts index 06c4ba52823..6eaf2b6c2c1 100644 --- a/src/services/code-index/embedders/openai-compatible.ts +++ b/src/services/code-index/embedders/openai-compatible.ts @@ -12,6 +12,7 @@ import { withValidationErrorHandling, HttpError, formatEmbeddingError } from ".. import { TelemetryEventName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { Mutex } from "async-mutex" +import { handleOpenAIError } from "../../../api/providers/utils/openai-error-handler" interface EmbeddingItem { embedding: string | number[] @@ -66,10 +67,18 @@ export class OpenAICompatibleEmbedder implements IEmbedder { this.baseUrl = baseUrl this.apiKey = apiKey - this.embeddingsClient = new OpenAI({ - baseURL: baseUrl, - apiKey: apiKey, - }) + + // Wrap OpenAI client creation to handle invalid API key characters + try { + this.embeddingsClient = new OpenAI({ + baseURL: baseUrl, + apiKey: apiKey, + }) + } catch (error) { + // Use the error handler to transform ByteString conversion errors + throw handleOpenAIError(error, "OpenAI Compatible") + } + this.defaultModelId = modelId || getDefaultModelId("openai-compatible") // Cache the URL type check for performance this.isFullUrl = this.isFullEndpointUrl(baseUrl) diff --git a/src/services/code-index/embedders/openai.ts b/src/services/code-index/embedders/openai.ts index 471c3fd090d..b993e280d98 100644 --- a/src/services/code-index/embedders/openai.ts +++ b/src/services/code-index/embedders/openai.ts @@ -13,6 +13,7 @@ import { t } from "../../../i18n" import { withValidationErrorHandling, formatEmbeddingError, HttpError } from "../shared/validation-helpers" import { TelemetryEventName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" +import { handleOpenAIError } from "../../../api/providers/utils/openai-error-handler" /** * OpenAI implementation of the embedder interface with batching and rate limiting @@ -28,7 +29,15 @@ export class OpenAiEmbedder extends OpenAiNativeHandler implements IEmbedder { constructor(options: ApiHandlerOptions & { openAiEmbeddingModelId?: string }) { super(options) const apiKey = this.options.openAiNativeApiKey ?? "not-provided" - this.embeddingsClient = new OpenAI({ apiKey }) + + // Wrap OpenAI client creation to handle invalid API key characters + try { + this.embeddingsClient = new OpenAI({ apiKey }) + } catch (error) { + // Use the error handler to transform ByteString conversion errors + throw handleOpenAIError(error, "OpenAI") + } + this.defaultModelId = options.openAiEmbeddingModelId || "text-embedding-3-small" }