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. 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..4a0b978d858 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,22 @@ 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 +115,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 +131,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 +146,22 @@ 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..7aa7f806e7f 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,22 @@ 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..cac14845961 100644 --- a/packages/vertexai/src/models/vertexai-model.ts +++ b/packages/vertexai/src/models/vertexai-model.ts @@ -68,10 +68,18 @@ 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, + 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 b6d0ecb9b71..3fb2b175558 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' }) @@ -124,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'); @@ -135,6 +181,7 @@ describe('request methods', () => { { apiKey: 'key', project: 'myproject', + appId: 'my-appid', location: 'moon' }, true, @@ -167,6 +214,7 @@ describe('request methods', () => { { apiKey: 'key', project: 'myproject', + appId: 'my-appid', location: 'moon', getAppCheckToken: () => Promise.resolve({ token: 'dummytoken', error: Error('oops') }) @@ -193,6 +241,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..a93e1dbbd0e 100644 --- a/packages/vertexai/src/requests/request.ts +++ b/packages/vertexai/src/requests/request.ts @@ -84,6 +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); + 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/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..a3476afd028 100644 --- a/packages/vertexai/src/types/internal.ts +++ b/packages/vertexai/src/types/internal.ts @@ -23,7 +23,9 @@ export * from './imagen/internal'; export interface ApiSettings { apiKey: string; project: string; + appId: string; location: string; + automaticDataCollectionEnabled?: boolean; getAuthToken?: () => Promise; getAppCheckToken?: () => Promise; }