diff --git a/packages/cli/src/acp/acpSessionManager.ts b/packages/cli/src/acp/acpSessionManager.ts index 2109257317..cfa7037a24 100644 --- a/packages/cli/src/acp/acpSessionManager.ts +++ b/packages/cli/src/acp/acpSessionManager.ts @@ -69,7 +69,10 @@ export class AcpSessionManager { ); const authType = - loadedSettings.merged.security.auth.selectedType || AuthType.USE_GEMINI; + loadedSettings.merged.security.auth.selectedType || + (authDetails.baseUrl || process.env['GOOGLE_GEMINI_BASE_URL'] + ? AuthType.GATEWAY + : AuthType.USE_GEMINI); let isAuthenticated = false; let authErrorMessage = ''; @@ -231,7 +234,12 @@ export class AcpSessionManager { mcpServers: acp.McpServer[], authDetails: AuthDetails, ): Promise { - const selectedAuthType = this.settings.merged.security.auth.selectedType; + const selectedAuthType = + this.settings.merged.security.auth.selectedType || + (authDetails.baseUrl || process.env['GOOGLE_GEMINI_BASE_URL'] + ? AuthType.GATEWAY + : undefined); + if (!selectedAuthType) { throw acp.RequestError.authRequired(); } diff --git a/packages/core/src/core/contentGenerator.test.ts b/packages/core/src/core/contentGenerator.test.ts index 4efd9f65c6..db1369b206 100644 --- a/packages/core/src/core/contentGenerator.test.ts +++ b/packages/core/src/core/contentGenerator.test.ts @@ -9,6 +9,7 @@ import { createContentGenerator, AuthType, createContentGeneratorConfig, + getAuthTypeFromEnv, type ContentGenerator, } from './contentGenerator.js'; import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js'; @@ -35,6 +36,45 @@ const mockConfig = { getClientName: vi.fn().mockReturnValue(undefined), } as unknown as Config; +describe('getAuthTypeFromEnv', () => { + beforeEach(() => { + vi.stubEnv('GEMINI_API_KEY', ''); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('should detect LOGIN_WITH_GOOGLE when GOOGLE_GENAI_USE_GCA is true', () => { + vi.stubEnv('GOOGLE_GENAI_USE_GCA', 'true'); + expect(getAuthTypeFromEnv()).toBe(AuthType.LOGIN_WITH_GOOGLE); + }); + + it('should detect USE_VERTEX_AI when GOOGLE_GENAI_USE_VERTEXAI is true', () => { + vi.stubEnv('GOOGLE_GENAI_USE_VERTEXAI', 'true'); + expect(getAuthTypeFromEnv()).toBe(AuthType.USE_VERTEX_AI); + }); + + it('should detect GATEWAY when GOOGLE_GEMINI_BASE_URL is present', () => { + vi.stubEnv('GOOGLE_GEMINI_BASE_URL', 'https://gateway.example.com'); + expect(getAuthTypeFromEnv()).toBe(AuthType.GATEWAY); + }); + + it('should detect USE_GEMINI when GEMINI_API_KEY is present', () => { + vi.stubEnv('GEMINI_API_KEY', 'fake-key'); + expect(getAuthTypeFromEnv()).toBe(AuthType.USE_GEMINI); + }); + + it('should detect COMPUTE_ADC when CLOUD_SHELL is true', () => { + vi.stubEnv('CLOUD_SHELL', 'true'); + expect(getAuthTypeFromEnv()).toBe(AuthType.COMPUTE_ADC); + }); + + it('should return undefined when no matching env variables are set', () => { + expect(getAuthTypeFromEnv()).toBeUndefined(); + }); +}); + describe('createContentGenerator', () => { beforeEach(() => { resetVersionCache(); @@ -851,6 +891,40 @@ describe('createContentGenerator', () => { ), ).rejects.toThrow('Invalid custom base URL: not-a-url'); }); + + it('should set empty x-goog-api-key header for GATEWAY auth when apiKey is empty string', 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); + + await createContentGenerator( + { + apiKey: '', + authType: AuthType.GATEWAY, + baseUrl: 'https://gateway.test.local', + }, + mockConfig, + ); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: '', + httpOptions: expect.objectContaining({ + headers: expect.objectContaining({ + 'x-goog-api-key': '', + }), + }), + }), + ); + }); }); describe('createContentGeneratorConfig', () => { @@ -955,24 +1029,33 @@ describe('createContentGeneratorConfig', () => { expect(config.apiKey).toBeUndefined(); expect(config.vertexai).toBeUndefined(); }); - it('should configure for GATEWAY using dummy placeholder if GEMINI_API_KEY is set', async () => { - vi.stubEnv('GEMINI_API_KEY', 'env-gemini-key'); + it('should configure for GATEWAY using provided apiKey if available', async () => { const config = await createContentGeneratorConfig( mockConfig, AuthType.GATEWAY, + 'custom-gateway-key', ); - expect(config.apiKey).toBe('gateway-placeholder-key'); + expect(config.apiKey).toBe('custom-gateway-key'); expect(config.vertexai).toBe(false); }); - it('should configure for GATEWAY using dummy placeholder if GEMINI_API_KEY is not set', async () => { - vi.stubEnv('GEMINI_API_KEY', ''); - vi.mocked(loadApiKey).mockResolvedValue(null); + it('should configure for GATEWAY using GEMINI_API_KEY from environment if set', async () => { + vi.stubEnv('GEMINI_API_KEY', 'env-gateway-key'); const config = await createContentGeneratorConfig( mockConfig, AuthType.GATEWAY, ); - expect(config.apiKey).toBe('gateway-placeholder-key'); + expect(config.apiKey).toBe('env-gateway-key'); + expect(config.vertexai).toBe(false); + }); + + it('should configure for GATEWAY using empty string if no apiKey is provided', async () => { + vi.stubEnv('GEMINI_API_KEY', ''); + const config = await createContentGeneratorConfig( + mockConfig, + AuthType.GATEWAY, + ); + expect(config.apiKey).toBe(''); expect(config.vertexai).toBe(false); }); }); diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index bcee8cfef4..3fb771c431 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -80,6 +80,9 @@ export function getAuthTypeFromEnv(): AuthType | undefined { if (process.env['GOOGLE_GENAI_USE_VERTEXAI'] === 'true') { return AuthType.USE_VERTEX_AI; } + if (process.env['GOOGLE_GEMINI_BASE_URL']) { + return AuthType.GATEWAY; + } if (process.env['GEMINI_API_KEY']) { return AuthType.USE_GEMINI; } @@ -178,7 +181,8 @@ export async function createContentGeneratorConfig( } if (authType === AuthType.GATEWAY) { - contentGeneratorConfig.apiKey = apiKey || 'gateway-placeholder-key'; + contentGeneratorConfig.apiKey = + apiKey || process.env['GEMINI_API_KEY'] || ''; contentGeneratorConfig.vertexai = false; return contentGeneratorConfig; @@ -313,6 +317,9 @@ export async function createContentGenerator( 'x-gemini-api-privileged-user-id': `${installationId}`, }; } + if (config.authType === AuthType.GATEWAY && config.apiKey === '') { + headers['x-goog-api-key'] = ''; + } let baseUrl = config.baseUrl; if (!baseUrl) { const envBaseUrl = @@ -337,7 +344,12 @@ export async function createContentGenerator( } const googleGenAI = new GoogleGenAI({ - apiKey: config.apiKey === '' ? undefined : config.apiKey, + apiKey: + config.authType === AuthType.GATEWAY + ? config.apiKey + : config.apiKey === '' + ? undefined + : config.apiKey, vertexai: config.vertexai ?? config.authType === AuthType.USE_VERTEX_AI, httpOptions, ...(apiVersionEnv && { apiVersion: apiVersionEnv }),