From 1df52ade447477efe79b2e9cd8981f52c3ce26c4 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 25 Feb 2025 11:28:10 -0500 Subject: [PATCH 1/4] Add X-Firebase-AppId header to requests --- common/api-review/vertexai.api.md | 1 + docs-devsite/vertexai.md | 1 + packages/vertexai/src/api.test.ts | 47 +++++++++++++++++-- .../vertexai/src/methods/chat-session.test.ts | 1 + .../vertexai/src/methods/count-tokens.test.ts | 1 + .../src/methods/generate-content.test.ts | 1 + .../src/models/generative-model.test.ts | 3 +- .../vertexai/src/models/imagen-model.test.ts | 3 +- .../src/models/vertexai-model.test.ts | 23 ++++++++- .../vertexai/src/models/vertexai-model.ts | 6 +++ .../vertexai/src/requests/request.test.ts | 5 ++ packages/vertexai/src/requests/request.ts | 1 + packages/vertexai/src/types/error.ts | 3 ++ packages/vertexai/src/types/internal.ts | 1 + 14 files changed, 89 insertions(+), 8 deletions(-) diff --git a/common/api-review/vertexai.api.md b/common/api-review/vertexai.api.md index 8b1dd83f51a..c7f5ab89487 100644 --- a/common/api-review/vertexai.api.md +++ b/common/api-review/vertexai.api.md @@ -817,6 +817,7 @@ export const enum VertexAIErrorCode { INVALID_CONTENT = "invalid-content", INVALID_SCHEMA = "invalid-schema", NO_API_KEY = "no-api-key", + NO_APP_ID = "no-app-id", NO_MODEL = "no-model", NO_PROJECT_ID = "no-project-id", PARSE_FAILED = "parse-failed", diff --git a/docs-devsite/vertexai.md b/docs-devsite/vertexai.md index d174bef7bcf..9f6478b6258 100644 --- a/docs-devsite/vertexai.md +++ b/docs-devsite/vertexai.md @@ -545,6 +545,7 @@ export declare const enum VertexAIErrorCode | INVALID\_CONTENT | "invalid-content" | An error associated with a Content object. | | INVALID\_SCHEMA | "invalid-schema" | An error due to invalid Schema input. | | NO\_API\_KEY | "no-api-key" | An error occurred due to a missing Firebase API key. | +| NO\_APP\_ID | "no-app-id" | An error occured due to a missing app ID. | | NO\_MODEL | "no-model" | An error occurred due to a model name not being specified during initialization. | | NO\_PROJECT\_ID | "no-project-id" | An error occurred due to a missing project ID. | | PARSE\_FAILED | "parse-failed" | An error occurred while parsing. | diff --git a/packages/vertexai/src/api.test.ts b/packages/vertexai/src/api.test.ts index c1b2635ce70..f59e441693d 100644 --- a/packages/vertexai/src/api.test.ts +++ b/packages/vertexai/src/api.test.ts @@ -27,7 +27,8 @@ const fakeVertexAI: VertexAI = { automaticDataCollectionEnabled: true, options: { apiKey: 'key', - projectId: 'my-project' + projectId: 'my-project', + appId: 'my-appid' } }, location: 'us-central1' @@ -48,7 +49,7 @@ describe('Top level API', () => { it('getGenerativeModel throws if no apiKey is provided', () => { const fakeVertexNoApiKey = { ...fakeVertexAI, - app: { options: { projectId: 'my-project' } } + app: { options: { projectId: 'my-project', appId: 'my-appid' } } } as VertexAI; try { getGenerativeModel(fakeVertexNoApiKey, { model: 'my-model' }); @@ -64,7 +65,7 @@ describe('Top level API', () => { it('getGenerativeModel throws if no projectId is provided', () => { const fakeVertexNoProject = { ...fakeVertexAI, - app: { options: { apiKey: 'my-key' } } + app: { options: { apiKey: 'my-key', appId: 'my-appid' } } } as VertexAI; try { getGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); @@ -79,6 +80,24 @@ describe('Top level API', () => { ); } }); + it('getGenerativeModel throws if no appId is provided', () => { + const fakeVertexNoProject = { + ...fakeVertexAI, + app: { options: { apiKey: 'my-key' , projectId: 'my-projectid'} } + } as VertexAI; + try { + getGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as VertexAIError).code).includes( + VertexAIErrorCode.NO_APP_ID + ); + expect((e as VertexAIError).message).equals( + `VertexAI: The "appId" field is empty in the local` + + ` Firebase config. Firebase VertexAI requires this field ` + + `to contain a valid app ID. (vertexAI/${VertexAIErrorCode.NO_APP_ID})` + ); + } + }); it('getGenerativeModel gets a GenerativeModel', () => { const genModel = getGenerativeModel(fakeVertexAI, { model: 'my-model' }); expect(genModel).to.be.an.instanceOf(GenerativeModel); @@ -98,7 +117,7 @@ describe('Top level API', () => { it('getImagenModel throws if no apiKey is provided', () => { const fakeVertexNoApiKey = { ...fakeVertexAI, - app: { options: { projectId: 'my-project' } } + app: { options: { projectId: 'my-project', appId: 'my-appid' } } } as VertexAI; try { getImagenModel(fakeVertexNoApiKey, { model: 'my-model' }); @@ -114,7 +133,7 @@ describe('Top level API', () => { it('getImagenModel throws if no projectId is provided', () => { const fakeVertexNoProject = { ...fakeVertexAI, - app: { options: { apiKey: 'my-key' } } + app: { options: { apiKey: 'my-key', appId: 'my-appid' } } } as VertexAI; try { getImagenModel(fakeVertexNoProject, { model: 'my-model' }); @@ -129,6 +148,24 @@ describe('Top level API', () => { ); } }); + it('getImagenModel throws if no appId is provided', () => { + const fakeVertexNoProject = { + ...fakeVertexAI, + app: { options: { apiKey: 'my-key', projectId: 'my-project' } } + } as VertexAI; + try { + getImagenModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as VertexAIError).code).includes( + VertexAIErrorCode.NO_APP_ID + ); + expect((e as VertexAIError).message).equals( + `VertexAI: The "appId" field is empty in the local` + + ` Firebase config. Firebase VertexAI requires this field ` + + `to contain a valid app ID. (vertexAI/${VertexAIErrorCode.NO_APP_ID})` + ); + } + }); it('getImagenModel gets an ImagenModel', () => { const genModel = getImagenModel(fakeVertexAI, { model: 'my-model' }); expect(genModel).to.be.an.instanceOf(ImagenModel); diff --git a/packages/vertexai/src/methods/chat-session.test.ts b/packages/vertexai/src/methods/chat-session.test.ts index 7741c33ea0b..bd389a3d778 100644 --- a/packages/vertexai/src/methods/chat-session.test.ts +++ b/packages/vertexai/src/methods/chat-session.test.ts @@ -30,6 +30,7 @@ use(chaiAsPromised); const fakeApiSettings: ApiSettings = { apiKey: 'key', project: 'my-project', + appId: 'my-appid', location: 'us-central1' }; diff --git a/packages/vertexai/src/methods/count-tokens.test.ts b/packages/vertexai/src/methods/count-tokens.test.ts index 2032e884fb4..a3d7c99b4ba 100644 --- a/packages/vertexai/src/methods/count-tokens.test.ts +++ b/packages/vertexai/src/methods/count-tokens.test.ts @@ -32,6 +32,7 @@ use(chaiAsPromised); const fakeApiSettings: ApiSettings = { apiKey: 'key', project: 'my-project', + appId: 'my-appid', location: 'us-central1' }; diff --git a/packages/vertexai/src/methods/generate-content.test.ts b/packages/vertexai/src/methods/generate-content.test.ts index 001fe12c9c8..426bd5176db 100644 --- a/packages/vertexai/src/methods/generate-content.test.ts +++ b/packages/vertexai/src/methods/generate-content.test.ts @@ -37,6 +37,7 @@ use(chaiAsPromised); const fakeApiSettings: ApiSettings = { apiKey: 'key', project: 'my-project', + appId: 'my-appid', location: 'us-central1' }; diff --git a/packages/vertexai/src/models/generative-model.test.ts b/packages/vertexai/src/models/generative-model.test.ts index c2dbdfac75c..26dff4e04c6 100644 --- a/packages/vertexai/src/models/generative-model.test.ts +++ b/packages/vertexai/src/models/generative-model.test.ts @@ -30,7 +30,8 @@ const fakeVertexAI: VertexAI = { automaticDataCollectionEnabled: true, options: { apiKey: 'key', - projectId: 'my-project' + projectId: 'my-project', + appId: 'my-appid' } }, location: 'us-central1' diff --git a/packages/vertexai/src/models/imagen-model.test.ts b/packages/vertexai/src/models/imagen-model.test.ts index 000b2f07f90..c566a88e5b0 100644 --- a/packages/vertexai/src/models/imagen-model.test.ts +++ b/packages/vertexai/src/models/imagen-model.test.ts @@ -37,7 +37,8 @@ const fakeVertexAI: VertexAI = { automaticDataCollectionEnabled: true, options: { apiKey: 'key', - projectId: 'my-project' + projectId: 'my-project', + appId: 'my-appid' } }, location: 'us-central1' diff --git a/packages/vertexai/src/models/vertexai-model.test.ts b/packages/vertexai/src/models/vertexai-model.test.ts index 2aa36d56f0d..afcc4183d97 100644 --- a/packages/vertexai/src/models/vertexai-model.test.ts +++ b/packages/vertexai/src/models/vertexai-model.test.ts @@ -38,7 +38,8 @@ const fakeVertexAI: VertexAI = { automaticDataCollectionEnabled: true, options: { apiKey: 'key', - projectId: 'my-project' + projectId: 'my-project', + appId: 'my-appid' } }, location: 'us-central1' @@ -100,4 +101,24 @@ describe('VertexAIModel', () => { ); } }); + it('throws if not passed an app ID', () => { + const fakeVertexAI: VertexAI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project' + } + }, + location: 'us-central1' + }; + try { + new TestModel(fakeVertexAI, 'my-model'); + } catch (e) { + expect((e as VertexAIError).code).to.equal( + VertexAIErrorCode.NO_APP_ID + ); + } + }); }); diff --git a/packages/vertexai/src/models/vertexai-model.ts b/packages/vertexai/src/models/vertexai-model.ts index 4e211c0cf94..59c6d0d8dc1 100644 --- a/packages/vertexai/src/models/vertexai-model.ts +++ b/packages/vertexai/src/models/vertexai-model.ts @@ -68,10 +68,16 @@ export abstract class VertexAIModel { VertexAIErrorCode.NO_PROJECT_ID, `The "projectId" field is empty in the local Firebase config. Firebase VertexAI requires this field to contain a valid project ID.` ); + } else if (!vertexAI.app?.options?.appId) { + throw new VertexAIError( + VertexAIErrorCode.NO_APP_ID, + `The "appId" field is empty in the local Firebase config. Firebase VertexAI requires this field to contain a valid app ID.` + ); } else { this._apiSettings = { apiKey: vertexAI.app.options.apiKey, project: vertexAI.app.options.projectId, + appId: vertexAI.app.options.appId, location: vertexAI.location }; diff --git a/packages/vertexai/src/requests/request.test.ts b/packages/vertexai/src/requests/request.test.ts index b6d0ecb9b71..2c4648a22ba 100644 --- a/packages/vertexai/src/requests/request.test.ts +++ b/packages/vertexai/src/requests/request.test.ts @@ -32,6 +32,7 @@ use(chaiAsPromised); const fakeApiSettings: ApiSettings = { apiKey: 'key', project: 'my-project', + appId: 'my-appid', location: 'us-central1' }; @@ -103,6 +104,7 @@ describe('request methods', () => { const fakeApiSettings: ApiSettings = { apiKey: 'key', project: 'myproject', + appId: 'my-appid', location: 'moon', getAuthToken: () => Promise.resolve({ accessToken: 'authtoken' }), getAppCheckToken: () => Promise.resolve({ token: 'appchecktoken' }) @@ -135,6 +137,7 @@ describe('request methods', () => { { apiKey: 'key', project: 'myproject', + appId: 'my-appid', location: 'moon' }, true, @@ -167,6 +170,7 @@ describe('request methods', () => { { apiKey: 'key', project: 'myproject', + appId: 'my-appid', location: 'moon', getAppCheckToken: () => Promise.resolve({ token: 'dummytoken', error: Error('oops') }) @@ -193,6 +197,7 @@ describe('request methods', () => { { apiKey: 'key', project: 'myproject', + appId: 'my-appid', location: 'moon' }, true, diff --git a/packages/vertexai/src/requests/request.ts b/packages/vertexai/src/requests/request.ts index 9b9465db776..f07f4818b39 100644 --- a/packages/vertexai/src/requests/request.ts +++ b/packages/vertexai/src/requests/request.ts @@ -84,6 +84,7 @@ export async function getHeaders(url: RequestUrl): Promise { headers.append('Content-Type', 'application/json'); headers.append('x-goog-api-client', getClientHeaders()); headers.append('x-goog-api-key', url.apiSettings.apiKey); + headers.append('X-Firebase-AppId', url.apiSettings.appId); // Will be converted to 'X-Firebase-Appid' before it's sent in the browser. if (url.apiSettings.getAppCheckToken) { const appCheckToken = await url.apiSettings.getAppCheckToken(); if (appCheckToken) { diff --git a/packages/vertexai/src/types/error.ts b/packages/vertexai/src/types/error.ts index 8d83a52a0aa..d5ca79868f4 100644 --- a/packages/vertexai/src/types/error.ts +++ b/packages/vertexai/src/types/error.ts @@ -87,6 +87,9 @@ export const enum VertexAIErrorCode { /** An error occurred due to a missing Firebase API key. */ NO_API_KEY = 'no-api-key', + /** An error occured due to a missing app ID. */ + NO_APP_ID = 'no-app-id', + /** An error occurred due to a model name not being specified during initialization. */ NO_MODEL = 'no-model', diff --git a/packages/vertexai/src/types/internal.ts b/packages/vertexai/src/types/internal.ts index 87c28a02ab2..d35893a1b9f 100644 --- a/packages/vertexai/src/types/internal.ts +++ b/packages/vertexai/src/types/internal.ts @@ -23,6 +23,7 @@ export * from './imagen/internal'; export interface ApiSettings { apiKey: string; project: string; + appId: string; location: string; getAuthToken?: () => Promise; getAppCheckToken?: () => Promise; From 7c62bc1fbb9726e95c213d7bf8815a36698326c9 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 25 Feb 2025 11:39:50 -0500 Subject: [PATCH 2/4] Add changeset --- .changeset/red-hornets-peel.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/red-hornets-peel.md diff --git a/.changeset/red-hornets-peel.md b/.changeset/red-hornets-peel.md new file mode 100644 index 00000000000..1c6eb50df13 --- /dev/null +++ b/.changeset/red-hornets-peel.md @@ -0,0 +1,5 @@ +--- +'@firebase/vertexai': patch +--- + +Throw an error when initializing models if `appId` is not defined in the given `VertexAI` instance. From 7f14785bacbfd74bfa1190daf34ccfc14ea2a2b1 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 25 Feb 2025 11:46:48 -0500 Subject: [PATCH 3/4] Run formatter --- packages/vertexai/src/api.test.ts | 10 +++------- packages/vertexai/src/models/vertexai-model.test.ts | 4 +--- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/vertexai/src/api.test.ts b/packages/vertexai/src/api.test.ts index f59e441693d..4a0b978d858 100644 --- a/packages/vertexai/src/api.test.ts +++ b/packages/vertexai/src/api.test.ts @@ -83,14 +83,12 @@ describe('Top level API', () => { it('getGenerativeModel throws if no appId is provided', () => { const fakeVertexNoProject = { ...fakeVertexAI, - app: { options: { apiKey: 'my-key' , projectId: 'my-projectid'} } + app: { options: { apiKey: 'my-key', projectId: 'my-projectid' } } } as VertexAI; try { getGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); } catch (e) { - expect((e as VertexAIError).code).includes( - VertexAIErrorCode.NO_APP_ID - ); + expect((e as VertexAIError).code).includes(VertexAIErrorCode.NO_APP_ID); expect((e as VertexAIError).message).equals( `VertexAI: The "appId" field is empty in the local` + ` Firebase config. Firebase VertexAI requires this field ` + @@ -156,9 +154,7 @@ describe('Top level API', () => { try { getImagenModel(fakeVertexNoProject, { model: 'my-model' }); } catch (e) { - expect((e as VertexAIError).code).includes( - VertexAIErrorCode.NO_APP_ID - ); + expect((e as VertexAIError).code).includes(VertexAIErrorCode.NO_APP_ID); expect((e as VertexAIError).message).equals( `VertexAI: The "appId" field is empty in the local` + ` Firebase config. Firebase VertexAI requires this field ` + diff --git a/packages/vertexai/src/models/vertexai-model.test.ts b/packages/vertexai/src/models/vertexai-model.test.ts index afcc4183d97..7aa7f806e7f 100644 --- a/packages/vertexai/src/models/vertexai-model.test.ts +++ b/packages/vertexai/src/models/vertexai-model.test.ts @@ -116,9 +116,7 @@ describe('VertexAIModel', () => { try { new TestModel(fakeVertexAI, 'my-model'); } catch (e) { - expect((e as VertexAIError).code).to.equal( - VertexAIErrorCode.NO_APP_ID - ); + expect((e as VertexAIError).code).to.equal(VertexAIErrorCode.NO_APP_ID); } }); }); From 767cd7717b8487852a0aaf8dac1daca13e64c501 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 25 Feb 2025 14:13:54 -0500 Subject: [PATCH 4/4] Only send if data collection is on --- .../vertexai/src/models/vertexai-model.ts | 2 + .../vertexai/src/requests/request.test.ts | 44 +++++++++++++++++++ packages/vertexai/src/requests/request.ts | 4 +- packages/vertexai/src/types/internal.ts | 1 + 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/vertexai/src/models/vertexai-model.ts b/packages/vertexai/src/models/vertexai-model.ts index 59c6d0d8dc1..cac14845961 100644 --- a/packages/vertexai/src/models/vertexai-model.ts +++ b/packages/vertexai/src/models/vertexai-model.ts @@ -78,6 +78,8 @@ export abstract class VertexAIModel { apiKey: vertexAI.app.options.apiKey, project: vertexAI.app.options.projectId, appId: vertexAI.app.options.appId, + automaticDataCollectionEnabled: + vertexAI.app.automaticDataCollectionEnabled, location: vertexAI.location }; diff --git a/packages/vertexai/src/requests/request.test.ts b/packages/vertexai/src/requests/request.test.ts index 2c4648a22ba..3fb2b175558 100644 --- a/packages/vertexai/src/requests/request.test.ts +++ b/packages/vertexai/src/requests/request.test.ts @@ -126,6 +126,50 @@ describe('request methods', () => { const headers = await getHeaders(fakeUrl); expect(headers.get('x-goog-api-key')).to.equal('key'); }); + it('adds app id if automatedDataCollectionEnabled is undefined', async () => { + const headers = await getHeaders(fakeUrl); + expect(headers.get('X-Firebase-AppId')).to.equal('my-appid'); + }); + it('adds app id if automatedDataCollectionEnabled is true', async () => { + const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'myproject', + appId: 'my-appid', + location: 'moon', + automaticDataCollectionEnabled: true, + getAuthToken: () => Promise.resolve({ accessToken: 'authtoken' }), + getAppCheckToken: () => Promise.resolve({ token: 'appchecktoken' }) + }; + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + true, + {} + ); + const headers = await getHeaders(fakeUrl); + expect(headers.get('X-Firebase-AppId')).to.equal('my-appid'); + }); + it('does not add app id if automatedDataCollectionEnabled is false', async () => { + const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'myproject', + appId: 'my-appid', + location: 'moon', + automaticDataCollectionEnabled: false, + getAuthToken: () => Promise.resolve({ accessToken: 'authtoken' }), + getAppCheckToken: () => Promise.resolve({ token: 'appchecktoken' }) + }; + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + true, + {} + ); + const headers = await getHeaders(fakeUrl); + expect(headers.get('X-Firebase-AppId')).to.be.null; + }); it('adds app check token if it exists', async () => { const headers = await getHeaders(fakeUrl); expect(headers.get('X-Firebase-AppCheck')).to.equal('appchecktoken'); diff --git a/packages/vertexai/src/requests/request.ts b/packages/vertexai/src/requests/request.ts index f07f4818b39..a93e1dbbd0e 100644 --- a/packages/vertexai/src/requests/request.ts +++ b/packages/vertexai/src/requests/request.ts @@ -84,7 +84,9 @@ export async function getHeaders(url: RequestUrl): Promise { headers.append('Content-Type', 'application/json'); headers.append('x-goog-api-client', getClientHeaders()); headers.append('x-goog-api-key', url.apiSettings.apiKey); - headers.append('X-Firebase-AppId', url.apiSettings.appId); // Will be converted to 'X-Firebase-Appid' before it's sent in the browser. + if (url.apiSettings.automaticDataCollectionEnabled !== false) { + headers.append('X-Firebase-AppId', url.apiSettings.appId); + } if (url.apiSettings.getAppCheckToken) { const appCheckToken = await url.apiSettings.getAppCheckToken(); if (appCheckToken) { diff --git a/packages/vertexai/src/types/internal.ts b/packages/vertexai/src/types/internal.ts index d35893a1b9f..a3476afd028 100644 --- a/packages/vertexai/src/types/internal.ts +++ b/packages/vertexai/src/types/internal.ts @@ -25,6 +25,7 @@ export interface ApiSettings { project: string; appId: string; location: string; + automaticDataCollectionEnabled?: boolean; getAuthToken?: () => Promise; getAppCheckToken?: () => Promise; }