From 9fcaaf41aec30c105a4ae14b73deb57a21d63c91 Mon Sep 17 00:00:00 2001 From: Christopher Thomas Date: Mon, 13 Apr 2026 22:38:08 -0500 Subject: [PATCH 1/4] fix(core): honor custom Gemini and Vertex base URLs --- .../core/src/core/contentGenerator.test.ts | 154 ++++++++++++++++++ packages/core/src/core/contentGenerator.ts | 34 +++- 2 files changed, 186 insertions(+), 2 deletions(-) diff --git a/packages/core/src/core/contentGenerator.test.ts b/packages/core/src/core/contentGenerator.test.ts index 35d7879f96c..95ca7885eae 100644 --- a/packages/core/src/core/contentGenerator.test.ts +++ b/packages/core/src/core/contentGenerator.test.ts @@ -638,6 +638,160 @@ describe('createContentGenerator', () => { apiVersion: 'v1alpha', }); }); + + it('should pass baseUrl to GoogleGenAI when GOOGLE_GEMINI_BASE_URL is set', async () => { + const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), + getUsageStatisticsEnabled: () => false, + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never); + vi.stubEnv('GOOGLE_GEMINI_BASE_URL', 'https://gemini.test.local'); + vi.stubEnv('GEMINI_API_KEY', 'test-api-key'); + + const config = await createContentGeneratorConfig( + mockConfig, + AuthType.USE_GEMINI, + ); + await createContentGenerator(config, mockConfig); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: 'test-api-key', + vertexai: false, + httpOptions: expect.objectContaining({ + baseUrl: 'https://gemini.test.local', + }), + }), + ); + }); + + it('should pass baseUrl to GoogleGenAI when GOOGLE_VERTEX_BASE_URL is set', async () => { + const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), + getUsageStatisticsEnabled: () => false, + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never); + vi.stubEnv('GOOGLE_VERTEX_BASE_URL', 'https://vertex.test.local'); + vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'my-project'); + vi.stubEnv('GOOGLE_CLOUD_LOCATION', 'us-central1'); + + const config = await createContentGeneratorConfig( + mockConfig, + AuthType.USE_VERTEX_AI, + ); + await createContentGenerator(config, mockConfig); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: undefined, + vertexai: true, + httpOptions: expect.objectContaining({ + baseUrl: 'https://vertex.test.local', + }), + }), + ); + }); + + it('should prefer an explicit baseUrl over GOOGLE_GEMINI_BASE_URL', async () => { + const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), + getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never); + vi.stubEnv('GOOGLE_GEMINI_BASE_URL', 'https://env.test.local'); + vi.stubEnv('GEMINI_API_KEY', 'test-api-key'); + + const config = await createContentGeneratorConfig( + mockConfig, + AuthType.USE_GEMINI, + undefined, + 'https://explicit.test.local', + ); + await createContentGenerator(config, mockConfig); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + httpOptions: expect.objectContaining({ + baseUrl: 'https://explicit.test.local', + }), + }), + ); + }); + + it('should allow localhost baseUrl overrides over http', async () => { + const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), + getUsageStatisticsEnabled: () => false, + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never); + + await createContentGenerator( + { + apiKey: 'test-api-key', + authType: AuthType.USE_GEMINI, + baseUrl: 'http://127.0.0.1:8080', + }, + mockConfig, + ); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + httpOptions: expect.objectContaining({ + baseUrl: 'http://127.0.0.1:8080', + }), + }), + ); + }); + + it('should reject invalid custom baseUrl values', async () => { + await expect( + createContentGenerator( + { + apiKey: 'test-api-key', + authType: AuthType.USE_GEMINI, + baseUrl: 'not-a-url', + }, + mockConfig, + ), + ).rejects.toThrow('Invalid custom base URL: not-a-url'); + }); + + it('should reject non-https remote custom baseUrl values', async () => { + await expect( + createContentGenerator( + { + apiKey: 'test-api-key', + authType: AuthType.USE_GEMINI, + baseUrl: 'http://example.com', + }, + mockConfig, + ), + ).rejects.toThrow( + 'Custom base URL must use HTTPS unless it is localhost.', + ); + }); + }); describe('createContentGeneratorConfig', () => { diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 4fc56b59b4b..b21683a33aa 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -101,6 +101,23 @@ export type ContentGeneratorConfig = { customHeaders?: Record; }; +const LOCAL_HOSTNAMES = ['localhost', '127.0.0.1', '[::1]']; + +function validateBaseUrl(baseUrl: string): void { + let url: URL; + try { + url = new URL(baseUrl); + } catch { + throw new Error(`Invalid custom base URL: ${baseUrl}`); + } + + if (url.protocol !== 'https:' && !LOCAL_HOSTNAMES.includes(url.hostname)) { + throw new Error( + 'Custom base URL must use HTTPS unless it is localhost.', + ); + } +} + export async function createContentGeneratorConfig( config: Config, authType: AuthType | undefined, @@ -273,13 +290,26 @@ export async function createContentGenerator( 'x-gemini-api-privileged-user-id': `${installationId}`, }; } + let baseUrl = config.baseUrl; + if (!baseUrl) { + const envBaseUrl = config.vertexai + ? process.env['GOOGLE_VERTEX_BASE_URL'] + : process.env['GOOGLE_GEMINI_BASE_URL']; + if (envBaseUrl) { + validateBaseUrl(envBaseUrl); + baseUrl = envBaseUrl; + } + } else { + validateBaseUrl(baseUrl); + } + const httpOptions: { baseUrl?: string; headers: Record; } = { headers }; - if (config.baseUrl) { - httpOptions.baseUrl = config.baseUrl; + if (baseUrl) { + httpOptions.baseUrl = baseUrl; } const googleGenAI = new GoogleGenAI({ From 2ec049b1a1b9f49fad60ff16fa25c2fb0df996ae Mon Sep 17 00:00:00 2001 From: Christopher Thomas Date: Mon, 13 Apr 2026 23:46:13 -0500 Subject: [PATCH 2/4] fix(core): use auth type for base URL env fallback --- .../core/src/core/contentGenerator.test.ts | 41 +++++++++++++++++-- packages/core/src/core/contentGenerator.ts | 11 +++-- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/packages/core/src/core/contentGenerator.test.ts b/packages/core/src/core/contentGenerator.test.ts index 95ca7885eae..39c29f83fc0 100644 --- a/packages/core/src/core/contentGenerator.test.ts +++ b/packages/core/src/core/contentGenerator.test.ts @@ -644,6 +644,7 @@ describe('createContentGenerator', () => { getModel: vi.fn().mockReturnValue('gemini-pro'), getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const mockGenerator = { @@ -675,6 +676,7 @@ describe('createContentGenerator', () => { getModel: vi.fn().mockReturnValue('gemini-pro'), getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const mockGenerator = { @@ -702,6 +704,39 @@ describe('createContentGenerator', () => { ); }); + it('should prefer GOOGLE_VERTEX_BASE_URL when authType is USE_VERTEX_AI without inferred vertex credentials', async () => { + const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), + getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never); + vi.stubEnv('GOOGLE_GEMINI_BASE_URL', 'https://gemini.test.local'); + vi.stubEnv('GOOGLE_VERTEX_BASE_URL', 'https://vertex.test.local'); + + await createContentGenerator( + { + authType: AuthType.USE_VERTEX_AI, + }, + mockConfig, + ); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: undefined, + vertexai: undefined, + httpOptions: expect.objectContaining({ + baseUrl: 'https://vertex.test.local', + }), + }), + ); + }); + it('should prefer an explicit baseUrl over GOOGLE_GEMINI_BASE_URL', async () => { const mockConfig = { getModel: vi.fn().mockReturnValue('gemini-pro'), @@ -739,6 +774,7 @@ describe('createContentGenerator', () => { getModel: vi.fn().mockReturnValue('gemini-pro'), getProxy: vi.fn().mockReturnValue(undefined), getUsageStatisticsEnabled: () => false, + getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; const mockGenerator = { @@ -787,11 +823,8 @@ describe('createContentGenerator', () => { }, mockConfig, ), - ).rejects.toThrow( - 'Custom base URL must use HTTPS unless it is localhost.', - ); + ).rejects.toThrow('Custom base URL must use HTTPS unless it is localhost.'); }); - }); describe('createContentGeneratorConfig', () => { diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index b21683a33aa..4340e2850f9 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -112,9 +112,7 @@ function validateBaseUrl(baseUrl: string): void { } if (url.protocol !== 'https:' && !LOCAL_HOSTNAMES.includes(url.hostname)) { - throw new Error( - 'Custom base URL must use HTTPS unless it is localhost.', - ); + throw new Error('Custom base URL must use HTTPS unless it is localhost.'); } } @@ -292,9 +290,10 @@ export async function createContentGenerator( } let baseUrl = config.baseUrl; if (!baseUrl) { - const envBaseUrl = config.vertexai - ? process.env['GOOGLE_VERTEX_BASE_URL'] - : process.env['GOOGLE_GEMINI_BASE_URL']; + const envBaseUrl = + config.authType === AuthType.USE_VERTEX_AI + ? process.env['GOOGLE_VERTEX_BASE_URL'] + : process.env['GOOGLE_GEMINI_BASE_URL']; if (envBaseUrl) { validateBaseUrl(envBaseUrl); baseUrl = envBaseUrl; From e80ff29c16c753424b698722aba144d7b1387797 Mon Sep 17 00:00:00 2001 From: Christopher Thomas Date: Tue, 14 Apr 2026 23:39:25 -0500 Subject: [PATCH 3/4] fix(core): ensure vertexai flag is set correctly based on authType --- .../core/src/core/contentGenerator.test.ts | 18 +++++++++--------- packages/core/src/core/contentGenerator.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/src/core/contentGenerator.test.ts b/packages/core/src/core/contentGenerator.test.ts index 39c29f83fc0..bf7eef167de 100644 --- a/packages/core/src/core/contentGenerator.test.ts +++ b/packages/core/src/core/contentGenerator.test.ts @@ -148,7 +148,7 @@ describe('createContentGenerator', () => { ); expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: 'test-api-key', - vertexai: undefined, + vertexai: false, httpOptions: expect.objectContaining({ headers: expect.objectContaining({ 'User-Agent': expect.stringMatching( @@ -365,7 +365,7 @@ describe('createContentGenerator', () => { expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: 'test-api-key', - vertexai: undefined, + vertexai: false, httpOptions: expect.objectContaining({ headers: expect.objectContaining({ 'User-Agent': expect.any(String), @@ -409,7 +409,7 @@ describe('createContentGenerator', () => { expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: 'test-api-key', - vertexai: undefined, + vertexai: false, httpOptions: expect.objectContaining({ headers: expect.objectContaining({ 'User-Agent': expect.any(String), @@ -443,7 +443,7 @@ describe('createContentGenerator', () => { expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: 'test-api-key', - vertexai: undefined, + vertexai: false, httpOptions: expect.objectContaining({ headers: expect.objectContaining({ 'User-Agent': expect.any(String), @@ -481,7 +481,7 @@ describe('createContentGenerator', () => { ); expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: 'test-api-key', - vertexai: undefined, + vertexai: false, httpOptions: expect.objectContaining({ headers: { 'User-Agent': expect.any(String), @@ -517,7 +517,7 @@ describe('createContentGenerator', () => { expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: 'test-api-key', - vertexai: undefined, + vertexai: false, httpOptions: expect.objectContaining({ headers: expect.objectContaining({ 'User-Agent': expect.any(String), @@ -550,7 +550,7 @@ describe('createContentGenerator', () => { expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: 'test-api-key', - vertexai: undefined, + vertexai: false, httpOptions: expect.objectContaining({ headers: expect.objectContaining({ 'User-Agent': expect.any(String), @@ -589,7 +589,7 @@ describe('createContentGenerator', () => { expect(GoogleGenAI).toHaveBeenCalledWith({ apiKey: 'test-api-key', - vertexai: undefined, + vertexai: false, httpOptions: expect.objectContaining({ headers: expect.objectContaining({ 'User-Agent': expect.any(String), @@ -729,7 +729,7 @@ describe('createContentGenerator', () => { expect(GoogleGenAI).toHaveBeenCalledWith( expect.objectContaining({ apiKey: undefined, - vertexai: undefined, + vertexai: true, httpOptions: expect.objectContaining({ baseUrl: 'https://vertex.test.local', }), diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 4340e2850f9..31e36ede41c 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -313,7 +313,7 @@ export async function createContentGenerator( const googleGenAI = new GoogleGenAI({ apiKey: config.apiKey === '' ? undefined : config.apiKey, - vertexai: config.vertexai, + vertexai: config.vertexai ?? config.authType === AuthType.USE_VERTEX_AI, httpOptions, ...(apiVersionEnv && { apiVersion: apiVersionEnv }), }); From a6d170da7e202317ad10ad0b8d7202fc93de3da2 Mon Sep 17 00:00:00 2001 From: Christopher Thomas Date: Wed, 15 Apr 2026 13:42:48 -0500 Subject: [PATCH 4/4] docs: document GOOGLE_GEMINI_BASE_URL and GOOGLE_VERTEX_BASE_URL --- docs/reference/configuration.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 05368f20fe6..2047a9b09df 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -2148,6 +2148,21 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file. - When set, overrides the default API version used by the SDK. - Example: `export GOOGLE_GENAI_API_VERSION="v1"` (Windows PowerShell: `$env:GOOGLE_GENAI_API_VERSION="v1"`) +- **`GOOGLE_GEMINI_BASE_URL`**: + - Overrides the default base URL for Gemini API requests (when using + `gemini-api-key` authentication). + - Must be a valid URL. For security, it must use HTTPS unless pointing to + `localhost` (or `127.0.0.1` / `[::1]`). + - Example: `export GOOGLE_GEMINI_BASE_URL="https://my-proxy.com"` (Windows + PowerShell: `$env:GOOGLE_GEMINI_BASE_URL="https://my-proxy.com"`) +- **`GOOGLE_VERTEX_BASE_URL`**: + - Overrides the default base URL for Vertex AI API requests (when using + `vertex-ai` authentication). + - Must be a valid URL. For security, it must use HTTPS unless pointing to + `localhost` (or `127.0.0.1` / `[::1]`). + - Example: `export GOOGLE_VERTEX_BASE_URL="https://my-vertex-proxy.com"` + (Windows PowerShell: + `$env:GOOGLE_VERTEX_BASE_URL="https://my-vertex-proxy.com"`) - **`OTLP_GOOGLE_CLOUD_PROJECT`**: - Your Google Cloud Project ID for Telemetry in Google Cloud - Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"` (Windows