mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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
This commit is contained in:
parent
193053110b
commit
4f32c441ed
7 changed files with 375 additions and 2 deletions
5
pnpm-lock.yaml
generated
5
pnpm-lock.yaml
generated
|
|
@ -872,6 +872,9 @@ importers:
|
|||
gray-matter:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
https-proxy-agent:
|
||||
specifier: ^7.0.6
|
||||
version: 7.0.6
|
||||
i18next:
|
||||
specifier: ^25.0.0
|
||||
version: 25.2.1(typescript@5.8.3)
|
||||
|
|
@ -17622,7 +17625,7 @@ snapshots:
|
|||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.3
|
||||
debug: 4.4.1(supports-color@8.1.1)
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { getProxyHttpAgent } from "../../utils/proxyFetch"
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
|
||||
export class AnthropicVertexHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
|
|
@ -40,10 +41,13 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
const projectId = this.options.vertexProjectId ?? "not-provided"
|
||||
const region = this.options.vertexRegion ?? "us-east5"
|
||||
|
||||
const httpAgent = getProxyHttpAgent()
|
||||
|
||||
if (this.options.vertexJsonCredentials) {
|
||||
this.client = new AnthropicVertex({
|
||||
projectId,
|
||||
region,
|
||||
httpAgent,
|
||||
googleAuth: new GoogleAuth({
|
||||
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
|
||||
credentials: safeJsonParse<JWTInput>(this.options.vertexJsonCredentials, undefined),
|
||||
|
|
@ -53,13 +57,14 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
this.client = new AnthropicVertex({
|
||||
projectId,
|
||||
region,
|
||||
httpAgent,
|
||||
googleAuth: new GoogleAuth({
|
||||
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
|
||||
keyFile: this.options.vertexKeyFile,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
this.client = new AnthropicVertex({ projectId, region })
|
||||
this.client = new AnthropicVertex({ projectId, region, httpAgent })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { ApiStream } from "../transform/stream"
|
|||
import { getModelParams } from "../transform/model-params"
|
||||
import { filterNonAnthropicBlocks } from "../transform/anthropic-filter"
|
||||
import { handleProviderError } from "./utils/error-handler"
|
||||
import { getProxyHttpAgent } from "../../utils/proxyFetch"
|
||||
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
|
|
@ -43,6 +44,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
this.client = new Anthropic({
|
||||
baseURL: this.options.anthropicBaseUrl || undefined,
|
||||
[apiKeyFieldName]: this.options.apiKey,
|
||||
httpAgent: getProxyHttpAgent(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { BaseProvider } from "./base-provider"
|
|||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { calculateApiCostAnthropic } from "../../shared/cost"
|
||||
import { convertOpenAIToolsToAnthropic } from "../../core/prompts/tools/native-tools/converters"
|
||||
import { getProxyHttpAgent } from "../../utils/proxyFetch"
|
||||
|
||||
/**
|
||||
* Converts OpenAI tool_choice to Anthropic ToolChoice format
|
||||
|
|
@ -73,6 +74,7 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
|
|||
this.client = new Anthropic({
|
||||
baseURL,
|
||||
apiKey: options.minimaxApiKey,
|
||||
httpAgent: getProxyHttpAgent(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -489,6 +489,7 @@
|
|||
"global-agent": "^3.0.0",
|
||||
"google-auth-library": "^9.15.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"i18next": "^25.0.0",
|
||||
"ignore": "^7.0.3",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
|
|
|
|||
255
src/utils/__tests__/proxyFetch.spec.ts
Normal file
255
src/utils/__tests__/proxyFetch.spec.ts
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
import * as vscode from "vscode"
|
||||
|
||||
const mockHttpsProxyAgentConstructor = vi.fn()
|
||||
|
||||
vi.mock("https-proxy-agent", () => ({
|
||||
HttpsProxyAgent: mockHttpsProxyAgentConstructor,
|
||||
}))
|
||||
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
getConfiguration: vi.fn(),
|
||||
onDidChangeConfiguration: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
},
|
||||
}))
|
||||
|
||||
function createMockContext(): vscode.ExtensionContext {
|
||||
return {
|
||||
extensionMode: 1, // Production
|
||||
subscriptions: [],
|
||||
extensionPath: "/test/path",
|
||||
globalState: {
|
||||
get: vi.fn(),
|
||||
update: vi.fn(),
|
||||
keys: vi.fn().mockReturnValue([]),
|
||||
setKeysForSync: vi.fn(),
|
||||
},
|
||||
workspaceState: {
|
||||
get: vi.fn(),
|
||||
update: vi.fn(),
|
||||
keys: vi.fn().mockReturnValue([]),
|
||||
},
|
||||
secrets: {
|
||||
get: vi.fn(),
|
||||
store: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
onDidChange: vi.fn(),
|
||||
},
|
||||
extensionUri: { fsPath: "/test/path" } as vscode.Uri,
|
||||
globalStorageUri: { fsPath: "/test/global" } as vscode.Uri,
|
||||
logUri: { fsPath: "/test/logs" } as vscode.Uri,
|
||||
storageUri: { fsPath: "/test/storage" } as vscode.Uri,
|
||||
storagePath: "/test/storage",
|
||||
globalStoragePath: "/test/global",
|
||||
logPath: "/test/logs",
|
||||
asAbsolutePath: vi.fn((p) => `/test/path/${p}`),
|
||||
environmentVariableCollection: {} as vscode.GlobalEnvironmentVariableCollection,
|
||||
extension: {} as vscode.Extension<unknown>,
|
||||
languageModelAccessInformation: {} as vscode.LanguageModelAccessInformation,
|
||||
} as unknown as vscode.ExtensionContext
|
||||
}
|
||||
|
||||
describe("proxyFetch", () => {
|
||||
let mockHttpConfig: { get: ReturnType<typeof vi.fn> }
|
||||
let savedFetch: typeof globalThis.fetch
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
|
||||
savedFetch = globalThis.fetch
|
||||
|
||||
mockHttpConfig = {
|
||||
get: vi.fn().mockReturnValue(undefined),
|
||||
}
|
||||
|
||||
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(
|
||||
mockHttpConfig as unknown as vscode.WorkspaceConfiguration,
|
||||
)
|
||||
|
||||
// Clear proxy env vars
|
||||
delete process.env.HTTPS_PROXY
|
||||
delete process.env.https_proxy
|
||||
delete process.env.HTTP_PROXY
|
||||
delete process.env.http_proxy
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = savedFetch
|
||||
})
|
||||
|
||||
describe("resolveProxyUrl", () => {
|
||||
it("should return undefined when no proxy is configured", async () => {
|
||||
const { resolveProxyUrl } = await import("../proxyFetch")
|
||||
|
||||
const result = resolveProxyUrl()
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should return VSCode http.proxy setting when configured", async () => {
|
||||
mockHttpConfig.get.mockImplementation((key: string) => {
|
||||
if (key === "proxy") return "http://corporate-proxy:8080"
|
||||
return undefined
|
||||
})
|
||||
|
||||
const { resolveProxyUrl } = await import("../proxyFetch")
|
||||
|
||||
const result = resolveProxyUrl()
|
||||
|
||||
expect(result).toBe("http://corporate-proxy:8080")
|
||||
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("http")
|
||||
})
|
||||
|
||||
it("should trim whitespace from VSCode proxy setting", async () => {
|
||||
mockHttpConfig.get.mockImplementation((key: string) => {
|
||||
if (key === "proxy") return " http://corporate-proxy:8080 "
|
||||
return undefined
|
||||
})
|
||||
|
||||
const { resolveProxyUrl } = await import("../proxyFetch")
|
||||
|
||||
const result = resolveProxyUrl()
|
||||
|
||||
expect(result).toBe("http://corporate-proxy:8080")
|
||||
})
|
||||
|
||||
it("should ignore empty VSCode proxy setting and fall back to env vars", async () => {
|
||||
mockHttpConfig.get.mockImplementation((key: string) => {
|
||||
if (key === "proxy") return " "
|
||||
return undefined
|
||||
})
|
||||
process.env.HTTPS_PROXY = "http://env-proxy:3128"
|
||||
|
||||
const { resolveProxyUrl } = await import("../proxyFetch")
|
||||
|
||||
const result = resolveProxyUrl()
|
||||
|
||||
expect(result).toBe("http://env-proxy:3128")
|
||||
})
|
||||
|
||||
it("should prefer HTTPS_PROXY over HTTP_PROXY", async () => {
|
||||
process.env.HTTPS_PROXY = "http://https-proxy:3128"
|
||||
process.env.HTTP_PROXY = "http://http-proxy:3128"
|
||||
|
||||
const { resolveProxyUrl } = await import("../proxyFetch")
|
||||
|
||||
const result = resolveProxyUrl()
|
||||
|
||||
expect(result).toBe("http://https-proxy:3128")
|
||||
})
|
||||
|
||||
it("should fall back to lowercase env vars", async () => {
|
||||
process.env.https_proxy = "http://lowercase-proxy:3128"
|
||||
|
||||
const { resolveProxyUrl } = await import("../proxyFetch")
|
||||
|
||||
const result = resolveProxyUrl()
|
||||
|
||||
expect(result).toBe("http://lowercase-proxy:3128")
|
||||
})
|
||||
|
||||
it("should prefer VSCode setting over env vars", async () => {
|
||||
mockHttpConfig.get.mockImplementation((key: string) => {
|
||||
if (key === "proxy") return "http://vscode-proxy:8080"
|
||||
return undefined
|
||||
})
|
||||
process.env.HTTPS_PROXY = "http://env-proxy:3128"
|
||||
|
||||
const { resolveProxyUrl } = await import("../proxyFetch")
|
||||
|
||||
const result = resolveProxyUrl()
|
||||
|
||||
expect(result).toBe("http://vscode-proxy:8080")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getProxyHttpAgent", () => {
|
||||
it("should return undefined when no proxy is configured", async () => {
|
||||
const { getProxyHttpAgent } = await import("../proxyFetch")
|
||||
|
||||
const agent = getProxyHttpAgent()
|
||||
|
||||
expect(agent).toBeUndefined()
|
||||
expect(mockHttpsProxyAgentConstructor).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should return an HttpsProxyAgent when proxy is configured", async () => {
|
||||
mockHttpConfig.get.mockImplementation((key: string) => {
|
||||
if (key === "proxy") return "http://corporate-proxy:8080"
|
||||
if (key === "proxyStrictSSL") return true
|
||||
return undefined
|
||||
})
|
||||
|
||||
const mockAgent = { mock: true }
|
||||
mockHttpsProxyAgentConstructor.mockReturnValue(mockAgent)
|
||||
|
||||
const { getProxyHttpAgent } = await import("../proxyFetch")
|
||||
|
||||
const agent = getProxyHttpAgent()
|
||||
|
||||
expect(agent).toBe(mockAgent)
|
||||
expect(mockHttpsProxyAgentConstructor).toHaveBeenCalledWith("http://corporate-proxy:8080", {
|
||||
rejectUnauthorized: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should disable TLS verification when proxyStrictSSL is false", async () => {
|
||||
mockHttpConfig.get.mockImplementation((key: string) => {
|
||||
if (key === "proxy") return "http://corporate-proxy:8080"
|
||||
if (key === "proxyStrictSSL") return false
|
||||
return undefined
|
||||
})
|
||||
|
||||
mockHttpsProxyAgentConstructor.mockReturnValue({ mock: true })
|
||||
|
||||
const { getProxyHttpAgent } = await import("../proxyFetch")
|
||||
|
||||
getProxyHttpAgent()
|
||||
|
||||
expect(mockHttpsProxyAgentConstructor).toHaveBeenCalledWith("http://corporate-proxy:8080", {
|
||||
rejectUnauthorized: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("should return undefined and log error when HttpsProxyAgent constructor throws", async () => {
|
||||
mockHttpConfig.get.mockImplementation((key: string) => {
|
||||
if (key === "proxy") return "http://bad-proxy:9999"
|
||||
if (key === "proxyStrictSSL") return true
|
||||
return undefined
|
||||
})
|
||||
|
||||
mockHttpsProxyAgentConstructor.mockImplementation(() => {
|
||||
throw new Error("Invalid proxy URL")
|
||||
})
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
const { getProxyHttpAgent } = await import("../proxyFetch")
|
||||
|
||||
const agent = getProxyHttpAgent()
|
||||
|
||||
expect(agent).toBeUndefined()
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("[ProxyFetch] Failed to create HttpsProxyAgent"),
|
||||
)
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should use env proxy when VSCode setting is not configured", async () => {
|
||||
process.env.HTTPS_PROXY = "http://env-proxy:3128"
|
||||
|
||||
mockHttpsProxyAgentConstructor.mockReturnValue({ mock: true })
|
||||
|
||||
const { getProxyHttpAgent } = await import("../proxyFetch")
|
||||
|
||||
const agent = getProxyHttpAgent()
|
||||
|
||||
expect(agent).toBeDefined()
|
||||
expect(mockHttpsProxyAgentConstructor).toHaveBeenCalledWith("http://env-proxy:3128", {
|
||||
rejectUnauthorized: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
105
src/utils/proxyFetch.ts
Normal file
105
src/utils/proxyFetch.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* Proxy-Aware HTTP Agent Module
|
||||
*
|
||||
* Provides proxy support for SDKs that use `node-fetch` with custom agents
|
||||
* (e.g. Anthropic SDK v0.x), routing their traffic through the user's
|
||||
* configured proxy (VSCode `http.proxy` setting or standard environment
|
||||
* variables).
|
||||
*
|
||||
* Background:
|
||||
* - VSCode patches Node.js `http`/`https` modules to honour `http.proxy`, so
|
||||
* libraries that use those modules (e.g. axios) are already proxied.
|
||||
* - The Anthropic SDK uses `node-fetch` with custom `agentkeepalive` agents,
|
||||
* which bypass VSCode's proxy patching. For these SDKs, we provide
|
||||
* `getProxyHttpAgent()` to create an `HttpsProxyAgent` that can be passed
|
||||
* as the `httpAgent` option.
|
||||
*/
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import type { Agent } from "node:http"
|
||||
import { HttpsProxyAgent } from "https-proxy-agent"
|
||||
|
||||
/**
|
||||
* Resolve the effective proxy URL.
|
||||
*
|
||||
* Priority:
|
||||
* 1. VSCode `http.proxy` setting (works in both local and remote mode)
|
||||
* 2. Standard environment variables (`HTTPS_PROXY`, `HTTP_PROXY`, `https_proxy`, `http_proxy`)
|
||||
*
|
||||
* Returns `undefined` when no proxy is configured.
|
||||
*/
|
||||
export function resolveProxyUrl(): string | undefined {
|
||||
// 1. VSCode setting
|
||||
const httpConfig = vscode.workspace.getConfiguration("http")
|
||||
const vsCodeProxy = httpConfig.get<string>("proxy")
|
||||
if (typeof vsCodeProxy === "string" && vsCodeProxy.trim()) {
|
||||
return vsCodeProxy.trim()
|
||||
}
|
||||
|
||||
// 2. Environment variables (standard precedence)
|
||||
const envProxy =
|
||||
process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy
|
||||
if (envProxy && envProxy.trim()) {
|
||||
return envProxy.trim()
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether TLS certificate verification should be strict.
|
||||
*
|
||||
* Reads VSCode's `http.proxyStrictSSL` setting (defaults to `true`).
|
||||
*/
|
||||
function isStrictSSL(): boolean {
|
||||
const httpConfig = vscode.workspace.getConfiguration("http")
|
||||
return httpConfig.get<boolean>("proxyStrictSSL") ?? true
|
||||
}
|
||||
|
||||
/**
|
||||
* Redact credentials from a proxy URL for safe logging.
|
||||
*/
|
||||
function redactUrl(url: string): string {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
parsed.username = ""
|
||||
parsed.password = ""
|
||||
return parsed.toString()
|
||||
} catch {
|
||||
return url.replace(/\/\/[^@/]+@/g, "//REDACTED@")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an `HttpsProxyAgent` for SDKs that use `node-fetch` with custom
|
||||
* agents (e.g. Anthropic SDK v0.x).
|
||||
*
|
||||
* Returns `undefined` when no proxy is configured, so callers can use:
|
||||
*
|
||||
* ```ts
|
||||
* new Anthropic({ httpAgent: getProxyHttpAgent() })
|
||||
* ```
|
||||
*
|
||||
* The Anthropic SDK falls back to its default `agentkeepalive` agent when
|
||||
* `httpAgent` is `undefined`, which is the correct behaviour when no proxy
|
||||
* is needed.
|
||||
*/
|
||||
export function getProxyHttpAgent(): Agent | undefined {
|
||||
const proxyUrl = resolveProxyUrl()
|
||||
if (!proxyUrl) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const strictSSL = isStrictSSL()
|
||||
|
||||
try {
|
||||
return new HttpsProxyAgent(proxyUrl, {
|
||||
rejectUnauthorized: strictSSL,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[ProxyFetch] Failed to create HttpsProxyAgent for "${redactUrl(proxyUrl)}": ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue