From 2f65967a0f633443f4fbf241f5556c281fc14231 Mon Sep 17 00:00:00 2001 From: Matthew Tang Date: Wed, 22 Jul 2026 17:16:11 -0700 Subject: [PATCH] feat: Allow api key + proj/location for enterprise mode PiperOrigin-RevId: 952419028 --- src/_api_client.ts | 15 +++------------ src/node/node_client.ts | 30 +++++++++++++++++++++++------- test/unit/api_client_test.ts | 12 ++++++------ test/unit/node/client_test.ts | 20 ++++++++++++++++---- 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/_api_client.ts b/src/_api_client.ts index b81b136c07..c6c017b7e1 100644 --- a/src/_api_client.ts +++ b/src/_api_client.ts @@ -178,15 +178,6 @@ export class ApiClient { this.customBaseUrl = opts.httpOptions?.baseUrl; - if (this.clientOptions.vertexai) { - if (this.clientOptions.project && this.clientOptions.location) { - this.clientOptions.apiKey = undefined; - } else if (this.clientOptions.apiKey) { - this.clientOptions.project = undefined; - this.clientOptions.location = undefined; - } - } - const initHttpOptions: types.HttpOptions = {}; if (this.clientOptions.vertexai) { @@ -216,7 +207,7 @@ export class ApiClient { this.clientOptions.project = undefined; this.clientOptions.location = undefined; } else if ( - this.clientOptions.apiKey || + (this.clientOptions.apiKey && !this.clientOptions.project) || this.clientOptions.location === 'global' ) { // Vertex Express or global endpoint case. @@ -382,10 +373,10 @@ export class ApiClient { ) { return false; } - if (this.clientOptions.apiKey) { + if (!this.clientOptions.vertexai) { return false; } - if (!this.clientOptions.vertexai) { + if (!this.clientOptions.project || !this.clientOptions.location) { return false; } if (request.path.startsWith('projects/')) { diff --git a/src/node/node_client.ts b/src/node/node_client.ts index b8db0d569e..dbe6efe9fa 100644 --- a/src/node/node_client.ts +++ b/src/node/node_client.ts @@ -207,15 +207,17 @@ export class GoogleGenAI { this._triggers = new GeminiNextGenTriggers(this.apiClient); return this._triggers; } + constructor(options: GoogleGenAIOptions) { + this.vertexai = resolveCloudFlag(options); + // Validate explicitly set initializer values. - if ((options.project || options.location) && options.apiKey) { + if ((options.project || options.location) && !this.vertexai) { throw new Error( - 'Project/location and API key are mutually exclusive in the client initializer.', + 'Project and location are not supported for Gemini API backend.', ); } - this.vertexai = resolveCloudFlag(options); const envApiKey = getApiKeyFromEnv(); const envProject = getEnv('GOOGLE_CLOUD_PROJECT'); const envLocation = getEnv('GOOGLE_CLOUD_LOCATION'); @@ -238,8 +240,12 @@ export class GoogleGenAI { ); this.apiKey = undefined; } - // Explicit api_key and explicit project/location already handled above. - if ((envProject || envLocation) && options.apiKey) { + if ( + !options.project && + !options.location && + (envProject || envLocation) && + options.apiKey + ) { // Explicit api_key takes precedence over implicit project/location. console.debug( 'The user provided Vertex AI API key will take precedence over' + @@ -247,14 +253,24 @@ export class GoogleGenAI { ); this.project = undefined; this.location = undefined; - } else if ((options.project || options.location) && envApiKey) { + } else if ( + (options.project || options.location) && + !options.apiKey && + envApiKey + ) { // Explicit project/location takes precedence over implicit api_key. console.debug( 'The user provided project/location will take precedence over' + ' the API key from the environment variables.', ); this.apiKey = undefined; - } else if ((envProject || envLocation) && envApiKey) { + } else if ( + !options.project && + !options.location && + !options.apiKey && + (envProject || envLocation) && + envApiKey + ) { // Implicit project/location takes precedence over implicit api_key. console.debug( 'The project/location from the environment variables will take' + diff --git a/test/unit/api_client_test.ts b/test/unit/api_client_test.ts index c1cd00db99..17f2dab3e8 100644 --- a/test/unit/api_client_test.ts +++ b/test/unit/api_client_test.ts @@ -456,7 +456,7 @@ describe('ApiClient', () => { expect(client.isVertexAI()).toBe(true); expect(client.getProject()).toBe('vertex-project'); expect(client.getLocation()).toBe('vertex-location'); - expect(client.getApiKey()).toBeUndefined(); // API key is ignored when setting opts.vertexai + expect(client.getApiKey()).toBe('apikey-from-opts'); expect(client.getRequestUrl()).toBe( 'https://vertex-location-aiplatform.googleapis.com/v1beta1', ); @@ -478,7 +478,7 @@ describe('ApiClient', () => { expect(client.isVertexAI()).toBe(true); expect(client.getProject()).toBe('vertex-project'); expect(client.getLocation()).toBe('us'); - expect(client.getApiKey()).toBeUndefined(); + expect(client.getApiKey()).toBe('apikey-from-opts'); expect(client.getRequestUrl()).toBe( 'https://aiplatform.us.rep.googleapis.com/v1beta1', ); @@ -500,7 +500,7 @@ describe('ApiClient', () => { expect(client.isVertexAI()).toBe(true); expect(client.getProject()).toBe('vertex-project'); expect(client.getLocation()).toBe('eu'); - expect(client.getApiKey()).toBeUndefined(); + expect(client.getApiKey()).toBe('apikey-from-opts'); expect(client.getRequestUrl()).toBe( 'https://aiplatform.eu.rep.googleapis.com/v1beta1', ); @@ -523,12 +523,12 @@ describe('ApiClient', () => { expect(client.isVertexAI()).toBe(true); expect(client.getProject()).toBe('vertex-project'); expect(client.getLocation()).toBe('us'); - expect(client.getApiKey()).toBeUndefined(); + expect(client.getApiKey()).toBe('apikey-from-opts'); expect(client.getRequestUrl()).toBe('https://my-custom-url.com/v1beta1'); expect(client.getApiVersion()).toBe('v1beta1'); }); - it('should not have api key if project/location is provided for vertexai', () => { + it('should preserve api key if project/location is provided for vertexai', () => { const client = new ApiClient({ auth: new FakeAuth(), project: 'vertex-project', @@ -542,7 +542,7 @@ describe('ApiClient', () => { expect(client.isVertexAI()).toBe(true); expect(client.getProject()).toBe('vertex-project'); expect(client.getLocation()).toBe('vertex-location'); - expect(client.getApiKey()).toBeUndefined(); + expect(client.getApiKey()).toBe('apikey-from-opts'); expect(client.getRequestUrl()).toBe( 'https://vertex-location-aiplatform.googleapis.com/v1beta1', ); diff --git a/test/unit/node/client_test.ts b/test/unit/node/client_test.ts index 59c33152f7..af78249ffc 100644 --- a/test/unit/node/client_test.ts +++ b/test/unit/node/client_test.ts @@ -123,18 +123,30 @@ describe('Client', () => { expect(client['project']).toBe('constructor_project'); expect(client['location']).toBe('constructor_location'); }); - it('should not allow both project and apikey in constructor', () => { + it('should not allow project or location for Gemini API backend', () => { expect(() => { new GoogleGenAI({ apiKey: 'constructor_api_key', - vertexai: true, + vertexai: false, project: 'constructor_project', location: 'constructor_location', }); }).toThrowError( - 'Project/location and API key are mutually exclusive in the client initializer.', + 'Project and location are not supported for Gemini API backend.', ); }); + it('should allow explicit apiKey with project and location when vertexai is true', () => { + const client = new GoogleGenAI({ + apiKey: 'constructor_api_key', + vertexai: true, + project: 'constructor_project', + location: 'constructor_location', + }); + expect(client.vertexai).toBe(true); + expect(client['apiKey']).toBe('constructor_api_key'); + expect(client['project']).toBe('constructor_project'); + expect(client['location']).toBe('constructor_location'); + }); it('should prioritize credentials over implicit api key', () => { process.env['GOOGLE_API_KEY'] = ''; @@ -604,7 +616,7 @@ describe('Client', () => { it('env enterprise should take precedence over env vertexai', () => { process.env['GOOGLE_GENAI_USE_ENTERPRISE'] = 'false'; process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true'; - const client = new GoogleGenAI({project: 'p', location: 'l'}); + const client = new GoogleGenAI({apiKey: 'key'}); expect(client.vertexai).toBeFalse(); });