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