From d48d067b423d44146fe5f6a67db867265b6fa2d0 Mon Sep 17 00:00:00 2001 From: sotokisehiro <101786086+sotokisehiro@users.noreply.github.com> Date: Thu, 14 May 2026 08:57:28 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix(core):=20proxy=20=E7=92=B0=E5=A2=83?= =?UTF-8?q?=E3=81=A7=20Vertex=20AI=20=E3=81=AB=E6=8E=A5=E7=B6=9A=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=20agent=20=E3=82=92?= =?UTF-8?q?=E9=9D=99=E7=9A=84=20import=20=E3=81=A7=E6=B3=A8=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit esbuild の ESM bundle (splitting: true + format: 'esm') では gaxios v7 が動的 import する https-proxy-agent の named export が 解決できず、proxy 経由 (HTTPS_PROXY / HTTP_PROXY) で Vertex AI を 呼び出すと TypeError: HttpsProxyAgent is not a constructor となる 問題を修正。 変更点: - packages/core/src/core/contentGenerator.ts に HttpsProxyAgent / HttpProxyAgent を静的 import し、proxy URL のスキーマ (http:// / https://) で agent を切り替え、GoogleGenAI に googleAuthOptions.clientOptions.transporterOptions.agent 経由で 注入。これにより gaxios の動的 import が skip される。 - packages/core/package.json に http-proxy-agent: ^7.0.2 を追加 (https-proxy-agent は既存) - root package.json の overrides に http-proxy-agent: ^7.0.2 を追加し monorepo ホイストで v4 が混入する罠を排除 - contentGenerator.test.ts に proxy 注入の 3 ケース (https / http / no-proxy) を追加 Fixes #24471 Related to #25169 --- package-lock.json | 1 + package.json | 3 +- packages/core/package.json | 1 + .../core/src/core/contentGenerator.test.ts | 96 +++++++++++++++++++ packages/core/src/core/contentGenerator.ts | 16 ++++ 5 files changed, 116 insertions(+), 1 deletion(-) 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/package.json b/package.json index 74d9826e592..3c75a50a2cb 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,8 @@ "glob": "^12.0.0", "node-domexception": "npm:empty@^0.10.1", "prebuild-install": "npm:nop@1.0.0", - "cross-spawn": "^7.0.6" + "cross-spawn": "^7.0.6", + "http-proxy-agent": "^7.0.2" }, "bin": { "gemini": "bundle/gemini.js" 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..37e92e12bcf 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,100 @@ 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 inject HttpProxyAgent into googleAuthOptions 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(HttpProxyAgent), + }, + }, + }, + }), + ); + }); + + 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..8c34bcb5436 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; + const proxyAgent = proxyUrl + ? proxyUrl.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); } From 1363a9d780d98d5412ed752ba00b1d08893da80e Mon Sep 17 00:00:00 2001 From: sotokisehiro <101786086+sotokisehiro@users.noreply.github.com> Date: Thu, 14 May 2026 09:14:23 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix(core):=20=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E6=8C=87=E6=91=98=E3=81=AB=E5=AF=BE=E5=BF=9C=20-=20ag?= =?UTF-8?q?ent=20=E9=81=B8=E6=8A=9E=E3=82=92=20destination=20URL=20?= =?UTF-8?q?=E3=83=99=E3=83=BC=E3=82=B9=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #26361 の自動レビュー指摘を反映: - proxy URL を使用前に trim する (前後の whitespace を除去) - proxy agent の選択を proxy URL のスキーマでなく destination URL (baseUrl) のスキーマで判定するよう変更。HTTPS エンドポイント (Vertex AI / Gemini API デフォルト) には HttpsProxyAgent (CONNECT tunnel) が必要なため、proxy URL が http:// であっても destination が HTTPS なら HttpsProxyAgent を選択する - root package.json の dependencies に http-proxy-agent / https-proxy-agent を明示 (consistency 確保) - contentGenerator.test.ts のテストケースを更新: - http:// proxy URL でも destination が HTTPS なら HttpsProxyAgent が選択されることを確認 - http:// baseUrl のとき HttpProxyAgent が選択されることを確認 - whitespace 入り proxy URL が trim されることを確認 --- package-lock.json | 2 + package.json | 2 + .../core/src/core/contentGenerator.test.ts | 76 ++++++++++++++++++- packages/core/src/core/contentGenerator.ts | 4 +- 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50178962a97..195f437cdab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "packages/*" ], "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "ink": "npm:@jrichman/ink@6.6.9", "latest-version": "^9.0.0", "node-fetch-native": "^1.6.7", diff --git a/package.json b/package.json index 3c75a50a2cb..ee2d0d540c2 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,8 @@ "yargs": "^17.7.2" }, "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "ink": "npm:@jrichman/ink@6.6.9", "latest-version": "^9.0.0", "node-fetch-native": "^1.6.7", diff --git a/packages/core/src/core/contentGenerator.test.ts b/packages/core/src/core/contentGenerator.test.ts index 37e92e12bcf..9e770564449 100644 --- a/packages/core/src/core/contentGenerator.test.ts +++ b/packages/core/src/core/contentGenerator.test.ts @@ -462,7 +462,7 @@ describe('createContentGenerator', () => { ); }); - it('should inject HttpProxyAgent into googleAuthOptions when proxy URL uses http://', async () => { + 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'), @@ -485,6 +485,44 @@ describe('createContentGenerator', () => { 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: { @@ -498,6 +536,42 @@ describe('createContentGenerator', () => { ); }); + 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: {}, diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 8c34bcb5436..1e24e16eff0 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -338,9 +338,9 @@ export async function createContentGenerator( httpOptions.baseUrl = baseUrl; } - const proxyUrl = config.proxy; + const proxyUrl = config.proxy?.trim(); const proxyAgent = proxyUrl - ? proxyUrl.startsWith('http://') + ? baseUrl?.startsWith('http://') ? new HttpProxyAgent(proxyUrl) : new HttpsProxyAgent(proxyUrl) : undefined; From 8e563e199f9d1a7b331ab3f0fa90d3bdf82cd4da Mon Sep 17 00:00:00 2001 From: sotokisehiro <101786086+sotokisehiro@users.noreply.github.com> Date: Thu, 14 May 2026 09:27:53 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore(deps):=20root=20=E3=81=AE=20overrides?= =?UTF-8?q?=20=E3=81=8B=E3=82=89=E4=B8=8D=E8=A6=81=E3=81=AB=E3=81=AA?= =?UTF-8?q?=E3=81=A3=E3=81=9F=20http-proxy-agent=20=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit レビュー指摘 (PR #26361) を反映。直接 dependency として root と packages/core 両方に http-proxy-agent ^7.0.2 を declared 済みのため、 overrides による version pinning は不要。重複削除で package.json を整理。 teeny-request が要求する v5 は nested に分離されたままで、本コードの 静的 import (HttpProxyAgent / HttpsProxyAgent) は root の v7 に解決される。 --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index ee2d0d540c2..a25b9bce155 100644 --- a/package.json +++ b/package.json @@ -81,8 +81,7 @@ "glob": "^12.0.0", "node-domexception": "npm:empty@^0.10.1", "prebuild-install": "npm:nop@1.0.0", - "cross-spawn": "^7.0.6", - "http-proxy-agent": "^7.0.2" + "cross-spawn": "^7.0.6" }, "bin": { "gemini": "bundle/gemini.js" From 4f796edc1c1d42139e8ae846f4f5a92fd42735b6 Mon Sep 17 00:00:00 2001 From: sotokisehiro <101786086+sotokisehiro@users.noreply.github.com> Date: Fri, 15 May 2026 07:12:14 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore(deps):=20root=20package.json=20?= =?UTF-8?q?=E3=81=8B=E3=82=89=20proxy-agent=20dep=20=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit メンテナレビュー (PR #26361) を反映。monorepo では proxy-agent は 実際に使う workspace (packages/core) でのみ declared すれば十分のため、 root の dependencies から http-proxy-agent / https-proxy-agent を撤去。 package-lock.json も再生成。 --- package-lock.json | 2 -- package.json | 2 -- 2 files changed, 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 195f437cdab..50178962a97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,6 @@ "packages/*" ], "dependencies": { - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", "ink": "npm:@jrichman/ink@6.6.9", "latest-version": "^9.0.0", "node-fetch-native": "^1.6.7", diff --git a/package.json b/package.json index a25b9bce155..74d9826e592 100644 --- a/package.json +++ b/package.json @@ -143,8 +143,6 @@ "yargs": "^17.7.2" }, "dependencies": { - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", "ink": "npm:@jrichman/ink@6.6.9", "latest-version": "^9.0.0", "node-fetch-native": "^1.6.7",