From d0f95a552895ab6f977f9f8faa8529baff4eb91a Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 6 Sep 2025 17:42:33 +0000 Subject: [PATCH] fix: add timeout configuration to Anthropic provider - Added getApiRequestTimeout() to Anthropic client initialization - This ensures API requests timeout properly instead of hanging indefinitely - Particularly important when dealing with large files that may cause long processing times - Added comprehensive tests for timeout configuration Fixes #7744 --- .../__tests__/anthropic-timeout.spec.ts | 151 ++++++++++++++++++ src/api/providers/anthropic.ts | 2 + 2 files changed, 153 insertions(+) create mode 100644 src/api/providers/__tests__/anthropic-timeout.spec.ts diff --git a/src/api/providers/__tests__/anthropic-timeout.spec.ts b/src/api/providers/__tests__/anthropic-timeout.spec.ts new file mode 100644 index 0000000000..092df99bd2 --- /dev/null +++ b/src/api/providers/__tests__/anthropic-timeout.spec.ts @@ -0,0 +1,151 @@ +// npx vitest run api/providers/__tests__/anthropic-timeout.spec.ts + +import { describe, it, expect, beforeEach, vitest } from "vitest" +import { ApiHandlerOptions } from "../../../shared/api" + +// Mock the timeout config utility +vitest.mock("../utils/timeout-config", () => ({ + getApiRequestTimeout: vitest.fn(), +})) + +import { getApiRequestTimeout } from "../utils/timeout-config" + +// Mock the Anthropic SDK +vitest.mock("@anthropic-ai/sdk", () => { + const mockAnthropicConstructor = vitest.fn().mockImplementation(() => ({ + messages: { + create: vitest.fn(), + countTokens: vitest.fn(), + }, + })) + return { + Anthropic: mockAnthropicConstructor, + } +}) + +// Import after mocks +import { Anthropic } from "@anthropic-ai/sdk" +import { AnthropicHandler } from "../anthropic" + +const mockAnthropicConstructor = Anthropic as any + +describe("AnthropicHandler timeout configuration", () => { + beforeEach(() => { + vitest.clearAllMocks() + }) + + it("should use default timeout of 600 seconds when no configuration is set", () => { + ;(getApiRequestTimeout as any).mockReturnValue(600000) + + const options: ApiHandlerOptions = { + apiKey: "test-api-key", + apiModelId: "claude-3-5-sonnet-20241022", + } + + new AnthropicHandler(options) + + expect(getApiRequestTimeout).toHaveBeenCalled() + expect(mockAnthropicConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: "test-api-key", + timeout: 600000, // 600 seconds in milliseconds + }), + ) + }) + + it("should use custom timeout when configuration is set", () => { + ;(getApiRequestTimeout as any).mockReturnValue(1800000) // 30 minutes + + const options: ApiHandlerOptions = { + apiKey: "test-api-key", + apiModelId: "claude-3-5-sonnet-20241022", + } + + new AnthropicHandler(options) + + expect(mockAnthropicConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: "test-api-key", + timeout: 1800000, // 1800 seconds in milliseconds + }), + ) + }) + + it("should handle zero timeout (no timeout)", () => { + ;(getApiRequestTimeout as any).mockReturnValue(0) + + const options: ApiHandlerOptions = { + apiKey: "test-api-key", + apiModelId: "claude-3-5-sonnet-20241022", + } + + new AnthropicHandler(options) + + expect(mockAnthropicConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: "test-api-key", + timeout: 0, // No timeout + }), + ) + }) + + it("should use custom base URL when provided", () => { + ;(getApiRequestTimeout as any).mockReturnValue(600000) + + const options: ApiHandlerOptions = { + apiKey: "test-api-key", + apiModelId: "claude-3-5-sonnet-20241022", + anthropicBaseUrl: "https://custom.anthropic.com", + } + + new AnthropicHandler(options) + + expect(mockAnthropicConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://custom.anthropic.com", + apiKey: "test-api-key", + timeout: 600000, + }), + ) + }) + + it("should use authToken when anthropicUseAuthToken is set with custom base URL", () => { + ;(getApiRequestTimeout as any).mockReturnValue(900000) // 15 minutes + + const options: ApiHandlerOptions = { + apiKey: "test-auth-token", + apiModelId: "claude-3-5-sonnet-20241022", + anthropicBaseUrl: "https://custom.anthropic.com", + anthropicUseAuthToken: true, + } + + new AnthropicHandler(options) + + expect(mockAnthropicConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://custom.anthropic.com", + authToken: "test-auth-token", + timeout: 900000, + }), + ) + }) + + it("should use apiKey when anthropicUseAuthToken is set but no custom base URL", () => { + ;(getApiRequestTimeout as any).mockReturnValue(600000) + + const options: ApiHandlerOptions = { + apiKey: "test-api-key", + apiModelId: "claude-3-5-sonnet-20241022", + anthropicUseAuthToken: true, + } + + new AnthropicHandler(options) + + expect(mockAnthropicConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: "test-api-key", + timeout: 600000, + }), + ) + }) +}) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index cb48492b60..6f9c93e473 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -18,6 +18,7 @@ import { getModelParams } from "../transform/model-params" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { calculateApiCostAnthropic } from "../../shared/cost" +import { getApiRequestTimeout } from "./utils/timeout-config" export class AnthropicHandler extends BaseProvider implements SingleCompletionHandler { private options: ApiHandlerOptions @@ -33,6 +34,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa this.client = new Anthropic({ baseURL: this.options.anthropicBaseUrl || undefined, [apiKeyFieldName]: this.options.apiKey, + timeout: getApiRequestTimeout(), }) }