From a5cca409d84a3f592c1e0ac113ecf81ac8c2c862 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 13 May 2026 16:12:18 +0200 Subject: [PATCH] refactor(ai): use HTTP proxy agents for Bedrock --- package-lock.json | 205 +------------------- packages/ai/CHANGELOG.md | 1 + packages/ai/package.json | 3 +- packages/ai/src/providers/amazon-bedrock.ts | 27 +-- packages/ai/src/utils/node-http-proxy.ts | 50 +++++ packages/ai/test/node-http-proxy.test.ts | 65 +++++++ 6 files changed, 127 insertions(+), 224 deletions(-) create mode 100644 packages/ai/src/utils/node-http-proxy.ts create mode 100644 packages/ai/test/node-http-proxy.test.ts diff --git a/package-lock.json b/package-lock.json index 1fba0423..f11dae5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3637,10 +3637,6 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "license": "MIT" - }, "node_modules/@types/chai": { "version": "5.2.3", "dev": true, @@ -3975,16 +3971,6 @@ "node": ">=12" } }, - "node_modules/ast-types": { - "version": "0.13.4", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "license": "MIT", @@ -4010,13 +3996,6 @@ ], "license": "MIT" }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/bignumber.js": { "version": "9.3.1", "license": "MIT", @@ -4369,18 +4348,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "dev": true, @@ -4496,43 +4463,6 @@ "node": ">=6" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/estree-walker": { "version": "3.0.3", "dev": true, @@ -4541,13 +4471,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/esutils": { - "version": "2.0.3", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/execa": { "version": "1.0.0", "dev": true, @@ -4774,25 +4697,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/get-uri/node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "dev": true, @@ -4972,13 +4876,6 @@ "node": ">= 0.10" } }, - "node_modules/ip-address": { - "version": "10.2.0", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/is-core-module": { "version": "2.16.2", "dev": true, @@ -5593,13 +5490,6 @@ "dev": true, "license": "MIT" }, - "node_modules/netmask": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/nice-try": { "version": "1.0.5", "dev": true, @@ -5722,34 +5612,6 @@ "version": "0.12.0", "license": "MIT" }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/pako": { "version": "1.0.11", "license": "(MIT AND Zlib)" @@ -5951,30 +5813,6 @@ "node": ">=12.0.0" } }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "license": "MIT" @@ -6321,46 +6159,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.8", - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "dev": true, @@ -7105,9 +6903,10 @@ "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "^2.2.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "openai": "6.26.0", "partial-json": "^0.1.7", - "proxy-agent": "^6.5.0", "proxy-from-env": "^1.1.0", "typebox": "^1.1.24" }, diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2be28e9b..47c1b361 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed Bedrock proxy handling to preserve `NO_PROXY` exclusions while using HTTP(S)-only proxy agents. - Fixed GitHub Copilot Claude test coverage to use the current Claude Sonnet 4.6 model ID. - Fixed OpenAI Responses requests for models that support disabling reasoning to send `reasoning.effort: "none"` when thinking is off. diff --git a/packages/ai/package.json b/packages/ai/package.json index 9fce8e35..149da8c3 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -73,9 +73,10 @@ "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "^2.2.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "openai": "6.26.0", "partial-json": "^0.1.7", - "proxy-agent": "^6.5.0", "proxy-from-env": "^1.1.0", "typebox": "^1.1.24" }, diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 1812d3c8..7c5ac164 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -20,6 +20,7 @@ import { type ToolConfiguration, ToolResultStatus, } from "@aws-sdk/client-bedrock-runtime"; +import { NodeHttpHandler } from "@smithy/node-http-handler"; import type { DocumentType } from "@smithy/types"; import { calculateCost } from "../models.js"; import type { @@ -42,6 +43,7 @@ import type { } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { parseStreamingJson } from "../utils/json-parse.js"; +import { createHttpProxyAgentsForTarget } from "../utils/node-http-proxy.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.js"; import { transformMessages } from "./transform-messages.js"; @@ -156,30 +158,15 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt }; } - if ( - process.env.HTTP_PROXY || - process.env.HTTPS_PROXY || - process.env.NO_PROXY || - process.env.http_proxy || - process.env.https_proxy || - process.env.no_proxy - ) { - const nodeHttpHandler = await import("@smithy/node-http-handler"); - const proxyAgent = await import("proxy-agent"); - - const agent = new proxyAgent.ProxyAgent(); - + const proxyAgents = createHttpProxyAgentsForTarget(model.baseUrl); + if (proxyAgents) { // Bedrock runtime uses NodeHttp2Handler by default since v3.798.0, which is based // on `http2` module and has no support for http agent. - // Use NodeHttpHandler to support http agent. - config.requestHandler = new nodeHttpHandler.NodeHttpHandler({ - httpAgent: agent, - httpsAgent: agent, - }); + // Use NodeHttpHandler to support HTTP(S) proxy agents. + config.requestHandler = new NodeHttpHandler(proxyAgents); } else if (process.env.AWS_BEDROCK_FORCE_HTTP1 === "1") { // Some custom endpoints require HTTP/1.1 instead of HTTP/2 - const nodeHttpHandler = await import("@smithy/node-http-handler"); - config.requestHandler = new nodeHttpHandler.NodeHttpHandler(); + config.requestHandler = new NodeHttpHandler(); } } else { // Non-Node environment (browser): fall back to us-east-1 since diff --git a/packages/ai/src/utils/node-http-proxy.ts b/packages/ai/src/utils/node-http-proxy.ts new file mode 100644 index 00000000..22babf60 --- /dev/null +++ b/packages/ai/src/utils/node-http-proxy.ts @@ -0,0 +1,50 @@ +import type { Agent as HttpAgent } from "node:http"; +import type { Agent as HttpsAgent } from "node:https"; +import { createRequire } from "node:module"; +import { HttpProxyAgent } from "http-proxy-agent"; +import { HttpsProxyAgent } from "https-proxy-agent"; + +const require = createRequire(import.meta.url); +const { getProxyForUrl } = require("proxy-from-env") as { getProxyForUrl: (url: string) => string }; + +export interface NodeHttpProxyAgents { + httpAgent: HttpAgent; + httpsAgent: HttpsAgent; +} + +export const UNSUPPORTED_PROXY_PROTOCOL_MESSAGE = + "Unsupported proxy protocol. SOCKS and PAC proxy URLs are not supported; use an HTTP or HTTPS proxy URL."; + +export function resolveHttpProxyUrlForTarget(targetUrl: string | URL): URL | undefined { + const proxy = getProxyForUrl(targetUrl.toString()); + if (!proxy) { + return undefined; + } + + let proxyUrl: URL; + try { + proxyUrl = new URL(proxy); + } catch (error) { + throw new Error( + `Invalid proxy URL ${JSON.stringify(proxy)}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + if (proxyUrl.protocol !== "http:" && proxyUrl.protocol !== "https:") { + throw new Error(`${UNSUPPORTED_PROXY_PROTOCOL_MESSAGE} Got ${proxyUrl.protocol}`); + } + + return proxyUrl; +} + +export function createHttpProxyAgentsForTarget(targetUrl: string | URL): NodeHttpProxyAgents | undefined { + const proxyUrl = resolveHttpProxyUrlForTarget(targetUrl); + if (!proxyUrl) { + return undefined; + } + + return { + httpAgent: new HttpProxyAgent(proxyUrl), + httpsAgent: new HttpsProxyAgent(proxyUrl) as unknown as HttpsAgent, + }; +} diff --git a/packages/ai/test/node-http-proxy.test.ts b/packages/ai/test/node-http-proxy.test.ts new file mode 100644 index 00000000..0057d9bd --- /dev/null +++ b/packages/ai/test/node-http-proxy.test.ts @@ -0,0 +1,65 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { resolveHttpProxyUrlForTarget, UNSUPPORTED_PROXY_PROTOCOL_MESSAGE } from "../src/utils/node-http-proxy.js"; + +const PROXY_ENV_KEYS = [ + "HTTP_PROXY", + "HTTPS_PROXY", + "NO_PROXY", + "ALL_PROXY", + "http_proxy", + "https_proxy", + "no_proxy", + "all_proxy", + "npm_config_http_proxy", + "npm_config_https_proxy", + "npm_config_proxy", + "npm_config_no_proxy", +] as const; + +const originalEnv = new Map(); +for (const key of PROXY_ENV_KEYS) { + originalEnv.set(key, process.env[key]); +} + +function resetProxyEnv(): void { + for (const key of PROXY_ENV_KEYS) { + delete process.env[key]; + } +} + +afterEach(() => { + resetProxyEnv(); + for (const [key, value] of originalEnv) { + if (value !== undefined) { + process.env[key] = value; + } + } +}); + +describe("node HTTP proxy resolution", () => { + it("respects NO_PROXY exclusions", () => { + resetProxyEnv(); + process.env.HTTPS_PROXY = "http://proxy.example:8080"; + process.env.NO_PROXY = "bedrock-runtime.us-east-1.amazonaws.com"; + + expect(resolveHttpProxyUrlForTarget("https://bedrock-runtime.us-east-1.amazonaws.com")).toBeUndefined(); + }); + + it("resolves HTTP and HTTPS proxy URLs", () => { + resetProxyEnv(); + process.env.HTTPS_PROXY = "http://proxy.example:8080"; + + expect(resolveHttpProxyUrlForTarget("https://bedrock-runtime.us-east-1.amazonaws.com")?.toString()).toBe( + "http://proxy.example:8080/", + ); + }); + + it("rejects SOCKS and PAC proxy URLs explicitly", () => { + resetProxyEnv(); + process.env.HTTPS_PROXY = "socks5://proxy.example:1080"; + + expect(() => resolveHttpProxyUrlForTarget("https://bedrock-runtime.us-east-1.amazonaws.com")).toThrow( + UNSUPPORTED_PROXY_PROTOCOL_MESSAGE, + ); + }); +});