Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion src/api/providers/anthropic-vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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),
Expand All @@ -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 })
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/api/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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(),
})
}

Expand Down
2 changes: 2 additions & 0 deletions src/api/providers/minimax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -73,6 +74,7 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
this.client = new Anthropic({
baseURL,
apiKey: options.minimaxApiKey,
httpAgent: getProxyHttpAgent(),
})
}

Expand Down
1 change: 1 addition & 0 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
255 changes: 255 additions & 0 deletions src/utils/__tests__/proxyFetch.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
})
})
})
})
Loading
Loading