diff --git a/package-lock.json b/package-lock.json index a0928c02a1b..50178962a97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18401,6 +18401,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 b6a26d0db86..3a65b0c5e5e 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 4efd9f65c6e..9e770564449 100644 --- a/packages/core/src/core/contentGenerator.test.ts +++ b/packages/core/src/core/contentGenerator.test.ts @@ -13,6 +13,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'; @@ -424,6 +426,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 bcee8cfef4b..1e24e16eff0 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'; @@ -336,11 +338,25 @@ 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.apiKey === '' ? undefined : config.apiKey, 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); }