From ec3a89c5fd6b09c1e6bb5a9fb087ac005109857d Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 30 Jun 2026 15:16:18 +0900 Subject: [PATCH 1/3] refactor: remove deprecated openai-error-handler and consolidate into error-handler Replace all imports of the deprecated `openai-error-handler` shim with `error-handler`, which has been the canonical source since #10204. Delete `openai-error-handler.ts` and its duplicate test file, as `error-handler.spec.ts` already covers the same behavior. Signed-off-by: daewoongoh --- .../base-openai-compatible-provider.ts | 2 +- src/api/providers/deepseek.ts | 2 +- src/api/providers/lm-studio.ts | 2 +- src/api/providers/openai.ts | 2 +- src/api/providers/openrouter.ts | 2 +- src/api/providers/requesty.ts | 2 +- src/api/providers/unbound.ts | 2 +- .../__tests__/openai-error-handler.spec.ts | 186 ------------------ .../providers/utils/openai-error-handler.ts | 19 -- src/api/providers/xai.ts | 2 +- src/api/providers/zai.ts | 2 +- .../code-index/embedders/openai-compatible.ts | 2 +- src/services/code-index/embedders/openai.ts | 2 +- .../code-index/embedders/openrouter.ts | 2 +- 14 files changed, 12 insertions(+), 217 deletions(-) delete mode 100644 src/api/providers/utils/__tests__/openai-error-handler.spec.ts delete 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 b6094f9cc4..d6406f2a67 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -11,7 +11,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" import { calculateApiCostOpenAI } from "../../shared/cost" import { extractReasoningFromDelta } from "./utils/extract-reasoning" diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index 819fe6c7bc..45f7fba988 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -137,7 +137,7 @@ export class DeepSeekHandler extends OpenAiHandler { isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, ) } catch (error) { - const { handleOpenAIError } = await import("./utils/openai-error-handler") + const { handleOpenAIError } = await import("./utils/error-handler") throw handleOpenAIError(error, "DeepSeek") } diff --git a/src/api/providers/lm-studio.ts b/src/api/providers/lm-studio.ts index 4bc9497719..891695b5fc 100644 --- a/src/api/providers/lm-studio.ts +++ b/src/api/providers/lm-studio.ts @@ -15,7 +15,7 @@ import { ApiStream } from "../transform/stream" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { getModelsFromCache } from "./fetchers/modelCache" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" export class LmStudioHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index c8f17dac3e..07427c7dbe 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -22,7 +22,7 @@ import { getModelParams } from "../transform/model-params" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" import { extractReasoningFromDelta } from "./utils/extract-reasoning" // TODO: Rename this to OpenAICompatibleHandler. Also, I think the diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 1ac9c465b6..360d4c7f81 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -36,7 +36,7 @@ import { getModelEndpoints } from "./fetchers/modelEndpointCache" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" import { generateImageWithProvider, ImageGenerationResult } from "./utils/image-generation" import { applyRouterToolPreferences } from "./utils/router-tool-preferences" diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index c490227d44..006cd9cdfb 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 { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" import { applyRouterToolPreferences } from "./utils/router-tool-preferences" import { extractReasoningFromDelta } from "./utils/extract-reasoning" diff --git a/src/api/providers/unbound.ts b/src/api/providers/unbound.ts index 0ec7a2466f..cb41591634 100644 --- a/src/api/providers/unbound.ts +++ b/src/api/providers/unbound.ts @@ -15,7 +15,7 @@ import { DEFAULT_HEADERS } from "./constants" import { getModels } from "./fetchers/modelCache" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" import { applyRouterToolPreferences } from "./utils/router-tool-preferences" import { extractReasoningFromDelta } from "./utils/extract-reasoning" diff --git a/src/api/providers/utils/__tests__/openai-error-handler.spec.ts b/src/api/providers/utils/__tests__/openai-error-handler.spec.ts deleted file mode 100644 index 740d060ac8..0000000000 --- a/src/api/providers/utils/__tests__/openai-error-handler.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { handleOpenAIError } from "../openai-error-handler" - -describe("handleOpenAIError", () => { - const providerName = "TestProvider" - - describe("HTTP status preservation", () => { - it("should preserve status code from Error with status field", () => { - const error = new Error("API request failed") as any - error.status = 401 - - const result = handleOpenAIError(error, providerName) - - expect(result).toBeInstanceOf(Error) - expect(result.message).toContain("TestProvider completion error") - expect((result as any).status).toBe(401) - }) - - it("should preserve status code from Error with nested error structure", () => { - const error = new Error("Wrapped error") as any - error.status = 429 - error.errorDetails = [{ "@type": "type.googleapis.com/google.rpc.RetryInfo" }] - - const result = handleOpenAIError(error, providerName) - - expect((result as any).status).toBe(429) - expect((result as any).errorDetails).toBeDefined() - }) - - it("should preserve status from non-Error exception", () => { - const error = { - status: 500, - message: "Internal server error", - } - - const result = handleOpenAIError(error, providerName) - - expect(result).toBeInstanceOf(Error) - expect((result as any).status).toBe(500) - }) - - it("should not add status field if original error lacks it", () => { - const error = new Error("Generic error") - - const result = handleOpenAIError(error, providerName) - - expect(result).toBeInstanceOf(Error) - expect((result as any).status).toBeUndefined() - }) - }) - - describe("errorDetails preservation", () => { - it("should preserve errorDetails array from original error", () => { - const error = new Error("Rate limited") as any - error.status = 429 - error.errorDetails = [{ "@type": "type.googleapis.com/google.rpc.RetryInfo", retryDelay: "5s" }] - - const result = handleOpenAIError(error, providerName) - - expect((result as any).errorDetails).toEqual(error.errorDetails) - }) - - it("should preserve code field from original error", () => { - const error = new Error("Bad request") as any - error.code = "invalid_request" - - const result = handleOpenAIError(error, providerName) - - expect((result as any).code).toBe("invalid_request") - }) - }) - - describe("ByteString conversion errors", () => { - it("should return localized message for ByteString conversion errors", () => { - const error = new Error("Cannot convert argument to a ByteString") - - const result = handleOpenAIError(error, providerName) - - expect(result.message).not.toContain("TestProvider completion error") - // The actual translated message depends on i18n setup - expect(result.message).toBeTruthy() - }) - - it("should preserve status even for ByteString errors", () => { - const error = new Error("Cannot convert argument to a ByteString") as any - error.status = 400 - - const result = handleOpenAIError(error, providerName) - - // Even though ByteString errors are typically client-side, - // we preserve any status metadata that exists for debugging purposes - expect((result as any).status).toBe(400) - }) - }) - - describe("error message formatting", () => { - it("should wrap error message with provider name prefix", () => { - const error = new Error("Authentication failed") - - const result = handleOpenAIError(error, providerName) - - expect(result.message).toBe("TestProvider completion error: Authentication failed") - }) - - it("should handle error with nested metadata", () => { - const error = new Error("Network error") as any - error.error = { - metadata: { - raw: "Connection refused", - }, - } - - const result = handleOpenAIError(error, providerName) - - expect(result.message).toContain("Connection refused") - expect(result.message).toContain("TestProvider completion error") - }) - - it("should handle non-Error exceptions", () => { - const error = { message: "Something went wrong" } - - const result = handleOpenAIError(error, providerName) - - expect(result).toBeInstanceOf(Error) - expect(result.message).toContain("TestProvider completion error") - expect(result.message).toContain("[object Object]") - }) - - it("should handle string exceptions", () => { - const error = "Connection timeout" - - const result = handleOpenAIError(error, providerName) - - expect(result).toBeInstanceOf(Error) - expect(result.message).toBe("TestProvider completion error: Connection timeout") - }) - }) - - describe("real-world error scenarios", () => { - it("should handle 401 Unauthorized with status and message", () => { - const error = new Error("Unauthorized") as any - error.status = 401 - - const result = handleOpenAIError(error, providerName) - - expect(result.message).toContain("Unauthorized") - expect((result as any).status).toBe(401) - }) - - it("should handle 429 Rate Limit with RetryInfo", () => { - const error = new Error("Rate limit exceeded") as any - error.status = 429 - error.errorDetails = [ - { - "@type": "type.googleapis.com/google.rpc.RetryInfo", - retryDelay: "10s", - }, - ] - - const result = handleOpenAIError(error, providerName) - - expect((result as any).status).toBe(429) - expect((result as any).errorDetails).toBeDefined() - expect((result as any).errorDetails[0].retryDelay).toBe("10s") - }) - - it("should handle 500 Internal Server Error", () => { - const error = new Error("Internal server error") as any - error.status = 500 - - const result = handleOpenAIError(error, providerName) - - expect((result as any).status).toBe(500) - expect(result.message).toContain("Internal server error") - }) - - it("should handle errors without status gracefully", () => { - const error = new Error("Network connectivity issue") - - const result = handleOpenAIError(error, providerName) - - expect(result).toBeInstanceOf(Error) - expect((result as any).status).toBeUndefined() - expect(result.message).toContain("Network connectivity issue") - }) - }) -}) diff --git a/src/api/providers/utils/openai-error-handler.ts b/src/api/providers/utils/openai-error-handler.ts deleted file mode 100644 index f4bd7d0348..0000000000 --- a/src/api/providers/utils/openai-error-handler.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * General error handler for OpenAI client errors - * Transforms technical errors into user-friendly messages - * - * @deprecated Use handleProviderError from './error-handler' instead - * This file is kept for backward compatibility - */ - -import { handleProviderError } from "./error-handler" - -/** - * 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 { - return handleProviderError(error, providerName, { messagePrefix: "completion" }) -} diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index e5c0ba0a81..17ac40c8bf 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -14,7 +14,7 @@ import { getModelParams } from "../transform/model-params" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" import { isMcpTool } from "../../utils/mcp-name" const XAI_DEFAULT_TEMPERATURE = 0 diff --git a/src/api/providers/zai.ts b/src/api/providers/zai.ts index c8f720a971..4854c814fd 100644 --- a/src/api/providers/zai.ts +++ b/src/api/providers/zai.ts @@ -16,7 +16,7 @@ import { convertToZAiFormat } from "../transform/zai-format" import type { ApiHandlerCreateMessageMetadata } from "../index" import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" // Custom interface for Z.ai params to support thinking mode and reasoning effort tiers. // Z.ai accepts the standard `reasoning_effort` ladder (none/minimal/low/medium/high/xhigh/max) diff --git a/src/services/code-index/embedders/openai-compatible.ts b/src/services/code-index/embedders/openai-compatible.ts index 6eaf2b6c2c..e4e36a0272 100644 --- a/src/services/code-index/embedders/openai-compatible.ts +++ b/src/services/code-index/embedders/openai-compatible.ts @@ -12,7 +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" +import { handleOpenAIError } from "../../../api/providers/utils/error-handler" interface EmbeddingItem { embedding: string | number[] diff --git a/src/services/code-index/embedders/openai.ts b/src/services/code-index/embedders/openai.ts index b993e280d9..4ff8f27e47 100644 --- a/src/services/code-index/embedders/openai.ts +++ b/src/services/code-index/embedders/openai.ts @@ -13,7 +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" +import { handleOpenAIError } from "../../../api/providers/utils/error-handler" /** * OpenAI implementation of the embedder interface with batching and rate limiting diff --git a/src/services/code-index/embedders/openrouter.ts b/src/services/code-index/embedders/openrouter.ts index d98aaeeeb5..d483d531ca 100644 --- a/src/services/code-index/embedders/openrouter.ts +++ b/src/services/code-index/embedders/openrouter.ts @@ -12,7 +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" +import { handleOpenAIError } from "../../../api/providers/utils/error-handler" // Default provider name when no specific provider is selected export const OPENROUTER_DEFAULT_PROVIDER_NAME = "[default]" From a11eba353938d52e48f9057c379a7b9bef9dcd30 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 30 Jun 2026 15:59:17 +0900 Subject: [PATCH 2/3] test(deepseek): add test for API error handling in createMessage Cover the catch block that wraps API errors via handleOpenAIError, fixing 0% patch coverage on the error path. Signed-off-by: daewoongoh --- src/api/providers/__tests__/deepseek.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/api/providers/__tests__/deepseek.spec.ts b/src/api/providers/__tests__/deepseek.spec.ts index 2f0482eeef..61bbd45f00 100644 --- a/src/api/providers/__tests__/deepseek.spec.ts +++ b/src/api/providers/__tests__/deepseek.spec.ts @@ -426,6 +426,14 @@ describe("DeepSeekHandler", () => { expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" }) }) + it("should throw a wrapped error when the API call fails", async () => { + const apiError = Object.assign(new Error("Invalid API key"), { status: 401 }) + mockCreate.mockRejectedValueOnce(apiError) + + const stream = handler.createMessage(systemPrompt, messages) + await expect(stream.next()).rejects.toThrow("DeepSeek completion error: Invalid API key") + }) + it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => { mockCreate.mockImplementationOnce(async () => ({ [Symbol.asyncIterator]: async function* () { From 3951724b612e6d8e254a2b4e406b8cf078469c0d Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Thu, 2 Jul 2026 09:48:07 +0900 Subject: [PATCH 3/3] refactor(deepseek): convert dynamic import to static for handleOpenAIError Replace dynamic `await import()` with a static import for `handleOpenAIError` in the catch block, and update the error handler test to also assert the HTTP status code (401) on the thrown error. Signed-off-by: daewoongoh --- src/api/providers/__tests__/deepseek.spec.ts | 5 ++++- src/api/providers/deepseek.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/api/providers/__tests__/deepseek.spec.ts b/src/api/providers/__tests__/deepseek.spec.ts index 61bbd45f00..563b49bce8 100644 --- a/src/api/providers/__tests__/deepseek.spec.ts +++ b/src/api/providers/__tests__/deepseek.spec.ts @@ -431,7 +431,10 @@ describe("DeepSeekHandler", () => { mockCreate.mockRejectedValueOnce(apiError) const stream = handler.createMessage(systemPrompt, messages) - await expect(stream.next()).rejects.toThrow("DeepSeek completion error: Invalid API key") + const err = await stream.next().catch((e) => e) + + expect(err.message).toBe("DeepSeek completion error: Invalid API key") + expect((err as any).status).toBe(401) }) it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => { diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index 45f7fba988..ef7839ad34 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -18,6 +18,7 @@ import { convertToR1Format } from "../transform/r1-format" import { OpenAiHandler } from "./openai" import { extractReasoningFromDelta } from "./utils/extract-reasoning" import type { ApiHandlerCreateMessageMetadata } from "../index" +import { handleOpenAIError } from "./utils/error-handler" // Custom interface for DeepSeek params to support thinking mode type DeepSeekChatCompletionParams = Omit & { @@ -137,7 +138,6 @@ export class DeepSeekHandler extends OpenAiHandler { isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, ) } catch (error) { - const { handleOpenAIError } = await import("./utils/error-handler") throw handleOpenAIError(error, "DeepSeek") }