@firebase/ai 2.2.1-canary.4d834deb2 → 2.2.1-canary.9b8ab02c5

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.2.1-canary.4d834deb2";
7
+ var version = "2.2.1-canary.9b8ab02c5";
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
@@ -299,12 +355,50 @@ const ResponseModality = {
299
355
  /**
300
356
  * <b>(EXPERIMENTAL)</b>
301
357
  * Determines whether inference happens on-device or in-cloud.
358
+ *
359
+ * @remarks
360
+ * <b>PREFER_ON_DEVICE:</b> Attempt to make inference calls using an
361
+ * on-device model. If on-device inference is not available, the SDK
362
+ * will fall back to using a cloud-hosted model.
363
+ * <br/>
364
+ * <b>ONLY_ON_DEVICE:</b> Only attempt to make inference calls using an
365
+ * on-device model. The SDK will not fall back to a cloud-hosted model.
366
+ * If on-device inference is not available, inference methods will throw.
367
+ * <br/>
368
+ * <b>ONLY_IN_CLOUD:</b> Only attempt to make inference calls using a
369
+ * cloud-hosted model. The SDK will not fall back to an on-device model.
370
+ * <br/>
371
+ * <b>PREFER_IN_CLOUD:</b> Attempt to make inference calls to a
372
+ * cloud-hosted model. If not available, the SDK will fall back to an
373
+ * on-device model.
374
+ *
302
375
  * @public
303
376
  */
304
377
  const InferenceMode = {
305
378
  'PREFER_ON_DEVICE': 'prefer_on_device',
306
379
  'ONLY_ON_DEVICE': 'only_on_device',
307
- 'ONLY_IN_CLOUD': 'only_in_cloud'
380
+ 'ONLY_IN_CLOUD': 'only_in_cloud',
381
+ 'PREFER_IN_CLOUD': 'prefer_in_cloud'
382
+ };
383
+ /**
384
+ * Represents the result of the code execution.
385
+ *
386
+ * @public
387
+ */
388
+ const Outcome = {
389
+ UNSPECIFIED: 'OUTCOME_UNSPECIFIED',
390
+ OK: 'OUTCOME_OK',
391
+ FAILED: 'OUTCOME_FAILED',
392
+ DEADLINE_EXCEEDED: 'OUTCOME_DEADLINE_EXCEEDED'
393
+ };
394
+ /**
395
+ * The programming language of the code.
396
+ *
397
+ * @public
398
+ */
399
+ const Language = {
400
+ UNSPECIFIED: 'LANGUAGE_UNSPECIFIED',
401
+ PYTHON: 'PYTHON'
308
402
  };
309
403
 
310
404
  /**
@@ -653,105 +747,6 @@ class VertexAIBackend extends Backend {
653
747
  }
654
748
  }
655
749
 
656
- /**
657
- * @license
658
- * Copyright 2024 Google LLC
659
- *
660
- * Licensed under the Apache License, Version 2.0 (the "License");
661
- * you may not use this file except in compliance with the License.
662
- * You may obtain a copy of the License at
663
- *
664
- * http://www.apache.org/licenses/LICENSE-2.0
665
- *
666
- * Unless required by applicable law or agreed to in writing, software
667
- * distributed under the License is distributed on an "AS IS" BASIS,
668
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
669
- * See the License for the specific language governing permissions and
670
- * limitations under the License.
671
- */
672
- class AIService {
673
- constructor(app, backend, authProvider, appCheckProvider, chromeAdapterFactory) {
674
- this.app = app;
675
- this.backend = backend;
676
- this.chromeAdapterFactory = chromeAdapterFactory;
677
- const appCheck = appCheckProvider?.getImmediate({ optional: true });
678
- const auth = authProvider?.getImmediate({ optional: true });
679
- this.auth = auth || null;
680
- this.appCheck = appCheck || null;
681
- if (backend instanceof VertexAIBackend) {
682
- this.location = backend.location;
683
- }
684
- else {
685
- this.location = '';
686
- }
687
- }
688
- _delete() {
689
- return Promise.resolve();
690
- }
691
- set options(optionsToSet) {
692
- this._options = optionsToSet;
693
- }
694
- get options() {
695
- return this._options;
696
- }
697
- }
698
-
699
- /**
700
- * @license
701
- * Copyright 2024 Google LLC
702
- *
703
- * Licensed under the Apache License, Version 2.0 (the "License");
704
- * you may not use this file except in compliance with the License.
705
- * You may obtain a copy of the License at
706
- *
707
- * http://www.apache.org/licenses/LICENSE-2.0
708
- *
709
- * Unless required by applicable law or agreed to in writing, software
710
- * distributed under the License is distributed on an "AS IS" BASIS,
711
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
712
- * See the License for the specific language governing permissions and
713
- * limitations under the License.
714
- */
715
- /**
716
- * Error class for the Firebase AI SDK.
717
- *
718
- * @public
719
- */
720
- class AIError extends FirebaseError {
721
- /**
722
- * Constructs a new instance of the `AIError` class.
723
- *
724
- * @param code - The error code from {@link (AIErrorCode:type)}.
725
- * @param message - A human-readable message describing the error.
726
- * @param customErrorData - Optional error data.
727
- */
728
- constructor(code, message, customErrorData) {
729
- // Match error format used by FirebaseError from ErrorFactory
730
- const service = AI_TYPE;
731
- const fullCode = `${service}/${code}`;
732
- const fullMessage = `${service}: ${message} (${fullCode})`;
733
- super(code, fullMessage);
734
- this.code = code;
735
- this.customErrorData = customErrorData;
736
- // FirebaseError initializes a stack trace, but it assumes the error is created from the error
737
- // factory. Since we break this assumption, we set the stack trace to be originating from this
738
- // constructor.
739
- // This is only supported in V8.
740
- if (Error.captureStackTrace) {
741
- // Allows us to initialize the stack trace without including the constructor itself at the
742
- // top level of the stack trace.
743
- Error.captureStackTrace(this, AIError);
744
- }
745
- // Allows instanceof AIError in ES5/ES6
746
- // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
747
- // TODO(dlarocque): Replace this with `new.target`: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
748
- // which we can now use since we no longer target ES5.
749
- Object.setPrototypeOf(this, AIError.prototype);
750
- // Since Error is an interface, we don't inherit toString and so we define it ourselves.
751
- this.toString = () => fullMessage;
752
- }
753
- }
754
-
755
750
  /**
756
751
  * @license
757
752
  * Copyright 2025 Google LLC
@@ -812,7 +807,7 @@ function decodeInstanceIdentifier(instanceIdentifier) {
812
807
 
813
808
  /**
814
809
  * @license
815
- * Copyright 2025 Google LLC
810
+ * Copyright 2024 Google LLC
816
811
  *
817
812
  * Licensed under the Apache License, Version 2.0 (the "License");
818
813
  * you may not use this file except in compliance with the License.
@@ -826,113 +821,300 @@ function decodeInstanceIdentifier(instanceIdentifier) {
826
821
  * See the License for the specific language governing permissions and
827
822
  * limitations under the License.
828
823
  */
824
+ const logger = new Logger('@firebase/vertexai');
825
+
829
826
  /**
830
- * Base class for Firebase AI model APIs.
827
+ * @internal
828
+ */
829
+ var Availability;
830
+ (function (Availability) {
831
+ Availability["UNAVAILABLE"] = "unavailable";
832
+ Availability["DOWNLOADABLE"] = "downloadable";
833
+ Availability["DOWNLOADING"] = "downloading";
834
+ Availability["AVAILABLE"] = "available";
835
+ })(Availability || (Availability = {}));
836
+
837
+ /**
838
+ * @license
839
+ * Copyright 2025 Google LLC
831
840
  *
832
- * Instances of this class are associated with a specific Firebase AI {@link Backend}
833
- * and provide methods for interacting with the configured generative model.
841
+ * Licensed under the Apache License, Version 2.0 (the "License");
842
+ * you may not use this file except in compliance with the License.
843
+ * You may obtain a copy of the License at
834
844
  *
835
- * @public
845
+ * http://www.apache.org/licenses/LICENSE-2.0
846
+ *
847
+ * Unless required by applicable law or agreed to in writing, software
848
+ * distributed under the License is distributed on an "AS IS" BASIS,
849
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
850
+ * See the License for the specific language governing permissions and
851
+ * limitations under the License.
836
852
  */
837
- class AIModel {
853
+ /**
854
+ * Defines an inference "backend" that uses Chrome's on-device model,
855
+ * and encapsulates logic for detecting when on-device inference is
856
+ * possible.
857
+ */
858
+ class ChromeAdapterImpl {
859
+ constructor(languageModelProvider, mode, onDeviceParams = {
860
+ createOptions: {
861
+ // Defaults to support image inputs for convenience.
862
+ expectedInputs: [{ type: 'image' }]
863
+ }
864
+ }) {
865
+ this.languageModelProvider = languageModelProvider;
866
+ this.mode = mode;
867
+ this.onDeviceParams = onDeviceParams;
868
+ this.isDownloading = false;
869
+ }
838
870
  /**
839
- * Constructs a new instance of the {@link AIModel} class.
840
- *
841
- * This constructor should only be called from subclasses that provide
842
- * a model API.
871
+ * Checks if a given request can be made on-device.
843
872
  *
844
- * @param ai - an {@link AI} instance.
845
- * @param modelName - The name of the model being used. It can be in one of the following formats:
846
- * - `my-model` (short name, will resolve to `publishers/google/models/my-model`)
847
- * - `models/my-model` (will resolve to `publishers/google/models/my-model`)
848
- * - `publishers/my-publisher/models/my-model` (fully qualified model name)
873
+ * Encapsulates a few concerns:
874
+ * the mode
875
+ * API existence
876
+ * prompt formatting
877
+ * model availability, including triggering download if necessary
849
878
  *
850
- * @throws If the `apiKey` or `projectId` fields are missing in your
851
- * Firebase config.
852
879
  *
853
- * @internal
880
+ * Pros: callers needn't be concerned with details of on-device availability.</p>
881
+ * Cons: this method spans a few concerns and splits request validation from usage.
882
+ * If instance variables weren't already part of the API, we could consider a better
883
+ * separation of concerns.
854
884
  */
855
- constructor(ai, modelName) {
856
- if (!ai.app?.options?.apiKey) {
857
- throw new AIError(AIErrorCode.NO_API_KEY, `The "apiKey" field is empty in the local Firebase config. Firebase AI requires this field to contain a valid API key.`);
858
- }
859
- else if (!ai.app?.options?.projectId) {
860
- throw new AIError(AIErrorCode.NO_PROJECT_ID, `The "projectId" field is empty in the local Firebase config. Firebase AI requires this field to contain a valid project ID.`);
885
+ async isAvailable(request) {
886
+ if (!this.mode) {
887
+ logger.debug(`On-device inference unavailable because mode is undefined.`);
888
+ return false;
861
889
  }
862
- else if (!ai.app?.options?.appId) {
863
- throw new AIError(AIErrorCode.NO_APP_ID, `The "appId" field is empty in the local Firebase config. Firebase AI requires this field to contain a valid app ID.`);
890
+ if (this.mode === InferenceMode.ONLY_IN_CLOUD) {
891
+ logger.debug(`On-device inference unavailable because mode is "only_in_cloud".`);
892
+ return false;
864
893
  }
865
- else {
866
- this._apiSettings = {
867
- apiKey: ai.app.options.apiKey,
868
- project: ai.app.options.projectId,
869
- appId: ai.app.options.appId,
870
- automaticDataCollectionEnabled: ai.app.automaticDataCollectionEnabled,
871
- location: ai.location,
872
- backend: ai.backend
873
- };
874
- if (_isFirebaseServerApp(ai.app) && ai.app.settings.appCheckToken) {
875
- const token = ai.app.settings.appCheckToken;
876
- this._apiSettings.getAppCheckToken = () => {
877
- return Promise.resolve({ token });
878
- };
894
+ // Triggers out-of-band download so model will eventually become available.
895
+ const availability = await this.downloadIfAvailable();
896
+ if (this.mode === InferenceMode.ONLY_ON_DEVICE) {
897
+ // If it will never be available due to API inavailability, throw.
898
+ if (availability === Availability.UNAVAILABLE) {
899
+ throw new AIError(AIErrorCode.API_NOT_ENABLED, 'Local LanguageModel API not available in this environment.');
879
900
  }
880
- else if (ai.appCheck) {
881
- if (ai.options?.useLimitedUseAppCheckTokens) {
882
- this._apiSettings.getAppCheckToken = () => ai.appCheck.getLimitedUseToken();
883
- }
884
- else {
885
- this._apiSettings.getAppCheckToken = () => ai.appCheck.getToken();
886
- }
901
+ else if (availability === Availability.DOWNLOADABLE ||
902
+ availability === Availability.DOWNLOADING) {
903
+ // TODO(chholland): Better user experience during download - progress?
904
+ logger.debug(`Waiting for download of LanguageModel to complete.`);
905
+ await this.downloadPromise;
906
+ return true;
887
907
  }
888
- if (ai.auth) {
889
- this._apiSettings.getAuthToken = () => ai.auth.getToken();
908
+ return true;
909
+ }
910
+ // Applies prefer_on_device logic.
911
+ if (availability !== Availability.AVAILABLE) {
912
+ logger.debug(`On-device inference unavailable because availability is "${availability}".`);
913
+ return false;
914
+ }
915
+ if (!ChromeAdapterImpl.isOnDeviceRequest(request)) {
916
+ logger.debug(`On-device inference unavailable because request is incompatible.`);
917
+ return false;
918
+ }
919
+ return true;
920
+ }
921
+ /**
922
+ * Generates content on device.
923
+ *
924
+ * @remarks
925
+ * This is comparable to {@link GenerativeModel.generateContent} for generating content in
926
+ * Cloud.
927
+ * @param request - a standard Firebase AI {@link GenerateContentRequest}
928
+ * @returns {@link Response}, so we can reuse common response formatting.
929
+ */
930
+ async generateContent(request) {
931
+ const session = await this.createSession();
932
+ const contents = await Promise.all(request.contents.map(ChromeAdapterImpl.toLanguageModelMessage));
933
+ const text = await session.prompt(contents, this.onDeviceParams.promptOptions);
934
+ return ChromeAdapterImpl.toResponse(text);
935
+ }
936
+ /**
937
+ * Generates content stream on device.
938
+ *
939
+ * @remarks
940
+ * This is comparable to {@link GenerativeModel.generateContentStream} for generating content in
941
+ * Cloud.
942
+ * @param request - a standard Firebase AI {@link GenerateContentRequest}
943
+ * @returns {@link Response}, so we can reuse common response formatting.
944
+ */
945
+ async generateContentStream(request) {
946
+ const session = await this.createSession();
947
+ const contents = await Promise.all(request.contents.map(ChromeAdapterImpl.toLanguageModelMessage));
948
+ const stream = session.promptStreaming(contents, this.onDeviceParams.promptOptions);
949
+ return ChromeAdapterImpl.toStreamResponse(stream);
950
+ }
951
+ async countTokens(_request) {
952
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'Count Tokens is not yet available for on-device model.');
953
+ }
954
+ /**
955
+ * Asserts inference for the given request can be performed by an on-device model.
956
+ */
957
+ static isOnDeviceRequest(request) {
958
+ // Returns false if the prompt is empty.
959
+ if (request.contents.length === 0) {
960
+ logger.debug('Empty prompt rejected for on-device inference.');
961
+ return false;
962
+ }
963
+ for (const content of request.contents) {
964
+ if (content.role === 'function') {
965
+ logger.debug(`"Function" role rejected for on-device inference.`);
966
+ return false;
967
+ }
968
+ // Returns false if request contains an image with an unsupported mime type.
969
+ for (const part of content.parts) {
970
+ if (part.inlineData &&
971
+ ChromeAdapterImpl.SUPPORTED_MIME_TYPES.indexOf(part.inlineData.mimeType) === -1) {
972
+ logger.debug(`Unsupported mime type "${part.inlineData.mimeType}" rejected for on-device inference.`);
973
+ return false;
974
+ }
890
975
  }
891
- this.model = AIModel.normalizeModelName(modelName, this._apiSettings.backend.backendType);
892
976
  }
977
+ return true;
893
978
  }
894
979
  /**
895
- * Normalizes the given model name to a fully qualified model resource name.
980
+ * Encapsulates logic to get availability and download a model if one is downloadable.
981
+ */
982
+ async downloadIfAvailable() {
983
+ const availability = await this.languageModelProvider?.availability(this.onDeviceParams.createOptions);
984
+ if (availability === Availability.DOWNLOADABLE) {
985
+ this.download();
986
+ }
987
+ return availability;
988
+ }
989
+ /**
990
+ * Triggers out-of-band download of an on-device model.
896
991
  *
897
- * @param modelName - The model name to normalize.
898
- * @returns The fully qualified model resource name.
992
+ * Chrome only downloads models as needed. Chrome knows a model is needed when code calls
993
+ * LanguageModel.create.
899
994
  *
900
- * @internal
995
+ * Since Chrome manages the download, the SDK can only avoid redundant download requests by
996
+ * tracking if a download has previously been requested.
901
997
  */
902
- static normalizeModelName(modelName, backendType) {
903
- if (backendType === BackendType.GOOGLE_AI) {
904
- return AIModel.normalizeGoogleAIModelName(modelName);
998
+ download() {
999
+ if (this.isDownloading) {
1000
+ return;
905
1001
  }
906
- else {
907
- return AIModel.normalizeVertexAIModelName(modelName);
1002
+ this.isDownloading = true;
1003
+ this.downloadPromise = this.languageModelProvider
1004
+ ?.create(this.onDeviceParams.createOptions)
1005
+ .finally(() => {
1006
+ this.isDownloading = false;
1007
+ });
1008
+ }
1009
+ /**
1010
+ * Converts Firebase AI {@link Content} object to a Chrome {@link LanguageModelMessage} object.
1011
+ */
1012
+ static async toLanguageModelMessage(content) {
1013
+ const languageModelMessageContents = await Promise.all(content.parts.map(ChromeAdapterImpl.toLanguageModelMessageContent));
1014
+ return {
1015
+ role: ChromeAdapterImpl.toLanguageModelMessageRole(content.role),
1016
+ content: languageModelMessageContents
1017
+ };
1018
+ }
1019
+ /**
1020
+ * Converts a Firebase AI Part object to a Chrome LanguageModelMessageContent object.
1021
+ */
1022
+ static async toLanguageModelMessageContent(part) {
1023
+ if (part.text) {
1024
+ return {
1025
+ type: 'text',
1026
+ value: part.text
1027
+ };
1028
+ }
1029
+ else if (part.inlineData) {
1030
+ const formattedImageContent = await fetch(`data:${part.inlineData.mimeType};base64,${part.inlineData.data}`);
1031
+ const imageBlob = await formattedImageContent.blob();
1032
+ const imageBitmap = await createImageBitmap(imageBlob);
1033
+ return {
1034
+ type: 'image',
1035
+ value: imageBitmap
1036
+ };
908
1037
  }
1038
+ throw new AIError(AIErrorCode.REQUEST_ERROR, `Processing of this Part type is not currently supported.`);
909
1039
  }
910
1040
  /**
911
- * @internal
1041
+ * Converts a Firebase AI {@link Role} string to a {@link LanguageModelMessageRole} string.
912
1042
  */
913
- static normalizeGoogleAIModelName(modelName) {
914
- return `models/${modelName}`;
1043
+ static toLanguageModelMessageRole(role) {
1044
+ // Assumes 'function' rule has been filtered by isOnDeviceRequest
1045
+ return role === 'model' ? 'assistant' : 'user';
915
1046
  }
916
1047
  /**
917
- * @internal
1048
+ * Abstracts Chrome session creation.
1049
+ *
1050
+ * Chrome uses a multi-turn session for all inference. Firebase AI uses single-turn for all
1051
+ * inference. To map the Firebase AI API to Chrome's API, the SDK creates a new session for all
1052
+ * inference.
1053
+ *
1054
+ * Chrome will remove a model from memory if it's no longer in use, so this method ensures a
1055
+ * new session is created before an old session is destroyed.
918
1056
  */
919
- static normalizeVertexAIModelName(modelName) {
920
- let model;
921
- if (modelName.includes('/')) {
922
- if (modelName.startsWith('models/')) {
923
- // Add 'publishers/google' if the user is only passing in 'models/model-name'.
924
- model = `publishers/google/${modelName}`;
925
- }
926
- else {
927
- // Any other custom format (e.g. tuned models) must be passed in correctly.
928
- model = modelName;
929
- }
1057
+ async createSession() {
1058
+ if (!this.languageModelProvider) {
1059
+ throw new AIError(AIErrorCode.UNSUPPORTED, 'Chrome AI requested for unsupported browser version.');
930
1060
  }
931
- else {
932
- // If path is not included, assume it's a non-tuned model.
933
- model = `publishers/google/models/${modelName}`;
1061
+ const newSession = await this.languageModelProvider.create(this.onDeviceParams.createOptions);
1062
+ if (this.oldSession) {
1063
+ this.oldSession.destroy();
934
1064
  }
935
- return model;
1065
+ // Holds session reference, so model isn't unloaded from memory.
1066
+ this.oldSession = newSession;
1067
+ return newSession;
1068
+ }
1069
+ /**
1070
+ * Formats string returned by Chrome as a {@link Response} returned by Firebase AI.
1071
+ */
1072
+ static toResponse(text) {
1073
+ return {
1074
+ json: async () => ({
1075
+ candidates: [
1076
+ {
1077
+ content: {
1078
+ parts: [{ text }]
1079
+ }
1080
+ }
1081
+ ]
1082
+ })
1083
+ };
1084
+ }
1085
+ /**
1086
+ * Formats string stream returned by Chrome as SSE returned by Firebase AI.
1087
+ */
1088
+ static toStreamResponse(stream) {
1089
+ const encoder = new TextEncoder();
1090
+ return {
1091
+ body: stream.pipeThrough(new TransformStream({
1092
+ transform(chunk, controller) {
1093
+ const json = JSON.stringify({
1094
+ candidates: [
1095
+ {
1096
+ content: {
1097
+ role: 'model',
1098
+ parts: [{ text: chunk }]
1099
+ }
1100
+ }
1101
+ ]
1102
+ });
1103
+ controller.enqueue(encoder.encode(`data: ${json}\n\n`));
1104
+ }
1105
+ }))
1106
+ };
1107
+ }
1108
+ }
1109
+ // Visible for testing
1110
+ ChromeAdapterImpl.SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png'];
1111
+ /**
1112
+ * Creates a ChromeAdapterImpl on demand.
1113
+ */
1114
+ function chromeAdapterFactory(mode, window, params) {
1115
+ // Do not initialize a ChromeAdapter if we are not in hybrid mode.
1116
+ if (typeof window !== 'undefined' && mode) {
1117
+ return new ChromeAdapterImpl(window.LanguageModel, mode, params);
936
1118
  }
937
1119
  }
938
1120
 
@@ -952,8 +1134,187 @@ class AIModel {
952
1134
  * See the License for the specific language governing permissions and
953
1135
  * limitations under the License.
954
1136
  */
955
- const logger = new Logger('@firebase/vertexai');
956
-
1137
+ class AIService {
1138
+ constructor(app, backend, authProvider, appCheckProvider, chromeAdapterFactory) {
1139
+ this.app = app;
1140
+ this.backend = backend;
1141
+ this.chromeAdapterFactory = chromeAdapterFactory;
1142
+ const appCheck = appCheckProvider?.getImmediate({ optional: true });
1143
+ const auth = authProvider?.getImmediate({ optional: true });
1144
+ this.auth = auth || null;
1145
+ this.appCheck = appCheck || null;
1146
+ if (backend instanceof VertexAIBackend) {
1147
+ this.location = backend.location;
1148
+ }
1149
+ else {
1150
+ this.location = '';
1151
+ }
1152
+ }
1153
+ _delete() {
1154
+ return Promise.resolve();
1155
+ }
1156
+ set options(optionsToSet) {
1157
+ this._options = optionsToSet;
1158
+ }
1159
+ get options() {
1160
+ return this._options;
1161
+ }
1162
+ }
1163
+
1164
+ /**
1165
+ * @license
1166
+ * Copyright 2025 Google LLC
1167
+ *
1168
+ * Licensed under the Apache License, Version 2.0 (the "License");
1169
+ * you may not use this file except in compliance with the License.
1170
+ * You may obtain a copy of the License at
1171
+ *
1172
+ * http://www.apache.org/licenses/LICENSE-2.0
1173
+ *
1174
+ * Unless required by applicable law or agreed to in writing, software
1175
+ * distributed under the License is distributed on an "AS IS" BASIS,
1176
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1177
+ * See the License for the specific language governing permissions and
1178
+ * limitations under the License.
1179
+ */
1180
+ function factory(container, { instanceIdentifier }) {
1181
+ if (!instanceIdentifier) {
1182
+ throw new AIError(AIErrorCode.ERROR, 'AIService instance identifier is undefined.');
1183
+ }
1184
+ const backend = decodeInstanceIdentifier(instanceIdentifier);
1185
+ // getImmediate for FirebaseApp will always succeed
1186
+ const app = container.getProvider('app').getImmediate();
1187
+ const auth = container.getProvider('auth-internal');
1188
+ const appCheckProvider = container.getProvider('app-check-internal');
1189
+ return new AIService(app, backend, auth, appCheckProvider, chromeAdapterFactory);
1190
+ }
1191
+
1192
+ /**
1193
+ * @license
1194
+ * Copyright 2025 Google LLC
1195
+ *
1196
+ * Licensed under the Apache License, Version 2.0 (the "License");
1197
+ * you may not use this file except in compliance with the License.
1198
+ * You may obtain a copy of the License at
1199
+ *
1200
+ * http://www.apache.org/licenses/LICENSE-2.0
1201
+ *
1202
+ * Unless required by applicable law or agreed to in writing, software
1203
+ * distributed under the License is distributed on an "AS IS" BASIS,
1204
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1205
+ * See the License for the specific language governing permissions and
1206
+ * limitations under the License.
1207
+ */
1208
+ /**
1209
+ * Base class for Firebase AI model APIs.
1210
+ *
1211
+ * Instances of this class are associated with a specific Firebase AI {@link Backend}
1212
+ * and provide methods for interacting with the configured generative model.
1213
+ *
1214
+ * @public
1215
+ */
1216
+ class AIModel {
1217
+ /**
1218
+ * Constructs a new instance of the {@link AIModel} class.
1219
+ *
1220
+ * This constructor should only be called from subclasses that provide
1221
+ * a model API.
1222
+ *
1223
+ * @param ai - an {@link AI} instance.
1224
+ * @param modelName - The name of the model being used. It can be in one of the following formats:
1225
+ * - `my-model` (short name, will resolve to `publishers/google/models/my-model`)
1226
+ * - `models/my-model` (will resolve to `publishers/google/models/my-model`)
1227
+ * - `publishers/my-publisher/models/my-model` (fully qualified model name)
1228
+ *
1229
+ * @throws If the `apiKey` or `projectId` fields are missing in your
1230
+ * Firebase config.
1231
+ *
1232
+ * @internal
1233
+ */
1234
+ constructor(ai, modelName) {
1235
+ if (!ai.app?.options?.apiKey) {
1236
+ throw new AIError(AIErrorCode.NO_API_KEY, `The "apiKey" field is empty in the local Firebase config. Firebase AI requires this field to contain a valid API key.`);
1237
+ }
1238
+ else if (!ai.app?.options?.projectId) {
1239
+ throw new AIError(AIErrorCode.NO_PROJECT_ID, `The "projectId" field is empty in the local Firebase config. Firebase AI requires this field to contain a valid project ID.`);
1240
+ }
1241
+ else if (!ai.app?.options?.appId) {
1242
+ throw new AIError(AIErrorCode.NO_APP_ID, `The "appId" field is empty in the local Firebase config. Firebase AI requires this field to contain a valid app ID.`);
1243
+ }
1244
+ else {
1245
+ this._apiSettings = {
1246
+ apiKey: ai.app.options.apiKey,
1247
+ project: ai.app.options.projectId,
1248
+ appId: ai.app.options.appId,
1249
+ automaticDataCollectionEnabled: ai.app.automaticDataCollectionEnabled,
1250
+ location: ai.location,
1251
+ backend: ai.backend
1252
+ };
1253
+ if (_isFirebaseServerApp(ai.app) && ai.app.settings.appCheckToken) {
1254
+ const token = ai.app.settings.appCheckToken;
1255
+ this._apiSettings.getAppCheckToken = () => {
1256
+ return Promise.resolve({ token });
1257
+ };
1258
+ }
1259
+ else if (ai.appCheck) {
1260
+ if (ai.options?.useLimitedUseAppCheckTokens) {
1261
+ this._apiSettings.getAppCheckToken = () => ai.appCheck.getLimitedUseToken();
1262
+ }
1263
+ else {
1264
+ this._apiSettings.getAppCheckToken = () => ai.appCheck.getToken();
1265
+ }
1266
+ }
1267
+ if (ai.auth) {
1268
+ this._apiSettings.getAuthToken = () => ai.auth.getToken();
1269
+ }
1270
+ this.model = AIModel.normalizeModelName(modelName, this._apiSettings.backend.backendType);
1271
+ }
1272
+ }
1273
+ /**
1274
+ * Normalizes the given model name to a fully qualified model resource name.
1275
+ *
1276
+ * @param modelName - The model name to normalize.
1277
+ * @returns The fully qualified model resource name.
1278
+ *
1279
+ * @internal
1280
+ */
1281
+ static normalizeModelName(modelName, backendType) {
1282
+ if (backendType === BackendType.GOOGLE_AI) {
1283
+ return AIModel.normalizeGoogleAIModelName(modelName);
1284
+ }
1285
+ else {
1286
+ return AIModel.normalizeVertexAIModelName(modelName);
1287
+ }
1288
+ }
1289
+ /**
1290
+ * @internal
1291
+ */
1292
+ static normalizeGoogleAIModelName(modelName) {
1293
+ return `models/${modelName}`;
1294
+ }
1295
+ /**
1296
+ * @internal
1297
+ */
1298
+ static normalizeVertexAIModelName(modelName) {
1299
+ let model;
1300
+ if (modelName.includes('/')) {
1301
+ if (modelName.startsWith('models/')) {
1302
+ // Add 'publishers/google' if the user is only passing in 'models/model-name'.
1303
+ model = `publishers/google/${modelName}`;
1304
+ }
1305
+ else {
1306
+ // Any other custom format (e.g. tuned models) must be passed in correctly.
1307
+ model = modelName;
1308
+ }
1309
+ }
1310
+ else {
1311
+ // If path is not included, assume it's a non-tuned model.
1312
+ model = `publishers/google/models/${modelName}`;
1313
+ }
1314
+ return model;
1315
+ }
1316
+ }
1317
+
957
1318
  /**
958
1319
  * @license
959
1320
  * Copyright 2024 Google LLC
@@ -1734,6 +2095,72 @@ function aggregateResponses(responses) {
1734
2095
  return aggregatedResponse;
1735
2096
  }
1736
2097
 
2098
+ /**
2099
+ * @license
2100
+ * Copyright 2025 Google LLC
2101
+ *
2102
+ * Licensed under the Apache License, Version 2.0 (the "License");
2103
+ * you may not use this file except in compliance with the License.
2104
+ * You may obtain a copy of the License at
2105
+ *
2106
+ * http://www.apache.org/licenses/LICENSE-2.0
2107
+ *
2108
+ * Unless required by applicable law or agreed to in writing, software
2109
+ * distributed under the License is distributed on an "AS IS" BASIS,
2110
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2111
+ * See the License for the specific language governing permissions and
2112
+ * limitations under the License.
2113
+ */
2114
+ const errorsCausingFallback = [
2115
+ // most network errors
2116
+ AIErrorCode.FETCH_ERROR,
2117
+ // fallback code for all other errors in makeRequest
2118
+ AIErrorCode.ERROR,
2119
+ // error due to API not being enabled in project
2120
+ AIErrorCode.API_NOT_ENABLED
2121
+ ];
2122
+ /**
2123
+ * Dispatches a request to the appropriate backend (on-device or in-cloud)
2124
+ * based on the inference mode.
2125
+ *
2126
+ * @param request - The request to be sent.
2127
+ * @param chromeAdapter - The on-device model adapter.
2128
+ * @param onDeviceCall - The function to call for on-device inference.
2129
+ * @param inCloudCall - The function to call for in-cloud inference.
2130
+ * @returns The response from the backend.
2131
+ */
2132
+ async function callCloudOrDevice(request, chromeAdapter, onDeviceCall, inCloudCall) {
2133
+ if (!chromeAdapter) {
2134
+ return inCloudCall();
2135
+ }
2136
+ switch (chromeAdapter.mode) {
2137
+ case InferenceMode.ONLY_ON_DEVICE:
2138
+ if (await chromeAdapter.isAvailable(request)) {
2139
+ return onDeviceCall();
2140
+ }
2141
+ throw new AIError(AIErrorCode.UNSUPPORTED, 'Inference mode is ONLY_ON_DEVICE, but an on-device model is not available.');
2142
+ case InferenceMode.ONLY_IN_CLOUD:
2143
+ return inCloudCall();
2144
+ case InferenceMode.PREFER_IN_CLOUD:
2145
+ try {
2146
+ return await inCloudCall();
2147
+ }
2148
+ catch (e) {
2149
+ if (e instanceof AIError && errorsCausingFallback.includes(e.code)) {
2150
+ return onDeviceCall();
2151
+ }
2152
+ throw e;
2153
+ }
2154
+ case InferenceMode.PREFER_ON_DEVICE:
2155
+ if (await chromeAdapter.isAvailable(request)) {
2156
+ return onDeviceCall();
2157
+ }
2158
+ return inCloudCall();
2159
+ default:
2160
+ throw new AIError(AIErrorCode.ERROR, `Unexpected infererence mode: ${chromeAdapter.mode}`);
2161
+ }
2162
+ }
2163
+
1737
2164
  /**
1738
2165
  * @license
1739
2166
  * Copyright 2024 Google LLC
@@ -1758,13 +2185,7 @@ async function generateContentStreamOnCloud(apiSettings, model, params, requestO
1758
2185
  /* stream */ true, JSON.stringify(params), requestOptions);
1759
2186
  }
1760
2187
  async function generateContentStream(apiSettings, model, params, chromeAdapter, requestOptions) {
1761
- let response;
1762
- if (chromeAdapter && (await chromeAdapter.isAvailable(params))) {
1763
- response = await chromeAdapter.generateContentStream(params);
1764
- }
1765
- else {
1766
- response = await generateContentStreamOnCloud(apiSettings, model, params, requestOptions);
1767
- }
2188
+ const response = await callCloudOrDevice(params, chromeAdapter, () => chromeAdapter.generateContentStream(params), () => generateContentStreamOnCloud(apiSettings, model, params, requestOptions));
1768
2189
  return processStream(response, apiSettings); // TODO: Map streaming responses
1769
2190
  }
1770
2191
  async function generateContentOnCloud(apiSettings, model, params, requestOptions) {
@@ -1775,13 +2196,7 @@ async function generateContentOnCloud(apiSettings, model, params, requestOptions
1775
2196
  /* stream */ false, JSON.stringify(params), requestOptions);
1776
2197
  }
1777
2198
  async function generateContent(apiSettings, model, params, chromeAdapter, requestOptions) {
1778
- let response;
1779
- if (chromeAdapter && (await chromeAdapter.isAvailable(params))) {
1780
- response = await chromeAdapter.generateContent(params);
1781
- }
1782
- else {
1783
- response = await generateContentOnCloud(apiSettings, model, params, requestOptions);
1784
- }
2199
+ const response = await callCloudOrDevice(params, chromeAdapter, () => chromeAdapter.generateContent(params), () => generateContentOnCloud(apiSettings, model, params, requestOptions));
1785
2200
  const generateContentResponse = await processGenerateContentResponse(response, apiSettings);
1786
2201
  const enhancedResponse = createEnhancedContentResponse(generateContentResponse);
1787
2202
  return {
@@ -1991,7 +2406,9 @@ function validateChatHistory(history) {
1991
2406
  functionCall: 0,
1992
2407
  functionResponse: 0,
1993
2408
  thought: 0,
1994
- thoughtSignature: 0
2409
+ thoughtSignature: 0,
2410
+ executableCode: 0,
2411
+ codeExecutionResult: 0
1995
2412
  };
1996
2413
  for (const part of parts) {
1997
2414
  for (const key of VALID_PART_FIELDS) {
@@ -2192,8 +2609,8 @@ async function countTokensOnCloud(apiSettings, model, params, requestOptions) {
2192
2609
  return response.json();
2193
2610
  }
2194
2611
  async function countTokens(apiSettings, model, params, chromeAdapter, requestOptions) {
2195
- if (chromeAdapter && (await chromeAdapter.isAvailable(params))) {
2196
- return (await chromeAdapter.countTokens(params)).json();
2612
+ if (chromeAdapter?.mode === InferenceMode.ONLY_ON_DEVICE) {
2613
+ throw new AIError(AIErrorCode.UNSUPPORTED, 'countTokens() is not supported for on-device models.');
2197
2614
  }
2198
2615
  return countTokensOnCloud(apiSettings, model, params, requestOptions);
2199
2616
  }
@@ -3607,316 +4024,10 @@ function getLiveGenerativeModel(ai, modelParams) {
3607
4024
  }
3608
4025
 
3609
4026
  /**
3610
- * @internal
3611
- */
3612
- var Availability;
3613
- (function (Availability) {
3614
- Availability["UNAVAILABLE"] = "unavailable";
3615
- Availability["DOWNLOADABLE"] = "downloadable";
3616
- Availability["DOWNLOADING"] = "downloading";
3617
- Availability["AVAILABLE"] = "available";
3618
- })(Availability || (Availability = {}));
3619
-
3620
- /**
3621
- * @license
3622
- * Copyright 2025 Google LLC
3623
- *
3624
- * Licensed under the Apache License, Version 2.0 (the "License");
3625
- * you may not use this file except in compliance with the License.
3626
- * You may obtain a copy of the License at
3627
- *
3628
- * http://www.apache.org/licenses/LICENSE-2.0
4027
+ * The Firebase AI Web SDK.
3629
4028
  *
3630
- * Unless required by applicable law or agreed to in writing, software
3631
- * distributed under the License is distributed on an "AS IS" BASIS,
3632
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3633
- * See the License for the specific language governing permissions and
3634
- * limitations under the License.
3635
- */
3636
- /**
3637
- * Defines an inference "backend" that uses Chrome's on-device model,
3638
- * and encapsulates logic for detecting when on-device inference is
3639
- * possible.
4029
+ * @packageDocumentation
3640
4030
  */
3641
- class ChromeAdapterImpl {
3642
- constructor(languageModelProvider, mode, onDeviceParams = {
3643
- createOptions: {
3644
- // Defaults to support image inputs for convenience.
3645
- expectedInputs: [{ type: 'image' }]
3646
- }
3647
- }) {
3648
- this.languageModelProvider = languageModelProvider;
3649
- this.mode = mode;
3650
- this.onDeviceParams = onDeviceParams;
3651
- this.isDownloading = false;
3652
- }
3653
- /**
3654
- * Checks if a given request can be made on-device.
3655
- *
3656
- * Encapsulates a few concerns:
3657
- * the mode
3658
- * API existence
3659
- * prompt formatting
3660
- * model availability, including triggering download if necessary
3661
- *
3662
- *
3663
- * Pros: callers needn't be concerned with details of on-device availability.</p>
3664
- * Cons: this method spans a few concerns and splits request validation from usage.
3665
- * If instance variables weren't already part of the API, we could consider a better
3666
- * separation of concerns.
3667
- */
3668
- async isAvailable(request) {
3669
- if (!this.mode) {
3670
- logger.debug(`On-device inference unavailable because mode is undefined.`);
3671
- return false;
3672
- }
3673
- if (this.mode === InferenceMode.ONLY_IN_CLOUD) {
3674
- logger.debug(`On-device inference unavailable because mode is "only_in_cloud".`);
3675
- return false;
3676
- }
3677
- // Triggers out-of-band download so model will eventually become available.
3678
- const availability = await this.downloadIfAvailable();
3679
- if (this.mode === InferenceMode.ONLY_ON_DEVICE) {
3680
- // If it will never be available due to API inavailability, throw.
3681
- if (availability === Availability.UNAVAILABLE) {
3682
- throw new AIError(AIErrorCode.API_NOT_ENABLED, 'Local LanguageModel API not available in this environment.');
3683
- }
3684
- else if (availability === Availability.DOWNLOADABLE ||
3685
- availability === Availability.DOWNLOADING) {
3686
- // TODO(chholland): Better user experience during download - progress?
3687
- logger.debug(`Waiting for download of LanguageModel to complete.`);
3688
- await this.downloadPromise;
3689
- return true;
3690
- }
3691
- return true;
3692
- }
3693
- // Applies prefer_on_device logic.
3694
- if (availability !== Availability.AVAILABLE) {
3695
- logger.debug(`On-device inference unavailable because availability is "${availability}".`);
3696
- return false;
3697
- }
3698
- if (!ChromeAdapterImpl.isOnDeviceRequest(request)) {
3699
- logger.debug(`On-device inference unavailable because request is incompatible.`);
3700
- return false;
3701
- }
3702
- return true;
3703
- }
3704
- /**
3705
- * Generates content on device.
3706
- *
3707
- * @remarks
3708
- * This is comparable to {@link GenerativeModel.generateContent} for generating content in
3709
- * Cloud.
3710
- * @param request - a standard Firebase AI {@link GenerateContentRequest}
3711
- * @returns {@link Response}, so we can reuse common response formatting.
3712
- */
3713
- async generateContent(request) {
3714
- const session = await this.createSession();
3715
- const contents = await Promise.all(request.contents.map(ChromeAdapterImpl.toLanguageModelMessage));
3716
- const text = await session.prompt(contents, this.onDeviceParams.promptOptions);
3717
- return ChromeAdapterImpl.toResponse(text);
3718
- }
3719
- /**
3720
- * Generates content stream on device.
3721
- *
3722
- * @remarks
3723
- * This is comparable to {@link GenerativeModel.generateContentStream} for generating content in
3724
- * Cloud.
3725
- * @param request - a standard Firebase AI {@link GenerateContentRequest}
3726
- * @returns {@link Response}, so we can reuse common response formatting.
3727
- */
3728
- async generateContentStream(request) {
3729
- const session = await this.createSession();
3730
- const contents = await Promise.all(request.contents.map(ChromeAdapterImpl.toLanguageModelMessage));
3731
- const stream = session.promptStreaming(contents, this.onDeviceParams.promptOptions);
3732
- return ChromeAdapterImpl.toStreamResponse(stream);
3733
- }
3734
- async countTokens(_request) {
3735
- throw new AIError(AIErrorCode.REQUEST_ERROR, 'Count Tokens is not yet available for on-device model.');
3736
- }
3737
- /**
3738
- * Asserts inference for the given request can be performed by an on-device model.
3739
- */
3740
- static isOnDeviceRequest(request) {
3741
- // Returns false if the prompt is empty.
3742
- if (request.contents.length === 0) {
3743
- logger.debug('Empty prompt rejected for on-device inference.');
3744
- return false;
3745
- }
3746
- for (const content of request.contents) {
3747
- if (content.role === 'function') {
3748
- logger.debug(`"Function" role rejected for on-device inference.`);
3749
- return false;
3750
- }
3751
- // Returns false if request contains an image with an unsupported mime type.
3752
- for (const part of content.parts) {
3753
- if (part.inlineData &&
3754
- ChromeAdapterImpl.SUPPORTED_MIME_TYPES.indexOf(part.inlineData.mimeType) === -1) {
3755
- logger.debug(`Unsupported mime type "${part.inlineData.mimeType}" rejected for on-device inference.`);
3756
- return false;
3757
- }
3758
- }
3759
- }
3760
- return true;
3761
- }
3762
- /**
3763
- * Encapsulates logic to get availability and download a model if one is downloadable.
3764
- */
3765
- async downloadIfAvailable() {
3766
- const availability = await this.languageModelProvider?.availability(this.onDeviceParams.createOptions);
3767
- if (availability === Availability.DOWNLOADABLE) {
3768
- this.download();
3769
- }
3770
- return availability;
3771
- }
3772
- /**
3773
- * Triggers out-of-band download of an on-device model.
3774
- *
3775
- * Chrome only downloads models as needed. Chrome knows a model is needed when code calls
3776
- * LanguageModel.create.
3777
- *
3778
- * Since Chrome manages the download, the SDK can only avoid redundant download requests by
3779
- * tracking if a download has previously been requested.
3780
- */
3781
- download() {
3782
- if (this.isDownloading) {
3783
- return;
3784
- }
3785
- this.isDownloading = true;
3786
- this.downloadPromise = this.languageModelProvider
3787
- ?.create(this.onDeviceParams.createOptions)
3788
- .finally(() => {
3789
- this.isDownloading = false;
3790
- });
3791
- }
3792
- /**
3793
- * Converts Firebase AI {@link Content} object to a Chrome {@link LanguageModelMessage} object.
3794
- */
3795
- static async toLanguageModelMessage(content) {
3796
- const languageModelMessageContents = await Promise.all(content.parts.map(ChromeAdapterImpl.toLanguageModelMessageContent));
3797
- return {
3798
- role: ChromeAdapterImpl.toLanguageModelMessageRole(content.role),
3799
- content: languageModelMessageContents
3800
- };
3801
- }
3802
- /**
3803
- * Converts a Firebase AI Part object to a Chrome LanguageModelMessageContent object.
3804
- */
3805
- static async toLanguageModelMessageContent(part) {
3806
- if (part.text) {
3807
- return {
3808
- type: 'text',
3809
- value: part.text
3810
- };
3811
- }
3812
- else if (part.inlineData) {
3813
- const formattedImageContent = await fetch(`data:${part.inlineData.mimeType};base64,${part.inlineData.data}`);
3814
- const imageBlob = await formattedImageContent.blob();
3815
- const imageBitmap = await createImageBitmap(imageBlob);
3816
- return {
3817
- type: 'image',
3818
- value: imageBitmap
3819
- };
3820
- }
3821
- throw new AIError(AIErrorCode.REQUEST_ERROR, `Processing of this Part type is not currently supported.`);
3822
- }
3823
- /**
3824
- * Converts a Firebase AI {@link Role} string to a {@link LanguageModelMessageRole} string.
3825
- */
3826
- static toLanguageModelMessageRole(role) {
3827
- // Assumes 'function' rule has been filtered by isOnDeviceRequest
3828
- return role === 'model' ? 'assistant' : 'user';
3829
- }
3830
- /**
3831
- * Abstracts Chrome session creation.
3832
- *
3833
- * Chrome uses a multi-turn session for all inference. Firebase AI uses single-turn for all
3834
- * inference. To map the Firebase AI API to Chrome's API, the SDK creates a new session for all
3835
- * inference.
3836
- *
3837
- * Chrome will remove a model from memory if it's no longer in use, so this method ensures a
3838
- * new session is created before an old session is destroyed.
3839
- */
3840
- async createSession() {
3841
- if (!this.languageModelProvider) {
3842
- throw new AIError(AIErrorCode.UNSUPPORTED, 'Chrome AI requested for unsupported browser version.');
3843
- }
3844
- const newSession = await this.languageModelProvider.create(this.onDeviceParams.createOptions);
3845
- if (this.oldSession) {
3846
- this.oldSession.destroy();
3847
- }
3848
- // Holds session reference, so model isn't unloaded from memory.
3849
- this.oldSession = newSession;
3850
- return newSession;
3851
- }
3852
- /**
3853
- * Formats string returned by Chrome as a {@link Response} returned by Firebase AI.
3854
- */
3855
- static toResponse(text) {
3856
- return {
3857
- json: async () => ({
3858
- candidates: [
3859
- {
3860
- content: {
3861
- parts: [{ text }]
3862
- }
3863
- }
3864
- ]
3865
- })
3866
- };
3867
- }
3868
- /**
3869
- * Formats string stream returned by Chrome as SSE returned by Firebase AI.
3870
- */
3871
- static toStreamResponse(stream) {
3872
- const encoder = new TextEncoder();
3873
- return {
3874
- body: stream.pipeThrough(new TransformStream({
3875
- transform(chunk, controller) {
3876
- const json = JSON.stringify({
3877
- candidates: [
3878
- {
3879
- content: {
3880
- role: 'model',
3881
- parts: [{ text: chunk }]
3882
- }
3883
- }
3884
- ]
3885
- });
3886
- controller.enqueue(encoder.encode(`data: ${json}\n\n`));
3887
- }
3888
- }))
3889
- };
3890
- }
3891
- }
3892
- // Visible for testing
3893
- ChromeAdapterImpl.SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png'];
3894
- /**
3895
- * Creates a ChromeAdapterImpl on demand.
3896
- */
3897
- function chromeAdapterFactory(mode, window, params) {
3898
- // Do not initialize a ChromeAdapter if we are not in hybrid mode.
3899
- if (typeof window !== 'undefined' && mode) {
3900
- return new ChromeAdapterImpl(window.LanguageModel, mode, params);
3901
- }
3902
- }
3903
-
3904
- /**
3905
- * The Firebase AI Web SDK.
3906
- *
3907
- * @packageDocumentation
3908
- */
3909
- function factory(container, { instanceIdentifier }) {
3910
- if (!instanceIdentifier) {
3911
- throw new AIError(AIErrorCode.ERROR, 'AIService instance identifier is undefined.');
3912
- }
3913
- const backend = decodeInstanceIdentifier(instanceIdentifier);
3914
- // getImmediate for FirebaseApp will always succeed
3915
- const app = container.getProvider('app').getImmediate();
3916
- const auth = container.getProvider('auth-internal');
3917
- const appCheckProvider = container.getProvider('app-check-internal');
3918
- return new AIService(app, backend, auth, appCheckProvider, chromeAdapterFactory);
3919
- }
3920
4031
  function registerAI() {
3921
4032
  _registerComponent(new Component(AI_TYPE, factory, "PUBLIC" /* ComponentType.PUBLIC */).setMultipleInstances(true));
3922
4033
  registerVersion(name, version);
@@ -3925,5 +4036,5 @@ function registerAI() {
3925
4036
  }
3926
4037
  registerAI();
3927
4038
 
3928
- export { AIError, AIErrorCode, AIModel, AnyOfSchema, ArraySchema, Backend, BackendType, BlockReason, BooleanSchema, ChatSession, FinishReason, FunctionCallingMode, GenerativeModel, GoogleAIBackend, HarmBlockMethod, HarmBlockThreshold, HarmCategory, HarmProbability, HarmSeverity, ImagenAspectRatio, ImagenImageFormat, ImagenModel, ImagenPersonFilterLevel, ImagenSafetyFilterLevel, InferenceMode, IntegerSchema, LiveGenerativeModel, LiveResponseType, LiveSession, Modality, NumberSchema, ObjectSchema, POSSIBLE_ROLES, ResponseModality, Schema, SchemaType, StringSchema, VertexAIBackend, getAI, getGenerativeModel, getImagenModel, getLiveGenerativeModel, startAudioConversation };
4039
+ export { AIError, AIErrorCode, AIModel, AnyOfSchema, ArraySchema, Backend, BackendType, BlockReason, BooleanSchema, ChatSession, FinishReason, FunctionCallingMode, GenerativeModel, GoogleAIBackend, HarmBlockMethod, HarmBlockThreshold, HarmCategory, HarmProbability, HarmSeverity, ImagenAspectRatio, ImagenImageFormat, ImagenModel, ImagenPersonFilterLevel, ImagenSafetyFilterLevel, InferenceMode, IntegerSchema, Language, LiveGenerativeModel, LiveResponseType, LiveSession, Modality, NumberSchema, ObjectSchema, Outcome, POSSIBLE_ROLES, ResponseModality, Schema, SchemaType, StringSchema, VertexAIBackend, getAI, getGenerativeModel, getImagenModel, getLiveGenerativeModel, startAudioConversation };
3929
4040
  //# sourceMappingURL=index.esm.js.map