@firebase/ai 2.1.0-canary.9b63cd60e → 2.1.0-canary.cbef6c6e5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -8,7 +8,7 @@ var util = require('@firebase/util');
8
8
  var logger$1 = require('@firebase/logger');
9
9
 
10
10
  var name = "@firebase/ai";
11
- var version = "2.1.0-canary.9b63cd60e";
11
+ var version = "2.1.0-canary.cbef6c6e5";
12
12
 
13
13
  /**
14
14
  * @license
@@ -640,9 +640,10 @@ class VertexAIBackend extends Backend {
640
640
  * limitations under the License.
641
641
  */
642
642
  class AIService {
643
- constructor(app, backend, authProvider, appCheckProvider) {
643
+ constructor(app, backend, authProvider, appCheckProvider, chromeAdapterFactory) {
644
644
  this.app = app;
645
645
  this.backend = backend;
646
+ this.chromeAdapterFactory = chromeAdapterFactory;
646
647
  const appCheck = appCheckProvider?.getImmediate({ optional: true });
647
648
  const auth = authProvider?.getImmediate({ optional: true });
648
649
  this.auth = auth || null;
@@ -1328,8 +1329,9 @@ async function handlePredictResponse(response) {
1328
1329
  gcsURI: prediction.gcsUri
1329
1330
  });
1330
1331
  }
1332
+ else if (prediction.safetyAttributes) ;
1331
1333
  else {
1332
- throw new AIError(AIErrorCode.RESPONSE_ERROR, `Predictions array in response has missing properties. Response: ${JSON.stringify(responseJson)}`);
1334
+ throw new AIError(AIErrorCode.RESPONSE_ERROR, `Unexpected element in 'predictions' array in response: '${JSON.stringify(prediction)}'`);
1333
1335
  }
1334
1336
  }
1335
1337
  return { images, filteredReason };
@@ -1870,7 +1872,8 @@ function createPredictRequestBody(prompt, { gcsURI, imageFormat, addWatermark, n
1870
1872
  addWatermark,
1871
1873
  safetyFilterLevel,
1872
1874
  personGeneration: personFilterLevel,
1873
- includeRaiReason: true
1875
+ includeRaiReason: true,
1876
+ includeSafetyAttributes: true
1874
1877
  }
1875
1878
  };
1876
1879
  return body;
@@ -2349,20 +2352,9 @@ class ImagenModel extends AIModel {
2349
2352
  }
2350
2353
  }
2351
2354
 
2352
- /**
2353
- * @internal
2354
- */
2355
- var Availability;
2356
- (function (Availability) {
2357
- Availability["UNAVAILABLE"] = "unavailable";
2358
- Availability["DOWNLOADABLE"] = "downloadable";
2359
- Availability["DOWNLOADING"] = "downloading";
2360
- Availability["AVAILABLE"] = "available";
2361
- })(Availability || (Availability = {}));
2362
-
2363
2355
  /**
2364
2356
  * @license
2365
- * Copyright 2025 Google LLC
2357
+ * Copyright 2024 Google LLC
2366
2358
  *
2367
2359
  * Licensed under the Apache License, Version 2.0 (the "License");
2368
2360
  * you may not use this file except in compliance with the License.
@@ -2377,362 +2369,87 @@ var Availability;
2377
2369
  * limitations under the License.
2378
2370
  */
2379
2371
  /**
2380
- * Defines an inference "backend" that uses Chrome's on-device model,
2381
- * and encapsulates logic for detecting when on-device inference is
2382
- * possible.
2372
+ * Parent class encompassing all Schema types, with static methods that
2373
+ * allow building specific Schema types. This class can be converted with
2374
+ * `JSON.stringify()` into a JSON string accepted by Vertex AI REST endpoints.
2375
+ * (This string conversion is automatically done when calling SDK methods.)
2376
+ * @public
2383
2377
  */
2384
- class ChromeAdapterImpl {
2385
- constructor(languageModelProvider, mode, onDeviceParams = {
2386
- createOptions: {
2387
- // Defaults to support image inputs for convenience.
2388
- expectedInputs: [{ type: 'image' }]
2378
+ class Schema {
2379
+ constructor(schemaParams) {
2380
+ // TODO(dlarocque): Enforce this with union types
2381
+ if (!schemaParams.type && !schemaParams.anyOf) {
2382
+ throw new AIError(AIErrorCode.INVALID_SCHEMA, "A schema must have either a 'type' or an 'anyOf' array of sub-schemas.");
2389
2383
  }
2390
- }) {
2391
- this.languageModelProvider = languageModelProvider;
2392
- this.mode = mode;
2393
- this.onDeviceParams = onDeviceParams;
2394
- this.isDownloading = false;
2384
+ // eslint-disable-next-line guard-for-in
2385
+ for (const paramKey in schemaParams) {
2386
+ this[paramKey] = schemaParams[paramKey];
2387
+ }
2388
+ // Ensure these are explicitly set to avoid TS errors.
2389
+ this.type = schemaParams.type;
2390
+ this.format = schemaParams.hasOwnProperty('format')
2391
+ ? schemaParams.format
2392
+ : undefined;
2393
+ this.nullable = schemaParams.hasOwnProperty('nullable')
2394
+ ? !!schemaParams.nullable
2395
+ : false;
2395
2396
  }
2396
2397
  /**
2397
- * Checks if a given request can be made on-device.
2398
- *
2399
- * Encapsulates a few concerns:
2400
- * the mode
2401
- * API existence
2402
- * prompt formatting
2403
- * model availability, including triggering download if necessary
2404
- *
2405
- *
2406
- * Pros: callers needn't be concerned with details of on-device availability.</p>
2407
- * Cons: this method spans a few concerns and splits request validation from usage.
2408
- * If instance variables weren't already part of the API, we could consider a better
2409
- * separation of concerns.
2398
+ * Defines how this Schema should be serialized as JSON.
2399
+ * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior
2400
+ * @internal
2410
2401
  */
2411
- async isAvailable(request) {
2412
- if (!this.mode) {
2413
- logger.debug(`On-device inference unavailable because mode is undefined.`);
2414
- return false;
2415
- }
2416
- if (this.mode === InferenceMode.ONLY_IN_CLOUD) {
2417
- logger.debug(`On-device inference unavailable because mode is "only_in_cloud".`);
2418
- return false;
2419
- }
2420
- // Triggers out-of-band download so model will eventually become available.
2421
- const availability = await this.downloadIfAvailable();
2422
- if (this.mode === InferenceMode.ONLY_ON_DEVICE) {
2423
- // If it will never be available due to API inavailability, throw.
2424
- if (availability === Availability.UNAVAILABLE) {
2425
- throw new AIError(AIErrorCode.API_NOT_ENABLED, 'Local LanguageModel API not available in this environment.');
2426
- }
2427
- else if (availability === Availability.DOWNLOADABLE ||
2428
- availability === Availability.DOWNLOADING) {
2429
- // TODO(chholland): Better user experience during download - progress?
2430
- logger.debug(`Waiting for download of LanguageModel to complete.`);
2431
- await this.downloadPromise;
2432
- return true;
2402
+ toJSON() {
2403
+ const obj = {
2404
+ type: this.type
2405
+ };
2406
+ for (const prop in this) {
2407
+ if (this.hasOwnProperty(prop) && this[prop] !== undefined) {
2408
+ if (prop !== 'required' || this.type === SchemaType.OBJECT) {
2409
+ obj[prop] = this[prop];
2410
+ }
2433
2411
  }
2434
- return true;
2435
2412
  }
2436
- // Applies prefer_on_device logic.
2437
- if (availability !== Availability.AVAILABLE) {
2438
- logger.debug(`On-device inference unavailable because availability is "${availability}".`);
2439
- return false;
2440
- }
2441
- if (!ChromeAdapterImpl.isOnDeviceRequest(request)) {
2442
- logger.debug(`On-device inference unavailable because request is incompatible.`);
2443
- return false;
2444
- }
2445
- return true;
2413
+ return obj;
2446
2414
  }
2447
- /**
2448
- * Generates content on device.
2449
- *
2450
- * @remarks
2451
- * This is comparable to {@link GenerativeModel.generateContent} for generating content in
2452
- * Cloud.
2453
- * @param request - a standard Firebase AI {@link GenerateContentRequest}
2454
- * @returns {@link Response}, so we can reuse common response formatting.
2455
- */
2456
- async generateContent(request) {
2457
- const session = await this.createSession();
2458
- const contents = await Promise.all(request.contents.map(ChromeAdapterImpl.toLanguageModelMessage));
2459
- const text = await session.prompt(contents, this.onDeviceParams.promptOptions);
2460
- return ChromeAdapterImpl.toResponse(text);
2415
+ static array(arrayParams) {
2416
+ return new ArraySchema(arrayParams, arrayParams.items);
2461
2417
  }
2462
- /**
2463
- * Generates content stream on device.
2464
- *
2465
- * @remarks
2466
- * This is comparable to {@link GenerativeModel.generateContentStream} for generating content in
2467
- * Cloud.
2468
- * @param request - a standard Firebase AI {@link GenerateContentRequest}
2469
- * @returns {@link Response}, so we can reuse common response formatting.
2470
- */
2471
- async generateContentStream(request) {
2472
- const session = await this.createSession();
2473
- const contents = await Promise.all(request.contents.map(ChromeAdapterImpl.toLanguageModelMessage));
2474
- const stream = session.promptStreaming(contents, this.onDeviceParams.promptOptions);
2475
- return ChromeAdapterImpl.toStreamResponse(stream);
2418
+ static object(objectParams) {
2419
+ return new ObjectSchema(objectParams, objectParams.properties, objectParams.optionalProperties);
2476
2420
  }
2477
- async countTokens(_request) {
2478
- throw new AIError(AIErrorCode.REQUEST_ERROR, 'Count Tokens is not yet available for on-device model.');
2421
+ // eslint-disable-next-line id-blacklist
2422
+ static string(stringParams) {
2423
+ return new StringSchema(stringParams);
2479
2424
  }
2480
- /**
2481
- * Asserts inference for the given request can be performed by an on-device model.
2482
- */
2483
- static isOnDeviceRequest(request) {
2484
- // Returns false if the prompt is empty.
2485
- if (request.contents.length === 0) {
2486
- logger.debug('Empty prompt rejected for on-device inference.');
2487
- return false;
2488
- }
2489
- for (const content of request.contents) {
2490
- if (content.role === 'function') {
2491
- logger.debug(`"Function" role rejected for on-device inference.`);
2492
- return false;
2493
- }
2494
- // Returns false if request contains an image with an unsupported mime type.
2495
- for (const part of content.parts) {
2496
- if (part.inlineData &&
2497
- ChromeAdapterImpl.SUPPORTED_MIME_TYPES.indexOf(part.inlineData.mimeType) === -1) {
2498
- logger.debug(`Unsupported mime type "${part.inlineData.mimeType}" rejected for on-device inference.`);
2499
- return false;
2500
- }
2501
- }
2502
- }
2503
- return true;
2425
+ static enumString(stringParams) {
2426
+ return new StringSchema(stringParams, stringParams.enum);
2504
2427
  }
2505
- /**
2506
- * Encapsulates logic to get availability and download a model if one is downloadable.
2507
- */
2508
- async downloadIfAvailable() {
2509
- const availability = await this.languageModelProvider?.availability(this.onDeviceParams.createOptions);
2510
- if (availability === Availability.DOWNLOADABLE) {
2511
- this.download();
2512
- }
2513
- return availability;
2428
+ static integer(integerParams) {
2429
+ return new IntegerSchema(integerParams);
2514
2430
  }
2515
- /**
2516
- * Triggers out-of-band download of an on-device model.
2517
- *
2518
- * Chrome only downloads models as needed. Chrome knows a model is needed when code calls
2519
- * LanguageModel.create.
2520
- *
2521
- * Since Chrome manages the download, the SDK can only avoid redundant download requests by
2522
- * tracking if a download has previously been requested.
2523
- */
2524
- download() {
2525
- if (this.isDownloading) {
2526
- return;
2527
- }
2528
- this.isDownloading = true;
2529
- this.downloadPromise = this.languageModelProvider
2530
- ?.create(this.onDeviceParams.createOptions)
2531
- .finally(() => {
2532
- this.isDownloading = false;
2533
- });
2431
+ // eslint-disable-next-line id-blacklist
2432
+ static number(numberParams) {
2433
+ return new NumberSchema(numberParams);
2534
2434
  }
2535
- /**
2536
- * Converts Firebase AI {@link Content} object to a Chrome {@link LanguageModelMessage} object.
2537
- */
2538
- static async toLanguageModelMessage(content) {
2539
- const languageModelMessageContents = await Promise.all(content.parts.map(ChromeAdapterImpl.toLanguageModelMessageContent));
2540
- return {
2541
- role: ChromeAdapterImpl.toLanguageModelMessageRole(content.role),
2542
- content: languageModelMessageContents
2543
- };
2435
+ // eslint-disable-next-line id-blacklist
2436
+ static boolean(booleanParams) {
2437
+ return new BooleanSchema(booleanParams);
2544
2438
  }
2545
- /**
2546
- * Converts a Firebase AI Part object to a Chrome LanguageModelMessageContent object.
2547
- */
2548
- static async toLanguageModelMessageContent(part) {
2549
- if (part.text) {
2550
- return {
2551
- type: 'text',
2552
- value: part.text
2553
- };
2554
- }
2555
- else if (part.inlineData) {
2556
- const formattedImageContent = await fetch(`data:${part.inlineData.mimeType};base64,${part.inlineData.data}`);
2557
- const imageBlob = await formattedImageContent.blob();
2558
- const imageBitmap = await createImageBitmap(imageBlob);
2559
- return {
2560
- type: 'image',
2561
- value: imageBitmap
2562
- };
2563
- }
2564
- throw new AIError(AIErrorCode.REQUEST_ERROR, `Processing of this Part type is not currently supported.`);
2439
+ static anyOf(anyOfParams) {
2440
+ return new AnyOfSchema(anyOfParams);
2565
2441
  }
2566
- /**
2567
- * Converts a Firebase AI {@link Role} string to a {@link LanguageModelMessageRole} string.
2568
- */
2569
- static toLanguageModelMessageRole(role) {
2570
- // Assumes 'function' rule has been filtered by isOnDeviceRequest
2571
- return role === 'model' ? 'assistant' : 'user';
2572
- }
2573
- /**
2574
- * Abstracts Chrome session creation.
2575
- *
2576
- * Chrome uses a multi-turn session for all inference. Firebase AI uses single-turn for all
2577
- * inference. To map the Firebase AI API to Chrome's API, the SDK creates a new session for all
2578
- * inference.
2579
- *
2580
- * Chrome will remove a model from memory if it's no longer in use, so this method ensures a
2581
- * new session is created before an old session is destroyed.
2582
- */
2583
- async createSession() {
2584
- if (!this.languageModelProvider) {
2585
- throw new AIError(AIErrorCode.UNSUPPORTED, 'Chrome AI requested for unsupported browser version.');
2586
- }
2587
- const newSession = await this.languageModelProvider.create(this.onDeviceParams.createOptions);
2588
- if (this.oldSession) {
2589
- this.oldSession.destroy();
2590
- }
2591
- // Holds session reference, so model isn't unloaded from memory.
2592
- this.oldSession = newSession;
2593
- return newSession;
2594
- }
2595
- /**
2596
- * Formats string returned by Chrome as a {@link Response} returned by Firebase AI.
2597
- */
2598
- static toResponse(text) {
2599
- return {
2600
- json: async () => ({
2601
- candidates: [
2602
- {
2603
- content: {
2604
- parts: [{ text }]
2605
- }
2606
- }
2607
- ]
2608
- })
2609
- };
2610
- }
2611
- /**
2612
- * Formats string stream returned by Chrome as SSE returned by Firebase AI.
2613
- */
2614
- static toStreamResponse(stream) {
2615
- const encoder = new TextEncoder();
2616
- return {
2617
- body: stream.pipeThrough(new TransformStream({
2618
- transform(chunk, controller) {
2619
- const json = JSON.stringify({
2620
- candidates: [
2621
- {
2622
- content: {
2623
- role: 'model',
2624
- parts: [{ text: chunk }]
2625
- }
2626
- }
2627
- ]
2628
- });
2629
- controller.enqueue(encoder.encode(`data: ${json}\n\n`));
2630
- }
2631
- }))
2632
- };
2633
- }
2634
- }
2635
- // Visible for testing
2636
- ChromeAdapterImpl.SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png'];
2637
-
2638
- /**
2639
- * @license
2640
- * Copyright 2024 Google LLC
2641
- *
2642
- * Licensed under the Apache License, Version 2.0 (the "License");
2643
- * you may not use this file except in compliance with the License.
2644
- * You may obtain a copy of the License at
2645
- *
2646
- * http://www.apache.org/licenses/LICENSE-2.0
2647
- *
2648
- * Unless required by applicable law or agreed to in writing, software
2649
- * distributed under the License is distributed on an "AS IS" BASIS,
2650
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2651
- * See the License for the specific language governing permissions and
2652
- * limitations under the License.
2653
- */
2654
- /**
2655
- * Parent class encompassing all Schema types, with static methods that
2656
- * allow building specific Schema types. This class can be converted with
2657
- * `JSON.stringify()` into a JSON string accepted by Vertex AI REST endpoints.
2658
- * (This string conversion is automatically done when calling SDK methods.)
2659
- * @public
2660
- */
2661
- class Schema {
2662
- constructor(schemaParams) {
2663
- // TODO(dlarocque): Enforce this with union types
2664
- if (!schemaParams.type && !schemaParams.anyOf) {
2665
- throw new AIError(AIErrorCode.INVALID_SCHEMA, "A schema must have either a 'type' or an 'anyOf' array of sub-schemas.");
2666
- }
2667
- // eslint-disable-next-line guard-for-in
2668
- for (const paramKey in schemaParams) {
2669
- this[paramKey] = schemaParams[paramKey];
2670
- }
2671
- // Ensure these are explicitly set to avoid TS errors.
2672
- this.type = schemaParams.type;
2673
- this.format = schemaParams.hasOwnProperty('format')
2674
- ? schemaParams.format
2675
- : undefined;
2676
- this.nullable = schemaParams.hasOwnProperty('nullable')
2677
- ? !!schemaParams.nullable
2678
- : false;
2679
- }
2680
- /**
2681
- * Defines how this Schema should be serialized as JSON.
2682
- * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior
2683
- * @internal
2684
- */
2685
- toJSON() {
2686
- const obj = {
2687
- type: this.type
2688
- };
2689
- for (const prop in this) {
2690
- if (this.hasOwnProperty(prop) && this[prop] !== undefined) {
2691
- if (prop !== 'required' || this.type === SchemaType.OBJECT) {
2692
- obj[prop] = this[prop];
2693
- }
2694
- }
2695
- }
2696
- return obj;
2697
- }
2698
- static array(arrayParams) {
2699
- return new ArraySchema(arrayParams, arrayParams.items);
2700
- }
2701
- static object(objectParams) {
2702
- return new ObjectSchema(objectParams, objectParams.properties, objectParams.optionalProperties);
2703
- }
2704
- // eslint-disable-next-line id-blacklist
2705
- static string(stringParams) {
2706
- return new StringSchema(stringParams);
2707
- }
2708
- static enumString(stringParams) {
2709
- return new StringSchema(stringParams, stringParams.enum);
2710
- }
2711
- static integer(integerParams) {
2712
- return new IntegerSchema(integerParams);
2713
- }
2714
- // eslint-disable-next-line id-blacklist
2715
- static number(numberParams) {
2716
- return new NumberSchema(numberParams);
2717
- }
2718
- // eslint-disable-next-line id-blacklist
2719
- static boolean(booleanParams) {
2720
- return new BooleanSchema(booleanParams);
2721
- }
2722
- static anyOf(anyOfParams) {
2723
- return new AnyOfSchema(anyOfParams);
2724
- }
2725
- }
2726
- /**
2727
- * Schema class for "integer" types.
2728
- * @public
2729
- */
2730
- class IntegerSchema extends Schema {
2731
- constructor(schemaParams) {
2732
- super({
2733
- type: SchemaType.INTEGER,
2734
- ...schemaParams
2735
- });
2442
+ }
2443
+ /**
2444
+ * Schema class for "integer" types.
2445
+ * @public
2446
+ */
2447
+ class IntegerSchema extends Schema {
2448
+ constructor(schemaParams) {
2449
+ super({
2450
+ type: SchemaType.INTEGER,
2451
+ ...schemaParams
2452
+ });
2736
2453
  }
2737
2454
  }
2738
2455
  /**
@@ -3022,11 +2739,11 @@ function getGenerativeModel(ai, modelParams, requestOptions) {
3022
2739
  if (!inCloudParams.model) {
3023
2740
  throw new AIError(AIErrorCode.NO_MODEL, `Must provide a model name. Example: getGenerativeModel({ model: 'my-model-name' })`);
3024
2741
  }
3025
- let chromeAdapter;
3026
- // Do not initialize a ChromeAdapter if we are not in hybrid mode.
3027
- if (typeof window !== 'undefined' && hybridParams.mode) {
3028
- chromeAdapter = new ChromeAdapterImpl(window.LanguageModel, hybridParams.mode, hybridParams.onDeviceParams);
3029
- }
2742
+ /**
2743
+ * An AIService registered by index.node.ts will not have a
2744
+ * chromeAdapterFactory() method.
2745
+ */
2746
+ const chromeAdapter = ai.chromeAdapterFactory?.(hybridParams.mode, typeof window === 'undefined' ? undefined : window, hybridParams.onDeviceParams);
3030
2747
  return new GenerativeModel(ai, inCloudParams, requestOptions, chromeAdapter);
3031
2748
  }
3032
2749
  /**
@@ -3050,6 +2767,301 @@ function getImagenModel(ai, modelParams, requestOptions) {
3050
2767
  return new ImagenModel(ai, modelParams, requestOptions);
3051
2768
  }
3052
2769
 
2770
+ /**
2771
+ * @internal
2772
+ */
2773
+ var Availability;
2774
+ (function (Availability) {
2775
+ Availability["UNAVAILABLE"] = "unavailable";
2776
+ Availability["DOWNLOADABLE"] = "downloadable";
2777
+ Availability["DOWNLOADING"] = "downloading";
2778
+ Availability["AVAILABLE"] = "available";
2779
+ })(Availability || (Availability = {}));
2780
+
2781
+ /**
2782
+ * @license
2783
+ * Copyright 2025 Google LLC
2784
+ *
2785
+ * Licensed under the Apache License, Version 2.0 (the "License");
2786
+ * you may not use this file except in compliance with the License.
2787
+ * You may obtain a copy of the License at
2788
+ *
2789
+ * http://www.apache.org/licenses/LICENSE-2.0
2790
+ *
2791
+ * Unless required by applicable law or agreed to in writing, software
2792
+ * distributed under the License is distributed on an "AS IS" BASIS,
2793
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2794
+ * See the License for the specific language governing permissions and
2795
+ * limitations under the License.
2796
+ */
2797
+ /**
2798
+ * Defines an inference "backend" that uses Chrome's on-device model,
2799
+ * and encapsulates logic for detecting when on-device inference is
2800
+ * possible.
2801
+ */
2802
+ class ChromeAdapterImpl {
2803
+ constructor(languageModelProvider, mode, onDeviceParams = {
2804
+ createOptions: {
2805
+ // Defaults to support image inputs for convenience.
2806
+ expectedInputs: [{ type: 'image' }]
2807
+ }
2808
+ }) {
2809
+ this.languageModelProvider = languageModelProvider;
2810
+ this.mode = mode;
2811
+ this.onDeviceParams = onDeviceParams;
2812
+ this.isDownloading = false;
2813
+ }
2814
+ /**
2815
+ * Checks if a given request can be made on-device.
2816
+ *
2817
+ * Encapsulates a few concerns:
2818
+ * the mode
2819
+ * API existence
2820
+ * prompt formatting
2821
+ * model availability, including triggering download if necessary
2822
+ *
2823
+ *
2824
+ * Pros: callers needn't be concerned with details of on-device availability.</p>
2825
+ * Cons: this method spans a few concerns and splits request validation from usage.
2826
+ * If instance variables weren't already part of the API, we could consider a better
2827
+ * separation of concerns.
2828
+ */
2829
+ async isAvailable(request) {
2830
+ if (!this.mode) {
2831
+ logger.debug(`On-device inference unavailable because mode is undefined.`);
2832
+ return false;
2833
+ }
2834
+ if (this.mode === InferenceMode.ONLY_IN_CLOUD) {
2835
+ logger.debug(`On-device inference unavailable because mode is "only_in_cloud".`);
2836
+ return false;
2837
+ }
2838
+ // Triggers out-of-band download so model will eventually become available.
2839
+ const availability = await this.downloadIfAvailable();
2840
+ if (this.mode === InferenceMode.ONLY_ON_DEVICE) {
2841
+ // If it will never be available due to API inavailability, throw.
2842
+ if (availability === Availability.UNAVAILABLE) {
2843
+ throw new AIError(AIErrorCode.API_NOT_ENABLED, 'Local LanguageModel API not available in this environment.');
2844
+ }
2845
+ else if (availability === Availability.DOWNLOADABLE ||
2846
+ availability === Availability.DOWNLOADING) {
2847
+ // TODO(chholland): Better user experience during download - progress?
2848
+ logger.debug(`Waiting for download of LanguageModel to complete.`);
2849
+ await this.downloadPromise;
2850
+ return true;
2851
+ }
2852
+ return true;
2853
+ }
2854
+ // Applies prefer_on_device logic.
2855
+ if (availability !== Availability.AVAILABLE) {
2856
+ logger.debug(`On-device inference unavailable because availability is "${availability}".`);
2857
+ return false;
2858
+ }
2859
+ if (!ChromeAdapterImpl.isOnDeviceRequest(request)) {
2860
+ logger.debug(`On-device inference unavailable because request is incompatible.`);
2861
+ return false;
2862
+ }
2863
+ return true;
2864
+ }
2865
+ /**
2866
+ * Generates content on device.
2867
+ *
2868
+ * @remarks
2869
+ * This is comparable to {@link GenerativeModel.generateContent} for generating content in
2870
+ * Cloud.
2871
+ * @param request - a standard Firebase AI {@link GenerateContentRequest}
2872
+ * @returns {@link Response}, so we can reuse common response formatting.
2873
+ */
2874
+ async generateContent(request) {
2875
+ const session = await this.createSession();
2876
+ const contents = await Promise.all(request.contents.map(ChromeAdapterImpl.toLanguageModelMessage));
2877
+ const text = await session.prompt(contents, this.onDeviceParams.promptOptions);
2878
+ return ChromeAdapterImpl.toResponse(text);
2879
+ }
2880
+ /**
2881
+ * Generates content stream on device.
2882
+ *
2883
+ * @remarks
2884
+ * This is comparable to {@link GenerativeModel.generateContentStream} for generating content in
2885
+ * Cloud.
2886
+ * @param request - a standard Firebase AI {@link GenerateContentRequest}
2887
+ * @returns {@link Response}, so we can reuse common response formatting.
2888
+ */
2889
+ async generateContentStream(request) {
2890
+ const session = await this.createSession();
2891
+ const contents = await Promise.all(request.contents.map(ChromeAdapterImpl.toLanguageModelMessage));
2892
+ const stream = session.promptStreaming(contents, this.onDeviceParams.promptOptions);
2893
+ return ChromeAdapterImpl.toStreamResponse(stream);
2894
+ }
2895
+ async countTokens(_request) {
2896
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'Count Tokens is not yet available for on-device model.');
2897
+ }
2898
+ /**
2899
+ * Asserts inference for the given request can be performed by an on-device model.
2900
+ */
2901
+ static isOnDeviceRequest(request) {
2902
+ // Returns false if the prompt is empty.
2903
+ if (request.contents.length === 0) {
2904
+ logger.debug('Empty prompt rejected for on-device inference.');
2905
+ return false;
2906
+ }
2907
+ for (const content of request.contents) {
2908
+ if (content.role === 'function') {
2909
+ logger.debug(`"Function" role rejected for on-device inference.`);
2910
+ return false;
2911
+ }
2912
+ // Returns false if request contains an image with an unsupported mime type.
2913
+ for (const part of content.parts) {
2914
+ if (part.inlineData &&
2915
+ ChromeAdapterImpl.SUPPORTED_MIME_TYPES.indexOf(part.inlineData.mimeType) === -1) {
2916
+ logger.debug(`Unsupported mime type "${part.inlineData.mimeType}" rejected for on-device inference.`);
2917
+ return false;
2918
+ }
2919
+ }
2920
+ }
2921
+ return true;
2922
+ }
2923
+ /**
2924
+ * Encapsulates logic to get availability and download a model if one is downloadable.
2925
+ */
2926
+ async downloadIfAvailable() {
2927
+ const availability = await this.languageModelProvider?.availability(this.onDeviceParams.createOptions);
2928
+ if (availability === Availability.DOWNLOADABLE) {
2929
+ this.download();
2930
+ }
2931
+ return availability;
2932
+ }
2933
+ /**
2934
+ * Triggers out-of-band download of an on-device model.
2935
+ *
2936
+ * Chrome only downloads models as needed. Chrome knows a model is needed when code calls
2937
+ * LanguageModel.create.
2938
+ *
2939
+ * Since Chrome manages the download, the SDK can only avoid redundant download requests by
2940
+ * tracking if a download has previously been requested.
2941
+ */
2942
+ download() {
2943
+ if (this.isDownloading) {
2944
+ return;
2945
+ }
2946
+ this.isDownloading = true;
2947
+ this.downloadPromise = this.languageModelProvider
2948
+ ?.create(this.onDeviceParams.createOptions)
2949
+ .finally(() => {
2950
+ this.isDownloading = false;
2951
+ });
2952
+ }
2953
+ /**
2954
+ * Converts Firebase AI {@link Content} object to a Chrome {@link LanguageModelMessage} object.
2955
+ */
2956
+ static async toLanguageModelMessage(content) {
2957
+ const languageModelMessageContents = await Promise.all(content.parts.map(ChromeAdapterImpl.toLanguageModelMessageContent));
2958
+ return {
2959
+ role: ChromeAdapterImpl.toLanguageModelMessageRole(content.role),
2960
+ content: languageModelMessageContents
2961
+ };
2962
+ }
2963
+ /**
2964
+ * Converts a Firebase AI Part object to a Chrome LanguageModelMessageContent object.
2965
+ */
2966
+ static async toLanguageModelMessageContent(part) {
2967
+ if (part.text) {
2968
+ return {
2969
+ type: 'text',
2970
+ value: part.text
2971
+ };
2972
+ }
2973
+ else if (part.inlineData) {
2974
+ const formattedImageContent = await fetch(`data:${part.inlineData.mimeType};base64,${part.inlineData.data}`);
2975
+ const imageBlob = await formattedImageContent.blob();
2976
+ const imageBitmap = await createImageBitmap(imageBlob);
2977
+ return {
2978
+ type: 'image',
2979
+ value: imageBitmap
2980
+ };
2981
+ }
2982
+ throw new AIError(AIErrorCode.REQUEST_ERROR, `Processing of this Part type is not currently supported.`);
2983
+ }
2984
+ /**
2985
+ * Converts a Firebase AI {@link Role} string to a {@link LanguageModelMessageRole} string.
2986
+ */
2987
+ static toLanguageModelMessageRole(role) {
2988
+ // Assumes 'function' rule has been filtered by isOnDeviceRequest
2989
+ return role === 'model' ? 'assistant' : 'user';
2990
+ }
2991
+ /**
2992
+ * Abstracts Chrome session creation.
2993
+ *
2994
+ * Chrome uses a multi-turn session for all inference. Firebase AI uses single-turn for all
2995
+ * inference. To map the Firebase AI API to Chrome's API, the SDK creates a new session for all
2996
+ * inference.
2997
+ *
2998
+ * Chrome will remove a model from memory if it's no longer in use, so this method ensures a
2999
+ * new session is created before an old session is destroyed.
3000
+ */
3001
+ async createSession() {
3002
+ if (!this.languageModelProvider) {
3003
+ throw new AIError(AIErrorCode.UNSUPPORTED, 'Chrome AI requested for unsupported browser version.');
3004
+ }
3005
+ const newSession = await this.languageModelProvider.create(this.onDeviceParams.createOptions);
3006
+ if (this.oldSession) {
3007
+ this.oldSession.destroy();
3008
+ }
3009
+ // Holds session reference, so model isn't unloaded from memory.
3010
+ this.oldSession = newSession;
3011
+ return newSession;
3012
+ }
3013
+ /**
3014
+ * Formats string returned by Chrome as a {@link Response} returned by Firebase AI.
3015
+ */
3016
+ static toResponse(text) {
3017
+ return {
3018
+ json: async () => ({
3019
+ candidates: [
3020
+ {
3021
+ content: {
3022
+ parts: [{ text }]
3023
+ }
3024
+ }
3025
+ ]
3026
+ })
3027
+ };
3028
+ }
3029
+ /**
3030
+ * Formats string stream returned by Chrome as SSE returned by Firebase AI.
3031
+ */
3032
+ static toStreamResponse(stream) {
3033
+ const encoder = new TextEncoder();
3034
+ return {
3035
+ body: stream.pipeThrough(new TransformStream({
3036
+ transform(chunk, controller) {
3037
+ const json = JSON.stringify({
3038
+ candidates: [
3039
+ {
3040
+ content: {
3041
+ role: 'model',
3042
+ parts: [{ text: chunk }]
3043
+ }
3044
+ }
3045
+ ]
3046
+ });
3047
+ controller.enqueue(encoder.encode(`data: ${json}\n\n`));
3048
+ }
3049
+ }))
3050
+ };
3051
+ }
3052
+ }
3053
+ // Visible for testing
3054
+ ChromeAdapterImpl.SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png'];
3055
+ /**
3056
+ * Creates a ChromeAdapterImpl on demand.
3057
+ */
3058
+ function chromeAdapterFactory(mode, window, params) {
3059
+ // Do not initialize a ChromeAdapter if we are not in hybrid mode.
3060
+ if (typeof window !== 'undefined' && mode) {
3061
+ return new ChromeAdapterImpl(window.LanguageModel, mode, params);
3062
+ }
3063
+ }
3064
+
3053
3065
  /**
3054
3066
  * The Firebase AI Web SDK.
3055
3067
  *
@@ -3064,7 +3076,7 @@ function factory(container, { instanceIdentifier }) {
3064
3076
  const app = container.getProvider('app').getImmediate();
3065
3077
  const auth = container.getProvider('auth-internal');
3066
3078
  const appCheckProvider = container.getProvider('app-check-internal');
3067
- return new AIService(app, backend, auth, appCheckProvider);
3079
+ return new AIService(app, backend, auth, appCheckProvider, chromeAdapterFactory);
3068
3080
  }
3069
3081
  function registerAI() {
3070
3082
  app._registerComponent(new component.Component(AI_TYPE, factory, "PUBLIC" /* ComponentType.PUBLIC */).setMultipleInstances(true));