Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4c82de4
fix(api): apply apiRequestTimeout consistently across OpenAI/Anthropi…
Jun 11, 2026
e9461af
docs(i18n): list providers where apiRequestTimeout has no effect
Jun 11, 2026
94344e7
test(api): align provider tests with apiRequestTimeout wiring
Jun 11, 2026
59ff0ed
docs(i18n): drop provider examples from apiRequestTimeout recommendation
Jun 11, 2026
591efef
chore: fix typo in Turkish apiRequestTimeout description
daewoongoh Jun 11, 2026
eee6cb0
fix(api): round timeout ms and restrict apiRequestTimeout to 1-3600s
daewoongoh Jun 15, 2026
1bb1827
docs(i18n): remove '0 = no timeout', scope GCP Vertex AI, add Moonsho…
daewoongoh Jun 15, 2026
0bad013
docs(i18n): tighten apiRequestTimeout description across all locales
daewoongoh Jun 15, 2026
cca8e68
refactor(api): extract timeout constants and validate range bounds
daewoongoh Jun 15, 2026
5411599
test(api): cover timeout passthrough for Vertex auth variants
daewoongoh Jun 15, 2026
9be9cec
Merge branch 'Zoo-Code-Org:main' into fix/api-request-timeout-consist…
daewoongoh Jun 15, 2026
09be296
test(api): make GoogleAuth mock newable in Vertex tests
daewoongoh Jun 15, 2026
ca18a8e
Merge branch 'Zoo-Code-Org:main' into fix/api-request-timeout-consist…
daewoongoh Jun 17, 2026
2aeb510
Add changeset for API request timeout
daewoongoh Jun 18, 2026
ad547d2
refactor: standardize API request timeout handling across providers
daewoongoh Jun 18, 2026
8cba19e
refactor: use class field initialization for timeoutMs in BaseProvider
daewoongoh Jun 18, 2026
b25a2d0
fix(tests): add getConfiguration mock to vscode provider tests
daewoongoh Jun 18, 2026
72ee779
chore(i18n): update apiRequestTimeout description for Google Gemini c…
daewoongoh Jun 18, 2026
fa1cb19
test: add timeout config mock to provider tests
daewoongoh Jun 18, 2026
2bbc093
Merge branch 'main' into fix/api-request-timeout-consistency
navedmerchant Jun 19, 2026
760f2ee
Merge branch 'Zoo-Code-Org:main' into fix/api-request-timeout-consist…
daewoongoh Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/api-request-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"zoo-code": patch
---

Updated `apiRequestTimeout` validation. Values must be integers between 1 and 3600 seconds; invalid or out-of-range values, including `0`, now fall back to 600 seconds. This aligns with the SDK's default timeout value.
69 changes: 69 additions & 0 deletions src/api/providers/__tests__/anthropic-vertex.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@

import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { GoogleAuth } from "google-auth-library"

import { VERTEX_1M_CONTEXT_MODEL_IDS } from "@roo-code/types"

import { ApiStreamChunk } from "../../transform/stream"

import { AnthropicVertexHandler } from "../anthropic-vertex"

vitest.mock("../utils/timeout-config", () => ({
getApiRequestTimeout: vitest.fn().mockReturnValue(300_000),
}))

const MOCK_TIMEOUT_MS = 300_000

vitest.mock("google-auth-library", () => ({
GoogleAuth: vitest.fn().mockImplementation(function (opts) {
return { __googleAuthOptions: opts }
}),
}))

vitest.mock("@anthropic-ai/vertex-sdk", () => ({
AnthropicVertex: vitest.fn().mockImplementation(function () {
return {
Expand Down Expand Up @@ -56,6 +69,11 @@ describe("VertexHandler", () => {
let handler: AnthropicVertexHandler

describe("constructor", () => {
beforeEach(() => {
;(AnthropicVertex as any).mockClear()
;(GoogleAuth as any).mockClear()
})

it("should initialize with provided config for Claude", () => {
handler = new AnthropicVertexHandler({
apiModelId: "claude-3-5-sonnet-v2@20241022",
Expand All @@ -66,7 +84,58 @@ describe("VertexHandler", () => {
expect(AnthropicVertex).toHaveBeenCalledWith({
projectId: "test-project",
region: "us-central1",
timeout: MOCK_TIMEOUT_MS,
})
})

it("should pass timeout when initializing with vertexJsonCredentials", () => {
const credentials = {
type: "service_account",
client_email: "test@test-project.iam.gserviceaccount.com",
private_key: "-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----\n",
}

handler = new AnthropicVertexHandler({
apiModelId: "claude-3-5-sonnet-v2@20241022",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
vertexJsonCredentials: JSON.stringify(credentials),
})

expect(GoogleAuth).toHaveBeenCalledWith({
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
credentials,
})
expect(AnthropicVertex).toHaveBeenCalledWith(
expect.objectContaining({
projectId: "test-project",
region: "us-central1",
googleAuth: expect.any(Object),
timeout: MOCK_TIMEOUT_MS,
}),
)
})

it("should pass timeout when initializing with vertexKeyFile", () => {
handler = new AnthropicVertexHandler({
apiModelId: "claude-3-5-sonnet-v2@20241022",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
vertexKeyFile: "/tmp/sa-key.json",
})

expect(GoogleAuth).toHaveBeenCalledWith({
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
keyFile: "/tmp/sa-key.json",
})
expect(AnthropicVertex).toHaveBeenCalledWith(
expect.objectContaining({
projectId: "test-project",
region: "us-central1",
googleAuth: expect.any(Object),
timeout: MOCK_TIMEOUT_MS,
}),
)
})
})

Expand Down
8 changes: 7 additions & 1 deletion src/api/providers/__tests__/lite-llm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import { ApiHandlerOptions } from "../../../shared/api"
import { litellmDefaultModelId, litellmDefaultModelInfo } from "@roo-code/types"

// Mock vscode first to avoid import errors
vi.mock("vscode", () => ({}))
vi.mock("vscode", () => ({
workspace: {
getConfiguration: () => ({
get: (_key: string, defaultValue?: unknown) => defaultValue,
}),
},
}))

// Mock OpenAI
const mockCreate = vi.fn()
Expand Down
8 changes: 7 additions & 1 deletion src/api/providers/__tests__/openai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import { openAiModelInfoSaneDefaults, DEEP_SEEK_DEFAULT_TEMPERATURE } from "@roo
import { Package } from "../../../shared/package"
import axios from "axios"

vitest.mock("../utils/timeout-config", () => ({
getApiRequestTimeout: vitest.fn().mockReturnValue(300_000),
}))

const MOCK_TIMEOUT_MS = 300_000

const mockCreate = vitest.fn()

vitest.mock("openai", () => {
Expand Down Expand Up @@ -117,7 +123,7 @@ describe("OpenAiHandler", () => {
"X-Title": "Zoo Code",
"User-Agent": `ZooCode/${Package.version}`,
},
timeout: expect.any(Number),
timeout: MOCK_TIMEOUT_MS,
})
})
})
Expand Down
8 changes: 7 additions & 1 deletion src/api/providers/__tests__/opencode-go.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// npx vitest run src/api/providers/__tests__/opencode-go.spec.ts

// Mock vscode first to avoid import errors
vitest.mock("vscode", () => ({}))
vitest.mock("vscode", () => ({
workspace: {
getConfiguration: () => ({
get: (_key: string, defaultValue?: unknown) => defaultValue,
}),
},
}))

import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
Expand Down
15 changes: 14 additions & 1 deletion src/api/providers/__tests__/openrouter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
// pnpm --filter roo-cline test api/providers/__tests__/openrouter.spec.ts

vitest.mock("vscode", () => ({}))
vitest.mock("vscode", () => ({
workspace: {
getConfiguration: () => ({
get: (_key: string, defaultValue?: unknown) => defaultValue,
}),
},
}))

vitest.mock("../utils/timeout-config", () => ({
getApiRequestTimeout: vitest.fn().mockReturnValue(300_000),
}))

const MOCK_TIMEOUT_MS = 300_000

import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
Expand Down Expand Up @@ -108,6 +120,7 @@ describe("OpenRouterHandler", () => {
"X-Title": "Zoo Code",
"User-Agent": `ZooCode/${Package.version}`,
},
timeout: MOCK_TIMEOUT_MS,
})
})

Expand Down
8 changes: 8 additions & 0 deletions src/api/providers/__tests__/requesty.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// npx vitest run api/providers/__tests__/requesty.spec.ts

vitest.mock("../utils/timeout-config", () => ({
getApiRequestTimeout: vitest.fn().mockReturnValue(300_000),
}))

const MOCK_TIMEOUT_MS = 300_000

import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"

Expand Down Expand Up @@ -82,6 +88,7 @@ describe("RequestyHandler", () => {
"X-Title": "Zoo Code",
"User-Agent": `ZooCode/${Package.version}`,
},
timeout: MOCK_TIMEOUT_MS,
})
})

Expand All @@ -97,6 +104,7 @@ describe("RequestyHandler", () => {
"X-Title": "Zoo Code",
"User-Agent": `ZooCode/${Package.version}`,
},
timeout: MOCK_TIMEOUT_MS,
})
})

Expand Down
9 changes: 8 additions & 1 deletion src/api/providers/__tests__/vercel-ai-gateway.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// npx vitest run src/api/providers/__tests__/vercel-ai-gateway.spec.ts

// Mock vscode first to avoid import errors
vitest.mock("vscode", () => ({}))
vitest.mock("vscode", () => ({
workspace: {
getConfiguration: () => ({
get: (_key: string, defaultValue?: unknown) => defaultValue,
}),
},
}))

import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
Expand Down Expand Up @@ -118,6 +124,7 @@ describe("VercelAiGatewayHandler", () => {
"X-Title": "Zoo Code",
"User-Agent": expect.stringContaining("ZooCode/"),
}),
timeout: expect.any(Number),
})
})

Expand Down
8 changes: 7 additions & 1 deletion src/api/providers/__tests__/vertex-credentials.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

// Mock vscode first to avoid import errors when the provider stack pulls
// transitive vscode-dependent modules during construction.
vitest.mock("vscode", () => ({}))
vitest.mock("vscode", () => ({
workspace: {
getConfiguration: () => ({
get: (_key: string, defaultValue?: unknown) => defaultValue,
}),
},
}))

vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
Expand Down
8 changes: 7 additions & 1 deletion src/api/providers/__tests__/vertex.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// npx vitest run src/api/providers/__tests__/vertex.spec.ts

// Mock vscode first to avoid import errors
vitest.mock("vscode", () => ({}))
vitest.mock("vscode", () => ({
workspace: {
getConfiguration: vitest.fn(() => ({
get: vitest.fn((key: string, defaultValue: any) => defaultValue),
})),
},
}))

const mockCaptureException = vitest.fn()

Expand Down
3 changes: 3 additions & 0 deletions src/api/providers/__tests__/vscode-lm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ vi.mock("vscode", () => {

return {
workspace: {
getConfiguration: vi.fn(() => ({
get: vi.fn((key: string, defaultValue: any) => defaultValue),
})),
onDidChangeConfiguration: vi.fn((_callback) => ({
dispose: vi.fn(),
})),
Expand Down
12 changes: 12 additions & 0 deletions src/api/providers/__tests__/zoo-gateway.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// npx vitest run src/api/providers/__tests__/zoo-gateway.spec.ts

vitest.mock("../utils/timeout-config", () => ({
getApiRequestTimeout: vitest.fn().mockReturnValue(300_000),
}))

const MOCK_TIMEOUT_MS = 300_000

const { showErrorMessage, openExternal } = vitest.hoisted(() => ({
showErrorMessage: vitest.fn(async () => undefined as string | undefined),
openExternal: vitest.fn(async () => true),
Expand All @@ -9,6 +15,11 @@ vitest.mock("vscode", () => ({
window: { showErrorMessage },
env: { openExternal, uriScheme: "vscode", appName: "VS Code" },
Uri: { parse: (value: string) => ({ toString: () => value }) },
workspace: {
getConfiguration: () => ({
get: (_key: string, defaultValue?: unknown) => defaultValue,
}),
},
}))

vitest.mock("../../../i18n", () => ({
Expand Down Expand Up @@ -178,6 +189,7 @@ describe("ZooGatewayHandler", () => {
"X-Zoo-Editor": "vscode",
"X-Zoo-Extension-Version": Package.version,
}),
timeout: MOCK_TIMEOUT_MS,
})
})

Expand Down
4 changes: 3 additions & 1 deletion src/api/providers/anthropic-vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
credentials: parsedVertexCredentials,
}),
timeout: this.timeoutMs,
})
} else if (this.options.vertexKeyFile) {
this.client = new AnthropicVertex({
Expand All @@ -60,9 +61,10 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
keyFile: this.options.vertexKeyFile,
}),
timeout: this.timeoutMs,
})
} else {
this.client = new AnthropicVertex({ projectId, region })
this.client = new AnthropicVertex({ projectId, region, timeout: this.timeoutMs })
}
}

Expand Down
1 change: 1 addition & 0 deletions src/api/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
this.client = new Anthropic({
baseURL: this.options.anthropicBaseUrl || undefined,
[apiKeyFieldName]: this.options.apiKey,
timeout: this.timeoutMs,
})
}

Expand Down
3 changes: 1 addition & 2 deletions src/api/providers/base-openai-compatible-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { DEFAULT_HEADERS } from "./constants"
import { BaseProvider } from "./base-provider"
import { handleOpenAIError } from "./utils/openai-error-handler"
import { calculateApiCostOpenAI } from "../../shared/cost"
import { getApiRequestTimeout } from "./utils/timeout-config"
import { extractReasoningFromDelta } from "./utils/extract-reasoning"

type BaseOpenAiCompatibleProviderOptions<ModelName extends string> = ApiHandlerOptions & {
Expand Down Expand Up @@ -64,7 +63,7 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
baseURL,
apiKey: this.options.apiKey,
defaultHeaders: DEFAULT_HEADERS,
timeout: getApiRequestTimeout(),
timeout: this.timeoutMs,
})
}

Expand Down
3 changes: 3 additions & 0 deletions src/api/providers/base-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import type { ApiHandler, ApiHandlerCreateMessageMetadata } from "../index"
import { ApiStream } from "../transform/stream"
import { countTokens } from "../../utils/countTokens"
import { isMcpTool } from "../../utils/mcp-name"
import { getApiRequestTimeout } from "./utils/timeout-config"

/**
* Base class for API providers that implements common functionality.
*/
export abstract class BaseProvider implements ApiHandler {
protected readonly timeoutMs: number = getApiRequestTimeout()

abstract createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
Expand Down
3 changes: 1 addition & 2 deletions src/api/providers/lm-studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { ApiStream } from "../transform/stream"
import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import { getModelsFromCache } from "./fetchers/modelCache"
import { getApiRequestTimeout } from "./utils/timeout-config"
import { handleOpenAIError } from "./utils/openai-error-handler"

export class LmStudioHandler extends BaseProvider implements SingleCompletionHandler {
Expand All @@ -33,7 +32,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
this.client = new OpenAI({
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
apiKey: apiKey,
timeout: getApiRequestTimeout(),
timeout: this.timeoutMs,
})
}

Expand Down
1 change: 1 addition & 0 deletions src/api/providers/minimax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
this.client = new Anthropic({
baseURL,
apiKey: options.minimaxApiKey,
timeout: this.timeoutMs,
})
}

Expand Down
1 change: 1 addition & 0 deletions src/api/providers/openai-codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
apiKey: accessToken,
baseURL: CODEX_API_BASE_URL,
defaultHeaders: codexHeaders,
timeout: this.timeoutMs,
})

const stream = (await (client as any).responses.create(requestBody, {
Expand Down
Loading
Loading