Skip to content

Commit b62015d

Browse files
committed
feat: add configurable API request timeout for local providers
- Add new VSCode setting roo-cline.apiRequestTimeout (default: 600s, range: 0-3600s) - Update LM Studio, Ollama, and OpenAI handlers to use the timeout setting - Add comprehensive tests for timeout functionality - Helps users with local providers that need more processing time Fixes #6521
1 parent 5c05762 commit b62015d

File tree

9 files changed

+398
-1
lines changed

9 files changed

+398
-1
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// npx vitest run api/providers/__tests__/lm-studio-timeout.spec.ts
2+
3+
import { LmStudioHandler } from "../lm-studio"
4+
import { ApiHandlerOptions } from "../../../shared/api"
5+
import * as vscode from "vscode"
6+
7+
// Mock vscode
8+
vitest.mock("vscode", () => ({
9+
workspace: {
10+
getConfiguration: vitest.fn().mockReturnValue({
11+
get: vitest.fn(),
12+
}),
13+
},
14+
}))
15+
16+
// Mock OpenAI
17+
const mockOpenAIConstructor = vitest.fn()
18+
vitest.mock("openai", () => {
19+
return {
20+
__esModule: true,
21+
default: vitest.fn().mockImplementation((config) => {
22+
mockOpenAIConstructor(config)
23+
return {
24+
chat: {
25+
completions: {
26+
create: vitest.fn(),
27+
},
28+
},
29+
}
30+
}),
31+
}
32+
})
33+
34+
describe("LmStudioHandler timeout configuration", () => {
35+
let mockGetConfig: any
36+
37+
beforeEach(() => {
38+
vitest.clearAllMocks()
39+
mockGetConfig = vitest.fn()
40+
;(vscode.workspace.getConfiguration as any).mockReturnValue({
41+
get: mockGetConfig,
42+
})
43+
})
44+
45+
it("should use default timeout of 600 seconds when no configuration is set", () => {
46+
mockGetConfig.mockReturnValue(600)
47+
48+
const options: ApiHandlerOptions = {
49+
apiModelId: "llama2",
50+
lmStudioModelId: "llama2",
51+
lmStudioBaseUrl: "http://localhost:1234",
52+
}
53+
54+
new LmStudioHandler(options)
55+
56+
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline")
57+
expect(mockGetConfig).toHaveBeenCalledWith("apiRequestTimeout", 600)
58+
expect(mockOpenAIConstructor).toHaveBeenCalledWith(
59+
expect.objectContaining({
60+
baseURL: "http://localhost:1234/v1",
61+
apiKey: "noop",
62+
timeout: 600000, // 600 seconds in milliseconds
63+
}),
64+
)
65+
})
66+
67+
it("should use custom timeout when configuration is set", () => {
68+
mockGetConfig.mockReturnValue(1200) // 20 minutes
69+
70+
const options: ApiHandlerOptions = {
71+
apiModelId: "llama2",
72+
lmStudioModelId: "llama2",
73+
lmStudioBaseUrl: "http://localhost:1234",
74+
}
75+
76+
new LmStudioHandler(options)
77+
78+
expect(mockOpenAIConstructor).toHaveBeenCalledWith(
79+
expect.objectContaining({
80+
timeout: 1200000, // 1200 seconds in milliseconds
81+
}),
82+
)
83+
})
84+
85+
it("should handle zero timeout (no timeout)", () => {
86+
mockGetConfig.mockReturnValue(0)
87+
88+
const options: ApiHandlerOptions = {
89+
apiModelId: "llama2",
90+
lmStudioModelId: "llama2",
91+
}
92+
93+
new LmStudioHandler(options)
94+
95+
expect(mockOpenAIConstructor).toHaveBeenCalledWith(
96+
expect.objectContaining({
97+
timeout: 0, // No timeout
98+
}),
99+
)
100+
})
101+
})
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// npx vitest run api/providers/__tests__/ollama-timeout.spec.ts
2+
3+
import { OllamaHandler } from "../ollama"
4+
import { ApiHandlerOptions } from "../../../shared/api"
5+
import * as vscode from "vscode"
6+
7+
// Mock vscode
8+
vitest.mock("vscode", () => ({
9+
workspace: {
10+
getConfiguration: vitest.fn().mockReturnValue({
11+
get: vitest.fn(),
12+
}),
13+
},
14+
}))
15+
16+
// Mock OpenAI
17+
const mockOpenAIConstructor = vitest.fn()
18+
vitest.mock("openai", () => {
19+
return {
20+
__esModule: true,
21+
default: vitest.fn().mockImplementation((config) => {
22+
mockOpenAIConstructor(config)
23+
return {
24+
chat: {
25+
completions: {
26+
create: vitest.fn(),
27+
},
28+
},
29+
}
30+
}),
31+
}
32+
})
33+
34+
describe("OllamaHandler timeout configuration", () => {
35+
let mockGetConfig: any
36+
37+
beforeEach(() => {
38+
vitest.clearAllMocks()
39+
mockGetConfig = vitest.fn()
40+
;(vscode.workspace.getConfiguration as any).mockReturnValue({
41+
get: mockGetConfig,
42+
})
43+
})
44+
45+
it("should use default timeout of 600 seconds when no configuration is set", () => {
46+
mockGetConfig.mockReturnValue(600)
47+
48+
const options: ApiHandlerOptions = {
49+
apiModelId: "llama2",
50+
ollamaModelId: "llama2",
51+
ollamaBaseUrl: "http://localhost:11434",
52+
}
53+
54+
new OllamaHandler(options)
55+
56+
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline")
57+
expect(mockGetConfig).toHaveBeenCalledWith("apiRequestTimeout", 600)
58+
expect(mockOpenAIConstructor).toHaveBeenCalledWith(
59+
expect.objectContaining({
60+
baseURL: "http://localhost:11434/v1",
61+
apiKey: "ollama",
62+
timeout: 600000, // 600 seconds in milliseconds
63+
}),
64+
)
65+
})
66+
67+
it("should use custom timeout when configuration is set", () => {
68+
mockGetConfig.mockReturnValue(3600) // 1 hour
69+
70+
const options: ApiHandlerOptions = {
71+
apiModelId: "llama2",
72+
ollamaModelId: "llama2",
73+
}
74+
75+
new OllamaHandler(options)
76+
77+
expect(mockOpenAIConstructor).toHaveBeenCalledWith(
78+
expect.objectContaining({
79+
timeout: 3600000, // 3600 seconds in milliseconds
80+
}),
81+
)
82+
})
83+
84+
it("should handle zero timeout (no timeout)", () => {
85+
mockGetConfig.mockReturnValue(0)
86+
87+
const options: ApiHandlerOptions = {
88+
apiModelId: "llama2",
89+
ollamaModelId: "llama2",
90+
ollamaBaseUrl: "http://localhost:11434",
91+
}
92+
93+
new OllamaHandler(options)
94+
95+
expect(mockOpenAIConstructor).toHaveBeenCalledWith(
96+
expect.objectContaining({
97+
timeout: 0, // No timeout
98+
}),
99+
)
100+
})
101+
102+
it("should use default base URL when not provided", () => {
103+
mockGetConfig.mockReturnValue(600)
104+
105+
const options: ApiHandlerOptions = {
106+
apiModelId: "llama2",
107+
ollamaModelId: "llama2",
108+
}
109+
110+
new OllamaHandler(options)
111+
112+
expect(mockOpenAIConstructor).toHaveBeenCalledWith(
113+
expect.objectContaining({
114+
baseURL: "http://localhost:11434/v1",
115+
}),
116+
)
117+
})
118+
})
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// npx vitest run api/providers/__tests__/openai-timeout.spec.ts
2+
3+
import { OpenAiHandler } from "../openai"
4+
import { ApiHandlerOptions } from "../../../shared/api"
5+
import * as vscode from "vscode"
6+
7+
// Mock vscode
8+
vitest.mock("vscode", () => ({
9+
workspace: {
10+
getConfiguration: vitest.fn().mockReturnValue({
11+
get: vitest.fn(),
12+
}),
13+
},
14+
}))
15+
16+
// Mock OpenAI and AzureOpenAI
17+
const mockOpenAIConstructor = vitest.fn()
18+
const mockAzureOpenAIConstructor = vitest.fn()
19+
20+
vitest.mock("openai", () => {
21+
return {
22+
__esModule: true,
23+
default: vitest.fn().mockImplementation((config) => {
24+
mockOpenAIConstructor(config)
25+
return {
26+
chat: {
27+
completions: {
28+
create: vitest.fn(),
29+
},
30+
},
31+
}
32+
}),
33+
AzureOpenAI: vitest.fn().mockImplementation((config) => {
34+
mockAzureOpenAIConstructor(config)
35+
return {
36+
chat: {
37+
completions: {
38+
create: vitest.fn(),
39+
},
40+
},
41+
}
42+
}),
43+
}
44+
})
45+
46+
describe("OpenAiHandler timeout configuration", () => {
47+
let mockGetConfig: any
48+
49+
beforeEach(() => {
50+
vitest.clearAllMocks()
51+
mockGetConfig = vitest.fn()
52+
;(vscode.workspace.getConfiguration as any).mockReturnValue({
53+
get: mockGetConfig,
54+
})
55+
})
56+
57+
it("should use default timeout for standard OpenAI", () => {
58+
mockGetConfig.mockReturnValue(600)
59+
60+
const options: ApiHandlerOptions = {
61+
apiModelId: "gpt-4",
62+
openAiModelId: "gpt-4",
63+
openAiApiKey: "test-key",
64+
}
65+
66+
new OpenAiHandler(options)
67+
68+
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline")
69+
expect(mockGetConfig).toHaveBeenCalledWith("apiRequestTimeout", 600)
70+
expect(mockOpenAIConstructor).toHaveBeenCalledWith(
71+
expect.objectContaining({
72+
baseURL: "https://api.openai.com/v1",
73+
apiKey: "test-key",
74+
timeout: 600000, // 600 seconds in milliseconds
75+
}),
76+
)
77+
})
78+
79+
it("should use custom timeout for OpenAI-compatible providers", () => {
80+
mockGetConfig.mockReturnValue(1800) // 30 minutes
81+
82+
const options: ApiHandlerOptions = {
83+
apiModelId: "custom-model",
84+
openAiModelId: "custom-model",
85+
openAiBaseUrl: "http://localhost:8080/v1",
86+
openAiApiKey: "test-key",
87+
}
88+
89+
new OpenAiHandler(options)
90+
91+
expect(mockOpenAIConstructor).toHaveBeenCalledWith(
92+
expect.objectContaining({
93+
baseURL: "http://localhost:8080/v1",
94+
timeout: 1800000, // 1800 seconds in milliseconds
95+
}),
96+
)
97+
})
98+
99+
it("should use timeout for Azure OpenAI", () => {
100+
mockGetConfig.mockReturnValue(900) // 15 minutes
101+
102+
const options: ApiHandlerOptions = {
103+
apiModelId: "gpt-4",
104+
openAiModelId: "gpt-4",
105+
openAiBaseUrl: "https://myinstance.openai.azure.com",
106+
openAiApiKey: "test-key",
107+
openAiUseAzure: true,
108+
}
109+
110+
new OpenAiHandler(options)
111+
112+
expect(mockAzureOpenAIConstructor).toHaveBeenCalledWith(
113+
expect.objectContaining({
114+
timeout: 900000, // 900 seconds in milliseconds
115+
}),
116+
)
117+
})
118+
119+
it("should use timeout for Azure AI Inference", () => {
120+
mockGetConfig.mockReturnValue(1200) // 20 minutes
121+
122+
const options: ApiHandlerOptions = {
123+
apiModelId: "deepseek",
124+
openAiModelId: "deepseek",
125+
openAiBaseUrl: "https://myinstance.services.ai.azure.com",
126+
openAiApiKey: "test-key",
127+
}
128+
129+
new OpenAiHandler(options)
130+
131+
expect(mockOpenAIConstructor).toHaveBeenCalledWith(
132+
expect.objectContaining({
133+
timeout: 1200000, // 1200 seconds in milliseconds
134+
}),
135+
)
136+
})
137+
138+
it("should handle zero timeout (no timeout)", () => {
139+
mockGetConfig.mockReturnValue(0)
140+
141+
const options: ApiHandlerOptions = {
142+
apiModelId: "gpt-4",
143+
openAiModelId: "gpt-4",
144+
}
145+
146+
new OpenAiHandler(options)
147+
148+
expect(mockOpenAIConstructor).toHaveBeenCalledWith(
149+
expect.objectContaining({
150+
timeout: 0, // No timeout
151+
}),
152+
)
153+
})
154+
})

src/api/providers/__tests__/openai.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ describe("OpenAiHandler", () => {
107107
"X-Title": "Roo Code",
108108
"User-Agent": `RooCode/${Package.version}`,
109109
},
110+
timeout: expect.any(Number),
110111
})
111112
})
112113
})

0 commit comments

Comments
 (0)