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