From 5afb9c61f4b59ab1586bc06281f8af2eb1d6dd13 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Tue, 5 Aug 2025 14:50:26 -0700 Subject: [PATCH 1/5] Add limitedUseToken option to AI SDK --- common/api-review/ai.api.md | 8 ++++++++ packages/ai/src/api.ts | 18 ++++++++++++------ packages/ai/src/models/ai-model.ts | 9 +++++++-- packages/ai/src/public-types.ts | 15 +++++++++++++++ packages/ai/src/service.ts | 11 ++++++++++- 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/common/api-review/ai.api.md b/common/api-review/ai.api.md index 199b97b10a9..b8032833bd9 100644 --- a/common/api-review/ai.api.md +++ b/common/api-review/ai.api.md @@ -15,6 +15,7 @@ export interface AI { backend: Backend; // @deprecated (undocumented) location: string; + options?: AIOptions; } // @public @@ -61,6 +62,7 @@ export abstract class AIModel { // @public export interface AIOptions { + appCheck?: AppCheckOptions; backend: Backend; } @@ -75,6 +77,12 @@ export class AnyOfSchema extends Schema { toJSON(): SchemaRequest; } +// @public +export interface AppCheckOptions { + // (undocumented) + limitedUseTokens?: boolean; +} + // @public export class ArraySchema extends Schema { constructor(schemaParams: SchemaParams, items: TypedSchema); diff --git a/packages/ai/src/api.ts b/packages/ai/src/api.ts index e9264262145..439e7c16732 100644 --- a/packages/ai/src/api.ts +++ b/packages/ai/src/api.ts @@ -72,18 +72,24 @@ declare module '@firebase/component' { * * @public */ -export function getAI( - app: FirebaseApp = getApp(), - options: AIOptions = { backend: new GoogleAIBackend() } -): AI { +export function getAI(app: FirebaseApp = getApp(), options?: AIOptions): AI { app = getModularInstance(app); // Dependencies const AIProvider: Provider<'AI'> = _getProvider(app, AI_TYPE); - const identifier = encodeInstanceIdentifier(options.backend); - return AIProvider.getImmediate({ + const finalOptions = { + backend: options?.backend ?? new GoogleAIBackend(), + appCheck: options?.appCheck ?? { limitedUseTokens: false } + }; + + const identifier = encodeInstanceIdentifier(finalOptions.backend); + const aiInstance = AIProvider.getImmediate({ identifier }); + + aiInstance.options = finalOptions; + + return aiInstance; } /** diff --git a/packages/ai/src/models/ai-model.ts b/packages/ai/src/models/ai-model.ts index 084dbe329cc..f22f776aa50 100644 --- a/packages/ai/src/models/ai-model.ts +++ b/packages/ai/src/models/ai-model.ts @@ -90,8 +90,13 @@ export abstract class AIModel { return Promise.resolve({ token }); }; } else if ((ai as AIService).appCheck) { - this._apiSettings.getAppCheckToken = () => - (ai as AIService).appCheck!.getToken(); + if (ai.options?.appCheck?.limitedUseTokens) { + this._apiSettings.getAppCheckToken = () => + (ai as AIService).appCheck!.getLimitedUseToken(); + } else { + this._apiSettings.getAppCheckToken = () => + (ai as AIService).appCheck!.getToken(); + } } if ((ai as AIService).auth) { diff --git a/packages/ai/src/public-types.ts b/packages/ai/src/public-types.ts index 57812f20c1e..43b38767d63 100644 --- a/packages/ai/src/public-types.ts +++ b/packages/ai/src/public-types.ts @@ -38,6 +38,10 @@ export interface AI { * Vertex AI Gemini API (using {@link VertexAIBackend}). */ backend: Backend; + /** + * Options applied to this {@link AI} instance. + */ + options?: AIOptions; /** * @deprecated use `AI.backend.location` instead. * @@ -92,4 +96,15 @@ export interface AIOptions { * The backend configuration to use for the AI service instance. */ backend: Backend; + /** + * Configures App Check usage for this AI service instance. + */ + appCheck?: AppCheckOptions; +} + +/** + * Configures App Check usage for this AI service instance. + */ +export interface AppCheckOptions { + limitedUseTokens?: boolean; } diff --git a/packages/ai/src/service.ts b/packages/ai/src/service.ts index 006cc45a94e..78caa21863e 100644 --- a/packages/ai/src/service.ts +++ b/packages/ai/src/service.ts @@ -16,7 +16,7 @@ */ import { FirebaseApp, _FirebaseService } from '@firebase/app'; -import { AI } from './public-types'; +import { AI, AIOptions } from './public-types'; import { AppCheckInternalComponentName, FirebaseAppCheckInternal @@ -31,6 +31,7 @@ import { Backend, VertexAIBackend } from './backend'; export class AIService implements AI, _FirebaseService { auth: FirebaseAuthInternal | null; appCheck: FirebaseAppCheckInternal | null; + _options?: AIOptions; location: string; // This is here for backwards-compatibility constructor( @@ -54,4 +55,12 @@ export class AIService implements AI, _FirebaseService { _delete(): Promise { return Promise.resolve(); } + + set options(optionsToSet: AIOptions) { + this.options = optionsToSet; + } + + get options(): AIOptions { + return this.options; + } } From a9d69bce3d9d0711e2db890de589f5729b9b6d12 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Mon, 11 Aug 2025 15:30:42 -0700 Subject: [PATCH 2/5] Docs and tests --- .changeset/nasty-rings-drop.md | 6 ++ common/api-review/ai.api.md | 18 ++++- docs-devsite/_toc.yaml | 2 + docs-devsite/ai.ai.md | 11 +++ docs-devsite/ai.aioptions.md | 17 ++++- docs-devsite/ai.appcheckoptions.md | 35 +++++++++ docs-devsite/ai.md | 24 ++++++ packages/ai/src/api.test.ts | 31 +++++++- packages/ai/src/api.ts | 5 +- packages/ai/src/index.ts | 50 +++++++------ packages/ai/src/models/ai-model.test.ts | 48 ++++++++++++ packages/ai/src/models/ai-model.ts | 2 +- packages/ai/src/public-types.ts | 6 +- packages/ai/src/service.ts | 8 +- .../test-utils/get-fake-firebase-services.ts | 73 +++++++++++++++++++ 15 files changed, 299 insertions(+), 37 deletions(-) create mode 100644 .changeset/nasty-rings-drop.md create mode 100644 docs-devsite/ai.appcheckoptions.md create mode 100644 packages/ai/test-utils/get-fake-firebase-services.ts diff --git a/.changeset/nasty-rings-drop.md b/.changeset/nasty-rings-drop.md new file mode 100644 index 00000000000..7755dced9f1 --- /dev/null +++ b/.changeset/nasty-rings-drop.md @@ -0,0 +1,6 @@ +--- +'@firebase/ai': minor +'firebase': minor +--- + +Add App Check limited use token option to `getAI()`. diff --git a/common/api-review/ai.api.md b/common/api-review/ai.api.md index b8032833bd9..1f86e25c18b 100644 --- a/common/api-review/ai.api.md +++ b/common/api-review/ai.api.md @@ -4,10 +4,18 @@ ```ts +import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; import { AppCheckTokenResult } from '@firebase/app-check-interop-types'; +import { ComponentContainer } from '@firebase/component'; import { FirebaseApp } from '@firebase/app'; +import { FirebaseAppCheckInternal } from '@firebase/app-check-interop-types'; +import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; +import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { FirebaseAuthTokenData } from '@firebase/auth-interop-types'; import { FirebaseError } from '@firebase/util'; +import { _FirebaseService } from '@firebase/app'; +import { InstanceFactoryOptions } from '@firebase/component'; +import { Provider } from '@firebase/component'; // @public export interface AI { @@ -54,7 +62,7 @@ export abstract class AIModel { // Warning: (ae-forgotten-export) The symbol "ApiSettings" needs to be exported by the entry point index.d.ts // // @internal (undocumented) - protected _apiSettings: ApiSettings; + _apiSettings: ApiSettings; readonly model: string; // @internal static normalizeModelName(modelName: string, backendType: BackendType): string; @@ -63,7 +71,7 @@ export abstract class AIModel { // @public export interface AIOptions { appCheck?: AppCheckOptions; - backend: Backend; + backend?: Backend; } // @public @@ -79,7 +87,6 @@ export class AnyOfSchema extends Schema { // @public export interface AppCheckOptions { - // (undocumented) limitedUseTokens?: boolean; } @@ -228,6 +235,11 @@ export interface ErrorDetails { reason?: string; } +// Warning: (ae-forgotten-export) The symbol "AIService" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function factory(container: ComponentContainer, { instanceIdentifier }: InstanceFactoryOptions): AIService; + // @public export interface FileData { // (undocumented) diff --git a/docs-devsite/_toc.yaml b/docs-devsite/_toc.yaml index 6d548ffd8d6..597cf1d3787 100644 --- a/docs-devsite/_toc.yaml +++ b/docs-devsite/_toc.yaml @@ -14,6 +14,8 @@ toc: path: /docs/reference/js/ai.aioptions.md - title: AnyOfSchema path: /docs/reference/js/ai.anyofschema.md + - title: AppCheckOptions + path: /docs/reference/js/ai.appcheckoptions.md - title: ArraySchema path: /docs/reference/js/ai.arrayschema.md - title: Backend diff --git a/docs-devsite/ai.ai.md b/docs-devsite/ai.ai.md index d4127ffb7e8..d7aafb6a5b5 100644 --- a/docs-devsite/ai.ai.md +++ b/docs-devsite/ai.ai.md @@ -27,6 +27,7 @@ export interface AI | [app](./ai.ai.md#aiapp) | [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) | The [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) this [AI](./ai.ai.md#ai_interface) instance is associated with. | | [backend](./ai.ai.md#aibackend) | [Backend](./ai.backend.md#backend_class) | A [Backend](./ai.backend.md#backend_class) instance that specifies the configuration for the target backend, either the Gemini Developer API (using [GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)) or the Vertex AI Gemini API (using [VertexAIBackend](./ai.vertexaibackend.md#vertexaibackend_class)). | | [location](./ai.ai.md#ailocation) | string | | +| [options](./ai.ai.md#aioptions) | [AIOptions](./ai.aioptions.md#aioptions_interface) | Options applied to this [AI](./ai.ai.md#ai_interface) instance. | ## AI.app @@ -62,3 +63,13 @@ backend: Backend; ```typescript location: string; ``` + +## AI.options + +Options applied to this [AI](./ai.ai.md#ai_interface) instance. + +Signature: + +```typescript +options?: AIOptions; +``` diff --git a/docs-devsite/ai.aioptions.md b/docs-devsite/ai.aioptions.md index a092046900b..8f61ea4158f 100644 --- a/docs-devsite/ai.aioptions.md +++ b/docs-devsite/ai.aioptions.md @@ -22,14 +22,25 @@ export interface AIOptions | Property | Type | Description | | --- | --- | --- | -| [backend](./ai.aioptions.md#aioptionsbackend) | [Backend](./ai.backend.md#backend_class) | The backend configuration to use for the AI service instance. | +| [appCheck](./ai.aioptions.md#aioptionsappcheck) | [AppCheckOptions](./ai.appcheckoptions.md#appcheckoptions_interface) | Configures App Check usage for this AI service instance. | +| [backend](./ai.aioptions.md#aioptionsbackend) | [Backend](./ai.backend.md#backend_class) | The backend configuration to use for the AI service instance. Defaults to [GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class). | + +## AIOptions.appCheck + +Configures App Check usage for this AI service instance. + +Signature: + +```typescript +appCheck?: AppCheckOptions; +``` ## AIOptions.backend -The backend configuration to use for the AI service instance. +The backend configuration to use for the AI service instance. Defaults to [GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class). Signature: ```typescript -backend: Backend; +backend?: Backend; ``` diff --git a/docs-devsite/ai.appcheckoptions.md b/docs-devsite/ai.appcheckoptions.md new file mode 100644 index 00000000000..60ef3e2edea --- /dev/null +++ b/docs-devsite/ai.appcheckoptions.md @@ -0,0 +1,35 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# AppCheckOptions interface +Configures App Check usage for this AI service instance. + +Signature: + +```typescript +export interface AppCheckOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [limitedUseTokens](./ai.appcheckoptions.md#appcheckoptionslimitedusetokens) | boolean | Defaults to false. | + +## AppCheckOptions.limitedUseTokens + +Defaults to false. + +Signature: + +```typescript +limitedUseTokens?: boolean; +``` diff --git a/docs-devsite/ai.md b/docs-devsite/ai.md index 9900b3ecccc..6d9d2a8364a 100644 --- a/docs-devsite/ai.md +++ b/docs-devsite/ai.md @@ -21,6 +21,8 @@ The Firebase AI Web SDK. | function(ai, ...) | | [getGenerativeModel(ai, modelParams, requestOptions)](./ai.md#getgenerativemodel_80bd839) | Returns a [GenerativeModel](./ai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. | | [getImagenModel(ai, modelParams, requestOptions)](./ai.md#getimagenmodel_e1f6645) | (Public Preview) Returns an [ImagenModel](./ai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen.Only Imagen 3 models (named imagen-3.0-*) are supported. | +| function(container, ...) | +| [factory(container, { instanceIdentifier })](./ai.md#factory_6581aeb) | | ## Classes @@ -50,6 +52,7 @@ The Firebase AI Web SDK. | --- | --- | | [AI](./ai.ai.md#ai_interface) | An instance of the Firebase AI SDK.Do not create this instance directly. Instead, use [getAI()](./ai.md#getai_a94a413). | | [AIOptions](./ai.aioptions.md#aioptions_interface) | Options for initializing the AI service using [getAI()](./ai.md#getai_a94a413). This allows specifying which backend to use (Vertex AI Gemini API or Gemini Developer API) and configuring its specific options (like location for Vertex AI). | +| [AppCheckOptions](./ai.appcheckoptions.md#appcheckoptions_interface) | Configures App Check usage for this AI service instance. | | [BaseParams](./ai.baseparams.md#baseparams_interface) | Base parameters for a number of methods. | | [Citation](./ai.citation.md#citation_interface) | A single citation. | | [CitationMetadata](./ai.citationmetadata.md#citationmetadata_interface) | Citation metadata that may be found on a [GenerateContentCandidate](./ai.generatecontentcandidate.md#generatecontentcandidate_interface). | @@ -264,6 +267,27 @@ export declare function getImagenModel(ai: AI, modelParams: ImagenModelParams, r If the `apiKey` or `projectId` fields are missing in your Firebase config. +## function(container, ...) + +### factory(container, { instanceIdentifier }) {:#factory_6581aeb} + +Signature: + +```typescript +export declare function factory(container: ComponentContainer, { instanceIdentifier }: InstanceFactoryOptions): AIService; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| container | ComponentContainer | | +| { instanceIdentifier } | InstanceFactoryOptions | | + +Returns: + +AIService + ## AIErrorCode Standardized error codes that [AIError](./ai.aierror.md#aierror_class) can have. diff --git a/packages/ai/src/api.test.ts b/packages/ai/src/api.test.ts index 27237b4edd3..08175a88025 100644 --- a/packages/ai/src/api.test.ts +++ b/packages/ai/src/api.test.ts @@ -16,12 +16,13 @@ */ import { ImagenModelParams, ModelParams, AIErrorCode } from './types'; import { AIError } from './errors'; -import { ImagenModel, getGenerativeModel, getImagenModel } from './api'; +import { getAI, ImagenModel, getGenerativeModel, getImagenModel } from './api'; import { expect } from 'chai'; import { AI } from './public-types'; import { GenerativeModel } from './models/generative-model'; -import { VertexAIBackend } from './backend'; +import { GoogleAIBackend, VertexAIBackend } from './backend'; import { AI_TYPE } from './constants'; +import { getFullApp } from '../test-utils/get-fake-firebase-services'; const fakeAI: AI = { app: { @@ -38,6 +39,32 @@ const fakeAI: AI = { }; describe('Top level API', () => { + describe('getAI()', () => { + it('works without options', () => { + const ai = getAI(getFullApp()); + expect(ai.backend).to.be.instanceOf(GoogleAIBackend); + }); + it('works with options: no backend, limited use token', () => { + const ai = getAI(getFullApp(), { appCheck: { limitedUseTokens: true } }); + expect(ai.backend).to.be.instanceOf(GoogleAIBackend); + expect(ai.options?.appCheck?.limitedUseTokens).to.be.true; + }); + it('works with options: backend specified, limited use token', () => { + const ai = getAI(getFullApp(), { + backend: new VertexAIBackend('us-central1'), + appCheck: { limitedUseTokens: true } + }); + expect(ai.backend).to.be.instanceOf(VertexAIBackend); + expect(ai.options?.appCheck?.limitedUseTokens).to.be.true; + }); + it('works with options: backend specified only', () => { + const ai = getAI(getFullApp(), { + backend: new VertexAIBackend('us-central1') + }); + expect(ai.backend).to.be.instanceOf(VertexAIBackend); + expect(ai.options?.appCheck?.limitedUseTokens).to.be.false; + }); + }); it('getGenerativeModel throws if no model is provided', () => { try { getGenerativeModel(fakeAI, {} as ModelParams); diff --git a/packages/ai/src/api.ts b/packages/ai/src/api.ts index 439e7c16732..c8191981449 100644 --- a/packages/ai/src/api.ts +++ b/packages/ai/src/api.ts @@ -77,12 +77,13 @@ export function getAI(app: FirebaseApp = getApp(), options?: AIOptions): AI { // Dependencies const AIProvider: Provider<'AI'> = _getProvider(app, AI_TYPE); + const backend = options?.backend ?? new GoogleAIBackend(); + const finalOptions = { - backend: options?.backend ?? new GoogleAIBackend(), appCheck: options?.appCheck ?? { limitedUseTokens: false } }; - const identifier = encodeInstanceIdentifier(finalOptions.backend); + const identifier = encodeInstanceIdentifier(backend); const aiInstance = AIProvider.getImmediate({ identifier }); diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 6ad1f2e3f08..8ac9db61b6d 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -24,7 +24,12 @@ import { registerVersion, _registerComponent } from '@firebase/app'; import { AIService } from './service'; import { AI_TYPE } from './constants'; -import { Component, ComponentType } from '@firebase/component'; +import { + Component, + ComponentContainer, + ComponentType, + InstanceFactoryOptions +} from '@firebase/component'; import { name, version } from '../package.json'; import { decodeInstanceIdentifier } from './helpers'; import { AIError } from './api'; @@ -36,28 +41,31 @@ declare global { } } -function registerAI(): void { - _registerComponent( - new Component( - AI_TYPE, - (container, { instanceIdentifier }) => { - if (!instanceIdentifier) { - throw new AIError( - AIErrorCode.ERROR, - 'AIService instance identifier is undefined.' - ); - } +export function factory( + container: ComponentContainer, + { instanceIdentifier }: InstanceFactoryOptions +): AIService { + if (!instanceIdentifier) { + throw new AIError( + AIErrorCode.ERROR, + 'AIService instance identifier is undefined.' + ); + } - const backend = decodeInstanceIdentifier(instanceIdentifier); + const backend = decodeInstanceIdentifier(instanceIdentifier); - // getImmediate for FirebaseApp will always succeed - const app = container.getProvider('app').getImmediate(); - const auth = container.getProvider('auth-internal'); - const appCheckProvider = container.getProvider('app-check-internal'); - return new AIService(app, backend, auth, appCheckProvider); - }, - ComponentType.PUBLIC - ).setMultipleInstances(true) + // getImmediate for FirebaseApp will always succeed + const app = container.getProvider('app').getImmediate(); + const auth = container.getProvider('auth-internal'); + const appCheckProvider = container.getProvider('app-check-internal'); + return new AIService(app, backend, auth, appCheckProvider); +} + +function registerAI(): void { + _registerComponent( + new Component(AI_TYPE, factory, ComponentType.PUBLIC).setMultipleInstances( + true + ) ); registerVersion(name, version); diff --git a/packages/ai/src/models/ai-model.test.ts b/packages/ai/src/models/ai-model.test.ts index 4f23fe9d06f..f7ab0f8a98c 100644 --- a/packages/ai/src/models/ai-model.test.ts +++ b/packages/ai/src/models/ai-model.test.ts @@ -17,9 +17,11 @@ import { use, expect } from 'chai'; import { AI, AIErrorCode } from '../public-types'; import sinonChai from 'sinon-chai'; +import { stub } from 'sinon'; import { AIModel } from './ai-model'; import { AIError } from '../errors'; import { VertexAIBackend } from '../backend'; +import { AIService } from '../service'; use(sinonChai); @@ -67,6 +69,52 @@ describe('AIModel', () => { const testModel = new TestModel(fakeAI, 'tunedModels/my-model'); expect(testModel.model).to.equal('tunedModels/my-model'); }); + it('calls regular app check token when option is set', async () => { + const getTokenStub = stub().resolves(); + const getLimitedUseTokenStub = stub().resolves(); + const testModel = new TestModel( + //@ts-ignore + { + ...fakeAI, + options: { appCheck: { limitedUseTokens: false } }, + appCheck: { + getToken: getTokenStub, + getLimitedUseToken: getLimitedUseTokenStub + } + } as AIService, + 'models/my-model' + ); + if (testModel._apiSettings?.getAppCheckToken) { + await testModel._apiSettings.getAppCheckToken(); + } + expect(getTokenStub).to.be.called; + expect(getLimitedUseTokenStub).to.not.be.called; + getTokenStub.reset(); + getLimitedUseTokenStub.reset(); + }); + it('calls limited use token when option is set', async () => { + const getTokenStub = stub().resolves(); + const getLimitedUseTokenStub = stub().resolves(); + const testModel = new TestModel( + //@ts-ignore + { + ...fakeAI, + options: { appCheck: { limitedUseTokens: true } }, + appCheck: { + getToken: getTokenStub, + getLimitedUseToken: getLimitedUseTokenStub + } + } as AIService, + 'models/my-model' + ); + if (testModel._apiSettings?.getAppCheckToken) { + await testModel._apiSettings.getAppCheckToken(); + } + expect(getTokenStub).to.not.be.called; + expect(getLimitedUseTokenStub).to.be.called; + getTokenStub.reset(); + getLimitedUseTokenStub.reset(); + }); it('throws if not passed an api key', () => { const fakeAI: AI = { app: { diff --git a/packages/ai/src/models/ai-model.ts b/packages/ai/src/models/ai-model.ts index f22f776aa50..f29bc00419a 100644 --- a/packages/ai/src/models/ai-model.ts +++ b/packages/ai/src/models/ai-model.ts @@ -39,7 +39,7 @@ export abstract class AIModel { /** * @internal */ - protected _apiSettings: ApiSettings; + _apiSettings: ApiSettings; /** * Constructs a new instance of the {@link AIModel} class. diff --git a/packages/ai/src/public-types.ts b/packages/ai/src/public-types.ts index 43b38767d63..298ba0a30cb 100644 --- a/packages/ai/src/public-types.ts +++ b/packages/ai/src/public-types.ts @@ -94,8 +94,9 @@ export type BackendType = (typeof BackendType)[keyof typeof BackendType]; export interface AIOptions { /** * The backend configuration to use for the AI service instance. + * Defaults to {@link GoogleAIBackend}. */ - backend: Backend; + backend?: Backend; /** * Configures App Check usage for this AI service instance. */ @@ -106,5 +107,8 @@ export interface AIOptions { * Configures App Check usage for this AI service instance. */ export interface AppCheckOptions { + /** + * Defaults to false. + */ limitedUseTokens?: boolean; } diff --git a/packages/ai/src/service.ts b/packages/ai/src/service.ts index 78caa21863e..c5c9d9a036d 100644 --- a/packages/ai/src/service.ts +++ b/packages/ai/src/service.ts @@ -31,7 +31,7 @@ import { Backend, VertexAIBackend } from './backend'; export class AIService implements AI, _FirebaseService { auth: FirebaseAuthInternal | null; appCheck: FirebaseAppCheckInternal | null; - _options?: AIOptions; + _options?: Omit; location: string; // This is here for backwards-compatibility constructor( @@ -57,10 +57,10 @@ export class AIService implements AI, _FirebaseService { } set options(optionsToSet: AIOptions) { - this.options = optionsToSet; + this._options = optionsToSet; } - get options(): AIOptions { - return this.options; + get options(): AIOptions | undefined { + return this._options; } } diff --git a/packages/ai/test-utils/get-fake-firebase-services.ts b/packages/ai/test-utils/get-fake-firebase-services.ts new file mode 100644 index 00000000000..a5060be702c --- /dev/null +++ b/packages/ai/test-utils/get-fake-firebase-services.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + FirebaseApp, + initializeApp, + _registerComponent, + _addOrOverwriteComponent +} from '@firebase/app'; +import { Component, ComponentType } from '@firebase/component'; +import { FirebaseAppCheckInternal } from '@firebase/app-check-interop-types'; +import { AI_TYPE } from '../src/constants'; +import { factory } from '../src'; + +const fakeConfig = { + projectId: 'projectId', + authDomain: 'authDomain', + messagingSenderId: 'messagingSenderId', + databaseURL: 'databaseUrl', + storageBucket: 'storageBucket' +}; + +export function getFullApp(fakeAppParams?: { + appId?: string; + apiKey?: string; +}): FirebaseApp { + _registerComponent( + new Component( + AI_TYPE, + factory, + ComponentType.PUBLIC + ) + ); + _registerComponent( + new Component( + 'app-check-internal', + () => { + return {} as FirebaseAppCheckInternal; + }, + ComponentType.PUBLIC + ) + ); + const app = initializeApp({ ...fakeConfig, ...fakeAppParams }); + _addOrOverwriteComponent( + app, + //@ts-ignore + new Component( + 'heartbeat', + // @ts-ignore + () => { + return { + triggerHeartbeat: () => {} + }; + }, + ComponentType.PUBLIC + ) + ); + return app; +} From 72c370b7c37e361a6b73cc1c34da0fb4d1d1ee3d Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Mon, 11 Aug 2025 15:44:53 -0700 Subject: [PATCH 3/5] add a case and test --- packages/ai/src/api.test.ts | 8 ++++++++ packages/ai/src/api.ts | 14 +++++++++++--- .../ai/test-utils/get-fake-firebase-services.ts | 8 +------- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/ai/src/api.test.ts b/packages/ai/src/api.test.ts index 08175a88025..9ef20fa850e 100644 --- a/packages/ai/src/api.test.ts +++ b/packages/ai/src/api.test.ts @@ -57,6 +57,14 @@ describe('Top level API', () => { expect(ai.backend).to.be.instanceOf(VertexAIBackend); expect(ai.options?.appCheck?.limitedUseTokens).to.be.true; }); + it('works with options: appCheck option is empty', () => { + const ai = getAI(getFullApp(), { + backend: new VertexAIBackend('us-central1'), + appCheck: {} + }); + expect(ai.backend).to.be.instanceOf(VertexAIBackend); + expect(ai.options?.appCheck?.limitedUseTokens).to.be.false; + }); it('works with options: backend specified only', () => { const ai = getAI(getFullApp(), { backend: new VertexAIBackend('us-central1') diff --git a/packages/ai/src/api.ts b/packages/ai/src/api.ts index c8191981449..873c3064c0e 100644 --- a/packages/ai/src/api.ts +++ b/packages/ai/src/api.ts @@ -79,9 +79,17 @@ export function getAI(app: FirebaseApp = getApp(), options?: AIOptions): AI { const backend = options?.backend ?? new GoogleAIBackend(); - const finalOptions = { - appCheck: options?.appCheck ?? { limitedUseTokens: false } - }; + const finalOptions: Omit = {}; + + if (options?.appCheck) { + if (options.appCheck.hasOwnProperty('limitedUseTokens')) { + finalOptions.appCheck = options.appCheck; + } else { + finalOptions.appCheck = { ...options.appCheck, limitedUseTokens: false }; + } + } else { + finalOptions.appCheck = { limitedUseTokens: false }; + } const identifier = encodeInstanceIdentifier(backend); const aiInstance = AIProvider.getImmediate({ diff --git a/packages/ai/test-utils/get-fake-firebase-services.ts b/packages/ai/test-utils/get-fake-firebase-services.ts index a5060be702c..b55094590a1 100644 --- a/packages/ai/test-utils/get-fake-firebase-services.ts +++ b/packages/ai/test-utils/get-fake-firebase-services.ts @@ -38,13 +38,7 @@ export function getFullApp(fakeAppParams?: { appId?: string; apiKey?: string; }): FirebaseApp { - _registerComponent( - new Component( - AI_TYPE, - factory, - ComponentType.PUBLIC - ) - ); + _registerComponent(new Component(AI_TYPE, factory, ComponentType.PUBLIC)); _registerComponent( new Component( 'app-check-internal', From 9ed8490877a47eaebe4585554e15716b6f186271 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Tue, 12 Aug 2025 10:57:25 -0700 Subject: [PATCH 4/5] Update docs --- docs-devsite/ai.aioptions.md | 4 ++-- packages/ai/src/public-types.ts | 2 +- packages/ai/test-utils/get-fake-firebase-services.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs-devsite/ai.aioptions.md b/docs-devsite/ai.aioptions.md index 8f61ea4158f..0a82d5481da 100644 --- a/docs-devsite/ai.aioptions.md +++ b/docs-devsite/ai.aioptions.md @@ -23,7 +23,7 @@ export interface AIOptions | Property | Type | Description | | --- | --- | --- | | [appCheck](./ai.aioptions.md#aioptionsappcheck) | [AppCheckOptions](./ai.appcheckoptions.md#appcheckoptions_interface) | Configures App Check usage for this AI service instance. | -| [backend](./ai.aioptions.md#aioptionsbackend) | [Backend](./ai.backend.md#backend_class) | The backend configuration to use for the AI service instance. Defaults to [GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class). | +| [backend](./ai.aioptions.md#aioptionsbackend) | [Backend](./ai.backend.md#backend_class) | The backend configuration to use for the AI service instance. Defaults to the Gemini Developer API backend ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). | ## AIOptions.appCheck @@ -37,7 +37,7 @@ appCheck?: AppCheckOptions; ## AIOptions.backend -The backend configuration to use for the AI service instance. Defaults to [GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class). +The backend configuration to use for the AI service instance. Defaults to the Gemini Developer API backend ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). Signature: diff --git a/packages/ai/src/public-types.ts b/packages/ai/src/public-types.ts index 298ba0a30cb..167f3a4f787 100644 --- a/packages/ai/src/public-types.ts +++ b/packages/ai/src/public-types.ts @@ -94,7 +94,7 @@ export type BackendType = (typeof BackendType)[keyof typeof BackendType]; export interface AIOptions { /** * The backend configuration to use for the AI service instance. - * Defaults to {@link GoogleAIBackend}. + * Defaults to the Gemini Developer API backend ({@link GoogleAIBackend}). */ backend?: Backend; /** diff --git a/packages/ai/test-utils/get-fake-firebase-services.ts b/packages/ai/test-utils/get-fake-firebase-services.ts index b55094590a1..20ae7fb70be 100644 --- a/packages/ai/test-utils/get-fake-firebase-services.ts +++ b/packages/ai/test-utils/get-fake-firebase-services.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 5b3b6e52f1b92694d7e3ab130746679af5281882 Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Fri, 15 Aug 2025 10:20:25 -0700 Subject: [PATCH 5/5] Simplify limitedUseToken option --- common/api-review/ai.api.md | 7 +---- docs-devsite/_toc.yaml | 2 -- docs-devsite/ai.aioptions.md | 14 +++++----- docs-devsite/ai.appcheckoptions.md | 35 ------------------------- docs-devsite/ai.md | 1 - packages/ai/src/api.test.ts | 16 +++++------ packages/ai/src/api.ts | 14 +++------- packages/ai/src/models/ai-model.test.ts | 4 +-- packages/ai/src/models/ai-model.ts | 2 +- packages/ai/src/public-types.ts | 14 ++-------- 10 files changed, 24 insertions(+), 85 deletions(-) delete mode 100644 docs-devsite/ai.appcheckoptions.md diff --git a/common/api-review/ai.api.md b/common/api-review/ai.api.md index afc07053dfa..5f10f453308 100644 --- a/common/api-review/ai.api.md +++ b/common/api-review/ai.api.md @@ -70,8 +70,8 @@ export abstract class AIModel { // @public export interface AIOptions { - appCheck?: AppCheckOptions; backend?: Backend; + useLimitedUseAppCheckTokens?: boolean; } // @public @@ -85,11 +85,6 @@ export class AnyOfSchema extends Schema { toJSON(): SchemaRequest; } -// @public -export interface AppCheckOptions { - limitedUseTokens?: boolean; -} - // @public export class ArraySchema extends Schema { constructor(schemaParams: SchemaParams, items: TypedSchema); diff --git a/docs-devsite/_toc.yaml b/docs-devsite/_toc.yaml index 7160acb998d..da7c2500894 100644 --- a/docs-devsite/_toc.yaml +++ b/docs-devsite/_toc.yaml @@ -14,8 +14,6 @@ toc: path: /docs/reference/js/ai.aioptions.md - title: AnyOfSchema path: /docs/reference/js/ai.anyofschema.md - - title: AppCheckOptions - path: /docs/reference/js/ai.appcheckoptions.md - title: ArraySchema path: /docs/reference/js/ai.arrayschema.md - title: Backend diff --git a/docs-devsite/ai.aioptions.md b/docs-devsite/ai.aioptions.md index 0a82d5481da..a5b326ef004 100644 --- a/docs-devsite/ai.aioptions.md +++ b/docs-devsite/ai.aioptions.md @@ -22,25 +22,25 @@ export interface AIOptions | Property | Type | Description | | --- | --- | --- | -| [appCheck](./ai.aioptions.md#aioptionsappcheck) | [AppCheckOptions](./ai.appcheckoptions.md#appcheckoptions_interface) | Configures App Check usage for this AI service instance. | | [backend](./ai.aioptions.md#aioptionsbackend) | [Backend](./ai.backend.md#backend_class) | The backend configuration to use for the AI service instance. Defaults to the Gemini Developer API backend ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). | +| [useLimitedUseAppCheckTokens](./ai.aioptions.md#aioptionsuselimiteduseappchecktokens) | boolean | Whether to use App Check limited use tokens. Defaults to false. | -## AIOptions.appCheck +## AIOptions.backend -Configures App Check usage for this AI service instance. +The backend configuration to use for the AI service instance. Defaults to the Gemini Developer API backend ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). Signature: ```typescript -appCheck?: AppCheckOptions; +backend?: Backend; ``` -## AIOptions.backend +## AIOptions.useLimitedUseAppCheckTokens -The backend configuration to use for the AI service instance. Defaults to the Gemini Developer API backend ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). +Whether to use App Check limited use tokens. Defaults to false. Signature: ```typescript -backend?: Backend; +useLimitedUseAppCheckTokens?: boolean; ``` diff --git a/docs-devsite/ai.appcheckoptions.md b/docs-devsite/ai.appcheckoptions.md deleted file mode 100644 index 60ef3e2edea..00000000000 --- a/docs-devsite/ai.appcheckoptions.md +++ /dev/null @@ -1,35 +0,0 @@ -Project: /docs/reference/js/_project.yaml -Book: /docs/reference/_book.yaml -page_type: reference - -{% comment %} -DO NOT EDIT THIS FILE! -This is generated by the JS SDK team, and any local changes will be -overwritten. Changes should be made in the source code at -https://github.com/firebase/firebase-js-sdk -{% endcomment %} - -# AppCheckOptions interface -Configures App Check usage for this AI service instance. - -Signature: - -```typescript -export interface AppCheckOptions -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [limitedUseTokens](./ai.appcheckoptions.md#appcheckoptionslimitedusetokens) | boolean | Defaults to false. | - -## AppCheckOptions.limitedUseTokens - -Defaults to false. - -Signature: - -```typescript -limitedUseTokens?: boolean; -``` diff --git a/docs-devsite/ai.md b/docs-devsite/ai.md index 230c1b31efa..3c7669204a1 100644 --- a/docs-devsite/ai.md +++ b/docs-devsite/ai.md @@ -52,7 +52,6 @@ The Firebase AI Web SDK. | --- | --- | | [AI](./ai.ai.md#ai_interface) | An instance of the Firebase AI SDK.Do not create this instance directly. Instead, use [getAI()](./ai.md#getai_a94a413). | | [AIOptions](./ai.aioptions.md#aioptions_interface) | Options for initializing the AI service using [getAI()](./ai.md#getai_a94a413). This allows specifying which backend to use (Vertex AI Gemini API or Gemini Developer API) and configuring its specific options (like location for Vertex AI). | -| [AppCheckOptions](./ai.appcheckoptions.md#appcheckoptions_interface) | Configures App Check usage for this AI service instance. | | [BaseParams](./ai.baseparams.md#baseparams_interface) | Base parameters for a number of methods. | | [ChromeAdapter](./ai.chromeadapter.md#chromeadapter_interface) | (EXPERIMENTAL) Defines an inference "backend" that uses Chrome's on-device model, and encapsulates logic for detecting when on-device inference is possible.These methods should not be called directly by the user. | | [Citation](./ai.citation.md#citation_interface) | A single citation. | diff --git a/packages/ai/src/api.test.ts b/packages/ai/src/api.test.ts index 2e857f1f1ab..55d2eaa4ad3 100644 --- a/packages/ai/src/api.test.ts +++ b/packages/ai/src/api.test.ts @@ -45,32 +45,32 @@ describe('Top level API', () => { expect(ai.backend).to.be.instanceOf(GoogleAIBackend); }); it('works with options: no backend, limited use token', () => { - const ai = getAI(getFullApp(), { appCheck: { limitedUseTokens: true } }); + const ai = getAI(getFullApp(), { useLimitedUseAppCheckTokens: true }); expect(ai.backend).to.be.instanceOf(GoogleAIBackend); - expect(ai.options?.appCheck?.limitedUseTokens).to.be.true; + expect(ai.options?.useLimitedUseAppCheckTokens).to.be.true; }); it('works with options: backend specified, limited use token', () => { const ai = getAI(getFullApp(), { backend: new VertexAIBackend('us-central1'), - appCheck: { limitedUseTokens: true } + useLimitedUseAppCheckTokens: true }); expect(ai.backend).to.be.instanceOf(VertexAIBackend); - expect(ai.options?.appCheck?.limitedUseTokens).to.be.true; + expect(ai.options?.useLimitedUseAppCheckTokens).to.be.true; }); - it('works with options: appCheck option is empty', () => { + it('works with options: appCheck option is falsy', () => { const ai = getAI(getFullApp(), { backend: new VertexAIBackend('us-central1'), - appCheck: {} + useLimitedUseAppCheckTokens: undefined }); expect(ai.backend).to.be.instanceOf(VertexAIBackend); - expect(ai.options?.appCheck?.limitedUseTokens).to.be.false; + expect(ai.options?.useLimitedUseAppCheckTokens).to.be.false; }); it('works with options: backend specified only', () => { const ai = getAI(getFullApp(), { backend: new VertexAIBackend('us-central1') }); expect(ai.backend).to.be.instanceOf(VertexAIBackend); - expect(ai.options?.appCheck?.limitedUseTokens).to.be.false; + expect(ai.options?.useLimitedUseAppCheckTokens).to.be.false; }); }); it('getGenerativeModel throws if no model is provided', () => { diff --git a/packages/ai/src/api.ts b/packages/ai/src/api.ts index 74170f47754..26d2904150d 100644 --- a/packages/ai/src/api.ts +++ b/packages/ai/src/api.ts @@ -82,17 +82,9 @@ export function getAI(app: FirebaseApp = getApp(), options?: AIOptions): AI { const backend = options?.backend ?? new GoogleAIBackend(); - const finalOptions: Omit = {}; - - if (options?.appCheck) { - if (options.appCheck.hasOwnProperty('limitedUseTokens')) { - finalOptions.appCheck = options.appCheck; - } else { - finalOptions.appCheck = { ...options.appCheck, limitedUseTokens: false }; - } - } else { - finalOptions.appCheck = { limitedUseTokens: false }; - } + const finalOptions: Omit = { + useLimitedUseAppCheckTokens: options?.useLimitedUseAppCheckTokens ?? false + }; const identifier = encodeInstanceIdentifier(backend); const aiInstance = AIProvider.getImmediate({ diff --git a/packages/ai/src/models/ai-model.test.ts b/packages/ai/src/models/ai-model.test.ts index f7ab0f8a98c..2e8f8998c58 100644 --- a/packages/ai/src/models/ai-model.test.ts +++ b/packages/ai/src/models/ai-model.test.ts @@ -76,7 +76,7 @@ describe('AIModel', () => { //@ts-ignore { ...fakeAI, - options: { appCheck: { limitedUseTokens: false } }, + options: { useLimitedUseAppCheckTokens: false }, appCheck: { getToken: getTokenStub, getLimitedUseToken: getLimitedUseTokenStub @@ -99,7 +99,7 @@ describe('AIModel', () => { //@ts-ignore { ...fakeAI, - options: { appCheck: { limitedUseTokens: true } }, + options: { useLimitedUseAppCheckTokens: true }, appCheck: { getToken: getTokenStub, getLimitedUseToken: getLimitedUseTokenStub diff --git a/packages/ai/src/models/ai-model.ts b/packages/ai/src/models/ai-model.ts index f29bc00419a..3fe202d5eb2 100644 --- a/packages/ai/src/models/ai-model.ts +++ b/packages/ai/src/models/ai-model.ts @@ -90,7 +90,7 @@ export abstract class AIModel { return Promise.resolve({ token }); }; } else if ((ai as AIService).appCheck) { - if (ai.options?.appCheck?.limitedUseTokens) { + if (ai.options?.useLimitedUseAppCheckTokens) { this._apiSettings.getAppCheckToken = () => (ai as AIService).appCheck!.getLimitedUseToken(); } else { diff --git a/packages/ai/src/public-types.ts b/packages/ai/src/public-types.ts index 167f3a4f787..fff41251a01 100644 --- a/packages/ai/src/public-types.ts +++ b/packages/ai/src/public-types.ts @@ -98,17 +98,7 @@ export interface AIOptions { */ backend?: Backend; /** - * Configures App Check usage for this AI service instance. + * Whether to use App Check limited use tokens. Defaults to false. */ - appCheck?: AppCheckOptions; -} - -/** - * Configures App Check usage for this AI service instance. - */ -export interface AppCheckOptions { - /** - * Defaults to false. - */ - limitedUseTokens?: boolean; + useLimitedUseAppCheckTokens?: boolean; }