diff --git a/.changeset/itchy-moles-thank.md b/.changeset/itchy-moles-thank.md new file mode 100644 index 0000000000..42bb637024 --- /dev/null +++ b/.changeset/itchy-moles-thank.md @@ -0,0 +1,5 @@ +--- +"zoo-code": patch +--- + +Fix bedrock DNS resolution when behind corporate proxy diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17de8c1b34..7fe0a76d20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -466,6 +466,9 @@ importers: '@roo-code/types': specifier: workspace:^ version: link:../packages/types + '@smithy/node-http-handler': + specifier: ^4.0.0 + version: 4.9.3 ai-sdk-provider-poe: specifier: 2.0.18 version: 2.0.18(ai@6.0.218(zod@3.25.76))(zod@3.25.76) @@ -514,6 +517,9 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 + https-proxy-agent: + specifier: ^7.0.0 + version: 7.0.6 i18next: specifier: ^25.0.0 version: 25.2.1(typescript@5.9.3) diff --git a/src/api/providers/__tests__/bedrock.spec.ts b/src/api/providers/__tests__/bedrock.spec.ts index e2b17e4e87..97d3075f20 100644 --- a/src/api/providers/__tests__/bedrock.spec.ts +++ b/src/api/providers/__tests__/bedrock.spec.ts @@ -18,6 +18,18 @@ vi.mock("@aws-sdk/credential-providers", () => { return { fromIni: mockFromIni } }) +vi.mock("../../../utils/networkProxy", () => ({ + getSystemProxyUrl: vi.fn().mockReturnValue(undefined), +})) + +vi.mock("@smithy/node-http-handler", () => ({ + NodeHttpHandler: vi.fn(), +})) + +vi.mock("https-proxy-agent", () => ({ + HttpsProxyAgent: vi.fn(), +})) + // Mock BedrockRuntimeClient and ConverseStreamCommand vi.mock("@aws-sdk/client-bedrock-runtime", () => { const mockSend = vi.fn().mockResolvedValue({ @@ -46,10 +58,16 @@ import { } from "@roo-code/types" import type { Anthropic } from "@anthropic-ai/sdk" +import { getSystemProxyUrl } from "../../../utils/networkProxy" +import { NodeHttpHandler } from "@smithy/node-http-handler" +import { HttpsProxyAgent } from "https-proxy-agent" // Get access to the mocked functions const mockConverseStreamCommand = vi.mocked(ConverseStreamCommand) const mockBedrockRuntimeClient = vi.mocked(BedrockRuntimeClient) +const mockGetSystemProxyUrl = vi.mocked(getSystemProxyUrl) +const mockNodeHttpHandler = vi.mocked(NodeHttpHandler) +const mockHttpsProxyAgent = vi.mocked(HttpsProxyAgent) describe("AwsBedrockHandler", () => { let handler: AwsBedrockHandler @@ -118,6 +136,64 @@ describe("AwsBedrockHandler", () => { }) }) + describe("proxy configuration", () => { + afterEach(() => { + mockGetSystemProxyUrl.mockReturnValue(undefined) + }) + + it("should configure NodeHttpHandler with HttpsProxyAgent when proxy URL is set", () => { + mockGetSystemProxyUrl.mockReturnValue("http://proxy.corp.local:3128") + + new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test-access-key", + awsSecretKey: "test-secret-key", + awsRegion: "us-east-1", + }) + + expect(mockHttpsProxyAgent).toHaveBeenCalledWith("http://proxy.corp.local:3128") + expect(mockNodeHttpHandler).toHaveBeenCalledWith( + expect.objectContaining({ + httpsAgent: expect.anything(), + requestTimeout: 0, + }), + ) + expect(mockBedrockRuntimeClient).toHaveBeenLastCalledWith( + expect.objectContaining({ requestHandler: expect.anything() }), + ) + }) + + it("should not create a proxy requestHandler when no proxy is configured", () => { + new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test-access-key", + awsSecretKey: "test-secret-key", + awsRegion: "us-east-1", + }) + + expect(mockNodeHttpHandler).not.toHaveBeenCalled() + expect(mockBedrockRuntimeClient.mock.lastCall?.[0]?.requestHandler).toBeUndefined() + }) + + it("should apply proxy for API key authentication", () => { + mockGetSystemProxyUrl.mockReturnValue("http://proxy.corp.local:3128") + + new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsUseApiKey: true, + awsApiKey: "test-api-key", + awsRegion: "us-east-1", + }) + + expect(mockNodeHttpHandler).toHaveBeenCalledWith( + expect.objectContaining({ + httpsAgent: expect.anything(), + requestTimeout: 0, + }), + ) + }) + }) + describe("region mapping and cross-region inference", () => { describe("getPrefixForRegion", () => { it("should return correct prefix for US regions", () => { diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 8c2b5ace68..90cb16077f 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -10,9 +10,11 @@ import { ToolConfiguration, ToolChoice, } from "@aws-sdk/client-bedrock-runtime" +import { NodeHttpHandler } from "@smithy/node-http-handler" import OpenAI from "openai" import { fromIni } from "@aws-sdk/credential-providers" import { Anthropic } from "@anthropic-ai/sdk" +import { HttpsProxyAgent } from "https-proxy-agent" import { type ModelInfo, @@ -44,6 +46,7 @@ import { convertToBedrockConverseMessages as sharedConverter } from "../transfor import { getModelParams } from "../transform/model-params" import { shouldUseReasoningBudget } from "../../shared/api" import { normalizeToolSchema } from "../../utils/json-schema" +import { getSystemProxyUrl } from "../../utils/networkProxy" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" /************************************************************************************ @@ -294,6 +297,17 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH } } + // When a corporate proxy is configured, Node resolves DNS locally before tunneling, + // causing ENOTFOUND for endpoints that only the proxy can reach. HttpsProxyAgent + // uses CONNECT tunneling so the proxy handles DNS resolution instead. + const proxyUrl = getSystemProxyUrl() + if (proxyUrl) { + clientConfig.requestHandler = new NodeHttpHandler({ + httpsAgent: new HttpsProxyAgent(proxyUrl), + requestTimeout: 0, + }) + } + this.client = new BedrockRuntimeClient(clientConfig) } diff --git a/src/package.json b/src/package.json index 020705effc..c23a687246 100644 --- a/src/package.json +++ b/src/package.json @@ -461,6 +461,7 @@ "@anthropic-ai/vertex-sdk": "^0.19.0", "@aws-sdk/client-bedrock-runtime": "^3.922.0", "@aws-sdk/credential-providers": "^3.922.0", + "@smithy/node-http-handler": "^4.0.0", "@google/genai": "^1.29.1", "@lmstudio/sdk": "^1.1.1", "@mistralai/mistralai": "^1.9.18", @@ -486,6 +487,7 @@ "get-folder-size": "^5.0.0", "global-agent": "^3.0.0", "google-auth-library": "^10.2.0", + "https-proxy-agent": "^7.0.0", "gray-matter": "^4.0.3", "i18next": "^25.0.0", "ignore": "^7.0.3", diff --git a/src/utils/networkProxy.ts b/src/utils/networkProxy.ts index 448bc1b576..853401eb35 100644 --- a/src/utils/networkProxy.ts +++ b/src/utils/networkProxy.ts @@ -346,6 +346,30 @@ export function isDebugMode(): boolean { return extensionContext.extensionMode === vscode.ExtensionMode.Development } +/** + * Get the proxy URL from environment variables or VS Code settings. + * Works in all extension modes (production and debug). + */ +export function getSystemProxyUrl(): string | undefined { + // Standard proxy environment variables (HTTPS takes precedence over HTTP) + const fromEnv = + process.env.HTTPS_PROXY || + process.env.https_proxy || + process.env.HTTP_PROXY || + process.env.http_proxy + if (fromEnv) return fromEnv + + // Fall back to VS Code's http.proxy setting + try { + const vsCodeProxy = vscode.workspace.getConfiguration("http").get("proxy") + if (vsCodeProxy?.trim()) return vsCodeProxy.trim() + } catch { + // VS Code API may be unavailable in test environments + } + + return undefined +} + /** * Log a message to the output channel if available. */