From ce0fc7a7178d404337d85719d4e2b5184be7d726 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 1 Sep 2025 22:54:51 +0000 Subject: [PATCH 1/8] fix: add API key validation for non-ASCII characters in OpenAI-compatible providers - Created shared validation utility to check for non-ASCII characters in API keys - Added validation to all OpenAI-compatible providers to prevent ByteString conversion errors - Provides clear error messages when invalid characters are detected - Fixes issue #7483 where non-ASCII characters in API keys caused cryptic errors --- .../base-openai-compatible-provider.ts | 4 + src/api/providers/huggingface.ts | 4 + src/api/providers/lm-studio.ts | 7 +- src/api/providers/ollama.ts | 4 + src/api/providers/openai-native.ts | 5 + src/api/providers/openai.ts | 4 + src/api/providers/openrouter.ts | 4 + src/api/providers/requesty.ts | 8 +- src/api/providers/router-provider.ts | 4 + .../__tests__/api-key-validation.spec.ts | 93 +++++++++++++++++++ src/api/providers/utils/api-key-validation.ts | 52 +++++++++++ src/api/providers/xai.ts | 20 +++- 12 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 src/api/providers/utils/__tests__/api-key-validation.spec.ts create mode 100644 src/api/providers/utils/api-key-validation.ts diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index d079e22a1c43..0c7828a71a92 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -10,6 +10,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" +import { validateApiKeyForByteString } from "./utils/api-key-validation" type BaseOpenAiCompatibleProviderOptions = ApiHandlerOptions & { providerName: string @@ -55,6 +56,9 @@ export abstract class BaseOpenAiCompatibleProvider throw new Error("API key is required") } + // Validate API key for ByteString compatibility + validateApiKeyForByteString(this.options.apiKey, this.providerName) + this.client = new OpenAI({ baseURL, apiKey: this.options.apiKey, diff --git a/src/api/providers/huggingface.ts b/src/api/providers/huggingface.ts index aa158654c9af..95a9b7119ae9 100644 --- a/src/api/providers/huggingface.ts +++ b/src/api/providers/huggingface.ts @@ -8,6 +8,7 @@ import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from ". import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import { getHuggingFaceModels, getCachedHuggingFaceModels } from "./fetchers/huggingface" +import { validateApiKeyForByteString } from "./utils/api-key-validation" export class HuggingFaceHandler extends BaseProvider implements SingleCompletionHandler { private client: OpenAI @@ -22,6 +23,9 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion throw new Error("Hugging Face API key is required") } + // Validate API key for ByteString compatibility + validateApiKeyForByteString(this.options.huggingFaceApiKey, "HuggingFace") + this.client = new OpenAI({ baseURL: "https://router.huggingface.co/v1", apiKey: this.options.huggingFaceApiKey, diff --git a/src/api/providers/lm-studio.ts b/src/api/providers/lm-studio.ts index f3af46d1cec0..23d98616d8d2 100644 --- a/src/api/providers/lm-studio.ts +++ b/src/api/providers/lm-studio.ts @@ -15,6 +15,7 @@ import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { getModels, getModelsFromCache } from "./fetchers/modelCache" import { getApiRequestTimeout } from "./utils/timeout-config" +import { validateApiKeyForByteString } from "./utils/api-key-validation" export class LmStudioHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions @@ -24,9 +25,13 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan super() this.options = options + // LM Studio uses "noop" as a placeholder API key, but we should still validate if a real key is provided + const apiKey = "noop" + validateApiKeyForByteString(apiKey, "LM Studio") + this.client = new OpenAI({ baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1", - apiKey: "noop", + apiKey: apiKey, timeout: getApiRequestTimeout(), }) } diff --git a/src/api/providers/ollama.ts b/src/api/providers/ollama.ts index 75895908e9cf..ef920d35ce06 100644 --- a/src/api/providers/ollama.ts +++ b/src/api/providers/ollama.ts @@ -14,6 +14,7 @@ import { ApiStream } from "../transform/stream" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { getApiRequestTimeout } from "./utils/timeout-config" +import { validateApiKeyForByteString } from "./utils/api-key-validation" type CompletionUsage = OpenAI.Chat.Completions.ChatCompletionChunk["usage"] @@ -29,6 +30,9 @@ export class OllamaHandler extends BaseProvider implements SingleCompletionHandl // Otherwise use "ollama" as a placeholder for local instances const apiKey = this.options.ollamaApiKey || "ollama" + // Validate API key for ByteString compatibility + validateApiKeyForByteString(apiKey, "Ollama") + const headers: Record = {} if (this.options.ollamaApiKey) { headers["Authorization"] = `Bearer ${this.options.ollamaApiKey}` diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index c884091c02a6..24b3d82fd2c3 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -22,6 +22,7 @@ import { getModelParams } from "../transform/model-params" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import { validateApiKeyForByteString } from "./utils/api-key-validation" export type OpenAiNativeModel = ReturnType @@ -59,6 +60,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio this.options.enableGpt5ReasoningSummary = true } const apiKey = this.options.openAiNativeApiKey ?? "not-provided" + + // Validate API key for ByteString compatibility + validateApiKeyForByteString(apiKey, "OpenAI Native") + this.client = new OpenAI({ baseURL: this.options.openAiNativeBaseUrl, apiKey }) } diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 36158d770c17..a50b8324c26c 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -24,6 +24,7 @@ import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { getApiRequestTimeout } from "./utils/timeout-config" +import { validateApiKeyForByteString } from "./utils/api-key-validation" // TODO: Rename this to OpenAICompatibleHandler. Also, I think the // `OpenAINativeHandler` can subclass from this, since it's obviously @@ -42,6 +43,9 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const urlHost = this._getUrlHost(this.options.openAiBaseUrl) const isAzureOpenAi = urlHost === "azure.com" || urlHost.endsWith(".azure.com") || options.openAiUseAzure + // Validate API key for ByteString compatibility + validateApiKeyForByteString(apiKey, "OpenAI") + const headers = { ...DEFAULT_HEADERS, ...(this.options.openAiHeaders || {}), diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 208ba563c644..635c5950d60f 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -25,6 +25,7 @@ import { getModelEndpoints } from "./fetchers/modelEndpointCache" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler } from "../index" +import { validateApiKeyForByteString } from "./utils/api-key-validation" // Image generation types interface ImageGenerationResponse { @@ -93,6 +94,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH const baseURL = this.options.openRouterBaseUrl || "https://openrouter.ai/api/v1" const apiKey = this.options.openRouterApiKey ?? "not-provided" + // Validate API key for ByteString compatibility + validateApiKeyForByteString(apiKey, "OpenRouter") + this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: DEFAULT_HEADERS }) } diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index 0661cebe0989..388f97e0ca97 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -16,6 +16,7 @@ import { getModels } from "./fetchers/modelCache" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { toRequestyServiceUrl } from "../../shared/utils/requesty" +import { validateApiKeyForByteString } from "./utils/api-key-validation" // Requesty usage includes an extra field for Anthropic use cases. // Safely cast the prompt token details section to the appropriate structure. @@ -49,9 +50,14 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan this.options = options this.baseURL = toRequestyServiceUrl(options.requestyBaseUrl) + const apiKey = this.options.requestyApiKey ?? "not-provided" + + // Validate API key for ByteString compatibility + validateApiKeyForByteString(apiKey, "Requesty") + this.client = new OpenAI({ baseURL: this.baseURL, - apiKey: this.options.requestyApiKey ?? "not-provided", + apiKey: apiKey, defaultHeaders: DEFAULT_HEADERS, }) } diff --git a/src/api/providers/router-provider.ts b/src/api/providers/router-provider.ts index 25e9a11e1b2c..ace206353f29 100644 --- a/src/api/providers/router-provider.ts +++ b/src/api/providers/router-provider.ts @@ -8,6 +8,7 @@ import { BaseProvider } from "./base-provider" import { getModels } from "./fetchers/modelCache" import { DEFAULT_HEADERS } from "./constants" +import { validateApiKeyForByteString } from "./utils/api-key-validation" type RouterProviderOptions = { name: RouterName @@ -45,6 +46,9 @@ export abstract class RouterProvider extends BaseProvider { this.defaultModelId = defaultModelId this.defaultModelInfo = defaultModelInfo + // Validate API key for ByteString compatibility + validateApiKeyForByteString(apiKey, name) + this.client = new OpenAI({ baseURL, apiKey, diff --git a/src/api/providers/utils/__tests__/api-key-validation.spec.ts b/src/api/providers/utils/__tests__/api-key-validation.spec.ts new file mode 100644 index 000000000000..b3906339786a --- /dev/null +++ b/src/api/providers/utils/__tests__/api-key-validation.spec.ts @@ -0,0 +1,93 @@ +import { describe, it, expect } from "vitest" +import { validateApiKeyForByteString, validateApiKeyForAscii } from "../api-key-validation" + +describe("API Key Validation", () => { + describe("validateApiKeyForByteString", () => { + it("should accept valid ASCII characters", () => { + expect(() => validateApiKeyForByteString("abc123XYZ", "TestProvider")).not.toThrow() + expect(() => validateApiKeyForByteString("test-api-key_123", "TestProvider")).not.toThrow() + expect(() => validateApiKeyForByteString("!@#$%^&*()", "TestProvider")).not.toThrow() + }) + + it("should accept extended ASCII characters (128-255)", () => { + // Extended ASCII characters like ñ (241), ü (252) + expect(() => validateApiKeyForByteString("test\xF1\xFC", "TestProvider")).not.toThrow() + expect(() => validateApiKeyForByteString("key\xFF", "TestProvider")).not.toThrow() + }) + + it("should reject characters above 255", () => { + // Chinese character 中 (20013) + expect(() => validateApiKeyForByteString("test中key", "TestProvider")).toThrow( + "Invalid TestProvider API key: contains non-ASCII character at position 5", + ) + + // Emoji 😀 (128512) + expect(() => validateApiKeyForByteString("key😀", "TestProvider")).toThrow( + "Invalid TestProvider API key: contains non-ASCII character at position 4", + ) + + // Korean character 한 (54620) + expect(() => validateApiKeyForByteString("한글key", "TestProvider")).toThrow( + "Invalid TestProvider API key: contains non-ASCII character at position 1", + ) + }) + + it("should handle undefined and empty keys", () => { + expect(() => validateApiKeyForByteString(undefined, "TestProvider")).not.toThrow() + expect(() => validateApiKeyForByteString("", "TestProvider")).not.toThrow() + }) + + it("should provide clear error messages", () => { + expect(() => validateApiKeyForByteString("abc中def", "DeepSeek")).toThrow( + "Invalid DeepSeek API key: contains non-ASCII character at position 4. " + + "API keys must contain only ASCII characters (character codes 0-255). " + + "Please check your API key configuration.", + ) + }) + }) + + describe("validateApiKeyForAscii", () => { + it("should accept standard ASCII characters (0-127)", () => { + expect(() => validateApiKeyForAscii("abc123XYZ", "TestProvider")).not.toThrow() + expect(() => validateApiKeyForAscii("test-api-key_123", "TestProvider")).not.toThrow() + expect(() => validateApiKeyForAscii("!@#$%^&*()", "TestProvider")).not.toThrow() + }) + + it("should reject extended ASCII characters (128-255)", () => { + // Extended ASCII character ñ (241) + expect(() => validateApiKeyForAscii("test\xF1key", "TestProvider")).toThrow( + "Invalid TestProvider API key: contains non-ASCII character at position 5", + ) + + // Extended ASCII character ü (252) + expect(() => validateApiKeyForAscii("key\xFC", "TestProvider")).toThrow( + "Invalid TestProvider API key: contains non-ASCII character at position 4", + ) + }) + + it("should reject Unicode characters", () => { + // Chinese character 中 (20013) + expect(() => validateApiKeyForAscii("test中key", "TestProvider")).toThrow( + "Invalid TestProvider API key: contains non-ASCII character at position 5", + ) + + // Emoji 😀 (128512) + expect(() => validateApiKeyForAscii("key😀", "TestProvider")).toThrow( + "Invalid TestProvider API key: contains non-ASCII character at position 4", + ) + }) + + it("should handle undefined and empty keys", () => { + expect(() => validateApiKeyForAscii(undefined, "TestProvider")).not.toThrow() + expect(() => validateApiKeyForAscii("", "TestProvider")).not.toThrow() + }) + + it("should provide clear error messages", () => { + expect(() => validateApiKeyForAscii("abc\xF1def", "OpenAI")).toThrow( + "Invalid OpenAI API key: contains non-ASCII character at position 4. " + + "API keys must contain only standard ASCII characters (character codes 0-127). " + + "Please check your API key configuration.", + ) + }) + }) +}) diff --git a/src/api/providers/utils/api-key-validation.ts b/src/api/providers/utils/api-key-validation.ts new file mode 100644 index 000000000000..af0f794278e5 --- /dev/null +++ b/src/api/providers/utils/api-key-validation.ts @@ -0,0 +1,52 @@ +/** + * Validates that an API key contains only valid ByteString characters (0-255). + * The OpenAI client library requires API keys to be convertible to ByteString format, + * which only supports characters with values 0-255. + * + * @param apiKey - The API key to validate + * @param providerName - The name of the provider for error messaging + * @throws Error if the API key contains invalid characters + */ +export function validateApiKeyForByteString(apiKey: string | undefined, providerName: string): void { + if (!apiKey) { + return // No validation needed for undefined/empty keys + } + + // Check each character in the API key + for (let i = 0; i < apiKey.length; i++) { + const charCode = apiKey.charCodeAt(i) + if (charCode > 255) { + throw new Error( + `Invalid ${providerName} API key: contains non-ASCII character at position ${i + 1}. ` + + `API keys must contain only ASCII characters (character codes 0-255). ` + + `Please check your API key configuration.`, + ) + } + } +} + +/** + * Validates that an API key contains only standard ASCII characters (0-127). + * This is a stricter validation that only allows standard ASCII characters. + * + * @param apiKey - The API key to validate + * @param providerName - The name of the provider for error messaging + * @throws Error if the API key contains non-ASCII characters + */ +export function validateApiKeyForAscii(apiKey: string | undefined, providerName: string): void { + if (!apiKey) { + return // No validation needed for undefined/empty keys + } + + // Check each character in the API key + for (let i = 0; i < apiKey.length; i++) { + const charCode = apiKey.charCodeAt(i) + if (charCode > 127) { + throw new Error( + `Invalid ${providerName} API key: contains non-ASCII character at position ${i + 1}. ` + + `API keys must contain only standard ASCII characters (character codes 0-127). ` + + `Please check your API key configuration.`, + ) + } + } +} diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index 596c9e89b8ca..84f0e12465be 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -12,6 +12,7 @@ import { getModelParams } from "../transform/model-params" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import { validateApiKeyForByteString } from "./utils/api-key-validation" const XAI_DEFAULT_TEMPERATURE = 0 @@ -22,9 +23,15 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler constructor(options: ApiHandlerOptions) { super() this.options = options + + const apiKey = this.options.xaiApiKey ?? "not-provided" + + // Validate API key for ByteString compatibility + validateApiKeyForByteString(apiKey, "xAI") + this.client = new OpenAI({ baseURL: "https://api.x.ai/v1", - apiKey: this.options.xaiApiKey ?? "not-provided", + apiKey: apiKey, defaultHeaders: DEFAULT_HEADERS, }) } @@ -78,12 +85,15 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler if (chunk.usage) { // Extract detailed token information if available // First check for prompt_tokens_details structure (real API response) - const promptDetails = "prompt_tokens_details" in chunk.usage ? chunk.usage.prompt_tokens_details : null; - const cachedTokens = promptDetails && "cached_tokens" in promptDetails ? promptDetails.cached_tokens : 0; + const promptDetails = "prompt_tokens_details" in chunk.usage ? chunk.usage.prompt_tokens_details : null + const cachedTokens = promptDetails && "cached_tokens" in promptDetails ? promptDetails.cached_tokens : 0 // Fall back to direct fields in usage (used in test mocks) - const readTokens = cachedTokens || ("cache_read_input_tokens" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0); - const writeTokens = "cache_creation_input_tokens" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0; + const readTokens = + cachedTokens || + ("cache_read_input_tokens" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0) + const writeTokens = + "cache_creation_input_tokens" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0 yield { type: "usage", From 5f8874b6be5abf7252a9653035a0bd38f4e5e817 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Wed, 3 Sep 2025 17:51:29 -0500 Subject: [PATCH 2/8] feat: show localized 'API key contains invalid characters' at request-time; centralize in openai-error-handler; remove provider/index specifics; i18n: add invalidKeyInvalidChars, remove invalidKeyNonAscii --- .../base-openai-compatible-provider.ts | 17 ++-- src/api/providers/huggingface.ts | 32 ++++--- src/api/providers/lm-studio.ts | 19 +++- src/api/providers/ollama.ts | 45 +++++---- src/api/providers/openai-native.ts | 10 +- src/api/providers/openai.ts | 70 +++++++++----- src/api/providers/openrouter.ts | 19 ++-- src/api/providers/requesty.ts | 19 ++-- src/api/providers/router-provider.ts | 4 - .../__tests__/api-key-validation.spec.ts | 93 ------------------- src/api/providers/utils/api-key-validation.ts | 52 ----------- .../providers/utils/openai-error-handler.ts | 44 +++++++++ src/api/providers/xai.ts | 43 +++++---- src/i18n/locales/ca/common.json | 3 + src/i18n/locales/de/common.json | 3 + src/i18n/locales/en/common.json | 3 + src/i18n/locales/es/common.json | 3 + src/i18n/locales/fr/common.json | 3 + src/i18n/locales/hi/common.json | 3 + src/i18n/locales/id/common.json | 3 + src/i18n/locales/it/common.json | 3 + src/i18n/locales/ja/common.json | 3 + src/i18n/locales/ko/common.json | 3 + src/i18n/locales/nl/common.json | 3 + src/i18n/locales/pl/common.json | 3 + src/i18n/locales/pt-BR/common.json | 3 + src/i18n/locales/ru/common.json | 3 + src/i18n/locales/tr/common.json | 3 + src/i18n/locales/vi/common.json | 3 + src/i18n/locales/zh-CN/common.json | 3 + src/i18n/locales/zh-TW/common.json | 3 + 31 files changed, 264 insertions(+), 257 deletions(-) delete mode 100644 src/api/providers/utils/__tests__/api-key-validation.spec.ts delete mode 100644 src/api/providers/utils/api-key-validation.ts create mode 100644 src/api/providers/utils/openai-error-handler.ts diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index 0c7828a71a92..5d2b9425e7cf 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -10,7 +10,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" -import { validateApiKeyForByteString } from "./utils/api-key-validation" +import { handleOpenAIError } from "./utils/openai-error-handler" type BaseOpenAiCompatibleProviderOptions = ApiHandlerOptions & { providerName: string @@ -56,9 +56,6 @@ export abstract class BaseOpenAiCompatibleProvider throw new Error("API key is required") } - // Validate API key for ByteString compatibility - validateApiKeyForByteString(this.options.apiKey, this.providerName) - this.client = new OpenAI({ baseURL, apiKey: this.options.apiKey, @@ -90,7 +87,11 @@ export abstract class BaseOpenAiCompatibleProvider params.temperature = this.options.modelTemperature } - return this.client.chat.completions.create(params, requestOptions) + try { + return this.client.chat.completions.create(params, requestOptions) + } catch (error) { + throw handleOpenAIError(error, this.providerName) + } } override async *createMessage( @@ -131,11 +132,7 @@ export abstract class BaseOpenAiCompatibleProvider return response.choices[0]?.message.content || "" } catch (error) { - if (error instanceof Error) { - throw new Error(`${this.providerName} completion error: ${error.message}`) - } - - throw error + throw handleOpenAIError(error, this.providerName) } } diff --git a/src/api/providers/huggingface.ts b/src/api/providers/huggingface.ts index 95a9b7119ae9..f5b9c64eac4a 100644 --- a/src/api/providers/huggingface.ts +++ b/src/api/providers/huggingface.ts @@ -8,7 +8,7 @@ import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from ". import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import { getHuggingFaceModels, getCachedHuggingFaceModels } from "./fetchers/huggingface" -import { validateApiKeyForByteString } from "./utils/api-key-validation" +import { createOpenAIClientWithErrorHandling, handleOpenAIError } from "./utils/openai-error-handler" export class HuggingFaceHandler extends BaseProvider implements SingleCompletionHandler { private client: OpenAI @@ -23,14 +23,15 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion throw new Error("Hugging Face API key is required") } - // Validate API key for ByteString compatibility - validateApiKeyForByteString(this.options.huggingFaceApiKey, "HuggingFace") - - this.client = new OpenAI({ - baseURL: "https://router.huggingface.co/v1", - apiKey: this.options.huggingFaceApiKey, - defaultHeaders: DEFAULT_HEADERS, - }) + this.client = createOpenAIClientWithErrorHandling( + () => + new OpenAI({ + baseURL: "https://router.huggingface.co/v1", + apiKey: this.options.huggingFaceApiKey, + defaultHeaders: DEFAULT_HEADERS, + }), + "HuggingFace", + ) // Try to get cached models first this.modelCache = getCachedHuggingFaceModels() @@ -68,7 +69,12 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion params.max_tokens = this.options.modelMaxTokens } - const stream = await this.client.chat.completions.create(params) + let stream + try { + stream = await this.client.chat.completions.create(params) + } catch (error) { + throw handleOpenAIError(error, "HuggingFace") + } for await (const chunk of stream) { const delta = chunk.choices[0]?.delta @@ -101,11 +107,7 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion return response.choices[0]?.message.content || "" } catch (error) { - if (error instanceof Error) { - throw new Error(`Hugging Face completion error: ${error.message}`) - } - - throw error + throw handleOpenAIError(error, "HuggingFace") } } diff --git a/src/api/providers/lm-studio.ts b/src/api/providers/lm-studio.ts index 23d98616d8d2..e2b9b94b910d 100644 --- a/src/api/providers/lm-studio.ts +++ b/src/api/providers/lm-studio.ts @@ -15,7 +15,7 @@ import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { getModels, getModelsFromCache } from "./fetchers/modelCache" import { getApiRequestTimeout } from "./utils/timeout-config" -import { validateApiKeyForByteString } from "./utils/api-key-validation" +import { handleOpenAIError } from "./utils/openai-error-handler" export class LmStudioHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions @@ -25,9 +25,8 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan super() this.options = options - // LM Studio uses "noop" as a placeholder API key, but we should still validate if a real key is provided + // LM Studio uses "noop" as a placeholder API key const apiKey = "noop" - validateApiKeyForByteString(apiKey, "LM Studio") this.client = new OpenAI({ baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1", @@ -93,7 +92,12 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan params.draft_model = this.options.lmStudioDraftModelId } - const results = await this.client.chat.completions.create(params) + let results + try { + results = await this.client.chat.completions.create(params) + } catch (error) { + throw handleOpenAIError(error, "LM Studio") + } const matcher = new XmlMatcher( "think", @@ -169,7 +173,12 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan params.draft_model = this.options.lmStudioDraftModelId } - const response = await this.client.chat.completions.create(params) + let response + try { + response = await this.client.chat.completions.create(params) + } catch (error) { + throw handleOpenAIError(error, "LM Studio") + } return response.choices[0]?.message.content || "" } catch (error) { throw new Error( diff --git a/src/api/providers/ollama.ts b/src/api/providers/ollama.ts index ef920d35ce06..cba9f3203cc2 100644 --- a/src/api/providers/ollama.ts +++ b/src/api/providers/ollama.ts @@ -14,7 +14,7 @@ import { ApiStream } from "../transform/stream" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { getApiRequestTimeout } from "./utils/timeout-config" -import { validateApiKeyForByteString } from "./utils/api-key-validation" +import { handleOpenAIError } from "./utils/openai-error-handler" type CompletionUsage = OpenAI.Chat.Completions.ChatCompletionChunk["usage"] @@ -30,9 +30,6 @@ export class OllamaHandler extends BaseProvider implements SingleCompletionHandl // Otherwise use "ollama" as a placeholder for local instances const apiKey = this.options.ollamaApiKey || "ollama" - // Validate API key for ByteString compatibility - validateApiKeyForByteString(apiKey, "Ollama") - const headers: Record = {} if (this.options.ollamaApiKey) { headers["Authorization"] = `Bearer ${this.options.ollamaApiKey}` @@ -58,13 +55,18 @@ export class OllamaHandler extends BaseProvider implements SingleCompletionHandl ...(useR1Format ? convertToR1Format(messages) : convertToOpenAiMessages(messages)), ] - const stream = await this.client.chat.completions.create({ - model: this.getModel().id, - messages: openAiMessages, - temperature: this.options.modelTemperature ?? 0, - stream: true, - stream_options: { include_usage: true }, - }) + let stream + try { + stream = await this.client.chat.completions.create({ + model: this.getModel().id, + messages: openAiMessages, + temperature: this.options.modelTemperature ?? 0, + stream: true, + stream_options: { include_usage: true }, + }) + } catch (error) { + throw handleOpenAIError(error, "Ollama") + } const matcher = new XmlMatcher( "think", (chunk) => @@ -110,14 +112,19 @@ export class OllamaHandler extends BaseProvider implements SingleCompletionHandl try { const modelId = this.getModel().id const useR1Format = modelId.toLowerCase().includes("deepseek-r1") - const response = await this.client.chat.completions.create({ - model: this.getModel().id, - messages: useR1Format - ? convertToR1Format([{ role: "user", content: prompt }]) - : [{ role: "user", content: prompt }], - temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0), - stream: false, - }) + let response + try { + response = await this.client.chat.completions.create({ + model: this.getModel().id, + messages: useR1Format + ? convertToR1Format([{ role: "user", content: prompt }]) + : [{ role: "user", content: prompt }], + temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0), + stream: false, + }) + } catch (error) { + throw handleOpenAIError(error, "Ollama") + } return response.choices[0]?.message.content || "" } catch (error) { if (error instanceof Error) { diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 24b3d82fd2c3..9c2170ef381a 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -22,7 +22,7 @@ import { getModelParams } from "../transform/model-params" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" -import { validateApiKeyForByteString } from "./utils/api-key-validation" +import { createOpenAIClientWithErrorHandling } from "./utils/openai-error-handler" export type OpenAiNativeModel = ReturnType @@ -61,10 +61,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio } const apiKey = this.options.openAiNativeApiKey ?? "not-provided" - // Validate API key for ByteString compatibility - validateApiKeyForByteString(apiKey, "OpenAI Native") - - this.client = new OpenAI({ baseURL: this.options.openAiNativeBaseUrl, apiKey }) + this.client = createOpenAIClientWithErrorHandling( + () => new OpenAI({ baseURL: this.options.openAiNativeBaseUrl, apiKey }), + "OpenAI Native", + ) } private normalizeUsage(usage: any, model: OpenAiNativeModel): ApiStreamUsageChunk | undefined { diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index a50b8324c26c..1068cd75727d 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -24,7 +24,7 @@ import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { getApiRequestTimeout } from "./utils/timeout-config" -import { validateApiKeyForByteString } from "./utils/api-key-validation" +import { handleOpenAIError } from "./utils/openai-error-handler" // TODO: Rename this to OpenAICompatibleHandler. Also, I think the // `OpenAINativeHandler` can subclass from this, since it's obviously @@ -43,9 +43,6 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const urlHost = this._getUrlHost(this.options.openAiBaseUrl) const isAzureOpenAi = urlHost === "azure.com" || urlHost.endsWith(".azure.com") || options.openAiUseAzure - // Validate API key for ByteString compatibility - validateApiKeyForByteString(apiKey, "OpenAI") - const headers = { ...DEFAULT_HEADERS, ...(this.options.openAiHeaders || {}), @@ -178,10 +175,15 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl // Add max_tokens if needed this.addMaxTokensIfNeeded(requestOptions, modelInfo) - const stream = await this.client.chat.completions.create( - requestOptions, - isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, - ) + let stream + try { + stream = await this.client.chat.completions.create( + requestOptions, + isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, + ) + } catch (error) { + throw handleOpenAIError(error, "OpenAI") + } const matcher = new XmlMatcher( "think", @@ -240,10 +242,15 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl // Add max_tokens if needed this.addMaxTokensIfNeeded(requestOptions, modelInfo) - const response = await this.client.chat.completions.create( - requestOptions, - this._isAzureAiInference(modelUrl) ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, - ) + let response + try { + response = await this.client.chat.completions.create( + requestOptions, + this._isAzureAiInference(modelUrl) ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, + ) + } catch (error) { + throw handleOpenAIError(error, "OpenAI") + } yield { type: "text", @@ -285,10 +292,15 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl // Add max_tokens if needed this.addMaxTokensIfNeeded(requestOptions, modelInfo) - const response = await this.client.chat.completions.create( - requestOptions, - isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, - ) + let response + try { + response = await this.client.chat.completions.create( + requestOptions, + isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, + ) + } catch (error) { + throw handleOpenAIError(error, "OpenAI") + } return response.choices[0]?.message.content || "" } catch (error) { @@ -331,10 +343,15 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl // This allows O3 models to limit response length when includeMaxTokens is enabled this.addMaxTokensIfNeeded(requestOptions, modelInfo) - const stream = await this.client.chat.completions.create( - requestOptions, - methodIsAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, - ) + let stream + try { + stream = await this.client.chat.completions.create( + requestOptions, + methodIsAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, + ) + } catch (error) { + throw handleOpenAIError(error, "OpenAI") + } yield* this.handleStreamResponse(stream) } else { @@ -356,10 +373,15 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl // This allows O3 models to limit response length when includeMaxTokens is enabled this.addMaxTokensIfNeeded(requestOptions, modelInfo) - const response = await this.client.chat.completions.create( - requestOptions, - methodIsAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, - ) + let response + try { + response = await this.client.chat.completions.create( + requestOptions, + methodIsAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, + ) + } catch (error) { + throw handleOpenAIError(error, "OpenAI") + } yield { type: "text", diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 635c5950d60f..6848b1ad8db6 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -25,7 +25,7 @@ import { getModelEndpoints } from "./fetchers/modelEndpointCache" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler } from "../index" -import { validateApiKeyForByteString } from "./utils/api-key-validation" +import { handleOpenAIError } from "./utils/openai-error-handler" // Image generation types interface ImageGenerationResponse { @@ -94,9 +94,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH const baseURL = this.options.openRouterBaseUrl || "https://openrouter.ai/api/v1" const apiKey = this.options.openRouterApiKey ?? "not-provided" - // Validate API key for ByteString compatibility - validateApiKeyForByteString(apiKey, "OpenRouter") - this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: DEFAULT_HEADERS }) } @@ -165,7 +162,12 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH ...(reasoning && { reasoning }), } - const stream = await this.client.chat.completions.create(completionParams) + let stream + try { + stream = await this.client.chat.completions.create(completionParams) + } catch (error) { + throw handleOpenAIError(error, "OpenRouter") + } let lastUsage: CompletionUsage | undefined = undefined @@ -263,7 +265,12 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH ...(reasoning && { reasoning }), } - const response = await this.client.chat.completions.create(completionParams) + let response + try { + response = await this.client.chat.completions.create(completionParams) + } catch (error) { + throw handleOpenAIError(error, "OpenRouter") + } if ("error" in response) { const error = response.error as { message?: string; code?: number } diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index 388f97e0ca97..6bd8c622359e 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -16,7 +16,7 @@ import { getModels } from "./fetchers/modelCache" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { toRequestyServiceUrl } from "../../shared/utils/requesty" -import { validateApiKeyForByteString } from "./utils/api-key-validation" +import { handleOpenAIError } from "./utils/openai-error-handler" // Requesty usage includes an extra field for Anthropic use cases. // Safely cast the prompt token details section to the appropriate structure. @@ -52,9 +52,6 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan const apiKey = this.options.requestyApiKey ?? "not-provided" - // Validate API key for ByteString compatibility - validateApiKeyForByteString(apiKey, "Requesty") - this.client = new OpenAI({ baseURL: this.baseURL, apiKey: apiKey, @@ -132,7 +129,12 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan requesty: { trace_id: metadata?.taskId, extra: { mode: metadata?.mode } }, } - const stream = await this.client.chat.completions.create(completionParams) + let stream + try { + stream = await this.client.chat.completions.create(completionParams) + } catch (error) { + throw handleOpenAIError(error, "Requesty") + } let lastUsage: any = undefined for await (const chunk of stream) { @@ -168,7 +170,12 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan temperature: temperature, } - const response: OpenAI.Chat.ChatCompletion = await this.client.chat.completions.create(completionParams) + let response: OpenAI.Chat.ChatCompletion + try { + response = await this.client.chat.completions.create(completionParams) + } catch (error) { + throw handleOpenAIError(error, "Requesty") + } return response.choices[0]?.message.content || "" } } diff --git a/src/api/providers/router-provider.ts b/src/api/providers/router-provider.ts index ace206353f29..25e9a11e1b2c 100644 --- a/src/api/providers/router-provider.ts +++ b/src/api/providers/router-provider.ts @@ -8,7 +8,6 @@ import { BaseProvider } from "./base-provider" import { getModels } from "./fetchers/modelCache" import { DEFAULT_HEADERS } from "./constants" -import { validateApiKeyForByteString } from "./utils/api-key-validation" type RouterProviderOptions = { name: RouterName @@ -46,9 +45,6 @@ export abstract class RouterProvider extends BaseProvider { this.defaultModelId = defaultModelId this.defaultModelInfo = defaultModelInfo - // Validate API key for ByteString compatibility - validateApiKeyForByteString(apiKey, name) - this.client = new OpenAI({ baseURL, apiKey, diff --git a/src/api/providers/utils/__tests__/api-key-validation.spec.ts b/src/api/providers/utils/__tests__/api-key-validation.spec.ts deleted file mode 100644 index b3906339786a..000000000000 --- a/src/api/providers/utils/__tests__/api-key-validation.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, it, expect } from "vitest" -import { validateApiKeyForByteString, validateApiKeyForAscii } from "../api-key-validation" - -describe("API Key Validation", () => { - describe("validateApiKeyForByteString", () => { - it("should accept valid ASCII characters", () => { - expect(() => validateApiKeyForByteString("abc123XYZ", "TestProvider")).not.toThrow() - expect(() => validateApiKeyForByteString("test-api-key_123", "TestProvider")).not.toThrow() - expect(() => validateApiKeyForByteString("!@#$%^&*()", "TestProvider")).not.toThrow() - }) - - it("should accept extended ASCII characters (128-255)", () => { - // Extended ASCII characters like ñ (241), ü (252) - expect(() => validateApiKeyForByteString("test\xF1\xFC", "TestProvider")).not.toThrow() - expect(() => validateApiKeyForByteString("key\xFF", "TestProvider")).not.toThrow() - }) - - it("should reject characters above 255", () => { - // Chinese character 中 (20013) - expect(() => validateApiKeyForByteString("test中key", "TestProvider")).toThrow( - "Invalid TestProvider API key: contains non-ASCII character at position 5", - ) - - // Emoji 😀 (128512) - expect(() => validateApiKeyForByteString("key😀", "TestProvider")).toThrow( - "Invalid TestProvider API key: contains non-ASCII character at position 4", - ) - - // Korean character 한 (54620) - expect(() => validateApiKeyForByteString("한글key", "TestProvider")).toThrow( - "Invalid TestProvider API key: contains non-ASCII character at position 1", - ) - }) - - it("should handle undefined and empty keys", () => { - expect(() => validateApiKeyForByteString(undefined, "TestProvider")).not.toThrow() - expect(() => validateApiKeyForByteString("", "TestProvider")).not.toThrow() - }) - - it("should provide clear error messages", () => { - expect(() => validateApiKeyForByteString("abc中def", "DeepSeek")).toThrow( - "Invalid DeepSeek API key: contains non-ASCII character at position 4. " + - "API keys must contain only ASCII characters (character codes 0-255). " + - "Please check your API key configuration.", - ) - }) - }) - - describe("validateApiKeyForAscii", () => { - it("should accept standard ASCII characters (0-127)", () => { - expect(() => validateApiKeyForAscii("abc123XYZ", "TestProvider")).not.toThrow() - expect(() => validateApiKeyForAscii("test-api-key_123", "TestProvider")).not.toThrow() - expect(() => validateApiKeyForAscii("!@#$%^&*()", "TestProvider")).not.toThrow() - }) - - it("should reject extended ASCII characters (128-255)", () => { - // Extended ASCII character ñ (241) - expect(() => validateApiKeyForAscii("test\xF1key", "TestProvider")).toThrow( - "Invalid TestProvider API key: contains non-ASCII character at position 5", - ) - - // Extended ASCII character ü (252) - expect(() => validateApiKeyForAscii("key\xFC", "TestProvider")).toThrow( - "Invalid TestProvider API key: contains non-ASCII character at position 4", - ) - }) - - it("should reject Unicode characters", () => { - // Chinese character 中 (20013) - expect(() => validateApiKeyForAscii("test中key", "TestProvider")).toThrow( - "Invalid TestProvider API key: contains non-ASCII character at position 5", - ) - - // Emoji 😀 (128512) - expect(() => validateApiKeyForAscii("key😀", "TestProvider")).toThrow( - "Invalid TestProvider API key: contains non-ASCII character at position 4", - ) - }) - - it("should handle undefined and empty keys", () => { - expect(() => validateApiKeyForAscii(undefined, "TestProvider")).not.toThrow() - expect(() => validateApiKeyForAscii("", "TestProvider")).not.toThrow() - }) - - it("should provide clear error messages", () => { - expect(() => validateApiKeyForAscii("abc\xF1def", "OpenAI")).toThrow( - "Invalid OpenAI API key: contains non-ASCII character at position 4. " + - "API keys must contain only standard ASCII characters (character codes 0-127). " + - "Please check your API key configuration.", - ) - }) - }) -}) diff --git a/src/api/providers/utils/api-key-validation.ts b/src/api/providers/utils/api-key-validation.ts deleted file mode 100644 index af0f794278e5..000000000000 --- a/src/api/providers/utils/api-key-validation.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Validates that an API key contains only valid ByteString characters (0-255). - * The OpenAI client library requires API keys to be convertible to ByteString format, - * which only supports characters with values 0-255. - * - * @param apiKey - The API key to validate - * @param providerName - The name of the provider for error messaging - * @throws Error if the API key contains invalid characters - */ -export function validateApiKeyForByteString(apiKey: string | undefined, providerName: string): void { - if (!apiKey) { - return // No validation needed for undefined/empty keys - } - - // Check each character in the API key - for (let i = 0; i < apiKey.length; i++) { - const charCode = apiKey.charCodeAt(i) - if (charCode > 255) { - throw new Error( - `Invalid ${providerName} API key: contains non-ASCII character at position ${i + 1}. ` + - `API keys must contain only ASCII characters (character codes 0-255). ` + - `Please check your API key configuration.`, - ) - } - } -} - -/** - * Validates that an API key contains only standard ASCII characters (0-127). - * This is a stricter validation that only allows standard ASCII characters. - * - * @param apiKey - The API key to validate - * @param providerName - The name of the provider for error messaging - * @throws Error if the API key contains non-ASCII characters - */ -export function validateApiKeyForAscii(apiKey: string | undefined, providerName: string): void { - if (!apiKey) { - return // No validation needed for undefined/empty keys - } - - // Check each character in the API key - for (let i = 0; i < apiKey.length; i++) { - const charCode = apiKey.charCodeAt(i) - if (charCode > 127) { - throw new Error( - `Invalid ${providerName} API key: contains non-ASCII character at position ${i + 1}. ` + - `API keys must contain only standard ASCII characters (character codes 0-127). ` + - `Please check your API key configuration.`, - ) - } - } -} diff --git a/src/api/providers/utils/openai-error-handler.ts b/src/api/providers/utils/openai-error-handler.ts new file mode 100644 index 000000000000..d90aef239405 --- /dev/null +++ b/src/api/providers/utils/openai-error-handler.ts @@ -0,0 +1,44 @@ +/** + * General error handler for OpenAI client errors + * Transforms technical errors into user-friendly messages + */ + +import i18n from "../../../i18n/setup" + +/** + * Handles OpenAI client errors and transforms them into user-friendly messages + * @param error - The error to handle + * @param providerName - The name of the provider for context in error messages + * @returns The original error or a transformed user-friendly error + */ +export function handleOpenAIError(error: unknown, providerName: string): Error { + if (error instanceof Error) { + // Invalid character/ByteString conversion error in API key + if (error.message.includes("Cannot convert argument to a ByteString")) { + return new Error(i18n.t("common:errors.api.invalidKeyInvalidChars")) + } + + // Add more error message transformations here as needed + + // Return original error if no transformation matches + return error + } + + // If it's not even an Error object, wrap it + return new Error(`${providerName} error: ${String(error)}`) +} + +/** + * Wraps an OpenAI client instantiation with error handling + * @param createClient - Function that creates the OpenAI client + * @param providerName - The name of the provider for error messages + * @returns The created client + * @throws User-friendly error if client creation fails + */ +export function createOpenAIClientWithErrorHandling(createClient: () => T, providerName: string): T { + try { + return createClient() + } catch (error) { + throw handleOpenAIError(error, providerName) + } +} diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index 84f0e12465be..d31324ffa74c 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -12,7 +12,7 @@ import { getModelParams } from "../transform/model-params" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" -import { validateApiKeyForByteString } from "./utils/api-key-validation" +import { handleOpenAIError } from "./utils/openai-error-handler" const XAI_DEFAULT_TEMPERATURE = 0 @@ -26,9 +26,6 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler const apiKey = this.options.xaiApiKey ?? "not-provided" - // Validate API key for ByteString compatibility - validateApiKeyForByteString(apiKey, "xAI") - this.client = new OpenAI({ baseURL: "https://api.x.ai/v1", apiKey: apiKey, @@ -55,15 +52,20 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler const { id: modelId, info: modelInfo, reasoning } = this.getModel() // Use the OpenAI-compatible API. - const stream = await this.client.chat.completions.create({ - model: modelId, - max_tokens: modelInfo.maxTokens, - temperature: this.options.modelTemperature ?? XAI_DEFAULT_TEMPERATURE, - messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], - stream: true, - stream_options: { include_usage: true }, - ...(reasoning && reasoning), - }) + let stream + try { + stream = await this.client.chat.completions.create({ + model: modelId, + max_tokens: modelInfo.maxTokens, + temperature: this.options.modelTemperature ?? XAI_DEFAULT_TEMPERATURE, + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + ...(reasoning && reasoning), + }) + } catch (error) { + throw handleOpenAIError(error, "xAI") + } for await (const chunk of stream) { const delta = chunk.choices[0]?.delta @@ -110,11 +112,16 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler const { id: modelId, reasoning } = this.getModel() try { - const response = await this.client.chat.completions.create({ - model: modelId, - messages: [{ role: "user", content: prompt }], - ...(reasoning && reasoning), - }) + let response + try { + response = await this.client.chat.completions.create({ + model: modelId, + messages: [{ role: "user", content: prompt }], + ...(reasoning && reasoning), + }) + } catch (error) { + throw handleOpenAIError(error, "xAI") + } return response.choices[0]?.message.content || "" } catch (error) { diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 74b265f51313..583f82693a6a 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -107,6 +107,9 @@ "roo": { "authenticationRequired": "El proveïdor Roo requereix autenticació al núvol. Si us plau, inicieu sessió a Roo Code Cloud." }, + "api": { + "invalidKeyInvalidChars": "La clau API conté caràcters no vàlids." + }, "mode_import_failed": "Ha fallat la importació del mode: {{error}}" }, "warnings": { diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 856e4e1dce18..2d1a2778edcf 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Roo-Anbieter erfordert Cloud-Authentifizierung. Bitte melde dich bei Roo Code Cloud an." + }, + "api": { + "invalidKeyInvalidChars": "API-Schlüssel enthält ungültige Zeichen." } }, "warnings": { diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index e413bc0890ce..40a897ceb312 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Roo provider requires cloud authentication. Please sign in to Roo Code Cloud." + }, + "api": { + "invalidKeyInvalidChars": "API key contains invalid characters." } }, "warnings": { diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 7b2b9a43476c..0b7c7e12ee8f 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "El proveedor Roo requiere autenticación en la nube. Por favor, inicia sesión en Roo Code Cloud." + }, + "api": { + "invalidKeyInvalidChars": "La clave API contiene caracteres inválidos." } }, "warnings": { diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index e9282a0b97f2..9b92cf7240c8 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Le fournisseur Roo nécessite une authentification cloud. Veuillez vous connecter à Roo Code Cloud." + }, + "api": { + "invalidKeyInvalidChars": "La clé API contient des caractères invalides." } }, "warnings": { diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 3f5ab60413e9..f9bbed0dfcab 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Roo प्रदाता को क्लाउड प्रमाणीकरण की आवश्यकता है। कृपया Roo Code Cloud में साइन इन करें।" + }, + "api": { + "invalidKeyInvalidChars": "API कुंजी में अमान्य वर्ण हैं।" } }, "warnings": { diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 3c4305650342..147d88c4e74e 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Penyedia Roo memerlukan autentikasi cloud. Silakan masuk ke Roo Code Cloud." + }, + "api": { + "invalidKeyInvalidChars": "Kunci API mengandung karakter tidak valid." } }, "warnings": { diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index c19114baf1f5..c304896163e8 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Il provider Roo richiede l'autenticazione cloud. Accedi a Roo Code Cloud." + }, + "api": { + "invalidKeyInvalidChars": "La chiave API contiene caratteri non validi." } }, "warnings": { diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index d595484fa1f5..3f2863510814 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Rooプロバイダーはクラウド認証が必要です。Roo Code Cloudにサインインしてください。" + }, + "api": { + "invalidKeyInvalidChars": "APIキーに無効な文字が含まれています。" } }, "warnings": { diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 3209952c6d8e..d1f2ef9c44ad 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Roo 제공업체는 클라우드 인증이 필요합니다. Roo Code Cloud에 로그인하세요." + }, + "api": { + "invalidKeyInvalidChars": "API 키에 유효하지 않은 문자가 포함되어 있습니다." } }, "warnings": { diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index c0c6ba35e9fa..a2b29d8df03c 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Roo provider vereist cloud authenticatie. Log in bij Roo Code Cloud." + }, + "api": { + "invalidKeyInvalidChars": "API-sleutel bevat ongeldige karakters." } }, "warnings": { diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 475ba069eefe..45e0651fac6e 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Dostawca Roo wymaga uwierzytelnienia w chmurze. Zaloguj się do Roo Code Cloud." + }, + "api": { + "invalidKeyInvalidChars": "Klucz API zawiera nieprawidłowe znaki." } }, "warnings": { diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 55a41fcf1b78..001457707e02 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -107,6 +107,9 @@ }, "roo": { "authenticationRequired": "O provedor Roo requer autenticação na nuvem. Faça login no Roo Code Cloud." + }, + "api": { + "invalidKeyInvalidChars": "A chave API contém caracteres inválidos." } }, "warnings": { diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 505998daa22c..3500a9a5add6 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Провайдер Roo требует облачной аутентификации. Войдите в Roo Code Cloud." + }, + "api": { + "invalidKeyInvalidChars": "API-ключ содержит недопустимые символы." } }, "warnings": { diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 9b8af8d94cbe..4089cff21765 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Roo sağlayıcısı bulut kimlik doğrulaması gerektirir. Lütfen Roo Code Cloud'a giriş yapın." + }, + "api": { + "invalidKeyInvalidChars": "API anahtarı geçersiz karakterler içeriyor." } }, "warnings": { diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 4877f297adc7..ecf686f520e0 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Nhà cung cấp Roo yêu cầu xác thực đám mây. Vui lòng đăng nhập vào Roo Code Cloud." + }, + "api": { + "invalidKeyInvalidChars": "Khóa API chứa ký tự không hợp lệ." } }, "warnings": { diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 5bac0d2847ea..9f4d24f6ebfb 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -108,6 +108,9 @@ }, "roo": { "authenticationRequired": "Roo 提供商需要云认证。请登录 Roo Code Cloud。" + }, + "api": { + "invalidKeyInvalidChars": "API 密钥包含无效字符。" } }, "warnings": { diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 0f82f48d1372..d40b3e094f57 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -103,6 +103,9 @@ "roo": { "authenticationRequired": "Roo 提供者需要雲端認證。請登入 Roo Code Cloud。" }, + "api": { + "invalidKeyInvalidChars": "API 金鑰包含無效字元。" + }, "mode_import_failed": "匯入模式失敗:{{error}}" }, "warnings": { From d3f020f2caf2af4e7f685a5c07bbeba5b56c0bdf Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Thu, 4 Sep 2025 00:05:26 -0500 Subject: [PATCH 3/8] refactor(providers): remove createOpenAIClientWithErrorHandling; construct clients directly and keep request-time error mapping via handleOpenAIError --- src/api/providers/huggingface.ts | 16 ++++++---------- src/api/providers/openai-native.ts | 6 +----- src/api/providers/utils/openai-error-handler.ts | 15 --------------- 3 files changed, 7 insertions(+), 30 deletions(-) diff --git a/src/api/providers/huggingface.ts b/src/api/providers/huggingface.ts index f5b9c64eac4a..634fcf5737f7 100644 --- a/src/api/providers/huggingface.ts +++ b/src/api/providers/huggingface.ts @@ -8,7 +8,7 @@ import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from ". import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import { getHuggingFaceModels, getCachedHuggingFaceModels } from "./fetchers/huggingface" -import { createOpenAIClientWithErrorHandling, handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/openai-error-handler" export class HuggingFaceHandler extends BaseProvider implements SingleCompletionHandler { private client: OpenAI @@ -23,15 +23,11 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion throw new Error("Hugging Face API key is required") } - this.client = createOpenAIClientWithErrorHandling( - () => - new OpenAI({ - baseURL: "https://router.huggingface.co/v1", - apiKey: this.options.huggingFaceApiKey, - defaultHeaders: DEFAULT_HEADERS, - }), - "HuggingFace", - ) + this.client = new OpenAI({ + baseURL: "https://router.huggingface.co/v1", + apiKey: this.options.huggingFaceApiKey, + defaultHeaders: DEFAULT_HEADERS, + }) // Try to get cached models first this.modelCache = getCachedHuggingFaceModels() diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 9c2170ef381a..fa72e5d8c893 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -22,7 +22,6 @@ import { getModelParams } from "../transform/model-params" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" -import { createOpenAIClientWithErrorHandling } from "./utils/openai-error-handler" export type OpenAiNativeModel = ReturnType @@ -61,10 +60,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio } const apiKey = this.options.openAiNativeApiKey ?? "not-provided" - this.client = createOpenAIClientWithErrorHandling( - () => new OpenAI({ baseURL: this.options.openAiNativeBaseUrl, apiKey }), - "OpenAI Native", - ) + this.client = new OpenAI({ baseURL: this.options.openAiNativeBaseUrl, apiKey }) } private normalizeUsage(usage: any, model: OpenAiNativeModel): ApiStreamUsageChunk | undefined { diff --git a/src/api/providers/utils/openai-error-handler.ts b/src/api/providers/utils/openai-error-handler.ts index d90aef239405..6b1d71fd215e 100644 --- a/src/api/providers/utils/openai-error-handler.ts +++ b/src/api/providers/utils/openai-error-handler.ts @@ -27,18 +27,3 @@ export function handleOpenAIError(error: unknown, providerName: string): Error { // If it's not even an Error object, wrap it return new Error(`${providerName} error: ${String(error)}`) } - -/** - * Wraps an OpenAI client instantiation with error handling - * @param createClient - Function that creates the OpenAI client - * @param providerName - The name of the provider for error messages - * @returns The created client - * @throws User-friendly error if client creation fails - */ -export function createOpenAIClientWithErrorHandling(createClient: () => T, providerName: string): T { - try { - return createClient() - } catch (error) { - throw handleOpenAIError(error, providerName) - } -} From 20ba1048de8a0692344fd42d9af269d7e03f7651 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Thu, 4 Sep 2025 00:13:59 -0500 Subject: [PATCH 4/8] fix(xai): simplify completePrompt error handling; remove nested try/catch and use handleOpenAIError directly --- src/api/providers/xai.ts | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index d31324ffa74c..61cfed14c12f 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -112,24 +112,15 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler const { id: modelId, reasoning } = this.getModel() try { - let response - try { - response = await this.client.chat.completions.create({ - model: modelId, - messages: [{ role: "user", content: prompt }], - ...(reasoning && reasoning), - }) - } catch (error) { - throw handleOpenAIError(error, "xAI") - } + const response = await this.client.chat.completions.create({ + model: modelId, + messages: [{ role: "user", content: prompt }], + ...(reasoning && reasoning), + }) return response.choices[0]?.message.content || "" } catch (error) { - if (error instanceof Error) { - throw new Error(`xAI completion error: ${error.message}`) - } - - throw error + throw handleOpenAIError(error, "xAI") } } } From fc6e9c6f4feb12bbeb73196d6ecd6ee7722b398a Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Thu, 4 Sep 2025 00:27:08 -0500 Subject: [PATCH 5/8] fix: remove formatting change from openai-native.ts to match main --- src/api/providers/openai-native.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index fa72e5d8c893..c884091c02a6 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -59,7 +59,6 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio this.options.enableGpt5ReasoningSummary = true } const apiKey = this.options.openAiNativeApiKey ?? "not-provided" - this.client = new OpenAI({ baseURL: this.options.openAiNativeBaseUrl, apiKey }) } From 3f7b07b922f4978be3201c3f23698a4cf0e253f1 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Thu, 4 Sep 2025 00:45:54 -0500 Subject: [PATCH 6/8] fix: ensure consistent error message format in handleOpenAIError - Wrap all non-ByteString errors with provider-specific prefix - Handle potential undefined error messages safely - Maintain consistent error format expected by unit tests --- src/api/providers/utils/openai-error-handler.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/api/providers/utils/openai-error-handler.ts b/src/api/providers/utils/openai-error-handler.ts index 6b1d71fd215e..90be81f7c434 100644 --- a/src/api/providers/utils/openai-error-handler.ts +++ b/src/api/providers/utils/openai-error-handler.ts @@ -13,17 +13,17 @@ import i18n from "../../../i18n/setup" */ export function handleOpenAIError(error: unknown, providerName: string): Error { if (error instanceof Error) { + const msg = error.message || "" + // Invalid character/ByteString conversion error in API key - if (error.message.includes("Cannot convert argument to a ByteString")) { + if (msg.includes("Cannot convert argument to a ByteString")) { return new Error(i18n.t("common:errors.api.invalidKeyInvalidChars")) } - // Add more error message transformations here as needed - - // Return original error if no transformation matches - return error + // For other Error instances, wrap with provider-specific prefix + return new Error(`${providerName} completion error: ${msg}`) } - // If it's not even an Error object, wrap it - return new Error(`${providerName} error: ${String(error)}`) + // Non-Error: wrap with provider-specific prefix + return new Error(`${providerName} completion error: ${String(error)}`) } From 4bbfb897aaba84b78f03628adc8b75d2e9e6d4a2 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Thu, 4 Sep 2025 01:01:17 -0500 Subject: [PATCH 7/8] refactor: define provider names as constants across all providers - Add providerName constant to all provider classes that use handleOpenAIError - Replace hardcoded provider name strings with this.providerName - Improves maintainability and consistency across providers - Affected providers: OpenAI, HuggingFace, LM Studio, Ollama, OpenRouter, Requesty, xAI --- src/api/providers/huggingface.ts | 5 +++-- src/api/providers/lm-studio.ts | 5 +++-- src/api/providers/ollama.ts | 5 +++-- src/api/providers/openrouter.ts | 5 +++-- src/api/providers/requesty.ts | 5 +++-- src/api/providers/xai.ts | 5 +++-- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/api/providers/huggingface.ts b/src/api/providers/huggingface.ts index 634fcf5737f7..7b62046b99e7 100644 --- a/src/api/providers/huggingface.ts +++ b/src/api/providers/huggingface.ts @@ -14,6 +14,7 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion private client: OpenAI private options: ApiHandlerOptions private modelCache: ModelRecord | null = null + private readonly providerName = "HuggingFace" constructor(options: ApiHandlerOptions) { super() @@ -69,7 +70,7 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion try { stream = await this.client.chat.completions.create(params) } catch (error) { - throw handleOpenAIError(error, "HuggingFace") + throw handleOpenAIError(error, this.providerName) } for await (const chunk of stream) { @@ -103,7 +104,7 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion return response.choices[0]?.message.content || "" } catch (error) { - throw handleOpenAIError(error, "HuggingFace") + throw handleOpenAIError(error, this.providerName) } } diff --git a/src/api/providers/lm-studio.ts b/src/api/providers/lm-studio.ts index e2b9b94b910d..6c58a96ae1fa 100644 --- a/src/api/providers/lm-studio.ts +++ b/src/api/providers/lm-studio.ts @@ -20,6 +20,7 @@ import { handleOpenAIError } from "./utils/openai-error-handler" export class LmStudioHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions private client: OpenAI + private readonly providerName = "LM Studio" constructor(options: ApiHandlerOptions) { super() @@ -96,7 +97,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan try { results = await this.client.chat.completions.create(params) } catch (error) { - throw handleOpenAIError(error, "LM Studio") + throw handleOpenAIError(error, this.providerName) } const matcher = new XmlMatcher( @@ -177,7 +178,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan try { response = await this.client.chat.completions.create(params) } catch (error) { - throw handleOpenAIError(error, "LM Studio") + throw handleOpenAIError(error, this.providerName) } return response.choices[0]?.message.content || "" } catch (error) { diff --git a/src/api/providers/ollama.ts b/src/api/providers/ollama.ts index cba9f3203cc2..ab9df116aa84 100644 --- a/src/api/providers/ollama.ts +++ b/src/api/providers/ollama.ts @@ -21,6 +21,7 @@ type CompletionUsage = OpenAI.Chat.Completions.ChatCompletionChunk["usage"] export class OllamaHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions private client: OpenAI + private readonly providerName = "Ollama" constructor(options: ApiHandlerOptions) { super() @@ -65,7 +66,7 @@ export class OllamaHandler extends BaseProvider implements SingleCompletionHandl stream_options: { include_usage: true }, }) } catch (error) { - throw handleOpenAIError(error, "Ollama") + throw handleOpenAIError(error, this.providerName) } const matcher = new XmlMatcher( "think", @@ -123,7 +124,7 @@ export class OllamaHandler extends BaseProvider implements SingleCompletionHandl stream: false, }) } catch (error) { - throw handleOpenAIError(error, "Ollama") + throw handleOpenAIError(error, this.providerName) } return response.choices[0]?.message.content || "" } catch (error) { diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 6848b1ad8db6..580b17331194 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -86,6 +86,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH private client: OpenAI protected models: ModelRecord = {} protected endpoints: ModelRecord = {} + private readonly providerName = "OpenRouter" constructor(options: ApiHandlerOptions) { super() @@ -166,7 +167,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH try { stream = await this.client.chat.completions.create(completionParams) } catch (error) { - throw handleOpenAIError(error, "OpenRouter") + throw handleOpenAIError(error, this.providerName) } let lastUsage: CompletionUsage | undefined = undefined @@ -269,7 +270,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH try { response = await this.client.chat.completions.create(completionParams) } catch (error) { - throw handleOpenAIError(error, "OpenRouter") + throw handleOpenAIError(error, this.providerName) } if ("error" in response) { diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index 6bd8c622359e..16aefae52861 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -43,6 +43,7 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan protected models: ModelRecord = {} private client: OpenAI private baseURL: string + private readonly providerName = "Requesty" constructor(options: ApiHandlerOptions) { super() @@ -133,7 +134,7 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan try { stream = await this.client.chat.completions.create(completionParams) } catch (error) { - throw handleOpenAIError(error, "Requesty") + throw handleOpenAIError(error, this.providerName) } let lastUsage: any = undefined @@ -174,7 +175,7 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan try { response = await this.client.chat.completions.create(completionParams) } catch (error) { - throw handleOpenAIError(error, "Requesty") + throw handleOpenAIError(error, this.providerName) } return response.choices[0]?.message.content || "" } diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index 61cfed14c12f..7eb6e9866dd8 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -19,6 +19,7 @@ const XAI_DEFAULT_TEMPERATURE = 0 export class XAIHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions private client: OpenAI + private readonly providerName = "xAI" constructor(options: ApiHandlerOptions) { super() @@ -64,7 +65,7 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler ...(reasoning && reasoning), }) } catch (error) { - throw handleOpenAIError(error, "xAI") + throw handleOpenAIError(error, this.providerName) } for await (const chunk of stream) { @@ -120,7 +121,7 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler return response.choices[0]?.message.content || "" } catch (error) { - throw handleOpenAIError(error, "xAI") + throw handleOpenAIError(error, this.providerName) } } } From 276d57b1df1cced3dd17d6ad8b411106b3ef9ca9 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Thu, 4 Sep 2025 01:23:14 -0500 Subject: [PATCH 8/8] refactor: use class property for provider name in OpenAiHandler - Replace hardcoded 'OpenAI' strings with this.providerName property - Follows same pattern as other provider classes for consistency --- src/api/providers/openai.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 1068cd75727d..501ff3d5f556 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -32,6 +32,7 @@ import { handleOpenAIError } from "./utils/openai-error-handler" export class OpenAiHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions private client: OpenAI + private readonly providerName = "OpenAI" constructor(options: ApiHandlerOptions) { super() @@ -182,7 +183,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, ) } catch (error) { - throw handleOpenAIError(error, "OpenAI") + throw handleOpenAIError(error, this.providerName) } const matcher = new XmlMatcher( @@ -249,7 +250,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl this._isAzureAiInference(modelUrl) ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, ) } catch (error) { - throw handleOpenAIError(error, "OpenAI") + throw handleOpenAIError(error, this.providerName) } yield { @@ -299,13 +300,13 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, ) } catch (error) { - throw handleOpenAIError(error, "OpenAI") + throw handleOpenAIError(error, this.providerName) } return response.choices[0]?.message.content || "" } catch (error) { if (error instanceof Error) { - throw new Error(`OpenAI completion error: ${error.message}`) + throw new Error(`${this.providerName} completion error: ${error.message}`) } throw error @@ -350,7 +351,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl methodIsAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, ) } catch (error) { - throw handleOpenAIError(error, "OpenAI") + throw handleOpenAIError(error, this.providerName) } yield* this.handleStreamResponse(stream) @@ -380,7 +381,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl methodIsAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, ) } catch (error) { - throw handleOpenAIError(error, "OpenAI") + throw handleOpenAIError(error, this.providerName) } yield {