@firebase/ai 2.4.0-canary.44d9891f9 → 2.4.0-canary.6e0e30317

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, Deferred, getModularInstance } from '@firebase/util';
4
4
  import { Logger } from '@firebase/logger';
5
5
 
6
6
  var name = "@firebase/ai";
7
- var version = "2.4.0-canary.44d9891f9";
7
+ var version = "2.4.0-canary.6e0e30317";
8
8
 
9
9
  /**
10
10
  * @license
@@ -34,6 +34,62 @@ const DEFAULT_FETCH_TIMEOUT_MS = 180 * 1000;
34
34
  */
35
35
  const DEFAULT_HYBRID_IN_CLOUD_MODEL = 'gemini-2.0-flash-lite';
36
36
 
37
+ /**
38
+ * @license
39
+ * Copyright 2024 Google LLC
40
+ *
41
+ * Licensed under the Apache License, Version 2.0 (the "License");
42
+ * you may not use this file except in compliance with the License.
43
+ * You may obtain a copy of the License at
44
+ *
45
+ * http://www.apache.org/licenses/LICENSE-2.0
46
+ *
47
+ * Unless required by applicable law or agreed to in writing, software
48
+ * distributed under the License is distributed on an "AS IS" BASIS,
49
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
50
+ * See the License for the specific language governing permissions and
51
+ * limitations under the License.
52
+ */
53
+ /**
54
+ * Error class for the Firebase AI SDK.
55
+ *
56
+ * @public
57
+ */
58
+ class AIError extends FirebaseError {
59
+ /**
60
+ * Constructs a new instance of the `AIError` class.
61
+ *
62
+ * @param code - The error code from {@link (AIErrorCode:type)}.
63
+ * @param message - A human-readable message describing the error.
64
+ * @param customErrorData - Optional error data.
65
+ */
66
+ constructor(code, message, customErrorData) {
67
+ // Match error format used by FirebaseError from ErrorFactory
68
+ const service = AI_TYPE;
69
+ const fullCode = `${service}/${code}`;
70
+ const fullMessage = `${service}: ${message} (${fullCode})`;
71
+ super(code, fullMessage);
72
+ this.code = code;
73
+ this.customErrorData = customErrorData;
74
+ // FirebaseError initializes a stack trace, but it assumes the error is created from the error
75
+ // factory. Since we break this assumption, we set the stack trace to be originating from this
76
+ // constructor.
77
+ // This is only supported in V8.
78
+ if (Error.captureStackTrace) {
79
+ // Allows us to initialize the stack trace without including the constructor itself at the
80
+ // top level of the stack trace.
81
+ Error.captureStackTrace(this, AIError);
82
+ }
83
+ // Allows instanceof AIError in ES5/ES6
84
+ // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
85
+ // TODO(dlarocque): Replace this with `new.target`: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
86
+ // which we can now use since we no longer target ES5.
87
+ Object.setPrototypeOf(this, AIError.prototype);
88
+ // Since Error is an interface, we don't inherit toString and so we define it ourselves.
89
+ this.toString = () => fullMessage;
90
+ }
91
+ }
92
+
37
93
  /**
38
94
  * @license
39
95
  * Copyright 2024 Google LLC
@@ -738,6 +794,64 @@ class VertexAIBackend extends Backend {
738
794
  }
739
795
  }
740
796
 
797
+ /**
798
+ * @license
799
+ * Copyright 2025 Google LLC
800
+ *
801
+ * Licensed under the Apache License, Version 2.0 (the "License");
802
+ * you may not use this file except in compliance with the License.
803
+ * You may obtain a copy of the License at
804
+ *
805
+ * http://www.apache.org/licenses/LICENSE-2.0
806
+ *
807
+ * Unless required by applicable law or agreed to in writing, software
808
+ * distributed under the License is distributed on an "AS IS" BASIS,
809
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
810
+ * See the License for the specific language governing permissions and
811
+ * limitations under the License.
812
+ */
813
+ /**
814
+ * Encodes a {@link Backend} into a string that will be used to uniquely identify {@link AI}
815
+ * instances by backend type.
816
+ *
817
+ * @internal
818
+ */
819
+ function encodeInstanceIdentifier(backend) {
820
+ if (backend instanceof GoogleAIBackend) {
821
+ return `${AI_TYPE}/googleai`;
822
+ }
823
+ else if (backend instanceof VertexAIBackend) {
824
+ return `${AI_TYPE}/vertexai/${backend.location}`;
825
+ }
826
+ else {
827
+ throw new AIError(AIErrorCode.ERROR, `Invalid backend: ${JSON.stringify(backend.backendType)}`);
828
+ }
829
+ }
830
+ /**
831
+ * Decodes an instance identifier string into a {@link Backend}.
832
+ *
833
+ * @internal
834
+ */
835
+ function decodeInstanceIdentifier(instanceIdentifier) {
836
+ const identifierParts = instanceIdentifier.split('/');
837
+ if (identifierParts[0] !== AI_TYPE) {
838
+ throw new AIError(AIErrorCode.ERROR, `Invalid instance identifier, unknown prefix '${identifierParts[0]}'`);
839
+ }
840
+ const backendType = identifierParts[1];
841
+ switch (backendType) {
842
+ case 'vertexai':
843
+ const location = identifierParts[2];
844
+ if (!location) {
845
+ throw new AIError(AIErrorCode.ERROR, `Invalid instance identifier, unknown location '${instanceIdentifier}'`);
846
+ }
847
+ return new VertexAIBackend(location);
848
+ case 'googleai':
849
+ return new GoogleAIBackend();
850
+ default:
851
+ throw new AIError(AIErrorCode.ERROR, `Invalid instance identifier string: '${instanceIdentifier}'`);
852
+ }
853
+ }
854
+
741
855
  /**
742
856
  * @license
743
857
  * Copyright 2024 Google LLC
@@ -781,62 +895,6 @@ class AIService {
781
895
  }
782
896
  }
783
897
 
784
- /**
785
- * @license
786
- * Copyright 2024 Google LLC
787
- *
788
- * Licensed under the Apache License, Version 2.0 (the "License");
789
- * you may not use this file except in compliance with the License.
790
- * You may obtain a copy of the License at
791
- *
792
- * http://www.apache.org/licenses/LICENSE-2.0
793
- *
794
- * Unless required by applicable law or agreed to in writing, software
795
- * distributed under the License is distributed on an "AS IS" BASIS,
796
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
797
- * See the License for the specific language governing permissions and
798
- * limitations under the License.
799
- */
800
- /**
801
- * Error class for the Firebase AI SDK.
802
- *
803
- * @public
804
- */
805
- class AIError extends FirebaseError {
806
- /**
807
- * Constructs a new instance of the `AIError` class.
808
- *
809
- * @param code - The error code from {@link (AIErrorCode:type)}.
810
- * @param message - A human-readable message describing the error.
811
- * @param customErrorData - Optional error data.
812
- */
813
- constructor(code, message, customErrorData) {
814
- // Match error format used by FirebaseError from ErrorFactory
815
- const service = AI_TYPE;
816
- const fullCode = `${service}/${code}`;
817
- const fullMessage = `${service}: ${message} (${fullCode})`;
818
- super(code, fullMessage);
819
- this.code = code;
820
- this.customErrorData = customErrorData;
821
- // FirebaseError initializes a stack trace, but it assumes the error is created from the error
822
- // factory. Since we break this assumption, we set the stack trace to be originating from this
823
- // constructor.
824
- // This is only supported in V8.
825
- if (Error.captureStackTrace) {
826
- // Allows us to initialize the stack trace without including the constructor itself at the
827
- // top level of the stack trace.
828
- Error.captureStackTrace(this, AIError);
829
- }
830
- // Allows instanceof AIError in ES5/ES6
831
- // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
832
- // TODO(dlarocque): Replace this with `new.target`: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
833
- // which we can now use since we no longer target ES5.
834
- Object.setPrototypeOf(this, AIError.prototype);
835
- // Since Error is an interface, we don't inherit toString and so we define it ourselves.
836
- this.toString = () => fullMessage;
837
- }
838
- }
839
-
840
898
  /**
841
899
  * @license
842
900
  * Copyright 2025 Google LLC
@@ -853,46 +911,16 @@ class AIError extends FirebaseError {
853
911
  * See the License for the specific language governing permissions and
854
912
  * limitations under the License.
855
913
  */
856
- /**
857
- * Encodes a {@link Backend} into a string that will be used to uniquely identify {@link AI}
858
- * instances by backend type.
859
- *
860
- * @internal
861
- */
862
- function encodeInstanceIdentifier(backend) {
863
- if (backend instanceof GoogleAIBackend) {
864
- return `${AI_TYPE}/googleai`;
865
- }
866
- else if (backend instanceof VertexAIBackend) {
867
- return `${AI_TYPE}/vertexai/${backend.location}`;
868
- }
869
- else {
870
- throw new AIError(AIErrorCode.ERROR, `Invalid backend: ${JSON.stringify(backend.backendType)}`);
871
- }
872
- }
873
- /**
874
- * Decodes an instance identifier string into a {@link Backend}.
875
- *
876
- * @internal
877
- */
878
- function decodeInstanceIdentifier(instanceIdentifier) {
879
- const identifierParts = instanceIdentifier.split('/');
880
- if (identifierParts[0] !== AI_TYPE) {
881
- throw new AIError(AIErrorCode.ERROR, `Invalid instance identifier, unknown prefix '${identifierParts[0]}'`);
882
- }
883
- const backendType = identifierParts[1];
884
- switch (backendType) {
885
- case 'vertexai':
886
- const location = identifierParts[2];
887
- if (!location) {
888
- throw new AIError(AIErrorCode.ERROR, `Invalid instance identifier, unknown location '${instanceIdentifier}'`);
889
- }
890
- return new VertexAIBackend(location);
891
- case 'googleai':
892
- return new GoogleAIBackend();
893
- default:
894
- throw new AIError(AIErrorCode.ERROR, `Invalid instance identifier string: '${instanceIdentifier}'`);
914
+ function factory(container, { instanceIdentifier }) {
915
+ if (!instanceIdentifier) {
916
+ throw new AIError(AIErrorCode.ERROR, 'AIService instance identifier is undefined.');
895
917
  }
918
+ const backend = decodeInstanceIdentifier(instanceIdentifier);
919
+ // getImmediate for FirebaseApp will always succeed
920
+ const app = container.getProvider('app').getImmediate();
921
+ const auth = container.getProvider('auth-internal');
922
+ const appCheckProvider = container.getProvider('app-check-internal');
923
+ return new AIService(app, backend, auth, appCheckProvider);
896
924
  }
897
925
 
898
926
  /**
@@ -2534,75 +2562,104 @@ class LiveSession {
2534
2562
  this.webSocketHandler.send(JSON.stringify(message));
2535
2563
  }
2536
2564
  /**
2537
- * Sends realtime input to the server.
2565
+ * Sends text to the server in realtime.
2538
2566
  *
2539
- * @param mediaChunks - The media chunks to send.
2567
+ * @example
2568
+ * ```javascript
2569
+ * liveSession.sendTextRealtime("Hello, how are you?");
2570
+ * ```
2571
+ *
2572
+ * @param text - The text data to send.
2540
2573
  * @throws If this session has been closed.
2541
2574
  *
2542
2575
  * @beta
2543
2576
  */
2544
- async sendMediaChunks(mediaChunks) {
2577
+ async sendTextRealtime(text) {
2545
2578
  if (this.isClosed) {
2546
2579
  throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
2547
2580
  }
2548
- // The backend does not support sending more than one mediaChunk in one message.
2549
- // Work around this limitation by sending mediaChunks in separate messages.
2550
- mediaChunks.forEach(mediaChunk => {
2551
- const message = {
2552
- realtimeInput: { mediaChunks: [mediaChunk] }
2553
- };
2554
- this.webSocketHandler.send(JSON.stringify(message));
2555
- });
2581
+ const message = {
2582
+ realtimeInput: {
2583
+ text
2584
+ }
2585
+ };
2586
+ this.webSocketHandler.send(JSON.stringify(message));
2556
2587
  }
2557
2588
  /**
2558
- * Sends function responses to the server.
2589
+ * Sends audio data to the server in realtime.
2559
2590
  *
2560
- * @param functionResponses - The function responses to send.
2591
+ * @remarks The server requires that the audio data is base64-encoded 16-bit PCM at 16kHz
2592
+ * little-endian.
2593
+ *
2594
+ * @example
2595
+ * ```javascript
2596
+ * // const pcmData = ... base64-encoded 16-bit PCM at 16kHz little-endian.
2597
+ * const blob = { mimeType: "audio/pcm", data: pcmData };
2598
+ * liveSession.sendAudioRealtime(blob);
2599
+ * ```
2600
+ *
2601
+ * @param blob - The base64-encoded PCM data to send to the server in realtime.
2561
2602
  * @throws If this session has been closed.
2562
2603
  *
2563
2604
  * @beta
2564
2605
  */
2565
- async sendFunctionResponses(functionResponses) {
2606
+ async sendAudioRealtime(blob) {
2566
2607
  if (this.isClosed) {
2567
2608
  throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
2568
2609
  }
2569
2610
  const message = {
2570
- toolResponse: {
2571
- functionResponses
2611
+ realtimeInput: {
2612
+ audio: blob
2572
2613
  }
2573
2614
  };
2574
2615
  this.webSocketHandler.send(JSON.stringify(message));
2575
2616
  }
2576
2617
  /**
2577
- * Sends a stream of {@link GenerativeContentBlob}.
2618
+ * Sends video data to the server in realtime.
2578
2619
  *
2579
- * @param mediaChunkStream - The stream of {@link GenerativeContentBlob} to send.
2620
+ * @remarks The server requires that the video is sent as individual video frames at 1 FPS. It
2621
+ * is recommended to set `mimeType` to `image/jpeg`.
2622
+ *
2623
+ * @example
2624
+ * ```javascript
2625
+ * // const videoFrame = ... base64-encoded JPEG data
2626
+ * const blob = { mimeType: "image/jpeg", data: videoFrame };
2627
+ * liveSession.sendVideoRealtime(blob);
2628
+ * ```
2629
+ * @param blob - The base64-encoded video data to send to the server in realtime.
2580
2630
  * @throws If this session has been closed.
2581
2631
  *
2582
2632
  * @beta
2583
2633
  */
2584
- async sendMediaStream(mediaChunkStream) {
2634
+ async sendVideoRealtime(blob) {
2585
2635
  if (this.isClosed) {
2586
2636
  throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
2587
2637
  }
2588
- const reader = mediaChunkStream.getReader();
2589
- while (true) {
2590
- try {
2591
- const { done, value } = await reader.read();
2592
- if (done) {
2593
- break;
2594
- }
2595
- else if (!value) {
2596
- throw new Error('Missing chunk in reader, but reader is not done.');
2597
- }
2598
- await this.sendMediaChunks([value]);
2599
- }
2600
- catch (e) {
2601
- // Re-throw any errors that occur during stream consumption or sending.
2602
- const message = e instanceof Error ? e.message : 'Error processing media stream.';
2603
- throw new AIError(AIErrorCode.REQUEST_ERROR, message);
2638
+ const message = {
2639
+ realtimeInput: {
2640
+ video: blob
2604
2641
  }
2642
+ };
2643
+ this.webSocketHandler.send(JSON.stringify(message));
2644
+ }
2645
+ /**
2646
+ * Sends function responses to the server.
2647
+ *
2648
+ * @param functionResponses - The function responses to send.
2649
+ * @throws If this session has been closed.
2650
+ *
2651
+ * @beta
2652
+ */
2653
+ async sendFunctionResponses(functionResponses) {
2654
+ if (this.isClosed) {
2655
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
2605
2656
  }
2657
+ const message = {
2658
+ toolResponse: {
2659
+ functionResponses
2660
+ }
2661
+ };
2662
+ this.webSocketHandler.send(JSON.stringify(message));
2606
2663
  }
2607
2664
  /**
2608
2665
  * Yields messages received from the server.
@@ -2660,6 +2717,62 @@ class LiveSession {
2660
2717
  await this.webSocketHandler.close(1000, 'Client closed session.');
2661
2718
  }
2662
2719
  }
2720
+ /**
2721
+ * Sends realtime input to the server.
2722
+ *
2723
+ * @deprecated Use `sendTextRealtime()`, `sendAudioRealtime()`, and `sendVideoRealtime()` instead.
2724
+ *
2725
+ * @param mediaChunks - The media chunks to send.
2726
+ * @throws If this session has been closed.
2727
+ *
2728
+ * @beta
2729
+ */
2730
+ async sendMediaChunks(mediaChunks) {
2731
+ if (this.isClosed) {
2732
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
2733
+ }
2734
+ // The backend does not support sending more than one mediaChunk in one message.
2735
+ // Work around this limitation by sending mediaChunks in separate messages.
2736
+ mediaChunks.forEach(mediaChunk => {
2737
+ const message = {
2738
+ realtimeInput: { mediaChunks: [mediaChunk] }
2739
+ };
2740
+ this.webSocketHandler.send(JSON.stringify(message));
2741
+ });
2742
+ }
2743
+ /**
2744
+ * @deprecated Use `sendTextRealtime()`, `sendAudioRealtime()`, and `sendVideoRealtime()` instead.
2745
+ *
2746
+ * Sends a stream of {@link GenerativeContentBlob}.
2747
+ *
2748
+ * @param mediaChunkStream - The stream of {@link GenerativeContentBlob} to send.
2749
+ * @throws If this session has been closed.
2750
+ *
2751
+ * @beta
2752
+ */
2753
+ async sendMediaStream(mediaChunkStream) {
2754
+ if (this.isClosed) {
2755
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
2756
+ }
2757
+ const reader = mediaChunkStream.getReader();
2758
+ while (true) {
2759
+ try {
2760
+ const { done, value } = await reader.read();
2761
+ if (done) {
2762
+ break;
2763
+ }
2764
+ else if (!value) {
2765
+ throw new Error('Missing chunk in reader, but reader is not done.');
2766
+ }
2767
+ await this.sendMediaChunks([value]);
2768
+ }
2769
+ catch (e) {
2770
+ // Re-throw any errors that occur during stream consumption or sending.
2771
+ const message = e instanceof Error ? e.message : 'Error processing media stream.';
2772
+ throw new AIError(AIErrorCode.REQUEST_ERROR, message);
2773
+ }
2774
+ }
2775
+ }
2663
2776
  }
2664
2777
 
2665
2778
  /**
@@ -2720,13 +2833,18 @@ class LiveGenerativeModel extends AIModel {
2720
2833
  else {
2721
2834
  fullModelPath = `projects/${this._apiSettings.project}/locations/${this._apiSettings.location}/${this.model}`;
2722
2835
  }
2836
+ // inputAudioTranscription and outputAudioTranscription are on the generation config in the public API,
2837
+ // but the backend expects them to be in the `setup` message.
2838
+ const { inputAudioTranscription, outputAudioTranscription, ...generationConfig } = this.generationConfig;
2723
2839
  const setupMessage = {
2724
2840
  setup: {
2725
2841
  model: fullModelPath,
2726
- generationConfig: this.generationConfig,
2842
+ generationConfig,
2727
2843
  tools: this.tools,
2728
2844
  toolConfig: this.toolConfig,
2729
- systemInstruction: this.systemInstruction
2845
+ systemInstruction: this.systemInstruction,
2846
+ inputAudioTranscription,
2847
+ outputAudioTranscription
2730
2848
  }
2731
2849
  };
2732
2850
  try {
@@ -3432,7 +3550,7 @@ class AudioConversationRunner {
3432
3550
  mimeType: 'audio/pcm',
3433
3551
  data: base64
3434
3552
  };
3435
- void this.liveSession.sendMediaChunks([chunk]);
3553
+ void this.liveSession.sendAudioRealtime(chunk);
3436
3554
  };
3437
3555
  }
3438
3556
  /**
@@ -3810,17 +3928,7 @@ function getLiveGenerativeModel(ai, modelParams) {
3810
3928
  * @packageDocumentation
3811
3929
  */
3812
3930
  function registerAI() {
3813
- _registerComponent(new Component(AI_TYPE, (container, { instanceIdentifier }) => {
3814
- if (!instanceIdentifier) {
3815
- throw new AIError(AIErrorCode.ERROR, 'AIService instance identifier is undefined.');
3816
- }
3817
- const backend = decodeInstanceIdentifier(instanceIdentifier);
3818
- // getImmediate for FirebaseApp will always succeed
3819
- const app = container.getProvider('app').getImmediate();
3820
- const auth = container.getProvider('auth-internal');
3821
- const appCheckProvider = container.getProvider('app-check-internal');
3822
- return new AIService(app, backend, auth, appCheckProvider);
3823
- }, "PUBLIC" /* ComponentType.PUBLIC */).setMultipleInstances(true));
3931
+ _registerComponent(new Component(AI_TYPE, factory, "PUBLIC" /* ComponentType.PUBLIC */).setMultipleInstances(true));
3824
3932
  registerVersion(name, version, 'node');
3825
3933
  // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation
3826
3934
  registerVersion(name, version, 'esm2020');