Skip to content

Commit 02b07af

Browse files
committed
fix: pass proxy httpAgent to Anthropic SDK (node-fetch based)
The Anthropic SDK v0.x uses node-fetch with custom agentkeepalive agents, which bypass VSCode's http/https module proxy patching. Add https-proxy-agent dependency and getProxyHttpAgent() utility that creates an HttpsProxyAgent from the configured proxy URL. Pass it as httpAgent to all Anthropic/AnthropicVertex client constructors: - anthropic.ts - minimax.ts (uses Anthropic SDK for Anthropic-compatible endpoint) - anthropic-vertex.ts
1 parent 48e1592 commit 02b07af

8 files changed

Lines changed: 381 additions & 3 deletions

File tree

pnpm-lock.yaml

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/providers/__tests__/vertex-credentials.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
// Mock vscode first to avoid import errors when the provider stack pulls
44
// transitive vscode-dependent modules during construction.
5-
vitest.mock("vscode", () => ({}))
5+
vitest.mock("vscode", () => ({
6+
workspace: {
7+
getConfiguration: vitest.fn().mockReturnValue({
8+
get: vitest.fn().mockReturnValue(undefined),
9+
}),
10+
},
11+
}))
612

713
vitest.mock("@roo-code/telemetry", () => ({
814
TelemetryService: {

src/api/providers/anthropic-vertex.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
import { BaseProvider } from "./base-provider"
2727
import { parseVertexJsonCredentials } from "./utils/vertex-credentials"
2828
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
29+
import { getProxyHttpAgent } from "../../utils/proxyFetch"
2930

3031
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
3132
export class AnthropicVertexHandler extends BaseProvider implements SingleCompletionHandler {
@@ -42,11 +43,13 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
4243
const region = this.options.vertexRegion ?? "us-east5"
4344

4445
const parsedVertexCredentials = parseVertexJsonCredentials(this.options.vertexJsonCredentials)
46+
const httpAgent = getProxyHttpAgent()
4547

4648
if (parsedVertexCredentials) {
4749
this.client = new AnthropicVertex({
4850
projectId,
4951
region,
52+
httpAgent,
5053
googleAuth: new GoogleAuth({
5154
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
5255
credentials: parsedVertexCredentials,
@@ -56,13 +59,14 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
5659
this.client = new AnthropicVertex({
5760
projectId,
5861
region,
62+
httpAgent,
5963
googleAuth: new GoogleAuth({
6064
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
6165
keyFile: this.options.vertexKeyFile,
6266
}),
6367
})
6468
} else {
65-
this.client = new AnthropicVertex({ projectId, region })
69+
this.client = new AnthropicVertex({ projectId, region, httpAgent })
6670
}
6771
}
6872

src/api/providers/anthropic.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { getModelParams } from "../transform/model-params"
2020
import { filterNonAnthropicBlocks } from "../transform/anthropic-filter"
2121
import { getAnthropicProviderReasoning } from "../transform/reasoning"
2222
import { handleProviderError } from "./utils/error-handler"
23+
import { getProxyHttpAgent } from "../../utils/proxyFetch"
2324

2425
import { BaseProvider } from "./base-provider"
2526
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
@@ -44,6 +45,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
4445
this.client = new Anthropic({
4546
baseURL: this.options.anthropicBaseUrl || undefined,
4647
[apiKeyFieldName]: this.options.apiKey,
48+
httpAgent: getProxyHttpAgent(),
4749
})
4850
}
4951

src/api/providers/minimax.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { BaseProvider } from "./base-provider"
1515
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
1616
import { calculateApiCostAnthropic } from "../../shared/cost"
1717
import { convertOpenAIToolsToAnthropic } from "../../core/prompts/tools/native-tools/converters"
18+
import { getProxyHttpAgent } from "../../utils/proxyFetch"
1819

1920
/**
2021
* Converts OpenAI tool_choice to Anthropic ToolChoice format
@@ -73,6 +74,7 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
7374
this.client = new Anthropic({
7475
baseURL,
7576
apiKey: options.minimaxApiKey,
77+
httpAgent: getProxyHttpAgent(),
7678
})
7779
}
7880

src/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@
475475
"global-agent": "^3.0.0",
476476
"google-auth-library": "^9.15.1",
477477
"gray-matter": "^4.0.3",
478+
"https-proxy-agent": "^7.0.6",
478479
"i18next": "^25.0.0",
479480
"ignore": "^7.0.3",
480481
"isbinaryfile": "^5.0.7",
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import * as vscode from "vscode"
2+
3+
const mockHttpsProxyAgentConstructor = vi.fn()
4+
5+
vi.mock("https-proxy-agent", () => ({
6+
HttpsProxyAgent: mockHttpsProxyAgentConstructor,
7+
}))
8+
9+
vi.mock("vscode", () => ({
10+
workspace: {
11+
getConfiguration: vi.fn(),
12+
onDidChangeConfiguration: vi.fn(() => ({ dispose: vi.fn() })),
13+
},
14+
}))
15+
16+
function createMockContext(): vscode.ExtensionContext {
17+
return {
18+
extensionMode: 1, // Production
19+
subscriptions: [],
20+
extensionPath: "/test/path",
21+
globalState: {
22+
get: vi.fn(),
23+
update: vi.fn(),
24+
keys: vi.fn().mockReturnValue([]),
25+
setKeysForSync: vi.fn(),
26+
},
27+
workspaceState: {
28+
get: vi.fn(),
29+
update: vi.fn(),
30+
keys: vi.fn().mockReturnValue([]),
31+
},
32+
secrets: {
33+
get: vi.fn(),
34+
store: vi.fn(),
35+
delete: vi.fn(),
36+
onDidChange: vi.fn(),
37+
},
38+
extensionUri: { fsPath: "/test/path" } as vscode.Uri,
39+
globalStorageUri: { fsPath: "/test/global" } as vscode.Uri,
40+
logUri: { fsPath: "/test/logs" } as vscode.Uri,
41+
storageUri: { fsPath: "/test/storage" } as vscode.Uri,
42+
storagePath: "/test/storage",
43+
globalStoragePath: "/test/global",
44+
logPath: "/test/logs",
45+
asAbsolutePath: vi.fn((p) => `/test/path/${p}`),
46+
environmentVariableCollection: {} as vscode.GlobalEnvironmentVariableCollection,
47+
extension: {} as vscode.Extension<unknown>,
48+
languageModelAccessInformation: {} as vscode.LanguageModelAccessInformation,
49+
} as unknown as vscode.ExtensionContext
50+
}
51+
52+
describe("proxyFetch", () => {
53+
let mockHttpConfig: { get: ReturnType<typeof vi.fn> }
54+
let savedFetch: typeof globalThis.fetch
55+
56+
beforeEach(() => {
57+
vi.clearAllMocks()
58+
vi.resetModules()
59+
60+
savedFetch = globalThis.fetch
61+
62+
mockHttpConfig = {
63+
get: vi.fn().mockReturnValue(undefined),
64+
}
65+
66+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(
67+
mockHttpConfig as unknown as vscode.WorkspaceConfiguration,
68+
)
69+
70+
// Clear proxy env vars
71+
delete process.env.HTTPS_PROXY
72+
delete process.env.https_proxy
73+
delete process.env.HTTP_PROXY
74+
delete process.env.http_proxy
75+
})
76+
77+
afterEach(() => {
78+
globalThis.fetch = savedFetch
79+
})
80+
81+
describe("resolveProxyUrl", () => {
82+
it("should return undefined when no proxy is configured", async () => {
83+
const { resolveProxyUrl } = await import("../proxyFetch")
84+
85+
const result = resolveProxyUrl()
86+
87+
expect(result).toBeUndefined()
88+
})
89+
90+
it("should return VSCode http.proxy setting when configured", async () => {
91+
mockHttpConfig.get.mockImplementation((key: string) => {
92+
if (key === "proxy") return "http://corporate-proxy:8080"
93+
return undefined
94+
})
95+
96+
const { resolveProxyUrl } = await import("../proxyFetch")
97+
98+
const result = resolveProxyUrl()
99+
100+
expect(result).toBe("http://corporate-proxy:8080")
101+
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("http")
102+
})
103+
104+
it("should trim whitespace from VSCode proxy setting", async () => {
105+
mockHttpConfig.get.mockImplementation((key: string) => {
106+
if (key === "proxy") return " http://corporate-proxy:8080 "
107+
return undefined
108+
})
109+
110+
const { resolveProxyUrl } = await import("../proxyFetch")
111+
112+
const result = resolveProxyUrl()
113+
114+
expect(result).toBe("http://corporate-proxy:8080")
115+
})
116+
117+
it("should ignore empty VSCode proxy setting and fall back to env vars", async () => {
118+
mockHttpConfig.get.mockImplementation((key: string) => {
119+
if (key === "proxy") return " "
120+
return undefined
121+
})
122+
process.env.HTTPS_PROXY = "http://env-proxy:3128"
123+
124+
const { resolveProxyUrl } = await import("../proxyFetch")
125+
126+
const result = resolveProxyUrl()
127+
128+
expect(result).toBe("http://env-proxy:3128")
129+
})
130+
131+
it("should prefer HTTPS_PROXY over HTTP_PROXY", async () => {
132+
process.env.HTTPS_PROXY = "http://https-proxy:3128"
133+
process.env.HTTP_PROXY = "http://http-proxy:3128"
134+
135+
const { resolveProxyUrl } = await import("../proxyFetch")
136+
137+
const result = resolveProxyUrl()
138+
139+
expect(result).toBe("http://https-proxy:3128")
140+
})
141+
142+
it("should fall back to lowercase env vars", async () => {
143+
process.env.https_proxy = "http://lowercase-proxy:3128"
144+
145+
const { resolveProxyUrl } = await import("../proxyFetch")
146+
147+
const result = resolveProxyUrl()
148+
149+
expect(result).toBe("http://lowercase-proxy:3128")
150+
})
151+
152+
it("should prefer VSCode setting over env vars", async () => {
153+
mockHttpConfig.get.mockImplementation((key: string) => {
154+
if (key === "proxy") return "http://vscode-proxy:8080"
155+
return undefined
156+
})
157+
process.env.HTTPS_PROXY = "http://env-proxy:3128"
158+
159+
const { resolveProxyUrl } = await import("../proxyFetch")
160+
161+
const result = resolveProxyUrl()
162+
163+
expect(result).toBe("http://vscode-proxy:8080")
164+
})
165+
})
166+
167+
describe("getProxyHttpAgent", () => {
168+
it("should return undefined when no proxy is configured", async () => {
169+
const { getProxyHttpAgent } = await import("../proxyFetch")
170+
171+
const agent = getProxyHttpAgent()
172+
173+
expect(agent).toBeUndefined()
174+
expect(mockHttpsProxyAgentConstructor).not.toHaveBeenCalled()
175+
})
176+
177+
it("should return an HttpsProxyAgent when proxy is configured", async () => {
178+
mockHttpConfig.get.mockImplementation((key: string) => {
179+
if (key === "proxy") return "http://corporate-proxy:8080"
180+
if (key === "proxyStrictSSL") return true
181+
return undefined
182+
})
183+
184+
const mockAgent = { mock: true }
185+
mockHttpsProxyAgentConstructor.mockReturnValue(mockAgent)
186+
187+
const { getProxyHttpAgent } = await import("../proxyFetch")
188+
189+
const agent = getProxyHttpAgent()
190+
191+
expect(agent).toBe(mockAgent)
192+
expect(mockHttpsProxyAgentConstructor).toHaveBeenCalledWith("http://corporate-proxy:8080", {
193+
rejectUnauthorized: true,
194+
})
195+
})
196+
197+
it("should disable TLS verification when proxyStrictSSL is false", async () => {
198+
mockHttpConfig.get.mockImplementation((key: string) => {
199+
if (key === "proxy") return "http://corporate-proxy:8080"
200+
if (key === "proxyStrictSSL") return false
201+
return undefined
202+
})
203+
204+
mockHttpsProxyAgentConstructor.mockReturnValue({ mock: true })
205+
206+
const { getProxyHttpAgent } = await import("../proxyFetch")
207+
208+
getProxyHttpAgent()
209+
210+
expect(mockHttpsProxyAgentConstructor).toHaveBeenCalledWith("http://corporate-proxy:8080", {
211+
rejectUnauthorized: false,
212+
})
213+
})
214+
215+
it("should return undefined and log error when HttpsProxyAgent constructor throws", async () => {
216+
mockHttpConfig.get.mockImplementation((key: string) => {
217+
if (key === "proxy") return "http://bad-proxy:9999"
218+
if (key === "proxyStrictSSL") return true
219+
return undefined
220+
})
221+
222+
mockHttpsProxyAgentConstructor.mockImplementation(() => {
223+
throw new Error("Invalid proxy URL")
224+
})
225+
226+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
227+
228+
const { getProxyHttpAgent } = await import("../proxyFetch")
229+
230+
const agent = getProxyHttpAgent()
231+
232+
expect(agent).toBeUndefined()
233+
expect(consoleErrorSpy).toHaveBeenCalledWith(
234+
expect.stringContaining("[ProxyFetch] Failed to create HttpsProxyAgent"),
235+
)
236+
237+
consoleErrorSpy.mockRestore()
238+
})
239+
240+
it("should use env proxy when VSCode setting is not configured", async () => {
241+
process.env.HTTPS_PROXY = "http://env-proxy:3128"
242+
243+
mockHttpsProxyAgentConstructor.mockReturnValue({ mock: true })
244+
245+
const { getProxyHttpAgent } = await import("../proxyFetch")
246+
247+
const agent = getProxyHttpAgent()
248+
249+
expect(agent).toBeDefined()
250+
expect(mockHttpsProxyAgentConstructor).toHaveBeenCalledWith("http://env-proxy:3128", {
251+
rejectUnauthorized: true,
252+
})
253+
})
254+
})
255+
})

0 commit comments

Comments
 (0)