From 928a311fb0bea9f0c85fb835caca71b56c073916 Mon Sep 17 00:00:00 2001 From: sotokisehiro <101786086+sotokisehiro@users.noreply.github.com> Date: Fri, 15 May 2026 07:34:36 +0900 Subject: [PATCH] fix(core): externalize https-proxy-agent to fix proxy support (#26361) --- package-lock.json | 1 + packages/core/package.json | 1 + .../core/src/core/contentGenerator.test.ts | 170 ++++++++++++++++++ packages/core/src/core/contentGenerator.ts | 16 ++ 4 files changed, 188 insertions(+) diff --git a/package-lock.json b/package-lock.json index 71a9f04244..c23fd6dea0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18448,6 +18448,7 @@ "glob": "^12.0.0", "google-auth-library": "^9.11.0", "html-to-text": "^9.0.5", + "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "ignore": "^7.0.0", "ipaddr.js": "^1.9.1", diff --git a/packages/core/package.json b/packages/core/package.json index b6a26d0db8..3a65b0c5e5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -67,6 +67,7 @@ "glob": "^12.0.0", "google-auth-library": "^9.11.0", "html-to-text": "^9.0.5", + "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "ignore": "^7.0.0", "ipaddr.js": "^1.9.1", diff --git a/packages/core/src/core/contentGenerator.test.ts b/packages/core/src/core/contentGenerator.test.ts index db1369b206..72e9c5b514 100644 --- a/packages/core/src/core/contentGenerator.test.ts +++ b/packages/core/src/core/contentGenerator.test.ts @@ -14,6 +14,8 @@ import { } from './contentGenerator.js'; import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js'; import { GoogleGenAI } from '@google/genai'; +import { HttpProxyAgent } from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; import type { Config } from '../config/config.js'; import { LoggingContentGenerator } from './loggingContentGenerator.js'; import { loadApiKey } from './apiKeyCredentialStorage.js'; @@ -464,6 +466,174 @@ describe('createContentGenerator', () => { ); }); + it('should inject HttpsProxyAgent into googleAuthOptions when proxy URL uses https://', async () => { + const mockConfigWithProxy = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue('https://proxy.example.com:8080'), + getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator); + + await createContentGenerator( + { + apiKey: 'test-api-key', + vertexai: true, + authType: AuthType.USE_VERTEX_AI, + proxy: 'https://proxy.example.com:8080', + }, + mockConfigWithProxy, + ); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + googleAuthOptions: { + clientOptions: { + transporterOptions: { + agent: expect.any(HttpsProxyAgent), + }, + }, + }, + }), + ); + }); + + it('should still use HttpsProxyAgent for HTTPS destinations even when proxy URL uses http://', async () => { + const mockConfigWithProxy = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue('http://proxy.example.com:8080'), + getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator); + + await createContentGenerator( + { + apiKey: 'test-api-key', + vertexai: true, + authType: AuthType.USE_VERTEX_AI, + proxy: 'http://proxy.example.com:8080', + }, + mockConfigWithProxy, + ); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + googleAuthOptions: { + clientOptions: { + transporterOptions: { + agent: expect.any(HttpsProxyAgent), + }, + }, + }, + }), + ); + }); + + it('should inject HttpProxyAgent when destination baseUrl uses http://', async () => { + const mockConfigWithProxy = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue('http://proxy.example.com:8080'), + getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator); + + vi.stubEnv('GOOGLE_VERTEX_BASE_URL', 'http://localhost:9999'); + + await createContentGenerator( + { + apiKey: 'test-api-key', + vertexai: true, + authType: AuthType.USE_VERTEX_AI, + proxy: 'http://proxy.example.com:8080', + }, + mockConfigWithProxy, + ); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + googleAuthOptions: { + clientOptions: { + transporterOptions: { + agent: expect.any(HttpProxyAgent), + }, + }, + }, + }), + ); + }); + + it('should trim whitespace from proxy URL before instantiating agent', async () => { + const mockConfigWithProxy = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(' https://proxy.example.com:8080 '), + getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator); + + await createContentGenerator( + { + apiKey: 'test-api-key', + vertexai: true, + authType: AuthType.USE_VERTEX_AI, + proxy: ' https://proxy.example.com:8080 ', + }, + mockConfigWithProxy, + ); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + googleAuthOptions: { + clientOptions: { + transporterOptions: { + agent: expect.any(HttpsProxyAgent), + }, + }, + }, + }), + ); + }); + + it('should not include googleAuthOptions when no proxy is configured', async () => { + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator); + + await createContentGenerator( + { + apiKey: 'test-api-key', + vertexai: true, + authType: AuthType.USE_VERTEX_AI, + }, + mockConfig, + ); + + const callArg = vi.mocked(GoogleGenAI).mock.calls[0][0] as Record< + string, + unknown + >; + expect(callArg).not.toHaveProperty('googleAuthOptions'); + }); + it('should pass api key as Authorization Header when GEMINI_API_KEY_AUTH_MECHANISM is set to bearer', async () => { const mockConfig = { getModel: vi.fn().mockReturnValue('gemini-pro'), diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 3fb771c431..4494a5e9ff 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -13,6 +13,8 @@ import { type EmbedContentResponse, type EmbedContentParameters, } from '@google/genai'; +import { HttpProxyAgent } from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; import * as os from 'node:os'; import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js'; import { isCloudShell } from '../ide/detect-ide.js'; @@ -343,6 +345,13 @@ export async function createContentGenerator( httpOptions.baseUrl = baseUrl; } + const proxyUrl = config.proxy?.trim(); + const proxyAgent = proxyUrl + ? baseUrl?.startsWith('http://') + ? new HttpProxyAgent(proxyUrl) + : new HttpsProxyAgent(proxyUrl) + : undefined; + const googleGenAI = new GoogleGenAI({ apiKey: config.authType === AuthType.GATEWAY @@ -353,6 +362,13 @@ export async function createContentGenerator( vertexai: config.vertexai ?? config.authType === AuthType.USE_VERTEX_AI, httpOptions, ...(apiVersionEnv && { apiVersion: apiVersionEnv }), + ...(proxyAgent && { + googleAuthOptions: { + clientOptions: { + transporterOptions: { agent: proxyAgent }, + }, + }, + }), }); return new LoggingContentGenerator(googleGenAI.models, gcConfig); }