@depup/firebase__ai 2.9.0-depup.0

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.
Files changed (119) hide show
  1. package/README.md +31 -0
  2. package/changes.json +10 -0
  3. package/dist/ai-public.d.ts +3472 -0
  4. package/dist/ai.d.ts +3712 -0
  5. package/dist/esm/index.esm.js +4765 -0
  6. package/dist/esm/index.esm.js.map +1 -0
  7. package/dist/esm/package.json +1 -0
  8. package/dist/esm/src/api.d.ts +121 -0
  9. package/dist/esm/src/backend.d.ts +98 -0
  10. package/dist/esm/src/constants.d.ts +29 -0
  11. package/dist/esm/src/errors.d.ts +35 -0
  12. package/dist/esm/src/factory-browser.d.ts +19 -0
  13. package/dist/esm/src/factory-node.d.ts +19 -0
  14. package/dist/esm/src/googleai-mappers.d.ts +73 -0
  15. package/dist/esm/src/helpers.d.ts +30 -0
  16. package/dist/esm/src/index.d.ts +13 -0
  17. package/dist/esm/src/index.node.d.ts +7 -0
  18. package/dist/esm/src/logger.d.ts +18 -0
  19. package/dist/esm/src/methods/chat-session-helpers.d.ts +18 -0
  20. package/dist/esm/src/methods/chat-session.d.ts +77 -0
  21. package/dist/esm/src/methods/chrome-adapter.d.ts +124 -0
  22. package/dist/esm/src/methods/count-tokens.d.ts +21 -0
  23. package/dist/esm/src/methods/generate-content.d.ts +25 -0
  24. package/dist/esm/src/methods/live-session-helpers.d.ts +154 -0
  25. package/dist/esm/src/methods/live-session.d.ts +154 -0
  26. package/dist/esm/src/models/ai-model.d.ts +72 -0
  27. package/dist/esm/src/models/generative-model.d.ts +56 -0
  28. package/dist/esm/src/models/imagen-model.d.ts +102 -0
  29. package/dist/esm/src/models/index.d.ts +20 -0
  30. package/dist/esm/src/models/live-generative-model.d.ts +55 -0
  31. package/dist/esm/src/models/template-generative-model.d.ts +64 -0
  32. package/dist/esm/src/models/template-imagen-model.d.ts +51 -0
  33. package/dist/esm/src/models/utils.d.ts +26 -0
  34. package/dist/esm/src/public-types.d.ts +97 -0
  35. package/dist/esm/src/requests/hybrid-helpers.d.ts +33 -0
  36. package/dist/esm/src/requests/imagen-image-format.d.ts +61 -0
  37. package/dist/esm/src/requests/request-helpers.d.ts +28 -0
  38. package/dist/esm/src/requests/request.d.ts +69 -0
  39. package/dist/esm/src/requests/response-helpers.d.ts +57 -0
  40. package/dist/esm/src/requests/schema-builder.d.ts +170 -0
  41. package/dist/esm/src/requests/stream-reader.d.ts +39 -0
  42. package/dist/esm/src/service.d.ts +35 -0
  43. package/dist/esm/src/types/chrome-adapter.d.ts +61 -0
  44. package/dist/esm/src/types/content.d.ts +266 -0
  45. package/dist/esm/src/types/enums.d.ts +419 -0
  46. package/dist/esm/src/types/error.d.ts +89 -0
  47. package/dist/esm/src/types/googleai.d.ts +57 -0
  48. package/dist/esm/src/types/imagen/index.d.ts +18 -0
  49. package/dist/esm/src/types/imagen/internal.d.ts +134 -0
  50. package/dist/esm/src/types/imagen/requests.d.ts +245 -0
  51. package/dist/esm/src/types/imagen/responses.d.ts +79 -0
  52. package/dist/esm/src/types/index.d.ts +26 -0
  53. package/dist/esm/src/types/internal.d.ts +35 -0
  54. package/dist/esm/src/types/language-model.d.ts +107 -0
  55. package/dist/esm/src/types/live-responses.d.ts +79 -0
  56. package/dist/esm/src/types/requests.d.ts +543 -0
  57. package/dist/esm/src/types/responses.d.ts +607 -0
  58. package/dist/esm/src/types/schema.d.ts +139 -0
  59. package/dist/esm/src/websocket.d.ts +67 -0
  60. package/dist/index.cjs.js +4820 -0
  61. package/dist/index.cjs.js.map +1 -0
  62. package/dist/index.node.cjs.js +4512 -0
  63. package/dist/index.node.cjs.js.map +1 -0
  64. package/dist/index.node.mjs +4457 -0
  65. package/dist/index.node.mjs.map +1 -0
  66. package/dist/src/api.d.ts +121 -0
  67. package/dist/src/backend.d.ts +98 -0
  68. package/dist/src/constants.d.ts +29 -0
  69. package/dist/src/errors.d.ts +35 -0
  70. package/dist/src/factory-browser.d.ts +19 -0
  71. package/dist/src/factory-node.d.ts +19 -0
  72. package/dist/src/googleai-mappers.d.ts +73 -0
  73. package/dist/src/helpers.d.ts +30 -0
  74. package/dist/src/index.d.ts +13 -0
  75. package/dist/src/index.node.d.ts +7 -0
  76. package/dist/src/logger.d.ts +18 -0
  77. package/dist/src/methods/chat-session-helpers.d.ts +18 -0
  78. package/dist/src/methods/chat-session.d.ts +77 -0
  79. package/dist/src/methods/chrome-adapter.d.ts +124 -0
  80. package/dist/src/methods/count-tokens.d.ts +21 -0
  81. package/dist/src/methods/generate-content.d.ts +25 -0
  82. package/dist/src/methods/live-session-helpers.d.ts +154 -0
  83. package/dist/src/methods/live-session.d.ts +154 -0
  84. package/dist/src/models/ai-model.d.ts +72 -0
  85. package/dist/src/models/generative-model.d.ts +56 -0
  86. package/dist/src/models/imagen-model.d.ts +102 -0
  87. package/dist/src/models/index.d.ts +20 -0
  88. package/dist/src/models/live-generative-model.d.ts +55 -0
  89. package/dist/src/models/template-generative-model.d.ts +64 -0
  90. package/dist/src/models/template-imagen-model.d.ts +51 -0
  91. package/dist/src/models/utils.d.ts +26 -0
  92. package/dist/src/public-types.d.ts +97 -0
  93. package/dist/src/requests/hybrid-helpers.d.ts +33 -0
  94. package/dist/src/requests/imagen-image-format.d.ts +61 -0
  95. package/dist/src/requests/request-helpers.d.ts +28 -0
  96. package/dist/src/requests/request.d.ts +69 -0
  97. package/dist/src/requests/response-helpers.d.ts +57 -0
  98. package/dist/src/requests/schema-builder.d.ts +170 -0
  99. package/dist/src/requests/stream-reader.d.ts +39 -0
  100. package/dist/src/service.d.ts +35 -0
  101. package/dist/src/tsdoc-metadata.json +11 -0
  102. package/dist/src/types/chrome-adapter.d.ts +61 -0
  103. package/dist/src/types/content.d.ts +266 -0
  104. package/dist/src/types/enums.d.ts +419 -0
  105. package/dist/src/types/error.d.ts +89 -0
  106. package/dist/src/types/googleai.d.ts +57 -0
  107. package/dist/src/types/imagen/index.d.ts +18 -0
  108. package/dist/src/types/imagen/internal.d.ts +134 -0
  109. package/dist/src/types/imagen/requests.d.ts +245 -0
  110. package/dist/src/types/imagen/responses.d.ts +79 -0
  111. package/dist/src/types/index.d.ts +26 -0
  112. package/dist/src/types/internal.d.ts +35 -0
  113. package/dist/src/types/language-model.d.ts +107 -0
  114. package/dist/src/types/live-responses.d.ts +79 -0
  115. package/dist/src/types/requests.d.ts +543 -0
  116. package/dist/src/types/responses.d.ts +607 -0
  117. package/dist/src/types/schema.d.ts +139 -0
  118. package/dist/src/websocket.d.ts +67 -0
  119. package/package.json +106 -0
@@ -0,0 +1,4820 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var app = require('@firebase/app');
6
+ var component = require('@firebase/component');
7
+ var util = require('@firebase/util');
8
+ var logger$1 = require('@firebase/logger');
9
+
10
+ var name = "@firebase/ai";
11
+ var version = "2.9.0";
12
+
13
+ /**
14
+ * @license
15
+ * Copyright 2024 Google LLC
16
+ *
17
+ * Licensed under the Apache License, Version 2.0 (the "License");
18
+ * you may not use this file except in compliance with the License.
19
+ * You may obtain a copy of the License at
20
+ *
21
+ * http://www.apache.org/licenses/LICENSE-2.0
22
+ *
23
+ * Unless required by applicable law or agreed to in writing, software
24
+ * distributed under the License is distributed on an "AS IS" BASIS,
25
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26
+ * See the License for the specific language governing permissions and
27
+ * limitations under the License.
28
+ */
29
+ const AI_TYPE = 'AI';
30
+ const DEFAULT_LOCATION = 'us-central1';
31
+ const DEFAULT_DOMAIN = 'firebasevertexai.googleapis.com';
32
+ const DEFAULT_API_VERSION = 'v1beta';
33
+ const PACKAGE_VERSION = version;
34
+ const LANGUAGE_TAG = 'gl-js';
35
+ const HYBRID_TAG = 'hybrid';
36
+ const DEFAULT_FETCH_TIMEOUT_MS = 180 * 1000;
37
+ /**
38
+ * Defines the name of the default in-cloud model to use for hybrid inference.
39
+ */
40
+ const DEFAULT_HYBRID_IN_CLOUD_MODEL = 'gemini-2.5-flash-lite';
41
+
42
+ /**
43
+ * @license
44
+ * Copyright 2024 Google LLC
45
+ *
46
+ * Licensed under the Apache License, Version 2.0 (the "License");
47
+ * you may not use this file except in compliance with the License.
48
+ * You may obtain a copy of the License at
49
+ *
50
+ * http://www.apache.org/licenses/LICENSE-2.0
51
+ *
52
+ * Unless required by applicable law or agreed to in writing, software
53
+ * distributed under the License is distributed on an "AS IS" BASIS,
54
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
55
+ * See the License for the specific language governing permissions and
56
+ * limitations under the License.
57
+ */
58
+ /**
59
+ * Error class for the Firebase AI SDK.
60
+ *
61
+ * @public
62
+ */
63
+ class AIError extends util.FirebaseError {
64
+ /**
65
+ * Constructs a new instance of the `AIError` class.
66
+ *
67
+ * @param code - The error code from {@link (AIErrorCode:type)}.
68
+ * @param message - A human-readable message describing the error.
69
+ * @param customErrorData - Optional error data.
70
+ */
71
+ constructor(code, message, customErrorData) {
72
+ // Match error format used by FirebaseError from ErrorFactory
73
+ const service = AI_TYPE;
74
+ const fullCode = `${service}/${code}`;
75
+ const fullMessage = `${service}: ${message} (${fullCode})`;
76
+ super(code, fullMessage);
77
+ this.code = code;
78
+ this.customErrorData = customErrorData;
79
+ // FirebaseError initializes a stack trace, but it assumes the error is created from the error
80
+ // factory. Since we break this assumption, we set the stack trace to be originating from this
81
+ // constructor.
82
+ // This is only supported in V8.
83
+ if (Error.captureStackTrace) {
84
+ // Allows us to initialize the stack trace without including the constructor itself at the
85
+ // top level of the stack trace.
86
+ Error.captureStackTrace(this, AIError);
87
+ }
88
+ // Allows instanceof AIError in ES5/ES6
89
+ // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
90
+ // TODO(dlarocque): Replace this with `new.target`: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
91
+ // which we can now use since we no longer target ES5.
92
+ Object.setPrototypeOf(this, AIError.prototype);
93
+ // Since Error is an interface, we don't inherit toString and so we define it ourselves.
94
+ this.toString = () => fullMessage;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * @license
100
+ * Copyright 2024 Google LLC
101
+ *
102
+ * Licensed under the Apache License, Version 2.0 (the "License");
103
+ * you may not use this file except in compliance with the License.
104
+ * You may obtain a copy of the License at
105
+ *
106
+ * http://www.apache.org/licenses/LICENSE-2.0
107
+ *
108
+ * Unless required by applicable law or agreed to in writing, software
109
+ * distributed under the License is distributed on an "AS IS" BASIS,
110
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
111
+ * See the License for the specific language governing permissions and
112
+ * limitations under the License.
113
+ */
114
+ /**
115
+ * Possible roles.
116
+ * @public
117
+ */
118
+ const POSSIBLE_ROLES = ['user', 'model', 'function', 'system'];
119
+ /**
120
+ * Harm categories that would cause prompts or candidates to be blocked.
121
+ * @public
122
+ */
123
+ const HarmCategory = {
124
+ HARM_CATEGORY_HATE_SPEECH: 'HARM_CATEGORY_HATE_SPEECH',
125
+ HARM_CATEGORY_SEXUALLY_EXPLICIT: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
126
+ HARM_CATEGORY_HARASSMENT: 'HARM_CATEGORY_HARASSMENT',
127
+ HARM_CATEGORY_DANGEROUS_CONTENT: 'HARM_CATEGORY_DANGEROUS_CONTENT'
128
+ };
129
+ /**
130
+ * Threshold above which a prompt or candidate will be blocked.
131
+ * @public
132
+ */
133
+ const HarmBlockThreshold = {
134
+ /**
135
+ * Content with `NEGLIGIBLE` will be allowed.
136
+ */
137
+ BLOCK_LOW_AND_ABOVE: 'BLOCK_LOW_AND_ABOVE',
138
+ /**
139
+ * Content with `NEGLIGIBLE` and `LOW` will be allowed.
140
+ */
141
+ BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE',
142
+ /**
143
+ * Content with `NEGLIGIBLE`, `LOW`, and `MEDIUM` will be allowed.
144
+ */
145
+ BLOCK_ONLY_HIGH: 'BLOCK_ONLY_HIGH',
146
+ /**
147
+ * All content will be allowed.
148
+ */
149
+ BLOCK_NONE: 'BLOCK_NONE',
150
+ /**
151
+ * All content will be allowed. This is the same as `BLOCK_NONE`, but the metadata corresponding
152
+ * to the {@link (HarmCategory:type)} will not be present in the response.
153
+ */
154
+ OFF: 'OFF'
155
+ };
156
+ /**
157
+ * This property is not supported in the Gemini Developer API ({@link GoogleAIBackend}).
158
+ *
159
+ * @public
160
+ */
161
+ const HarmBlockMethod = {
162
+ /**
163
+ * The harm block method uses both probability and severity scores.
164
+ */
165
+ SEVERITY: 'SEVERITY',
166
+ /**
167
+ * The harm block method uses the probability score.
168
+ */
169
+ PROBABILITY: 'PROBABILITY'
170
+ };
171
+ /**
172
+ * Probability that a prompt or candidate matches a harm category.
173
+ * @public
174
+ */
175
+ const HarmProbability = {
176
+ /**
177
+ * Content has a negligible chance of being unsafe.
178
+ */
179
+ NEGLIGIBLE: 'NEGLIGIBLE',
180
+ /**
181
+ * Content has a low chance of being unsafe.
182
+ */
183
+ LOW: 'LOW',
184
+ /**
185
+ * Content has a medium chance of being unsafe.
186
+ */
187
+ MEDIUM: 'MEDIUM',
188
+ /**
189
+ * Content has a high chance of being unsafe.
190
+ */
191
+ HIGH: 'HIGH'
192
+ };
193
+ /**
194
+ * Harm severity levels.
195
+ * @public
196
+ */
197
+ const HarmSeverity = {
198
+ /**
199
+ * Negligible level of harm severity.
200
+ */
201
+ HARM_SEVERITY_NEGLIGIBLE: 'HARM_SEVERITY_NEGLIGIBLE',
202
+ /**
203
+ * Low level of harm severity.
204
+ */
205
+ HARM_SEVERITY_LOW: 'HARM_SEVERITY_LOW',
206
+ /**
207
+ * Medium level of harm severity.
208
+ */
209
+ HARM_SEVERITY_MEDIUM: 'HARM_SEVERITY_MEDIUM',
210
+ /**
211
+ * High level of harm severity.
212
+ */
213
+ HARM_SEVERITY_HIGH: 'HARM_SEVERITY_HIGH',
214
+ /**
215
+ * Harm severity is not supported.
216
+ *
217
+ * @remarks
218
+ * The GoogleAI backend does not support `HarmSeverity`, so this value is used as a fallback.
219
+ */
220
+ HARM_SEVERITY_UNSUPPORTED: 'HARM_SEVERITY_UNSUPPORTED'
221
+ };
222
+ /**
223
+ * Reason that a prompt was blocked.
224
+ * @public
225
+ */
226
+ const BlockReason = {
227
+ /**
228
+ * Content was blocked by safety settings.
229
+ */
230
+ SAFETY: 'SAFETY',
231
+ /**
232
+ * Content was blocked, but the reason is uncategorized.
233
+ */
234
+ OTHER: 'OTHER',
235
+ /**
236
+ * Content was blocked because it contained terms from the terminology blocklist.
237
+ */
238
+ BLOCKLIST: 'BLOCKLIST',
239
+ /**
240
+ * Content was blocked due to prohibited content.
241
+ */
242
+ PROHIBITED_CONTENT: 'PROHIBITED_CONTENT'
243
+ };
244
+ /**
245
+ * Reason that a candidate finished.
246
+ * @public
247
+ */
248
+ const FinishReason = {
249
+ /**
250
+ * Natural stop point of the model or provided stop sequence.
251
+ */
252
+ STOP: 'STOP',
253
+ /**
254
+ * The maximum number of tokens as specified in the request was reached.
255
+ */
256
+ MAX_TOKENS: 'MAX_TOKENS',
257
+ /**
258
+ * The candidate content was flagged for safety reasons.
259
+ */
260
+ SAFETY: 'SAFETY',
261
+ /**
262
+ * The candidate content was flagged for recitation reasons.
263
+ */
264
+ RECITATION: 'RECITATION',
265
+ /**
266
+ * Unknown reason.
267
+ */
268
+ OTHER: 'OTHER',
269
+ /**
270
+ * The candidate content contained forbidden terms.
271
+ */
272
+ BLOCKLIST: 'BLOCKLIST',
273
+ /**
274
+ * The candidate content potentially contained prohibited content.
275
+ */
276
+ PROHIBITED_CONTENT: 'PROHIBITED_CONTENT',
277
+ /**
278
+ * The candidate content potentially contained Sensitive Personally Identifiable Information (SPII).
279
+ */
280
+ SPII: 'SPII',
281
+ /**
282
+ * The function call generated by the model was invalid.
283
+ */
284
+ MALFORMED_FUNCTION_CALL: 'MALFORMED_FUNCTION_CALL'
285
+ };
286
+ /**
287
+ * @public
288
+ */
289
+ const FunctionCallingMode = {
290
+ /**
291
+ * Default model behavior; model decides to predict either a function call
292
+ * or a natural language response.
293
+ */
294
+ AUTO: 'AUTO',
295
+ /**
296
+ * Model is constrained to always predicting a function call only.
297
+ * If `allowed_function_names` is set, the predicted function call will be
298
+ * limited to any one of `allowed_function_names`, else the predicted
299
+ * function call will be any one of the provided `function_declarations`.
300
+ */
301
+ ANY: 'ANY',
302
+ /**
303
+ * Model will not predict any function call. Model behavior is same as when
304
+ * not passing any function declarations.
305
+ */
306
+ NONE: 'NONE'
307
+ };
308
+ /**
309
+ * Content part modality.
310
+ * @public
311
+ */
312
+ const Modality = {
313
+ /**
314
+ * Unspecified modality.
315
+ */
316
+ MODALITY_UNSPECIFIED: 'MODALITY_UNSPECIFIED',
317
+ /**
318
+ * Plain text.
319
+ */
320
+ TEXT: 'TEXT',
321
+ /**
322
+ * Image.
323
+ */
324
+ IMAGE: 'IMAGE',
325
+ /**
326
+ * Video.
327
+ */
328
+ VIDEO: 'VIDEO',
329
+ /**
330
+ * Audio.
331
+ */
332
+ AUDIO: 'AUDIO',
333
+ /**
334
+ * Document (for example, PDF).
335
+ */
336
+ DOCUMENT: 'DOCUMENT'
337
+ };
338
+ /**
339
+ * Generation modalities to be returned in generation responses.
340
+ *
341
+ * @beta
342
+ */
343
+ const ResponseModality = {
344
+ /**
345
+ * Text.
346
+ * @beta
347
+ */
348
+ TEXT: 'TEXT',
349
+ /**
350
+ * Image.
351
+ * @beta
352
+ */
353
+ IMAGE: 'IMAGE',
354
+ /**
355
+ * Audio.
356
+ * @beta
357
+ */
358
+ AUDIO: 'AUDIO'
359
+ };
360
+ /**
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
+ *
379
+ * @beta
380
+ */
381
+ const InferenceMode = {
382
+ 'PREFER_ON_DEVICE': 'prefer_on_device',
383
+ 'ONLY_ON_DEVICE': 'only_on_device',
384
+ 'ONLY_IN_CLOUD': 'only_in_cloud',
385
+ 'PREFER_IN_CLOUD': 'prefer_in_cloud'
386
+ };
387
+ /**
388
+ * Indicates whether inference happened on-device or in-cloud.
389
+ *
390
+ * @beta
391
+ */
392
+ const InferenceSource = {
393
+ 'ON_DEVICE': 'on_device',
394
+ 'IN_CLOUD': 'in_cloud'
395
+ };
396
+ /**
397
+ * Represents the result of the code execution.
398
+ *
399
+ * @public
400
+ */
401
+ const Outcome = {
402
+ UNSPECIFIED: 'OUTCOME_UNSPECIFIED',
403
+ OK: 'OUTCOME_OK',
404
+ FAILED: 'OUTCOME_FAILED',
405
+ DEADLINE_EXCEEDED: 'OUTCOME_DEADLINE_EXCEEDED'
406
+ };
407
+ /**
408
+ * The programming language of the code.
409
+ *
410
+ * @public
411
+ */
412
+ const Language = {
413
+ UNSPECIFIED: 'LANGUAGE_UNSPECIFIED',
414
+ PYTHON: 'PYTHON'
415
+ };
416
+ /**
417
+ * A preset that controls the model's "thinking" process. Use
418
+ * `ThinkingLevel.LOW` for faster responses on less complex tasks, and
419
+ * `ThinkingLevel.HIGH` for better reasoning on more complex tasks.
420
+ *
421
+ * @public
422
+ */
423
+ const ThinkingLevel = {
424
+ MINIMAL: 'MINIMAL',
425
+ LOW: 'LOW',
426
+ MEDIUM: 'MEDIUM',
427
+ HIGH: 'HIGH'
428
+ };
429
+
430
+ /**
431
+ * @license
432
+ * Copyright 2024 Google LLC
433
+ *
434
+ * Licensed under the Apache License, Version 2.0 (the "License");
435
+ * you may not use this file except in compliance with the License.
436
+ * You may obtain a copy of the License at
437
+ *
438
+ * http://www.apache.org/licenses/LICENSE-2.0
439
+ *
440
+ * Unless required by applicable law or agreed to in writing, software
441
+ * distributed under the License is distributed on an "AS IS" BASIS,
442
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
443
+ * See the License for the specific language governing permissions and
444
+ * limitations under the License.
445
+ */
446
+ /**
447
+ * The status of a URL retrieval.
448
+ *
449
+ * @remarks
450
+ * <b>URL_RETRIEVAL_STATUS_UNSPECIFIED:</b> Unspecified retrieval status.
451
+ * <br/>
452
+ * <b>URL_RETRIEVAL_STATUS_SUCCESS:</b> The URL retrieval was successful.
453
+ * <br/>
454
+ * <b>URL_RETRIEVAL_STATUS_ERROR:</b> The URL retrieval failed.
455
+ * <br/>
456
+ * <b>URL_RETRIEVAL_STATUS_PAYWALL:</b> The URL retrieval failed because the content is behind a paywall.
457
+ * <br/>
458
+ * <b>URL_RETRIEVAL_STATUS_UNSAFE:</b> The URL retrieval failed because the content is unsafe.
459
+ * <br/>
460
+ *
461
+ * @public
462
+ */
463
+ const URLRetrievalStatus = {
464
+ /**
465
+ * Unspecified retrieval status.
466
+ */
467
+ URL_RETRIEVAL_STATUS_UNSPECIFIED: 'URL_RETRIEVAL_STATUS_UNSPECIFIED',
468
+ /**
469
+ * The URL retrieval was successful.
470
+ */
471
+ URL_RETRIEVAL_STATUS_SUCCESS: 'URL_RETRIEVAL_STATUS_SUCCESS',
472
+ /**
473
+ * The URL retrieval failed.
474
+ */
475
+ URL_RETRIEVAL_STATUS_ERROR: 'URL_RETRIEVAL_STATUS_ERROR',
476
+ /**
477
+ * The URL retrieval failed because the content is behind a paywall.
478
+ */
479
+ URL_RETRIEVAL_STATUS_PAYWALL: 'URL_RETRIEVAL_STATUS_PAYWALL',
480
+ /**
481
+ * The URL retrieval failed because the content is unsafe.
482
+ */
483
+ URL_RETRIEVAL_STATUS_UNSAFE: 'URL_RETRIEVAL_STATUS_UNSAFE'
484
+ };
485
+ /**
486
+ * The types of responses that can be returned by {@link LiveSession.receive}.
487
+ *
488
+ * @beta
489
+ */
490
+ const LiveResponseType = {
491
+ SERVER_CONTENT: 'serverContent',
492
+ TOOL_CALL: 'toolCall',
493
+ TOOL_CALL_CANCELLATION: 'toolCallCancellation',
494
+ GOING_AWAY_NOTICE: 'goingAwayNotice'
495
+ };
496
+
497
+ /**
498
+ * @license
499
+ * Copyright 2024 Google LLC
500
+ *
501
+ * Licensed under the Apache License, Version 2.0 (the "License");
502
+ * you may not use this file except in compliance with the License.
503
+ * You may obtain a copy of the License at
504
+ *
505
+ * http://www.apache.org/licenses/LICENSE-2.0
506
+ *
507
+ * Unless required by applicable law or agreed to in writing, software
508
+ * distributed under the License is distributed on an "AS IS" BASIS,
509
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
510
+ * See the License for the specific language governing permissions and
511
+ * limitations under the License.
512
+ */
513
+ /**
514
+ * Standardized error codes that {@link AIError} can have.
515
+ *
516
+ * @public
517
+ */
518
+ const AIErrorCode = {
519
+ /** A generic error occurred. */
520
+ ERROR: 'error',
521
+ /** An error occurred in a request. */
522
+ REQUEST_ERROR: 'request-error',
523
+ /** An error occurred in a response. */
524
+ RESPONSE_ERROR: 'response-error',
525
+ /** An error occurred while performing a fetch. */
526
+ FETCH_ERROR: 'fetch-error',
527
+ /** An error occurred because an operation was attempted on a closed session. */
528
+ SESSION_CLOSED: 'session-closed',
529
+ /** An error associated with a Content object. */
530
+ INVALID_CONTENT: 'invalid-content',
531
+ /** An error due to the Firebase API not being enabled in the Console. */
532
+ API_NOT_ENABLED: 'api-not-enabled',
533
+ /** An error due to invalid Schema input. */
534
+ INVALID_SCHEMA: 'invalid-schema',
535
+ /** An error occurred due to a missing Firebase API key. */
536
+ NO_API_KEY: 'no-api-key',
537
+ /** An error occurred due to a missing Firebase app ID. */
538
+ NO_APP_ID: 'no-app-id',
539
+ /** An error occurred due to a model name not being specified during initialization. */
540
+ NO_MODEL: 'no-model',
541
+ /** An error occurred due to a missing project ID. */
542
+ NO_PROJECT_ID: 'no-project-id',
543
+ /** An error occurred while parsing. */
544
+ PARSE_FAILED: 'parse-failed',
545
+ /** An error occurred due an attempt to use an unsupported feature. */
546
+ UNSUPPORTED: 'unsupported'
547
+ };
548
+
549
+ /**
550
+ * @license
551
+ * Copyright 2024 Google LLC
552
+ *
553
+ * Licensed under the Apache License, Version 2.0 (the "License");
554
+ * you may not use this file except in compliance with the License.
555
+ * You may obtain a copy of the License at
556
+ *
557
+ * http://www.apache.org/licenses/LICENSE-2.0
558
+ *
559
+ * Unless required by applicable law or agreed to in writing, software
560
+ * distributed under the License is distributed on an "AS IS" BASIS,
561
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
562
+ * See the License for the specific language governing permissions and
563
+ * limitations under the License.
564
+ */
565
+ /**
566
+ * Contains the list of OpenAPI data types
567
+ * as defined by the
568
+ * {@link https://swagger.io/docs/specification/data-models/data-types/ | OpenAPI specification}
569
+ * @public
570
+ */
571
+ const SchemaType = {
572
+ /** String type. */
573
+ STRING: 'string',
574
+ /** Number type. */
575
+ NUMBER: 'number',
576
+ /** Integer type. */
577
+ INTEGER: 'integer',
578
+ /** Boolean type. */
579
+ BOOLEAN: 'boolean',
580
+ /** Array type. */
581
+ ARRAY: 'array',
582
+ /** Object type. */
583
+ OBJECT: 'object'
584
+ };
585
+
586
+ /**
587
+ * @license
588
+ * Copyright 2025 Google LLC
589
+ *
590
+ * Licensed under the Apache License, Version 2.0 (the "License");
591
+ * you may not use this file except in compliance with the License.
592
+ * You may obtain a copy of the License at
593
+ *
594
+ * http://www.apache.org/licenses/LICENSE-2.0
595
+ *
596
+ * Unless required by applicable law or agreed to in writing, software
597
+ * distributed under the License is distributed on an "AS IS" BASIS,
598
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
599
+ * See the License for the specific language governing permissions and
600
+ * limitations under the License.
601
+ */
602
+ /**
603
+ * A filter level controlling how aggressively to filter sensitive content.
604
+ *
605
+ * Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI
606
+ * are assessed against a list of safety filters, which include 'harmful categories' (for example,
607
+ * `violence`, `sexual`, `derogatory`, and `toxic`). This filter level controls how aggressively to
608
+ * filter out potentially harmful content from responses. See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation }
609
+ * and the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters | Responsible AI and usage guidelines}
610
+ * for more details.
611
+ *
612
+ * @public
613
+ */
614
+ const ImagenSafetyFilterLevel = {
615
+ /**
616
+ * The most aggressive filtering level; most strict blocking.
617
+ */
618
+ BLOCK_LOW_AND_ABOVE: 'block_low_and_above',
619
+ /**
620
+ * Blocks some sensitive prompts and responses.
621
+ */
622
+ BLOCK_MEDIUM_AND_ABOVE: 'block_medium_and_above',
623
+ /**
624
+ * Blocks few sensitive prompts and responses.
625
+ */
626
+ BLOCK_ONLY_HIGH: 'block_only_high',
627
+ /**
628
+ * The least aggressive filtering level; blocks very few sensitive prompts and responses.
629
+ *
630
+ * Access to this feature is restricted and may require your case to be reviewed and approved by
631
+ * Cloud support.
632
+ */
633
+ BLOCK_NONE: 'block_none'
634
+ };
635
+ /**
636
+ * A filter level controlling whether generation of images containing people or faces is allowed.
637
+ *
638
+ * See the <a href="http://firebase.google.com/docs/vertex-ai/generate-images">personGeneration</a>
639
+ * documentation for more details.
640
+ *
641
+ * @public
642
+ */
643
+ const ImagenPersonFilterLevel = {
644
+ /**
645
+ * Disallow generation of images containing people or faces; images of people are filtered out.
646
+ */
647
+ BLOCK_ALL: 'dont_allow',
648
+ /**
649
+ * Allow generation of images containing adults only; images of children are filtered out.
650
+ *
651
+ * Generation of images containing people or faces may require your use case to be
652
+ * reviewed and approved by Cloud support; see the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen | Responsible AI and usage guidelines}
653
+ * for more details.
654
+ */
655
+ ALLOW_ADULT: 'allow_adult',
656
+ /**
657
+ * Allow generation of images containing adults only; images of children are filtered out.
658
+ *
659
+ * Generation of images containing people or faces may require your use case to be
660
+ * reviewed and approved by Cloud support; see the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen | Responsible AI and usage guidelines}
661
+ * for more details.
662
+ */
663
+ ALLOW_ALL: 'allow_all'
664
+ };
665
+ /**
666
+ * Aspect ratios for Imagen images.
667
+ *
668
+ * To specify an aspect ratio for generated images, set the `aspectRatio` property in your
669
+ * {@link ImagenGenerationConfig}.
670
+ *
671
+ * See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation }
672
+ * for more details and examples of the supported aspect ratios.
673
+ *
674
+ * @public
675
+ */
676
+ const ImagenAspectRatio = {
677
+ /**
678
+ * Square (1:1) aspect ratio.
679
+ */
680
+ 'SQUARE': '1:1',
681
+ /**
682
+ * Landscape (3:4) aspect ratio.
683
+ */
684
+ 'LANDSCAPE_3x4': '3:4',
685
+ /**
686
+ * Portrait (4:3) aspect ratio.
687
+ */
688
+ 'PORTRAIT_4x3': '4:3',
689
+ /**
690
+ * Landscape (16:9) aspect ratio.
691
+ */
692
+ 'LANDSCAPE_16x9': '16:9',
693
+ /**
694
+ * Portrait (9:16) aspect ratio.
695
+ */
696
+ 'PORTRAIT_9x16': '9:16'
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
+ * An enum-like object containing constants that represent the supported backends
717
+ * for the Firebase AI SDK.
718
+ * This determines which backend service (Vertex AI Gemini API or Gemini Developer API)
719
+ * the SDK will communicate with.
720
+ *
721
+ * These values are assigned to the `backendType` property within the specific backend
722
+ * configuration objects ({@link GoogleAIBackend} or {@link VertexAIBackend}) to identify
723
+ * which service to target.
724
+ *
725
+ * @public
726
+ */
727
+ const BackendType = {
728
+ /**
729
+ * Identifies the backend service for the Vertex AI Gemini API provided through Google Cloud.
730
+ * Use this constant when creating a {@link VertexAIBackend} configuration.
731
+ */
732
+ VERTEX_AI: 'VERTEX_AI',
733
+ /**
734
+ * Identifies the backend service for the Gemini Developer API ({@link https://ai.google/ | Google AI}).
735
+ * Use this constant when creating a {@link GoogleAIBackend} configuration.
736
+ */
737
+ GOOGLE_AI: 'GOOGLE_AI'
738
+ }; // Using 'as const' makes the string values literal types
739
+
740
+ /**
741
+ * @license
742
+ * Copyright 2025 Google LLC
743
+ *
744
+ * Licensed under the Apache License, Version 2.0 (the "License");
745
+ * you may not use this file except in compliance with the License.
746
+ * You may obtain a copy of the License at
747
+ *
748
+ * http://www.apache.org/licenses/LICENSE-2.0
749
+ *
750
+ * Unless required by applicable law or agreed to in writing, software
751
+ * distributed under the License is distributed on an "AS IS" BASIS,
752
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
753
+ * See the License for the specific language governing permissions and
754
+ * limitations under the License.
755
+ */
756
+ /**
757
+ * Abstract base class representing the configuration for an AI service backend.
758
+ * This class should not be instantiated directly. Use its subclasses; {@link GoogleAIBackend} for
759
+ * the Gemini Developer API (via {@link https://ai.google/ | Google AI}), and
760
+ * {@link VertexAIBackend} for the Vertex AI Gemini API.
761
+ *
762
+ * @public
763
+ */
764
+ class Backend {
765
+ /**
766
+ * Protected constructor for use by subclasses.
767
+ * @param type - The backend type.
768
+ */
769
+ constructor(type) {
770
+ this.backendType = type;
771
+ }
772
+ }
773
+ /**
774
+ * Configuration class for the Gemini Developer API.
775
+ *
776
+ * Use this with {@link AIOptions} when initializing the AI service via
777
+ * {@link getAI | getAI()} to specify the Gemini Developer API as the backend.
778
+ *
779
+ * @public
780
+ */
781
+ class GoogleAIBackend extends Backend {
782
+ /**
783
+ * Creates a configuration object for the Gemini Developer API backend.
784
+ */
785
+ constructor() {
786
+ super(BackendType.GOOGLE_AI);
787
+ }
788
+ /**
789
+ * @internal
790
+ */
791
+ _getModelPath(project, model) {
792
+ return `/${DEFAULT_API_VERSION}/projects/${project}/${model}`;
793
+ }
794
+ /**
795
+ * @internal
796
+ */
797
+ _getTemplatePath(project, templateId) {
798
+ return `/${DEFAULT_API_VERSION}/projects/${project}/templates/${templateId}`;
799
+ }
800
+ }
801
+ /**
802
+ * Configuration class for the Vertex AI Gemini API.
803
+ *
804
+ * Use this with {@link AIOptions} when initializing the AI service via
805
+ * {@link getAI | getAI()} to specify the Vertex AI Gemini API as the backend.
806
+ *
807
+ * @public
808
+ */
809
+ class VertexAIBackend extends Backend {
810
+ /**
811
+ * Creates a configuration object for the Vertex AI backend.
812
+ *
813
+ * @param location - The region identifier, defaulting to `us-central1`;
814
+ * see {@link https://firebase.google.com/docs/vertex-ai/locations#available-locations | Vertex AI locations}
815
+ * for a list of supported locations.
816
+ */
817
+ constructor(location = DEFAULT_LOCATION) {
818
+ super(BackendType.VERTEX_AI);
819
+ if (!location) {
820
+ this.location = DEFAULT_LOCATION;
821
+ }
822
+ else {
823
+ this.location = location;
824
+ }
825
+ }
826
+ /**
827
+ * @internal
828
+ */
829
+ _getModelPath(project, model) {
830
+ return `/${DEFAULT_API_VERSION}/projects/${project}/locations/${this.location}/${model}`;
831
+ }
832
+ /**
833
+ * @internal
834
+ */
835
+ _getTemplatePath(project, templateId) {
836
+ return `/${DEFAULT_API_VERSION}/projects/${project}/locations/${this.location}/templates/${templateId}`;
837
+ }
838
+ }
839
+
840
+ /**
841
+ * @license
842
+ * Copyright 2025 Google LLC
843
+ *
844
+ * Licensed under the Apache License, Version 2.0 (the "License");
845
+ * you may not use this file except in compliance with the License.
846
+ * You may obtain a copy of the License at
847
+ *
848
+ * http://www.apache.org/licenses/LICENSE-2.0
849
+ *
850
+ * Unless required by applicable law or agreed to in writing, software
851
+ * distributed under the License is distributed on an "AS IS" BASIS,
852
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
853
+ * See the License for the specific language governing permissions and
854
+ * limitations under the License.
855
+ */
856
+ /**
857
+ * Encodes a {@link Backend} into a string that will be used to uniquely identify {@link AI}
858
+ * instances by backend type.
859
+ *
860
+ * @internal
861
+ */
862
+ function encodeInstanceIdentifier(backend) {
863
+ if (backend instanceof GoogleAIBackend) {
864
+ return `${AI_TYPE}/googleai`;
865
+ }
866
+ else if (backend instanceof VertexAIBackend) {
867
+ return `${AI_TYPE}/vertexai/${backend.location}`;
868
+ }
869
+ else {
870
+ throw new AIError(AIErrorCode.ERROR, `Invalid backend: ${JSON.stringify(backend.backendType)}`);
871
+ }
872
+ }
873
+ /**
874
+ * Decodes an instance identifier string into a {@link Backend}.
875
+ *
876
+ * @internal
877
+ */
878
+ function decodeInstanceIdentifier(instanceIdentifier) {
879
+ const identifierParts = instanceIdentifier.split('/');
880
+ if (identifierParts[0] !== AI_TYPE) {
881
+ throw new AIError(AIErrorCode.ERROR, `Invalid instance identifier, unknown prefix '${identifierParts[0]}'`);
882
+ }
883
+ const backendType = identifierParts[1];
884
+ switch (backendType) {
885
+ case 'vertexai':
886
+ const location = identifierParts[2];
887
+ if (!location) {
888
+ throw new AIError(AIErrorCode.ERROR, `Invalid instance identifier, unknown location '${instanceIdentifier}'`);
889
+ }
890
+ return new VertexAIBackend(location);
891
+ case 'googleai':
892
+ return new GoogleAIBackend();
893
+ default:
894
+ throw new AIError(AIErrorCode.ERROR, `Invalid instance identifier string: '${instanceIdentifier}'`);
895
+ }
896
+ }
897
+
898
+ /**
899
+ * @license
900
+ * Copyright 2024 Google LLC
901
+ *
902
+ * Licensed under the Apache License, Version 2.0 (the "License");
903
+ * you may not use this file except in compliance with the License.
904
+ * You may obtain a copy of the License at
905
+ *
906
+ * http://www.apache.org/licenses/LICENSE-2.0
907
+ *
908
+ * Unless required by applicable law or agreed to in writing, software
909
+ * distributed under the License is distributed on an "AS IS" BASIS,
910
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
911
+ * See the License for the specific language governing permissions and
912
+ * limitations under the License.
913
+ */
914
+ const logger = new logger$1.Logger('@firebase/vertexai');
915
+
916
+ /**
917
+ * @internal
918
+ */
919
+ var Availability;
920
+ (function (Availability) {
921
+ Availability["UNAVAILABLE"] = "unavailable";
922
+ Availability["DOWNLOADABLE"] = "downloadable";
923
+ Availability["DOWNLOADING"] = "downloading";
924
+ Availability["AVAILABLE"] = "available";
925
+ })(Availability || (Availability = {}));
926
+
927
+ /**
928
+ * @license
929
+ * Copyright 2025 Google LLC
930
+ *
931
+ * Licensed under the Apache License, Version 2.0 (the "License");
932
+ * you may not use this file except in compliance with the License.
933
+ * You may obtain a copy of the License at
934
+ *
935
+ * http://www.apache.org/licenses/LICENSE-2.0
936
+ *
937
+ * Unless required by applicable law or agreed to in writing, software
938
+ * distributed under the License is distributed on an "AS IS" BASIS,
939
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
940
+ * See the License for the specific language governing permissions and
941
+ * limitations under the License.
942
+ */
943
+ // Defaults to support image inputs for convenience.
944
+ const defaultExpectedInputs = [{ type: 'image' }];
945
+ /**
946
+ * Defines an inference "backend" that uses Chrome's on-device model,
947
+ * and encapsulates logic for detecting when on-device inference is
948
+ * possible.
949
+ */
950
+ class ChromeAdapterImpl {
951
+ constructor(languageModelProvider, mode, onDeviceParams) {
952
+ this.languageModelProvider = languageModelProvider;
953
+ this.mode = mode;
954
+ this.isDownloading = false;
955
+ this.onDeviceParams = {
956
+ createOptions: {
957
+ expectedInputs: defaultExpectedInputs
958
+ }
959
+ };
960
+ if (onDeviceParams) {
961
+ this.onDeviceParams = onDeviceParams;
962
+ if (!this.onDeviceParams.createOptions) {
963
+ this.onDeviceParams.createOptions = {
964
+ expectedInputs: defaultExpectedInputs
965
+ };
966
+ }
967
+ else if (!this.onDeviceParams.createOptions.expectedInputs) {
968
+ this.onDeviceParams.createOptions.expectedInputs =
969
+ defaultExpectedInputs;
970
+ }
971
+ }
972
+ }
973
+ /**
974
+ * Checks if a given request can be made on-device.
975
+ *
976
+ * Encapsulates a few concerns:
977
+ * the mode
978
+ * API existence
979
+ * prompt formatting
980
+ * model availability, including triggering download if necessary
981
+ *
982
+ *
983
+ * Pros: callers needn't be concerned with details of on-device availability.</p>
984
+ * Cons: this method spans a few concerns and splits request validation from usage.
985
+ * If instance variables weren't already part of the API, we could consider a better
986
+ * separation of concerns.
987
+ */
988
+ async isAvailable(request) {
989
+ if (!this.mode) {
990
+ logger.debug(`On-device inference unavailable because mode is undefined.`);
991
+ return false;
992
+ }
993
+ if (this.mode === InferenceMode.ONLY_IN_CLOUD) {
994
+ logger.debug(`On-device inference unavailable because mode is "only_in_cloud".`);
995
+ return false;
996
+ }
997
+ // Triggers out-of-band download so model will eventually become available.
998
+ const availability = await this.downloadIfAvailable();
999
+ if (this.mode === InferenceMode.ONLY_ON_DEVICE) {
1000
+ // If it will never be available due to API inavailability, throw.
1001
+ if (availability === Availability.UNAVAILABLE) {
1002
+ throw new AIError(AIErrorCode.API_NOT_ENABLED, 'Local LanguageModel API not available in this environment.');
1003
+ }
1004
+ else if (availability === Availability.DOWNLOADABLE ||
1005
+ availability === Availability.DOWNLOADING) {
1006
+ // TODO(chholland): Better user experience during download - progress?
1007
+ logger.debug(`Waiting for download of LanguageModel to complete.`);
1008
+ await this.downloadPromise;
1009
+ return true;
1010
+ }
1011
+ return true;
1012
+ }
1013
+ // Applies prefer_on_device logic.
1014
+ if (availability !== Availability.AVAILABLE) {
1015
+ logger.debug(`On-device inference unavailable because availability is "${availability}".`);
1016
+ return false;
1017
+ }
1018
+ if (!ChromeAdapterImpl.isOnDeviceRequest(request)) {
1019
+ logger.debug(`On-device inference unavailable because request is incompatible.`);
1020
+ return false;
1021
+ }
1022
+ return true;
1023
+ }
1024
+ /**
1025
+ * Generates content on device.
1026
+ *
1027
+ * @remarks
1028
+ * This is comparable to {@link GenerativeModel.generateContent} for generating content in
1029
+ * Cloud.
1030
+ * @param request - a standard Firebase AI {@link GenerateContentRequest}
1031
+ * @returns {@link Response}, so we can reuse common response formatting.
1032
+ */
1033
+ async generateContent(request) {
1034
+ const session = await this.createSession();
1035
+ const contents = await Promise.all(request.contents.map(ChromeAdapterImpl.toLanguageModelMessage));
1036
+ const text = await session.prompt(contents, this.onDeviceParams.promptOptions);
1037
+ return ChromeAdapterImpl.toResponse(text);
1038
+ }
1039
+ /**
1040
+ * Generates content stream on device.
1041
+ *
1042
+ * @remarks
1043
+ * This is comparable to {@link GenerativeModel.generateContentStream} for generating content in
1044
+ * Cloud.
1045
+ * @param request - a standard Firebase AI {@link GenerateContentRequest}
1046
+ * @returns {@link Response}, so we can reuse common response formatting.
1047
+ */
1048
+ async generateContentStream(request) {
1049
+ const session = await this.createSession();
1050
+ const contents = await Promise.all(request.contents.map(ChromeAdapterImpl.toLanguageModelMessage));
1051
+ const stream = session.promptStreaming(contents, this.onDeviceParams.promptOptions);
1052
+ return ChromeAdapterImpl.toStreamResponse(stream);
1053
+ }
1054
+ async countTokens(_request) {
1055
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'Count Tokens is not yet available for on-device model.');
1056
+ }
1057
+ /**
1058
+ * Asserts inference for the given request can be performed by an on-device model.
1059
+ */
1060
+ static isOnDeviceRequest(request) {
1061
+ // Returns false if the prompt is empty.
1062
+ if (request.contents.length === 0) {
1063
+ logger.debug('Empty prompt rejected for on-device inference.');
1064
+ return false;
1065
+ }
1066
+ for (const content of request.contents) {
1067
+ if (content.role === 'function') {
1068
+ logger.debug(`"Function" role rejected for on-device inference.`);
1069
+ return false;
1070
+ }
1071
+ // Returns false if request contains an image with an unsupported mime type.
1072
+ for (const part of content.parts) {
1073
+ if (part.inlineData &&
1074
+ ChromeAdapterImpl.SUPPORTED_MIME_TYPES.indexOf(part.inlineData.mimeType) === -1) {
1075
+ logger.debug(`Unsupported mime type "${part.inlineData.mimeType}" rejected for on-device inference.`);
1076
+ return false;
1077
+ }
1078
+ }
1079
+ }
1080
+ return true;
1081
+ }
1082
+ /**
1083
+ * Encapsulates logic to get availability and download a model if one is downloadable.
1084
+ */
1085
+ async downloadIfAvailable() {
1086
+ const availability = await this.languageModelProvider?.availability(this.onDeviceParams.createOptions);
1087
+ if (availability === Availability.DOWNLOADABLE) {
1088
+ this.download();
1089
+ }
1090
+ return availability;
1091
+ }
1092
+ /**
1093
+ * Triggers out-of-band download of an on-device model.
1094
+ *
1095
+ * Chrome only downloads models as needed. Chrome knows a model is needed when code calls
1096
+ * LanguageModel.create.
1097
+ *
1098
+ * Since Chrome manages the download, the SDK can only avoid redundant download requests by
1099
+ * tracking if a download has previously been requested.
1100
+ */
1101
+ download() {
1102
+ if (this.isDownloading) {
1103
+ return;
1104
+ }
1105
+ this.isDownloading = true;
1106
+ this.downloadPromise = this.languageModelProvider
1107
+ ?.create(this.onDeviceParams.createOptions)
1108
+ .finally(() => {
1109
+ this.isDownloading = false;
1110
+ });
1111
+ }
1112
+ /**
1113
+ * Converts Firebase AI {@link Content} object to a Chrome {@link LanguageModelMessage} object.
1114
+ */
1115
+ static async toLanguageModelMessage(content) {
1116
+ const languageModelMessageContents = await Promise.all(content.parts.map(ChromeAdapterImpl.toLanguageModelMessageContent));
1117
+ return {
1118
+ role: ChromeAdapterImpl.toLanguageModelMessageRole(content.role),
1119
+ content: languageModelMessageContents
1120
+ };
1121
+ }
1122
+ /**
1123
+ * Converts a Firebase AI Part object to a Chrome LanguageModelMessageContent object.
1124
+ */
1125
+ static async toLanguageModelMessageContent(part) {
1126
+ if (part.text) {
1127
+ return {
1128
+ type: 'text',
1129
+ value: part.text
1130
+ };
1131
+ }
1132
+ else if (part.inlineData) {
1133
+ const formattedImageContent = await fetch(`data:${part.inlineData.mimeType};base64,${part.inlineData.data}`);
1134
+ const imageBlob = await formattedImageContent.blob();
1135
+ const imageBitmap = await createImageBitmap(imageBlob);
1136
+ return {
1137
+ type: 'image',
1138
+ value: imageBitmap
1139
+ };
1140
+ }
1141
+ throw new AIError(AIErrorCode.REQUEST_ERROR, `Processing of this Part type is not currently supported.`);
1142
+ }
1143
+ /**
1144
+ * Converts a Firebase AI {@link Role} string to a {@link LanguageModelMessageRole} string.
1145
+ */
1146
+ static toLanguageModelMessageRole(role) {
1147
+ // Assumes 'function' rule has been filtered by isOnDeviceRequest
1148
+ return role === 'model' ? 'assistant' : 'user';
1149
+ }
1150
+ /**
1151
+ * Abstracts Chrome session creation.
1152
+ *
1153
+ * Chrome uses a multi-turn session for all inference. Firebase AI uses single-turn for all
1154
+ * inference. To map the Firebase AI API to Chrome's API, the SDK creates a new session for all
1155
+ * inference.
1156
+ *
1157
+ * Chrome will remove a model from memory if it's no longer in use, so this method ensures a
1158
+ * new session is created before an old session is destroyed.
1159
+ */
1160
+ async createSession() {
1161
+ if (!this.languageModelProvider) {
1162
+ throw new AIError(AIErrorCode.UNSUPPORTED, 'Chrome AI requested for unsupported browser version.');
1163
+ }
1164
+ const newSession = await this.languageModelProvider.create(this.onDeviceParams.createOptions);
1165
+ if (this.oldSession) {
1166
+ this.oldSession.destroy();
1167
+ }
1168
+ // Holds session reference, so model isn't unloaded from memory.
1169
+ this.oldSession = newSession;
1170
+ return newSession;
1171
+ }
1172
+ /**
1173
+ * Formats string returned by Chrome as a {@link Response} returned by Firebase AI.
1174
+ */
1175
+ static toResponse(text) {
1176
+ return {
1177
+ json: async () => ({
1178
+ candidates: [
1179
+ {
1180
+ content: {
1181
+ parts: [{ text }]
1182
+ }
1183
+ }
1184
+ ]
1185
+ })
1186
+ };
1187
+ }
1188
+ /**
1189
+ * Formats string stream returned by Chrome as SSE returned by Firebase AI.
1190
+ */
1191
+ static toStreamResponse(stream) {
1192
+ const encoder = new TextEncoder();
1193
+ return {
1194
+ body: stream.pipeThrough(new TransformStream({
1195
+ transform(chunk, controller) {
1196
+ const json = JSON.stringify({
1197
+ candidates: [
1198
+ {
1199
+ content: {
1200
+ role: 'model',
1201
+ parts: [{ text: chunk }]
1202
+ }
1203
+ }
1204
+ ]
1205
+ });
1206
+ controller.enqueue(encoder.encode(`data: ${json}\n\n`));
1207
+ }
1208
+ }))
1209
+ };
1210
+ }
1211
+ }
1212
+ // Visible for testing
1213
+ ChromeAdapterImpl.SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png'];
1214
+ /**
1215
+ * Creates a ChromeAdapterImpl on demand.
1216
+ */
1217
+ function chromeAdapterFactory(mode, window, params) {
1218
+ // Do not initialize a ChromeAdapter if we are not in hybrid mode.
1219
+ if (typeof window !== 'undefined' && mode) {
1220
+ return new ChromeAdapterImpl(window.LanguageModel, mode, params);
1221
+ }
1222
+ }
1223
+
1224
+ /**
1225
+ * @license
1226
+ * Copyright 2024 Google LLC
1227
+ *
1228
+ * Licensed under the Apache License, Version 2.0 (the "License");
1229
+ * you may not use this file except in compliance with the License.
1230
+ * You may obtain a copy of the License at
1231
+ *
1232
+ * http://www.apache.org/licenses/LICENSE-2.0
1233
+ *
1234
+ * Unless required by applicable law or agreed to in writing, software
1235
+ * distributed under the License is distributed on an "AS IS" BASIS,
1236
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1237
+ * See the License for the specific language governing permissions and
1238
+ * limitations under the License.
1239
+ */
1240
+ class AIService {
1241
+ constructor(app, backend, authProvider, appCheckProvider, chromeAdapterFactory) {
1242
+ this.app = app;
1243
+ this.backend = backend;
1244
+ this.chromeAdapterFactory = chromeAdapterFactory;
1245
+ const appCheck = appCheckProvider?.getImmediate({ optional: true });
1246
+ const auth = authProvider?.getImmediate({ optional: true });
1247
+ this.auth = auth || null;
1248
+ this.appCheck = appCheck || null;
1249
+ if (backend instanceof VertexAIBackend) {
1250
+ this.location = backend.location;
1251
+ }
1252
+ else {
1253
+ this.location = '';
1254
+ }
1255
+ }
1256
+ _delete() {
1257
+ return Promise.resolve();
1258
+ }
1259
+ set options(optionsToSet) {
1260
+ this._options = optionsToSet;
1261
+ }
1262
+ get options() {
1263
+ return this._options;
1264
+ }
1265
+ }
1266
+
1267
+ /**
1268
+ * @license
1269
+ * Copyright 2025 Google LLC
1270
+ *
1271
+ * Licensed under the Apache License, Version 2.0 (the "License");
1272
+ * you may not use this file except in compliance with the License.
1273
+ * You may obtain a copy of the License at
1274
+ *
1275
+ * http://www.apache.org/licenses/LICENSE-2.0
1276
+ *
1277
+ * Unless required by applicable law or agreed to in writing, software
1278
+ * distributed under the License is distributed on an "AS IS" BASIS,
1279
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1280
+ * See the License for the specific language governing permissions and
1281
+ * limitations under the License.
1282
+ */
1283
+ function factory(container, { instanceIdentifier }) {
1284
+ if (!instanceIdentifier) {
1285
+ throw new AIError(AIErrorCode.ERROR, 'AIService instance identifier is undefined.');
1286
+ }
1287
+ const backend = decodeInstanceIdentifier(instanceIdentifier);
1288
+ // getImmediate for FirebaseApp will always succeed
1289
+ const app = container.getProvider('app').getImmediate();
1290
+ const auth = container.getProvider('auth-internal');
1291
+ const appCheckProvider = container.getProvider('app-check-internal');
1292
+ return new AIService(app, backend, auth, appCheckProvider, chromeAdapterFactory);
1293
+ }
1294
+
1295
+ /**
1296
+ * @license
1297
+ * Copyright 2025 Google LLC
1298
+ *
1299
+ * Licensed under the Apache License, Version 2.0 (the "License");
1300
+ * you may not use this file except in compliance with the License.
1301
+ * You may obtain a copy of the License at
1302
+ *
1303
+ * http://www.apache.org/licenses/LICENSE-2.0
1304
+ *
1305
+ * Unless required by applicable law or agreed to in writing, software
1306
+ * distributed under the License is distributed on an "AS IS" BASIS,
1307
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1308
+ * See the License for the specific language governing permissions and
1309
+ * limitations under the License.
1310
+ */
1311
+ /**
1312
+ * Initializes an {@link ApiSettings} object from an {@link AI} instance.
1313
+ *
1314
+ * If this is a Server App, the {@link ApiSettings} object's `getAppCheckToken()` will resolve
1315
+ * with the `FirebaseServerAppSettings.appCheckToken`, instead of requiring that an App Check
1316
+ * instance is initialized.
1317
+ */
1318
+ function initApiSettings(ai) {
1319
+ if (!ai.app?.options?.apiKey) {
1320
+ 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.`);
1321
+ }
1322
+ else if (!ai.app?.options?.projectId) {
1323
+ 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.`);
1324
+ }
1325
+ else if (!ai.app?.options?.appId) {
1326
+ 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.`);
1327
+ }
1328
+ const apiSettings = {
1329
+ apiKey: ai.app.options.apiKey,
1330
+ project: ai.app.options.projectId,
1331
+ appId: ai.app.options.appId,
1332
+ automaticDataCollectionEnabled: ai.app.automaticDataCollectionEnabled,
1333
+ location: ai.location,
1334
+ backend: ai.backend
1335
+ };
1336
+ if (app._isFirebaseServerApp(ai.app) && ai.app.settings.appCheckToken) {
1337
+ const token = ai.app.settings.appCheckToken;
1338
+ apiSettings.getAppCheckToken = () => {
1339
+ return Promise.resolve({ token });
1340
+ };
1341
+ }
1342
+ else if (ai.appCheck) {
1343
+ if (ai.options?.useLimitedUseAppCheckTokens) {
1344
+ apiSettings.getAppCheckToken = () => ai.appCheck.getLimitedUseToken();
1345
+ }
1346
+ else {
1347
+ apiSettings.getAppCheckToken = () => ai.appCheck.getToken();
1348
+ }
1349
+ }
1350
+ if (ai.auth) {
1351
+ apiSettings.getAuthToken = () => ai.auth.getToken();
1352
+ }
1353
+ return apiSettings;
1354
+ }
1355
+
1356
+ /**
1357
+ * @license
1358
+ * Copyright 2025 Google LLC
1359
+ *
1360
+ * Licensed under the Apache License, Version 2.0 (the "License");
1361
+ * you may not use this file except in compliance with the License.
1362
+ * You may obtain a copy of the License at
1363
+ *
1364
+ * http://www.apache.org/licenses/LICENSE-2.0
1365
+ *
1366
+ * Unless required by applicable law or agreed to in writing, software
1367
+ * distributed under the License is distributed on an "AS IS" BASIS,
1368
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1369
+ * See the License for the specific language governing permissions and
1370
+ * limitations under the License.
1371
+ */
1372
+ /**
1373
+ * Base class for Firebase AI model APIs.
1374
+ *
1375
+ * Instances of this class are associated with a specific Firebase AI {@link Backend}
1376
+ * and provide methods for interacting with the configured generative model.
1377
+ *
1378
+ * @public
1379
+ */
1380
+ class AIModel {
1381
+ /**
1382
+ * Constructs a new instance of the {@link AIModel} class.
1383
+ *
1384
+ * This constructor should only be called from subclasses that provide
1385
+ * a model API.
1386
+ *
1387
+ * @param ai - an {@link AI} instance.
1388
+ * @param modelName - The name of the model being used. It can be in one of the following formats:
1389
+ * - `my-model` (short name, will resolve to `publishers/google/models/my-model`)
1390
+ * - `models/my-model` (will resolve to `publishers/google/models/my-model`)
1391
+ * - `publishers/my-publisher/models/my-model` (fully qualified model name)
1392
+ *
1393
+ * @throws If the `apiKey` or `projectId` fields are missing in your
1394
+ * Firebase config.
1395
+ *
1396
+ * @internal
1397
+ */
1398
+ constructor(ai, modelName) {
1399
+ this._apiSettings = initApiSettings(ai);
1400
+ this.model = AIModel.normalizeModelName(modelName, this._apiSettings.backend.backendType);
1401
+ }
1402
+ /**
1403
+ * Normalizes the given model name to a fully qualified model resource name.
1404
+ *
1405
+ * @param modelName - The model name to normalize.
1406
+ * @returns The fully qualified model resource name.
1407
+ *
1408
+ * @internal
1409
+ */
1410
+ static normalizeModelName(modelName, backendType) {
1411
+ if (backendType === BackendType.GOOGLE_AI) {
1412
+ return AIModel.normalizeGoogleAIModelName(modelName);
1413
+ }
1414
+ else {
1415
+ return AIModel.normalizeVertexAIModelName(modelName);
1416
+ }
1417
+ }
1418
+ /**
1419
+ * @internal
1420
+ */
1421
+ static normalizeGoogleAIModelName(modelName) {
1422
+ return `models/${modelName}`;
1423
+ }
1424
+ /**
1425
+ * @internal
1426
+ */
1427
+ static normalizeVertexAIModelName(modelName) {
1428
+ let model;
1429
+ if (modelName.includes('/')) {
1430
+ if (modelName.startsWith('models/')) {
1431
+ // Add 'publishers/google' if the user is only passing in 'models/model-name'.
1432
+ model = `publishers/google/${modelName}`;
1433
+ }
1434
+ else {
1435
+ // Any other custom format (e.g. tuned models) must be passed in correctly.
1436
+ model = modelName;
1437
+ }
1438
+ }
1439
+ else {
1440
+ // If path is not included, assume it's a non-tuned model.
1441
+ model = `publishers/google/models/${modelName}`;
1442
+ }
1443
+ return model;
1444
+ }
1445
+ }
1446
+
1447
+ /**
1448
+ * @license
1449
+ * Copyright 2025 Google LLC
1450
+ *
1451
+ * Licensed under the Apache License, Version 2.0 (the "License");
1452
+ * you may not use this file except in compliance with the License.
1453
+ * You may obtain a copy of the License at
1454
+ *
1455
+ * http://www.apache.org/licenses/LICENSE-2.0
1456
+ *
1457
+ * Unless required by applicable law or agreed to in writing, software
1458
+ * distributed under the License is distributed on an "AS IS" BASIS,
1459
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1460
+ * See the License for the specific language governing permissions and
1461
+ * limitations under the License.
1462
+ */
1463
+ const TIMEOUT_EXPIRED_MESSAGE = 'Timeout has expired.';
1464
+ const ABORT_ERROR_NAME = 'AbortError';
1465
+ class RequestURL {
1466
+ constructor(params) {
1467
+ this.params = params;
1468
+ }
1469
+ toString() {
1470
+ const url = new URL(this.baseUrl); // Throws if the URL is invalid
1471
+ url.pathname = this.pathname;
1472
+ url.search = this.queryParams.toString();
1473
+ return url.toString();
1474
+ }
1475
+ get pathname() {
1476
+ // We need to construct a different URL if the request is for server side prompt templates,
1477
+ // since the URL patterns are different. Server side prompt templates expect a templateId
1478
+ // instead of a model name.
1479
+ if (this.params.templateId) {
1480
+ return `${this.params.apiSettings.backend._getTemplatePath(this.params.apiSettings.project, this.params.templateId)}:${this.params.task}`;
1481
+ }
1482
+ else {
1483
+ return `${this.params.apiSettings.backend._getModelPath(this.params.apiSettings.project, this.params.model)}:${this.params.task}`;
1484
+ }
1485
+ }
1486
+ get baseUrl() {
1487
+ return (this.params.singleRequestOptions?.baseUrl ?? `https://${DEFAULT_DOMAIN}`);
1488
+ }
1489
+ get queryParams() {
1490
+ const params = new URLSearchParams();
1491
+ if (this.params.stream) {
1492
+ params.set('alt', 'sse');
1493
+ }
1494
+ return params;
1495
+ }
1496
+ }
1497
+ class WebSocketUrl {
1498
+ constructor(apiSettings) {
1499
+ this.apiSettings = apiSettings;
1500
+ }
1501
+ toString() {
1502
+ const url = new URL(`wss://${DEFAULT_DOMAIN}`);
1503
+ url.pathname = this.pathname;
1504
+ const queryParams = new URLSearchParams();
1505
+ queryParams.set('key', this.apiSettings.apiKey);
1506
+ url.search = queryParams.toString();
1507
+ return url.toString();
1508
+ }
1509
+ get pathname() {
1510
+ if (this.apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
1511
+ return 'ws/google.firebase.vertexai.v1beta.GenerativeService/BidiGenerateContent';
1512
+ }
1513
+ else {
1514
+ return `ws/google.firebase.vertexai.v1beta.LlmBidiService/BidiGenerateContent/locations/${this.apiSettings.location}`;
1515
+ }
1516
+ }
1517
+ }
1518
+ /**
1519
+ * Log language and "fire/version" to x-goog-api-client
1520
+ */
1521
+ function getClientHeaders(url) {
1522
+ const loggingTags = [];
1523
+ loggingTags.push(`${LANGUAGE_TAG}/${PACKAGE_VERSION}`);
1524
+ loggingTags.push(`fire/${PACKAGE_VERSION}`);
1525
+ /**
1526
+ * No call would be made if ONLY_ON_DEVICE.
1527
+ * ONLY_IN_CLOUD does not indicate an intention to use hybrid.
1528
+ */
1529
+ if (url.params.apiSettings.inferenceMode === InferenceMode.PREFER_ON_DEVICE ||
1530
+ url.params.apiSettings.inferenceMode === InferenceMode.PREFER_IN_CLOUD) {
1531
+ // No version
1532
+ loggingTags.push(HYBRID_TAG);
1533
+ }
1534
+ return loggingTags.join(' ');
1535
+ }
1536
+ async function getHeaders(url) {
1537
+ const headers = new Headers();
1538
+ headers.append('Content-Type', 'application/json');
1539
+ headers.append('x-goog-api-client', getClientHeaders(url));
1540
+ headers.append('x-goog-api-key', url.params.apiSettings.apiKey);
1541
+ if (url.params.apiSettings.automaticDataCollectionEnabled) {
1542
+ headers.append('X-Firebase-Appid', url.params.apiSettings.appId);
1543
+ }
1544
+ if (url.params.apiSettings.getAppCheckToken) {
1545
+ const appCheckToken = await url.params.apiSettings.getAppCheckToken();
1546
+ if (appCheckToken) {
1547
+ headers.append('X-Firebase-AppCheck', appCheckToken.token);
1548
+ if (appCheckToken.error) {
1549
+ logger.warn(`Unable to obtain a valid App Check token: ${appCheckToken.error.message}`);
1550
+ }
1551
+ }
1552
+ }
1553
+ if (url.params.apiSettings.getAuthToken) {
1554
+ const authToken = await url.params.apiSettings.getAuthToken();
1555
+ if (authToken) {
1556
+ headers.append('Authorization', `Firebase ${authToken.accessToken}`);
1557
+ }
1558
+ }
1559
+ return headers;
1560
+ }
1561
+ async function makeRequest(requestUrlParams, body) {
1562
+ const url = new RequestURL(requestUrlParams);
1563
+ let response;
1564
+ const externalSignal = requestUrlParams.singleRequestOptions?.signal;
1565
+ const timeoutMillis = requestUrlParams.singleRequestOptions?.timeout != null &&
1566
+ requestUrlParams.singleRequestOptions.timeout >= 0
1567
+ ? requestUrlParams.singleRequestOptions.timeout
1568
+ : DEFAULT_FETCH_TIMEOUT_MS;
1569
+ const internalAbortController = new AbortController();
1570
+ const fetchTimeoutId = setTimeout(() => {
1571
+ internalAbortController.abort(new DOMException(TIMEOUT_EXPIRED_MESSAGE, ABORT_ERROR_NAME));
1572
+ logger.debug(`Aborting request to ${url} due to timeout (${timeoutMillis}ms)`);
1573
+ }, timeoutMillis);
1574
+ // Used to abort the fetch if either the user-defined `externalSignal` is aborted, or if the
1575
+ // internal signal (triggered by timeouts) is aborted.
1576
+ const combinedSignal = AbortSignal.any(externalSignal
1577
+ ? [externalSignal, internalAbortController.signal]
1578
+ : [internalAbortController.signal]);
1579
+ if (externalSignal && externalSignal.aborted) {
1580
+ clearTimeout(fetchTimeoutId);
1581
+ throw new DOMException(externalSignal.reason ?? 'Aborted externally before fetch', ABORT_ERROR_NAME);
1582
+ }
1583
+ try {
1584
+ const fetchOptions = {
1585
+ method: 'POST',
1586
+ headers: await getHeaders(url),
1587
+ signal: combinedSignal,
1588
+ body
1589
+ };
1590
+ response = await fetch(url.toString(), fetchOptions);
1591
+ if (!response.ok) {
1592
+ let message = '';
1593
+ let errorDetails;
1594
+ try {
1595
+ const json = await response.json();
1596
+ message = json.error.message;
1597
+ if (json.error.details) {
1598
+ message += ` ${JSON.stringify(json.error.details)}`;
1599
+ errorDetails = json.error.details;
1600
+ }
1601
+ }
1602
+ catch (e) {
1603
+ // ignored
1604
+ }
1605
+ if (response.status === 403 &&
1606
+ errorDetails &&
1607
+ errorDetails.some((detail) => detail.reason === 'SERVICE_DISABLED') &&
1608
+ errorDetails.some((detail) => detail.links?.[0]?.description.includes('Google developers console API activation'))) {
1609
+ throw new AIError(AIErrorCode.API_NOT_ENABLED, `The Firebase AI SDK requires the Firebase AI ` +
1610
+ `API ('firebasevertexai.googleapis.com') to be enabled in your ` +
1611
+ `Firebase project. Enable this API by visiting the Firebase Console ` +
1612
+ `at https://console.firebase.google.com/project/${url.params.apiSettings.project}/ailogic/ ` +
1613
+ `and clicking "Get started". If you enabled this API recently, ` +
1614
+ `wait a few minutes for the action to propagate to our systems and ` +
1615
+ `then retry.`, {
1616
+ status: response.status,
1617
+ statusText: response.statusText,
1618
+ errorDetails
1619
+ });
1620
+ }
1621
+ throw new AIError(AIErrorCode.FETCH_ERROR, `Error fetching from ${url}: [${response.status} ${response.statusText}] ${message}`, {
1622
+ status: response.status,
1623
+ statusText: response.statusText,
1624
+ errorDetails
1625
+ });
1626
+ }
1627
+ }
1628
+ catch (e) {
1629
+ let err = e;
1630
+ if (e.code !== AIErrorCode.FETCH_ERROR &&
1631
+ e.code !== AIErrorCode.API_NOT_ENABLED &&
1632
+ e instanceof Error &&
1633
+ e.name !== ABORT_ERROR_NAME) {
1634
+ err = new AIError(AIErrorCode.ERROR, `Error fetching from ${url.toString()}: ${e.message}`);
1635
+ err.stack = e.stack;
1636
+ }
1637
+ throw err;
1638
+ }
1639
+ finally {
1640
+ // When doing streaming requests, this will clear the timeout once the stream begins.
1641
+ // If a timeout it 3000ms, and the stream starts after 300ms and ends after 5000ms, the
1642
+ // timeout will be cleared after 300ms, so it won't abort the request.
1643
+ clearTimeout(fetchTimeoutId);
1644
+ }
1645
+ return response;
1646
+ }
1647
+
1648
+ /**
1649
+ * @license
1650
+ * Copyright 2024 Google LLC
1651
+ *
1652
+ * Licensed under the Apache License, Version 2.0 (the "License");
1653
+ * you may not use this file except in compliance with the License.
1654
+ * You may obtain a copy of the License at
1655
+ *
1656
+ * http://www.apache.org/licenses/LICENSE-2.0
1657
+ *
1658
+ * Unless required by applicable law or agreed to in writing, software
1659
+ * distributed under the License is distributed on an "AS IS" BASIS,
1660
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1661
+ * See the License for the specific language governing permissions and
1662
+ * limitations under the License.
1663
+ */
1664
+ /**
1665
+ * Check that at least one candidate exists and does not have a bad
1666
+ * finish reason. Warns if multiple candidates exist.
1667
+ */
1668
+ function hasValidCandidates(response) {
1669
+ if (response.candidates && response.candidates.length > 0) {
1670
+ if (response.candidates.length > 1) {
1671
+ logger.warn(`This response had ${response.candidates.length} ` +
1672
+ `candidates. Returning text from the first candidate only. ` +
1673
+ `Access response.candidates directly to use the other candidates.`);
1674
+ }
1675
+ if (hadBadFinishReason(response.candidates[0])) {
1676
+ throw new AIError(AIErrorCode.RESPONSE_ERROR, `Response error: ${formatBlockErrorMessage(response)}. Response body stored in error.response`, {
1677
+ response
1678
+ });
1679
+ }
1680
+ return true;
1681
+ }
1682
+ else {
1683
+ return false;
1684
+ }
1685
+ }
1686
+ /**
1687
+ * Creates an EnhancedGenerateContentResponse object that has helper functions and
1688
+ * other modifications that improve usability.
1689
+ */
1690
+ function createEnhancedContentResponse(response, inferenceSource = InferenceSource.IN_CLOUD) {
1691
+ /**
1692
+ * The Vertex AI backend omits default values.
1693
+ * This causes the `index` property to be omitted from the first candidate in the
1694
+ * response, since it has index 0, and 0 is a default value.
1695
+ * See: https://github.com/firebase/firebase-js-sdk/issues/8566
1696
+ */
1697
+ if (response.candidates && !response.candidates[0].hasOwnProperty('index')) {
1698
+ response.candidates[0].index = 0;
1699
+ }
1700
+ const responseWithHelpers = addHelpers(response);
1701
+ responseWithHelpers.inferenceSource = inferenceSource;
1702
+ return responseWithHelpers;
1703
+ }
1704
+ /**
1705
+ * Adds convenience helper methods to a response object, including stream
1706
+ * chunks (as long as each chunk is a complete GenerateContentResponse JSON).
1707
+ */
1708
+ function addHelpers(response) {
1709
+ response.text = () => {
1710
+ if (hasValidCandidates(response)) {
1711
+ return getText(response, part => !part.thought);
1712
+ }
1713
+ else if (response.promptFeedback) {
1714
+ throw new AIError(AIErrorCode.RESPONSE_ERROR, `Text not available. ${formatBlockErrorMessage(response)}`, {
1715
+ response
1716
+ });
1717
+ }
1718
+ return '';
1719
+ };
1720
+ response.thoughtSummary = () => {
1721
+ if (hasValidCandidates(response)) {
1722
+ const result = getText(response, part => !!part.thought);
1723
+ return result === '' ? undefined : result;
1724
+ }
1725
+ else if (response.promptFeedback) {
1726
+ throw new AIError(AIErrorCode.RESPONSE_ERROR, `Thought summary not available. ${formatBlockErrorMessage(response)}`, {
1727
+ response
1728
+ });
1729
+ }
1730
+ return undefined;
1731
+ };
1732
+ response.inlineDataParts = () => {
1733
+ if (hasValidCandidates(response)) {
1734
+ return getInlineDataParts(response);
1735
+ }
1736
+ else if (response.promptFeedback) {
1737
+ throw new AIError(AIErrorCode.RESPONSE_ERROR, `Data not available. ${formatBlockErrorMessage(response)}`, {
1738
+ response
1739
+ });
1740
+ }
1741
+ return undefined;
1742
+ };
1743
+ response.functionCalls = () => {
1744
+ if (hasValidCandidates(response)) {
1745
+ return getFunctionCalls(response);
1746
+ }
1747
+ else if (response.promptFeedback) {
1748
+ throw new AIError(AIErrorCode.RESPONSE_ERROR, `Function call not available. ${formatBlockErrorMessage(response)}`, {
1749
+ response
1750
+ });
1751
+ }
1752
+ return undefined;
1753
+ };
1754
+ return response;
1755
+ }
1756
+ /**
1757
+ * Returns all text from the first candidate's parts, filtering by whether
1758
+ * `partFilter()` returns true.
1759
+ *
1760
+ * @param response - The `GenerateContentResponse` from which to extract text.
1761
+ * @param partFilter - Only return `Part`s for which this returns true
1762
+ */
1763
+ function getText(response, partFilter) {
1764
+ const textStrings = [];
1765
+ if (response.candidates?.[0].content?.parts) {
1766
+ for (const part of response.candidates?.[0].content?.parts) {
1767
+ if (part.text && partFilter(part)) {
1768
+ textStrings.push(part.text);
1769
+ }
1770
+ }
1771
+ }
1772
+ if (textStrings.length > 0) {
1773
+ return textStrings.join('');
1774
+ }
1775
+ else {
1776
+ return '';
1777
+ }
1778
+ }
1779
+ /**
1780
+ * Returns every {@link FunctionCall} associated with first candidate.
1781
+ */
1782
+ function getFunctionCalls(response) {
1783
+ if (!response) {
1784
+ return undefined;
1785
+ }
1786
+ const functionCalls = [];
1787
+ if (response.candidates?.[0].content?.parts) {
1788
+ for (const part of response.candidates?.[0].content?.parts) {
1789
+ if (part.functionCall) {
1790
+ functionCalls.push(part.functionCall);
1791
+ }
1792
+ }
1793
+ }
1794
+ if (functionCalls.length > 0) {
1795
+ return functionCalls;
1796
+ }
1797
+ else {
1798
+ return undefined;
1799
+ }
1800
+ }
1801
+ /**
1802
+ * Returns every {@link InlineDataPart} in the first candidate if present.
1803
+ *
1804
+ * @internal
1805
+ */
1806
+ function getInlineDataParts(response) {
1807
+ const data = [];
1808
+ if (response.candidates?.[0].content?.parts) {
1809
+ for (const part of response.candidates?.[0].content?.parts) {
1810
+ if (part.inlineData) {
1811
+ data.push(part);
1812
+ }
1813
+ }
1814
+ }
1815
+ if (data.length > 0) {
1816
+ return data;
1817
+ }
1818
+ else {
1819
+ return undefined;
1820
+ }
1821
+ }
1822
+ const badFinishReasons = [FinishReason.RECITATION, FinishReason.SAFETY];
1823
+ function hadBadFinishReason(candidate) {
1824
+ return (!!candidate.finishReason &&
1825
+ badFinishReasons.some(reason => reason === candidate.finishReason));
1826
+ }
1827
+ function formatBlockErrorMessage(response) {
1828
+ let message = '';
1829
+ if ((!response.candidates || response.candidates.length === 0) &&
1830
+ response.promptFeedback) {
1831
+ message += 'Response was blocked';
1832
+ if (response.promptFeedback?.blockReason) {
1833
+ message += ` due to ${response.promptFeedback.blockReason}`;
1834
+ }
1835
+ if (response.promptFeedback?.blockReasonMessage) {
1836
+ message += `: ${response.promptFeedback.blockReasonMessage}`;
1837
+ }
1838
+ }
1839
+ else if (response.candidates?.[0]) {
1840
+ const firstCandidate = response.candidates[0];
1841
+ if (hadBadFinishReason(firstCandidate)) {
1842
+ message += `Candidate was blocked due to ${firstCandidate.finishReason}`;
1843
+ if (firstCandidate.finishMessage) {
1844
+ message += `: ${firstCandidate.finishMessage}`;
1845
+ }
1846
+ }
1847
+ }
1848
+ return message;
1849
+ }
1850
+ /**
1851
+ * Convert a generic successful fetch response body to an Imagen response object
1852
+ * that can be returned to the user. This converts the REST APIs response format to our
1853
+ * APIs representation of a response.
1854
+ *
1855
+ * @internal
1856
+ */
1857
+ async function handlePredictResponse(response) {
1858
+ const responseJson = await response.json();
1859
+ const images = [];
1860
+ let filteredReason = undefined;
1861
+ // The backend should always send a non-empty array of predictions if the response was successful.
1862
+ if (!responseJson.predictions || responseJson.predictions?.length === 0) {
1863
+ throw new AIError(AIErrorCode.RESPONSE_ERROR, 'No predictions or filtered reason received from Vertex AI. Please report this issue with the full error details at https://github.com/firebase/firebase-js-sdk/issues.');
1864
+ }
1865
+ for (const prediction of responseJson.predictions) {
1866
+ if (prediction.raiFilteredReason) {
1867
+ filteredReason = prediction.raiFilteredReason;
1868
+ }
1869
+ else if (prediction.mimeType && prediction.bytesBase64Encoded) {
1870
+ images.push({
1871
+ mimeType: prediction.mimeType,
1872
+ bytesBase64Encoded: prediction.bytesBase64Encoded
1873
+ });
1874
+ }
1875
+ else if (prediction.mimeType && prediction.gcsUri) {
1876
+ images.push({
1877
+ mimeType: prediction.mimeType,
1878
+ gcsURI: prediction.gcsUri
1879
+ });
1880
+ }
1881
+ else if (prediction.safetyAttributes) ;
1882
+ else {
1883
+ throw new AIError(AIErrorCode.RESPONSE_ERROR, `Unexpected element in 'predictions' array in response: '${JSON.stringify(prediction)}'`);
1884
+ }
1885
+ }
1886
+ return { images, filteredReason };
1887
+ }
1888
+
1889
+ /**
1890
+ * @license
1891
+ * Copyright 2025 Google LLC
1892
+ *
1893
+ * Licensed under the Apache License, Version 2.0 (the "License");
1894
+ * you may not use this file except in compliance with the License.
1895
+ * You may obtain a copy of the License at
1896
+ *
1897
+ * http://www.apache.org/licenses/LICENSE-2.0
1898
+ *
1899
+ * Unless required by applicable law or agreed to in writing, software
1900
+ * distributed under the License is distributed on an "AS IS" BASIS,
1901
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1902
+ * See the License for the specific language governing permissions and
1903
+ * limitations under the License.
1904
+ */
1905
+ /**
1906
+ * This SDK supports both the Vertex AI Gemini API and the Gemini Developer API (using Google AI).
1907
+ * The public API prioritizes the format used by the Vertex AI Gemini API.
1908
+ * We avoid having two sets of types by translating requests and responses between the two API formats.
1909
+ * This translation allows developers to switch between the Vertex AI Gemini API and the Gemini Developer API
1910
+ * with minimal code changes.
1911
+ *
1912
+ * In here are functions that map requests and responses between the two API formats.
1913
+ * Requests in the Vertex AI format are mapped to the Google AI format before being sent.
1914
+ * Responses from the Google AI backend are mapped back to the Vertex AI format before being returned to the user.
1915
+ */
1916
+ /**
1917
+ * Maps a Vertex AI {@link GenerateContentRequest} to a format that can be sent to Google AI.
1918
+ *
1919
+ * @param generateContentRequest The {@link GenerateContentRequest} to map.
1920
+ * @returns A {@link GenerateContentResponse} that conforms to the Google AI format.
1921
+ *
1922
+ * @throws If the request contains properties that are unsupported by Google AI.
1923
+ *
1924
+ * @internal
1925
+ */
1926
+ function mapGenerateContentRequest(generateContentRequest) {
1927
+ generateContentRequest.safetySettings?.forEach(safetySetting => {
1928
+ if (safetySetting.method) {
1929
+ throw new AIError(AIErrorCode.UNSUPPORTED, 'SafetySetting.method is not supported in the the Gemini Developer API. Please remove this property.');
1930
+ }
1931
+ });
1932
+ if (generateContentRequest.generationConfig?.topK) {
1933
+ const roundedTopK = Math.round(generateContentRequest.generationConfig.topK);
1934
+ if (roundedTopK !== generateContentRequest.generationConfig.topK) {
1935
+ logger.warn('topK in GenerationConfig has been rounded to the nearest integer to match the format for requests to the Gemini Developer API.');
1936
+ generateContentRequest.generationConfig.topK = roundedTopK;
1937
+ }
1938
+ }
1939
+ return generateContentRequest;
1940
+ }
1941
+ /**
1942
+ * Maps a {@link GenerateContentResponse} from Google AI to the format of the
1943
+ * {@link GenerateContentResponse} that we get from VertexAI that is exposed in the public API.
1944
+ *
1945
+ * @param googleAIResponse The {@link GenerateContentResponse} from Google AI.
1946
+ * @returns A {@link GenerateContentResponse} that conforms to the public API's format.
1947
+ *
1948
+ * @internal
1949
+ */
1950
+ function mapGenerateContentResponse(googleAIResponse) {
1951
+ const generateContentResponse = {
1952
+ candidates: googleAIResponse.candidates
1953
+ ? mapGenerateContentCandidates(googleAIResponse.candidates)
1954
+ : undefined,
1955
+ prompt: googleAIResponse.promptFeedback
1956
+ ? mapPromptFeedback(googleAIResponse.promptFeedback)
1957
+ : undefined,
1958
+ usageMetadata: googleAIResponse.usageMetadata
1959
+ };
1960
+ return generateContentResponse;
1961
+ }
1962
+ /**
1963
+ * Maps a Vertex AI {@link CountTokensRequest} to a format that can be sent to Google AI.
1964
+ *
1965
+ * @param countTokensRequest The {@link CountTokensRequest} to map.
1966
+ * @param model The model to count tokens with.
1967
+ * @returns A {@link CountTokensRequest} that conforms to the Google AI format.
1968
+ *
1969
+ * @internal
1970
+ */
1971
+ function mapCountTokensRequest(countTokensRequest, model) {
1972
+ const mappedCountTokensRequest = {
1973
+ generateContentRequest: {
1974
+ model,
1975
+ ...countTokensRequest
1976
+ }
1977
+ };
1978
+ return mappedCountTokensRequest;
1979
+ }
1980
+ /**
1981
+ * Maps a Google AI {@link GoogleAIGenerateContentCandidate} to a format that conforms
1982
+ * to the Vertex AI API format.
1983
+ *
1984
+ * @param candidates The {@link GoogleAIGenerateContentCandidate} to map.
1985
+ * @returns A {@link GenerateContentCandidate} that conforms to the Vertex AI format.
1986
+ *
1987
+ * @throws If any {@link Part} in the candidates has a `videoMetadata` property.
1988
+ *
1989
+ * @internal
1990
+ */
1991
+ function mapGenerateContentCandidates(candidates) {
1992
+ const mappedCandidates = [];
1993
+ let mappedSafetyRatings;
1994
+ if (mappedCandidates) {
1995
+ candidates.forEach(candidate => {
1996
+ // Map citationSources to citations.
1997
+ let citationMetadata;
1998
+ if (candidate.citationMetadata) {
1999
+ citationMetadata = {
2000
+ citations: candidate.citationMetadata.citationSources
2001
+ };
2002
+ }
2003
+ // Assign missing candidate SafetyRatings properties to their defaults if undefined.
2004
+ if (candidate.safetyRatings) {
2005
+ mappedSafetyRatings = candidate.safetyRatings.map(safetyRating => {
2006
+ return {
2007
+ ...safetyRating,
2008
+ severity: safetyRating.severity ?? HarmSeverity.HARM_SEVERITY_UNSUPPORTED,
2009
+ probabilityScore: safetyRating.probabilityScore ?? 0,
2010
+ severityScore: safetyRating.severityScore ?? 0
2011
+ };
2012
+ });
2013
+ }
2014
+ // videoMetadata is not supported.
2015
+ // Throw early since developers may send a long video as input and only expect to pay
2016
+ // for inference on a small portion of the video.
2017
+ if (candidate.content?.parts?.some(part => part?.videoMetadata)) {
2018
+ throw new AIError(AIErrorCode.UNSUPPORTED, 'Part.videoMetadata is not supported in the Gemini Developer API. Please remove this property.');
2019
+ }
2020
+ const mappedCandidate = {
2021
+ index: candidate.index,
2022
+ content: candidate.content,
2023
+ finishReason: candidate.finishReason,
2024
+ finishMessage: candidate.finishMessage,
2025
+ safetyRatings: mappedSafetyRatings,
2026
+ citationMetadata,
2027
+ groundingMetadata: candidate.groundingMetadata,
2028
+ urlContextMetadata: candidate.urlContextMetadata
2029
+ };
2030
+ mappedCandidates.push(mappedCandidate);
2031
+ });
2032
+ }
2033
+ return mappedCandidates;
2034
+ }
2035
+ function mapPromptFeedback(promptFeedback) {
2036
+ // Assign missing SafetyRating properties to their defaults if undefined.
2037
+ const mappedSafetyRatings = [];
2038
+ promptFeedback.safetyRatings.forEach(safetyRating => {
2039
+ mappedSafetyRatings.push({
2040
+ category: safetyRating.category,
2041
+ probability: safetyRating.probability,
2042
+ severity: safetyRating.severity ?? HarmSeverity.HARM_SEVERITY_UNSUPPORTED,
2043
+ probabilityScore: safetyRating.probabilityScore ?? 0,
2044
+ severityScore: safetyRating.severityScore ?? 0,
2045
+ blocked: safetyRating.blocked
2046
+ });
2047
+ });
2048
+ const mappedPromptFeedback = {
2049
+ blockReason: promptFeedback.blockReason,
2050
+ safetyRatings: mappedSafetyRatings,
2051
+ blockReasonMessage: promptFeedback.blockReasonMessage
2052
+ };
2053
+ return mappedPromptFeedback;
2054
+ }
2055
+
2056
+ /**
2057
+ * @license
2058
+ * Copyright 2024 Google LLC
2059
+ *
2060
+ * Licensed under the Apache License, Version 2.0 (the "License");
2061
+ * you may not use this file except in compliance with the License.
2062
+ * You may obtain a copy of the License at
2063
+ *
2064
+ * http://www.apache.org/licenses/LICENSE-2.0
2065
+ *
2066
+ * Unless required by applicable law or agreed to in writing, software
2067
+ * distributed under the License is distributed on an "AS IS" BASIS,
2068
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2069
+ * See the License for the specific language governing permissions and
2070
+ * limitations under the License.
2071
+ */
2072
+ const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/;
2073
+ /**
2074
+ * Process a response.body stream from the backend and return an
2075
+ * iterator that provides one complete GenerateContentResponse at a time
2076
+ * and a promise that resolves with a single aggregated
2077
+ * GenerateContentResponse.
2078
+ *
2079
+ * @param response - Response from a fetch call
2080
+ */
2081
+ async function processStream(response, apiSettings, inferenceSource) {
2082
+ const inputStream = response.body.pipeThrough(new TextDecoderStream('utf8', { fatal: true }));
2083
+ const responseStream = getResponseStream(inputStream);
2084
+ // We split the stream so the user can iterate over partial results (stream1)
2085
+ // while we aggregate the full result for history/final response (stream2).
2086
+ const [stream1, stream2] = responseStream.tee();
2087
+ const { response: internalResponse, firstValue } = await processStreamInternal(stream2, apiSettings, inferenceSource);
2088
+ return {
2089
+ stream: generateResponseSequence(stream1, apiSettings, inferenceSource),
2090
+ response: internalResponse,
2091
+ firstValue
2092
+ };
2093
+ }
2094
+ /**
2095
+ * Consumes streams teed from the input stream for internal needs.
2096
+ * The streams need to be teed because each stream can only be consumed
2097
+ * by one reader.
2098
+ *
2099
+ * "streamForPeek"
2100
+ * This tee is used to peek at the first value for relevant information
2101
+ * that we need to evaluate before returning the stream handle to the
2102
+ * client. For example, we need to check if the response is a function
2103
+ * call that may need to be handled by automatic function calling before
2104
+ * returning a response to the client.
2105
+ *
2106
+ * "streamForAggregation"
2107
+ * We iterate through this tee independently from the user and aggregate
2108
+ * it into a single response when the stream is complete. We need this
2109
+ * aggregate object to add to chat history when using ChatSession. It's
2110
+ * also provided to the user if they want it.
2111
+ */
2112
+ async function processStreamInternal(stream, apiSettings, inferenceSource) {
2113
+ const [streamForPeek, streamForAggregation] = stream.tee();
2114
+ const reader = streamForPeek.getReader();
2115
+ const { value } = await reader.read();
2116
+ return {
2117
+ firstValue: value,
2118
+ response: getResponsePromise(streamForAggregation, apiSettings, inferenceSource)
2119
+ };
2120
+ }
2121
+ async function getResponsePromise(stream, apiSettings, inferenceSource) {
2122
+ const allResponses = [];
2123
+ const reader = stream.getReader();
2124
+ while (true) {
2125
+ const { done, value } = await reader.read();
2126
+ if (done) {
2127
+ let generateContentResponse = aggregateResponses(allResponses);
2128
+ if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
2129
+ generateContentResponse = mapGenerateContentResponse(generateContentResponse);
2130
+ }
2131
+ return createEnhancedContentResponse(generateContentResponse, inferenceSource);
2132
+ }
2133
+ allResponses.push(value);
2134
+ }
2135
+ }
2136
+ async function* generateResponseSequence(stream, apiSettings, inferenceSource) {
2137
+ const reader = stream.getReader();
2138
+ while (true) {
2139
+ const { value, done } = await reader.read();
2140
+ if (done) {
2141
+ break;
2142
+ }
2143
+ let enhancedResponse;
2144
+ if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
2145
+ enhancedResponse = createEnhancedContentResponse(mapGenerateContentResponse(value), inferenceSource);
2146
+ }
2147
+ else {
2148
+ enhancedResponse = createEnhancedContentResponse(value, inferenceSource);
2149
+ }
2150
+ const firstCandidate = enhancedResponse.candidates?.[0];
2151
+ if (!firstCandidate?.content?.parts &&
2152
+ !firstCandidate?.finishReason &&
2153
+ !firstCandidate?.citationMetadata &&
2154
+ !firstCandidate?.urlContextMetadata) {
2155
+ continue;
2156
+ }
2157
+ yield enhancedResponse;
2158
+ }
2159
+ }
2160
+ /**
2161
+ * Reads a raw string stream, buffers incomplete chunks, and yields parsed JSON objects.
2162
+ */
2163
+ function getResponseStream(inputStream) {
2164
+ const reader = inputStream.getReader();
2165
+ const stream = new ReadableStream({
2166
+ start(controller) {
2167
+ let currentText = '';
2168
+ return pump();
2169
+ function pump() {
2170
+ return reader.read().then(({ value, done }) => {
2171
+ if (done) {
2172
+ if (currentText.trim()) {
2173
+ controller.error(new AIError(AIErrorCode.PARSE_FAILED, 'Failed to parse stream'));
2174
+ return;
2175
+ }
2176
+ controller.close();
2177
+ return;
2178
+ }
2179
+ currentText += value;
2180
+ // SSE events may span chunk boundaries, so we buffer until we match
2181
+ // the full "data: {json}\n\n" pattern.
2182
+ let match = currentText.match(responseLineRE);
2183
+ let parsedResponse;
2184
+ while (match) {
2185
+ try {
2186
+ parsedResponse = JSON.parse(match[1]);
2187
+ }
2188
+ catch (e) {
2189
+ controller.error(new AIError(AIErrorCode.PARSE_FAILED, `Error parsing JSON response: "${match[1]}`));
2190
+ return;
2191
+ }
2192
+ controller.enqueue(parsedResponse);
2193
+ currentText = currentText.substring(match[0].length);
2194
+ match = currentText.match(responseLineRE);
2195
+ }
2196
+ return pump();
2197
+ });
2198
+ }
2199
+ }
2200
+ });
2201
+ return stream;
2202
+ }
2203
+ /**
2204
+ * Aggregates an array of `GenerateContentResponse`s into a single
2205
+ * GenerateContentResponse.
2206
+ */
2207
+ function aggregateResponses(responses) {
2208
+ const lastResponse = responses[responses.length - 1];
2209
+ const aggregatedResponse = {
2210
+ promptFeedback: lastResponse?.promptFeedback
2211
+ };
2212
+ for (const response of responses) {
2213
+ if (response.candidates) {
2214
+ for (const candidate of response.candidates) {
2215
+ // Use 0 if index is undefined (protobuf default value omission).
2216
+ const i = candidate.index || 0;
2217
+ if (!aggregatedResponse.candidates) {
2218
+ aggregatedResponse.candidates = [];
2219
+ }
2220
+ if (!aggregatedResponse.candidates[i]) {
2221
+ aggregatedResponse.candidates[i] = {
2222
+ index: candidate.index
2223
+ };
2224
+ }
2225
+ // Overwrite with the latest metadata
2226
+ aggregatedResponse.candidates[i].citationMetadata =
2227
+ candidate.citationMetadata;
2228
+ aggregatedResponse.candidates[i].finishReason = candidate.finishReason;
2229
+ aggregatedResponse.candidates[i].finishMessage =
2230
+ candidate.finishMessage;
2231
+ aggregatedResponse.candidates[i].safetyRatings =
2232
+ candidate.safetyRatings;
2233
+ aggregatedResponse.candidates[i].groundingMetadata =
2234
+ candidate.groundingMetadata;
2235
+ // The urlContextMetadata object is defined in the first chunk of the response stream.
2236
+ // In all subsequent chunks, the urlContextMetadata object will be undefined. We need to
2237
+ // make sure that we don't overwrite the first value urlContextMetadata object with undefined.
2238
+ // FIXME: What happens if we receive a second, valid urlContextMetadata object?
2239
+ const urlContextMetadata = candidate.urlContextMetadata;
2240
+ if (typeof urlContextMetadata === 'object' &&
2241
+ urlContextMetadata !== null &&
2242
+ Object.keys(urlContextMetadata).length > 0) {
2243
+ aggregatedResponse.candidates[i].urlContextMetadata =
2244
+ urlContextMetadata;
2245
+ }
2246
+ if (candidate.content) {
2247
+ if (!candidate.content.parts) {
2248
+ continue;
2249
+ }
2250
+ if (!aggregatedResponse.candidates[i].content) {
2251
+ aggregatedResponse.candidates[i].content = {
2252
+ role: candidate.content.role || 'user',
2253
+ parts: []
2254
+ };
2255
+ }
2256
+ for (const part of candidate.content.parts) {
2257
+ const newPart = { ...part };
2258
+ // The backend can send empty text parts. If these are sent back
2259
+ // (e.g. in chat history), the backend will respond with an error.
2260
+ // To prevent this, ignore empty text parts.
2261
+ if (part.text === '') {
2262
+ continue;
2263
+ }
2264
+ if (Object.keys(newPart).length > 0) {
2265
+ aggregatedResponse.candidates[i].content.parts.push(newPart);
2266
+ }
2267
+ }
2268
+ }
2269
+ }
2270
+ }
2271
+ }
2272
+ return aggregatedResponse;
2273
+ }
2274
+
2275
+ /**
2276
+ * @license
2277
+ * Copyright 2025 Google LLC
2278
+ *
2279
+ * Licensed under the Apache License, Version 2.0 (the "License");
2280
+ * you may not use this file except in compliance with the License.
2281
+ * You may obtain a copy of the License at
2282
+ *
2283
+ * http://www.apache.org/licenses/LICENSE-2.0
2284
+ *
2285
+ * Unless required by applicable law or agreed to in writing, software
2286
+ * distributed under the License is distributed on an "AS IS" BASIS,
2287
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2288
+ * See the License for the specific language governing permissions and
2289
+ * limitations under the License.
2290
+ */
2291
+ const errorsCausingFallback = [
2292
+ // most network errors
2293
+ AIErrorCode.FETCH_ERROR,
2294
+ // fallback code for all other errors in makeRequest
2295
+ AIErrorCode.ERROR,
2296
+ // error due to API not being enabled in project
2297
+ AIErrorCode.API_NOT_ENABLED
2298
+ ];
2299
+ /**
2300
+ * Dispatches a request to the appropriate backend (on-device or in-cloud)
2301
+ * based on the inference mode.
2302
+ *
2303
+ * @param request - The request to be sent.
2304
+ * @param chromeAdapter - The on-device model adapter.
2305
+ * @param onDeviceCall - The function to call for on-device inference.
2306
+ * @param inCloudCall - The function to call for in-cloud inference.
2307
+ * @returns The response from the backend.
2308
+ */
2309
+ async function callCloudOrDevice(request, chromeAdapter, onDeviceCall, inCloudCall) {
2310
+ if (!chromeAdapter) {
2311
+ return {
2312
+ response: await inCloudCall(),
2313
+ inferenceSource: InferenceSource.IN_CLOUD
2314
+ };
2315
+ }
2316
+ switch (chromeAdapter.mode) {
2317
+ case InferenceMode.ONLY_ON_DEVICE:
2318
+ if (await chromeAdapter.isAvailable(request)) {
2319
+ return {
2320
+ response: await onDeviceCall(),
2321
+ inferenceSource: InferenceSource.ON_DEVICE
2322
+ };
2323
+ }
2324
+ throw new AIError(AIErrorCode.UNSUPPORTED, 'Inference mode is ONLY_ON_DEVICE, but an on-device model is not available.');
2325
+ case InferenceMode.ONLY_IN_CLOUD:
2326
+ return {
2327
+ response: await inCloudCall(),
2328
+ inferenceSource: InferenceSource.IN_CLOUD
2329
+ };
2330
+ case InferenceMode.PREFER_IN_CLOUD:
2331
+ try {
2332
+ return {
2333
+ response: await inCloudCall(),
2334
+ inferenceSource: InferenceSource.IN_CLOUD
2335
+ };
2336
+ }
2337
+ catch (e) {
2338
+ if (e instanceof AIError && errorsCausingFallback.includes(e.code)) {
2339
+ return {
2340
+ response: await onDeviceCall(),
2341
+ inferenceSource: InferenceSource.ON_DEVICE
2342
+ };
2343
+ }
2344
+ throw e;
2345
+ }
2346
+ case InferenceMode.PREFER_ON_DEVICE:
2347
+ if (await chromeAdapter.isAvailable(request)) {
2348
+ return {
2349
+ response: await onDeviceCall(),
2350
+ inferenceSource: InferenceSource.ON_DEVICE
2351
+ };
2352
+ }
2353
+ return {
2354
+ response: await inCloudCall(),
2355
+ inferenceSource: InferenceSource.IN_CLOUD
2356
+ };
2357
+ default:
2358
+ throw new AIError(AIErrorCode.ERROR, `Unexpected infererence mode: ${chromeAdapter.mode}`);
2359
+ }
2360
+ }
2361
+
2362
+ /**
2363
+ * @license
2364
+ * Copyright 2024 Google LLC
2365
+ *
2366
+ * Licensed under the Apache License, Version 2.0 (the "License");
2367
+ * you may not use this file except in compliance with the License.
2368
+ * You may obtain a copy of the License at
2369
+ *
2370
+ * http://www.apache.org/licenses/LICENSE-2.0
2371
+ *
2372
+ * Unless required by applicable law or agreed to in writing, software
2373
+ * distributed under the License is distributed on an "AS IS" BASIS,
2374
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2375
+ * See the License for the specific language governing permissions and
2376
+ * limitations under the License.
2377
+ */
2378
+ async function generateContentStreamOnCloud(apiSettings, model, params, singleRequestOptions) {
2379
+ if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
2380
+ params = mapGenerateContentRequest(params);
2381
+ }
2382
+ return makeRequest({
2383
+ task: "streamGenerateContent" /* Task.STREAM_GENERATE_CONTENT */,
2384
+ model,
2385
+ apiSettings,
2386
+ stream: true,
2387
+ singleRequestOptions
2388
+ }, JSON.stringify(params));
2389
+ }
2390
+ async function generateContentStream(apiSettings, model, params, chromeAdapter, singleRequestOptions) {
2391
+ const callResult = await callCloudOrDevice(params, chromeAdapter, () => chromeAdapter.generateContentStream(params), () => generateContentStreamOnCloud(apiSettings, model, params, singleRequestOptions));
2392
+ return processStream(callResult.response, apiSettings, callResult.inferenceSource);
2393
+ }
2394
+ async function generateContentOnCloud(apiSettings, model, params, singleRequestOptions) {
2395
+ if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
2396
+ params = mapGenerateContentRequest(params);
2397
+ }
2398
+ return makeRequest({
2399
+ model,
2400
+ task: "generateContent" /* Task.GENERATE_CONTENT */,
2401
+ apiSettings,
2402
+ stream: false,
2403
+ singleRequestOptions
2404
+ }, JSON.stringify(params));
2405
+ }
2406
+ async function templateGenerateContent(apiSettings, templateId, templateParams, singleRequestOptions) {
2407
+ const response = await makeRequest({
2408
+ task: "templateGenerateContent" /* ServerPromptTemplateTask.TEMPLATE_GENERATE_CONTENT */,
2409
+ templateId,
2410
+ apiSettings,
2411
+ stream: false,
2412
+ singleRequestOptions
2413
+ }, JSON.stringify(templateParams));
2414
+ const generateContentResponse = await processGenerateContentResponse(response, apiSettings);
2415
+ const enhancedResponse = createEnhancedContentResponse(generateContentResponse);
2416
+ return {
2417
+ response: enhancedResponse
2418
+ };
2419
+ }
2420
+ async function templateGenerateContentStream(apiSettings, templateId, templateParams, singleRequestOptions) {
2421
+ const response = await makeRequest({
2422
+ task: "templateStreamGenerateContent" /* ServerPromptTemplateTask.TEMPLATE_STREAM_GENERATE_CONTENT */,
2423
+ templateId,
2424
+ apiSettings,
2425
+ stream: true,
2426
+ singleRequestOptions
2427
+ }, JSON.stringify(templateParams));
2428
+ return processStream(response, apiSettings);
2429
+ }
2430
+ async function generateContent(apiSettings, model, params, chromeAdapter, singleRequestOptions) {
2431
+ const callResult = await callCloudOrDevice(params, chromeAdapter, () => chromeAdapter.generateContent(params), () => generateContentOnCloud(apiSettings, model, params, singleRequestOptions));
2432
+ const generateContentResponse = await processGenerateContentResponse(callResult.response, apiSettings);
2433
+ const enhancedResponse = createEnhancedContentResponse(generateContentResponse, callResult.inferenceSource);
2434
+ return {
2435
+ response: enhancedResponse
2436
+ };
2437
+ }
2438
+ async function processGenerateContentResponse(response, apiSettings) {
2439
+ const responseJson = await response.json();
2440
+ if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
2441
+ return mapGenerateContentResponse(responseJson);
2442
+ }
2443
+ else {
2444
+ return responseJson;
2445
+ }
2446
+ }
2447
+
2448
+ /**
2449
+ * @license
2450
+ * Copyright 2024 Google LLC
2451
+ *
2452
+ * Licensed under the Apache License, Version 2.0 (the "License");
2453
+ * you may not use this file except in compliance with the License.
2454
+ * You may obtain a copy of the License at
2455
+ *
2456
+ * http://www.apache.org/licenses/LICENSE-2.0
2457
+ *
2458
+ * Unless required by applicable law or agreed to in writing, software
2459
+ * distributed under the License is distributed on an "AS IS" BASIS,
2460
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2461
+ * See the License for the specific language governing permissions and
2462
+ * limitations under the License.
2463
+ */
2464
+ function formatSystemInstruction(input) {
2465
+ // null or undefined
2466
+ if (input == null) {
2467
+ return undefined;
2468
+ }
2469
+ else if (typeof input === 'string') {
2470
+ return { role: 'system', parts: [{ text: input }] };
2471
+ }
2472
+ else if (input.text) {
2473
+ return { role: 'system', parts: [input] };
2474
+ }
2475
+ else if (input.parts) {
2476
+ if (!input.role) {
2477
+ return { role: 'system', parts: input.parts };
2478
+ }
2479
+ else {
2480
+ return input;
2481
+ }
2482
+ }
2483
+ }
2484
+ function formatNewContent(request) {
2485
+ let newParts = [];
2486
+ if (typeof request === 'string') {
2487
+ newParts = [{ text: request }];
2488
+ }
2489
+ else {
2490
+ for (const partOrString of request) {
2491
+ if (typeof partOrString === 'string') {
2492
+ newParts.push({ text: partOrString });
2493
+ }
2494
+ else {
2495
+ newParts.push(partOrString);
2496
+ }
2497
+ }
2498
+ }
2499
+ return assignRoleToPartsAndValidateSendMessageRequest(newParts);
2500
+ }
2501
+ /**
2502
+ * When multiple Part types (i.e. FunctionResponsePart and TextPart) are
2503
+ * passed in a single Part array, we may need to assign different roles to each
2504
+ * part. Currently only FunctionResponsePart requires a role other than 'user'.
2505
+ * @private
2506
+ * @param parts Array of parts to pass to the model
2507
+ * @returns Array of content items
2508
+ */
2509
+ function assignRoleToPartsAndValidateSendMessageRequest(parts) {
2510
+ const userContent = { role: 'user', parts: [] };
2511
+ const functionContent = { role: 'function', parts: [] };
2512
+ let hasUserContent = false;
2513
+ let hasFunctionContent = false;
2514
+ for (const part of parts) {
2515
+ if ('functionResponse' in part) {
2516
+ functionContent.parts.push(part);
2517
+ hasFunctionContent = true;
2518
+ }
2519
+ else {
2520
+ userContent.parts.push(part);
2521
+ hasUserContent = true;
2522
+ }
2523
+ }
2524
+ if (hasUserContent && hasFunctionContent) {
2525
+ throw new AIError(AIErrorCode.INVALID_CONTENT, 'Within a single message, FunctionResponse cannot be mixed with other type of Part in the request for sending chat message.');
2526
+ }
2527
+ if (!hasUserContent && !hasFunctionContent) {
2528
+ throw new AIError(AIErrorCode.INVALID_CONTENT, 'No Content is provided for sending chat message.');
2529
+ }
2530
+ if (hasUserContent) {
2531
+ return userContent;
2532
+ }
2533
+ return functionContent;
2534
+ }
2535
+ function formatGenerateContentInput(params) {
2536
+ let formattedRequest;
2537
+ if (params.contents) {
2538
+ formattedRequest = params;
2539
+ }
2540
+ else {
2541
+ // Array or string
2542
+ const content = formatNewContent(params);
2543
+ formattedRequest = { contents: [content] };
2544
+ }
2545
+ if (params.systemInstruction) {
2546
+ formattedRequest.systemInstruction = formatSystemInstruction(params.systemInstruction);
2547
+ }
2548
+ return formattedRequest;
2549
+ }
2550
+ /**
2551
+ * Convert the user-defined parameters in {@link ImagenGenerationParams} to the format
2552
+ * that is expected from the REST API.
2553
+ *
2554
+ * @internal
2555
+ */
2556
+ function createPredictRequestBody(prompt, { gcsURI, imageFormat, addWatermark, numberOfImages = 1, negativePrompt, aspectRatio, safetyFilterLevel, personFilterLevel }) {
2557
+ // Properties that are undefined will be omitted from the JSON string that is sent in the request.
2558
+ const body = {
2559
+ instances: [
2560
+ {
2561
+ prompt
2562
+ }
2563
+ ],
2564
+ parameters: {
2565
+ storageUri: gcsURI,
2566
+ negativePrompt,
2567
+ sampleCount: numberOfImages,
2568
+ aspectRatio,
2569
+ outputOptions: imageFormat,
2570
+ addWatermark,
2571
+ safetyFilterLevel,
2572
+ personGeneration: personFilterLevel,
2573
+ includeRaiReason: true,
2574
+ includeSafetyAttributes: true
2575
+ }
2576
+ };
2577
+ return body;
2578
+ }
2579
+
2580
+ /**
2581
+ * @license
2582
+ * Copyright 2024 Google LLC
2583
+ *
2584
+ * Licensed under the Apache License, Version 2.0 (the "License");
2585
+ * you may not use this file except in compliance with the License.
2586
+ * You may obtain a copy of the License at
2587
+ *
2588
+ * http://www.apache.org/licenses/LICENSE-2.0
2589
+ *
2590
+ * Unless required by applicable law or agreed to in writing, software
2591
+ * distributed under the License is distributed on an "AS IS" BASIS,
2592
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2593
+ * See the License for the specific language governing permissions and
2594
+ * limitations under the License.
2595
+ */
2596
+ // https://ai.google.dev/api/rest/v1beta/Content#part
2597
+ const VALID_PART_FIELDS = [
2598
+ 'text',
2599
+ 'inlineData',
2600
+ 'functionCall',
2601
+ 'functionResponse',
2602
+ 'thought',
2603
+ 'thoughtSignature'
2604
+ ];
2605
+ const VALID_PARTS_PER_ROLE = {
2606
+ user: ['text', 'inlineData'],
2607
+ function: ['functionResponse'],
2608
+ model: ['text', 'functionCall', 'thought', 'thoughtSignature'],
2609
+ // System instructions shouldn't be in history anyway.
2610
+ system: ['text']
2611
+ };
2612
+ const VALID_PREVIOUS_CONTENT_ROLES = {
2613
+ user: ['model'],
2614
+ function: ['model'],
2615
+ model: ['user', 'function'],
2616
+ // System instructions shouldn't be in history.
2617
+ system: []
2618
+ };
2619
+ function validateChatHistory(history) {
2620
+ let prevContent = null;
2621
+ for (const currContent of history) {
2622
+ const { role, parts } = currContent;
2623
+ if (!prevContent && role !== 'user') {
2624
+ throw new AIError(AIErrorCode.INVALID_CONTENT, `First Content should be with role 'user', got ${role}`);
2625
+ }
2626
+ if (!POSSIBLE_ROLES.includes(role)) {
2627
+ throw new AIError(AIErrorCode.INVALID_CONTENT, `Each item should include role field. Got ${role} but valid roles are: ${JSON.stringify(POSSIBLE_ROLES)}`);
2628
+ }
2629
+ if (!Array.isArray(parts)) {
2630
+ throw new AIError(AIErrorCode.INVALID_CONTENT, `Content should have 'parts' property with an array of Parts`);
2631
+ }
2632
+ if (parts.length === 0) {
2633
+ throw new AIError(AIErrorCode.INVALID_CONTENT, `Each Content should have at least one part`);
2634
+ }
2635
+ const countFields = {
2636
+ text: 0,
2637
+ inlineData: 0,
2638
+ functionCall: 0,
2639
+ functionResponse: 0,
2640
+ thought: 0,
2641
+ thoughtSignature: 0,
2642
+ executableCode: 0,
2643
+ codeExecutionResult: 0
2644
+ };
2645
+ for (const part of parts) {
2646
+ for (const key of VALID_PART_FIELDS) {
2647
+ if (key in part) {
2648
+ countFields[key] += 1;
2649
+ }
2650
+ }
2651
+ }
2652
+ const validParts = VALID_PARTS_PER_ROLE[role];
2653
+ for (const key of VALID_PART_FIELDS) {
2654
+ if (!validParts.includes(key) && countFields[key] > 0) {
2655
+ throw new AIError(AIErrorCode.INVALID_CONTENT, `Content with role '${role}' can't contain '${key}' part`);
2656
+ }
2657
+ }
2658
+ if (prevContent) {
2659
+ const validPreviousContentRoles = VALID_PREVIOUS_CONTENT_ROLES[role];
2660
+ if (!validPreviousContentRoles.includes(prevContent.role)) {
2661
+ throw new AIError(AIErrorCode.INVALID_CONTENT, `Content with role '${role}' can't follow '${prevContent.role}'. Valid previous roles: ${JSON.stringify(VALID_PREVIOUS_CONTENT_ROLES)}`);
2662
+ }
2663
+ }
2664
+ prevContent = currContent;
2665
+ }
2666
+ }
2667
+
2668
+ /**
2669
+ * @license
2670
+ * Copyright 2024 Google LLC
2671
+ *
2672
+ * Licensed under the Apache License, Version 2.0 (the "License");
2673
+ * you may not use this file except in compliance with the License.
2674
+ * You may obtain a copy of the License at
2675
+ *
2676
+ * http://www.apache.org/licenses/LICENSE-2.0
2677
+ *
2678
+ * Unless required by applicable law or agreed to in writing, software
2679
+ * distributed under the License is distributed on an "AS IS" BASIS,
2680
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2681
+ * See the License for the specific language governing permissions and
2682
+ * limitations under the License.
2683
+ */
2684
+ /**
2685
+ * Used to break the internal promise chain when an error is already handled
2686
+ * by the user, preventing duplicate console logs.
2687
+ */
2688
+ const SILENT_ERROR = 'SILENT_ERROR';
2689
+ /**
2690
+ * Prevent infinite loop if the model continues to request sequential
2691
+ * function calls during automatic function calling.
2692
+ */
2693
+ const DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS = 10;
2694
+ /**
2695
+ * ChatSession class that enables sending chat messages and stores
2696
+ * history of sent and received messages so far.
2697
+ *
2698
+ * @public
2699
+ */
2700
+ class ChatSession {
2701
+ constructor(apiSettings, model, chromeAdapter, params, requestOptions) {
2702
+ this.model = model;
2703
+ this.chromeAdapter = chromeAdapter;
2704
+ this.params = params;
2705
+ this.requestOptions = requestOptions;
2706
+ this._history = [];
2707
+ /**
2708
+ * Ensures sequential execution of chat messages to maintain history order.
2709
+ * Each call waits for the previous one to settle before proceeding.
2710
+ */
2711
+ this._sendPromise = Promise.resolve();
2712
+ this._apiSettings = apiSettings;
2713
+ if (params?.history) {
2714
+ validateChatHistory(params.history);
2715
+ this._history = params.history;
2716
+ }
2717
+ }
2718
+ /**
2719
+ * Gets the chat history so far. Blocked prompts are not added to history.
2720
+ * Neither blocked candidates nor the prompts that generated them are added
2721
+ * to history.
2722
+ */
2723
+ async getHistory() {
2724
+ await this._sendPromise;
2725
+ return this._history;
2726
+ }
2727
+ /**
2728
+ * Format Content into a request for generateContent or
2729
+ * generateContentStream.
2730
+ * @internal
2731
+ */
2732
+ _formatRequest(incomingContent, tempHistory) {
2733
+ return {
2734
+ safetySettings: this.params?.safetySettings,
2735
+ generationConfig: this.params?.generationConfig,
2736
+ tools: this.params?.tools,
2737
+ toolConfig: this.params?.toolConfig,
2738
+ systemInstruction: this.params?.systemInstruction,
2739
+ contents: [...this._history, ...tempHistory, incomingContent]
2740
+ };
2741
+ }
2742
+ /**
2743
+ * Sends a chat message and receives a non-streaming
2744
+ * {@link GenerateContentResult}
2745
+ */
2746
+ async sendMessage(request, singleRequestOptions) {
2747
+ let finalResult = {};
2748
+ await this._sendPromise;
2749
+ /**
2750
+ * Temporarily store multiple turns for cases like automatic function
2751
+ * calling, only writing them to official history when the entire
2752
+ * sequence has completed successfully.
2753
+ */
2754
+ const tempHistory = [];
2755
+ this._sendPromise = this._sendPromise.then(async () => {
2756
+ let functionCalls;
2757
+ let functionCallTurnCount = 0;
2758
+ const functionCallMaxTurns = this.requestOptions?.maxSequentalFunctionCalls ??
2759
+ DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS;
2760
+ // Repeats until model returns a response with no function calls
2761
+ // or until `functionCallMaxTurns` is met or exceeded.
2762
+ do {
2763
+ let formattedContent;
2764
+ if (functionCalls) {
2765
+ functionCallTurnCount++;
2766
+ const functionResponseParts = await this._callFunctionsAsNeeded(functionCalls);
2767
+ formattedContent = formatNewContent(functionResponseParts);
2768
+ }
2769
+ else {
2770
+ formattedContent = formatNewContent(request);
2771
+ }
2772
+ const formattedRequest = this._formatRequest(formattedContent, tempHistory);
2773
+ tempHistory.push(formattedContent);
2774
+ const result = await generateContent(this._apiSettings, this.model, formattedRequest, this.chromeAdapter, {
2775
+ ...this.requestOptions,
2776
+ ...singleRequestOptions
2777
+ });
2778
+ if (result) {
2779
+ finalResult = result;
2780
+ functionCalls = this._getCallableFunctionCalls(result.response);
2781
+ if (result.response.candidates &&
2782
+ result.response.candidates.length > 0) {
2783
+ // TODO: Make this update atomic. If creating `responseContent` throws,
2784
+ // history will contain the user message but not the response, causing
2785
+ // validation errors on the next request.
2786
+ const responseContent = {
2787
+ parts: result.response.candidates?.[0].content.parts || [],
2788
+ // Response seems to come back without a role set.
2789
+ role: result.response.candidates?.[0].content.role || 'model'
2790
+ };
2791
+ tempHistory.push(responseContent);
2792
+ }
2793
+ else {
2794
+ const blockErrorMessage = formatBlockErrorMessage(result.response);
2795
+ if (blockErrorMessage) {
2796
+ logger.warn(`sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`);
2797
+ }
2798
+ }
2799
+ }
2800
+ else {
2801
+ functionCalls = undefined;
2802
+ }
2803
+ } while (functionCalls && functionCallTurnCount < functionCallMaxTurns);
2804
+ if (functionCalls && functionCallTurnCount >= functionCallMaxTurns) {
2805
+ logger.warn(`Automatic function calling exceeded the limit of` +
2806
+ ` ${functionCallMaxTurns} function calls. Returning last model response.`);
2807
+ }
2808
+ });
2809
+ await this._sendPromise;
2810
+ this._history = this._history.concat(tempHistory);
2811
+ return finalResult;
2812
+ }
2813
+ /**
2814
+ * Sends a chat message and receives the response as a
2815
+ * {@link GenerateContentStreamResult} containing an iterable stream
2816
+ * and a response promise.
2817
+ */
2818
+ async sendMessageStream(request, singleRequestOptions) {
2819
+ await this._sendPromise;
2820
+ /**
2821
+ * Temporarily store multiple turns for cases like automatic function
2822
+ * calling, only writing them to official history when the entire
2823
+ * sequence has completed successfully.
2824
+ */
2825
+ const tempHistory = [];
2826
+ const callGenerateContentStream = async () => {
2827
+ let functionCalls;
2828
+ let functionCallTurnCount = 0;
2829
+ const functionCallMaxTurns = this.requestOptions?.maxSequentalFunctionCalls ??
2830
+ DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS;
2831
+ let result;
2832
+ // Repeats until model returns a response with no function calls
2833
+ // or until `functionCallMaxTurns` is met or exceeded.
2834
+ do {
2835
+ let formattedContent;
2836
+ if (functionCalls) {
2837
+ functionCallTurnCount++;
2838
+ const functionResponseParts = await this._callFunctionsAsNeeded(functionCalls);
2839
+ formattedContent = formatNewContent(functionResponseParts);
2840
+ }
2841
+ else {
2842
+ formattedContent = formatNewContent(request);
2843
+ }
2844
+ tempHistory.push(formattedContent);
2845
+ const formattedRequest = this._formatRequest(formattedContent, tempHistory);
2846
+ result = await generateContentStream(this._apiSettings, this.model, formattedRequest, this.chromeAdapter, {
2847
+ ...this.requestOptions,
2848
+ ...singleRequestOptions
2849
+ });
2850
+ functionCalls = this._getCallableFunctionCalls(result.firstValue);
2851
+ if (functionCalls &&
2852
+ result.firstValue &&
2853
+ result.firstValue.candidates &&
2854
+ result.firstValue.candidates.length > 0) {
2855
+ const responseContent = {
2856
+ ...result.firstValue.candidates[0].content
2857
+ };
2858
+ if (!responseContent.role) {
2859
+ responseContent.role = 'model';
2860
+ }
2861
+ tempHistory.push(responseContent);
2862
+ }
2863
+ } while (functionCalls && functionCallTurnCount < functionCallMaxTurns);
2864
+ if (functionCalls && functionCallTurnCount >= functionCallMaxTurns) {
2865
+ logger.warn(`Automatic function calling exceeded the limit of` +
2866
+ ` ${functionCallMaxTurns} function calls. Returning last model response.`);
2867
+ }
2868
+ return { stream: result.stream, response: result.response };
2869
+ };
2870
+ const streamPromise = callGenerateContentStream();
2871
+ // Add onto the chain.
2872
+ this._sendPromise = this._sendPromise
2873
+ .then(async () => streamPromise)
2874
+ // This must be handled to avoid unhandled rejection, but jump
2875
+ // to the final catch block with a label to not log this error.
2876
+ .catch(_ignored => {
2877
+ // If the initial fetch fails, the user's `streamPromise` rejects.
2878
+ // We swallow the error here to prevent double logging in the final catch.
2879
+ throw new Error(SILENT_ERROR);
2880
+ })
2881
+ .then(streamResult => streamResult.response)
2882
+ .then(response => {
2883
+ // This runs after the stream completes. Runtime errors here cannot be
2884
+ // caught by the user because their promise has likely already resolved.
2885
+ // TODO: Move response validation logic upstream to `stream-reader` so
2886
+ // errors propagate to the user's `result.response` promise.
2887
+ if (response.candidates && response.candidates.length > 0) {
2888
+ this._history = this._history.concat(tempHistory);
2889
+ // TODO: Validate that `response.candidates[0].content` is not null.
2890
+ const responseContent = { ...response.candidates[0].content };
2891
+ if (!responseContent.role) {
2892
+ responseContent.role = 'model';
2893
+ }
2894
+ this._history.push(responseContent);
2895
+ }
2896
+ else {
2897
+ const blockErrorMessage = formatBlockErrorMessage(response);
2898
+ if (blockErrorMessage) {
2899
+ logger.warn(`sendMessageStream() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`);
2900
+ }
2901
+ }
2902
+ })
2903
+ .catch(e => {
2904
+ // Filter out errors already handled by the user or initiated by them.
2905
+ if (e.message !== SILENT_ERROR && e.name !== 'AbortError') {
2906
+ logger.error(e);
2907
+ }
2908
+ });
2909
+ return streamPromise;
2910
+ }
2911
+ /**
2912
+ * Get function calls that the SDK has references to actually call.
2913
+ * This is all-or-nothing. If the model is requesting multiple
2914
+ * function calls, all of them must have references in order for
2915
+ * automatic function calling to work.
2916
+ *
2917
+ * @internal
2918
+ */
2919
+ _getCallableFunctionCalls(response) {
2920
+ const functionDeclarationsTool = this.params?.tools?.find(tool => tool.functionDeclarations);
2921
+ if (!functionDeclarationsTool?.functionDeclarations) {
2922
+ return;
2923
+ }
2924
+ const functionCalls = getFunctionCalls(response);
2925
+ if (!functionCalls) {
2926
+ return;
2927
+ }
2928
+ for (const functionCall of functionCalls) {
2929
+ const hasFunctionReference = functionDeclarationsTool.functionDeclarations?.some(declaration => declaration.name === functionCall.name &&
2930
+ typeof declaration.functionReference === 'function');
2931
+ if (!hasFunctionReference) {
2932
+ return;
2933
+ }
2934
+ }
2935
+ return functionCalls;
2936
+ }
2937
+ /**
2938
+ * Call user-defined functions if requested by the model, and return
2939
+ * the response that should be sent to the model.
2940
+ * @internal
2941
+ */
2942
+ async _callFunctionsAsNeeded(functionCalls) {
2943
+ const activeCallList = new Map();
2944
+ const promiseList = [];
2945
+ const functionDeclarationsTool = this.params?.tools?.find(tool => tool.functionDeclarations);
2946
+ if (functionDeclarationsTool &&
2947
+ functionDeclarationsTool.functionDeclarations) {
2948
+ for (const functionCall of functionCalls) {
2949
+ const functionDeclaration = functionDeclarationsTool.functionDeclarations.find(declaration => declaration.name === functionCall.name);
2950
+ if (functionDeclaration?.functionReference) {
2951
+ const results = Promise.resolve(functionDeclaration.functionReference(functionCall.args)).catch(e => {
2952
+ const wrappedError = new AIError(AIErrorCode.ERROR, `Error in user-defined function "${functionDeclaration.name}": ${e.message}`);
2953
+ wrappedError.stack = e.stack;
2954
+ throw wrappedError;
2955
+ });
2956
+ activeCallList.set(functionCall.name, {
2957
+ id: functionCall.id,
2958
+ results
2959
+ });
2960
+ promiseList.push(results);
2961
+ }
2962
+ }
2963
+ // Wait for promises to finish.
2964
+ await Promise.all(promiseList);
2965
+ const functionResponseParts = [];
2966
+ for (const [name, callData] of activeCallList) {
2967
+ functionResponseParts.push({
2968
+ functionResponse: {
2969
+ name,
2970
+ response: await callData.results
2971
+ }
2972
+ });
2973
+ }
2974
+ return functionResponseParts;
2975
+ }
2976
+ else {
2977
+ throw new AIError(AIErrorCode.REQUEST_ERROR, `No function declarations were provided in "tools".`);
2978
+ }
2979
+ }
2980
+ }
2981
+
2982
+ /**
2983
+ * @license
2984
+ * Copyright 2024 Google LLC
2985
+ *
2986
+ * Licensed under the Apache License, Version 2.0 (the "License");
2987
+ * you may not use this file except in compliance with the License.
2988
+ * You may obtain a copy of the License at
2989
+ *
2990
+ * http://www.apache.org/licenses/LICENSE-2.0
2991
+ *
2992
+ * Unless required by applicable law or agreed to in writing, software
2993
+ * distributed under the License is distributed on an "AS IS" BASIS,
2994
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2995
+ * See the License for the specific language governing permissions and
2996
+ * limitations under the License.
2997
+ */
2998
+ async function countTokensOnCloud(apiSettings, model, params, singleRequestOptions) {
2999
+ let body = '';
3000
+ if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
3001
+ const mappedParams = mapCountTokensRequest(params, model);
3002
+ body = JSON.stringify(mappedParams);
3003
+ }
3004
+ else {
3005
+ body = JSON.stringify(params);
3006
+ }
3007
+ const response = await makeRequest({
3008
+ model,
3009
+ task: "countTokens" /* Task.COUNT_TOKENS */,
3010
+ apiSettings,
3011
+ stream: false,
3012
+ singleRequestOptions
3013
+ }, body);
3014
+ return response.json();
3015
+ }
3016
+ async function countTokens(apiSettings, model, params, chromeAdapter, requestOptions) {
3017
+ if (chromeAdapter?.mode === InferenceMode.ONLY_ON_DEVICE) {
3018
+ throw new AIError(AIErrorCode.UNSUPPORTED, 'countTokens() is not supported for on-device models.');
3019
+ }
3020
+ return countTokensOnCloud(apiSettings, model, params, requestOptions);
3021
+ }
3022
+
3023
+ /**
3024
+ * @license
3025
+ * Copyright 2024 Google LLC
3026
+ *
3027
+ * Licensed under the Apache License, Version 2.0 (the "License");
3028
+ * you may not use this file except in compliance with the License.
3029
+ * You may obtain a copy of the License at
3030
+ *
3031
+ * http://www.apache.org/licenses/LICENSE-2.0
3032
+ *
3033
+ * Unless required by applicable law or agreed to in writing, software
3034
+ * distributed under the License is distributed on an "AS IS" BASIS,
3035
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3036
+ * See the License for the specific language governing permissions and
3037
+ * limitations under the License.
3038
+ */
3039
+ /**
3040
+ * Class for generative model APIs.
3041
+ * @public
3042
+ */
3043
+ class GenerativeModel extends AIModel {
3044
+ constructor(ai, modelParams, requestOptions, chromeAdapter) {
3045
+ super(ai, modelParams.model);
3046
+ this.chromeAdapter = chromeAdapter;
3047
+ this.generationConfig = modelParams.generationConfig || {};
3048
+ validateGenerationConfig(this.generationConfig);
3049
+ this.safetySettings = modelParams.safetySettings || [];
3050
+ this.tools = modelParams.tools;
3051
+ this.toolConfig = modelParams.toolConfig;
3052
+ this.systemInstruction = formatSystemInstruction(modelParams.systemInstruction);
3053
+ this.requestOptions = requestOptions || {};
3054
+ }
3055
+ /**
3056
+ * Makes a single non-streaming call to the model
3057
+ * and returns an object containing a single {@link GenerateContentResponse}.
3058
+ */
3059
+ async generateContent(request, singleRequestOptions) {
3060
+ const formattedParams = formatGenerateContentInput(request);
3061
+ return generateContent(this._apiSettings, this.model, {
3062
+ generationConfig: this.generationConfig,
3063
+ safetySettings: this.safetySettings,
3064
+ tools: this.tools,
3065
+ toolConfig: this.toolConfig,
3066
+ systemInstruction: this.systemInstruction,
3067
+ ...formattedParams
3068
+ }, this.chromeAdapter,
3069
+ // Merge request options
3070
+ {
3071
+ ...this.requestOptions,
3072
+ ...singleRequestOptions
3073
+ });
3074
+ }
3075
+ /**
3076
+ * Makes a single streaming call to the model
3077
+ * and returns an object containing an iterable stream that iterates
3078
+ * over all chunks in the streaming response as well as
3079
+ * a promise that returns the final aggregated response.
3080
+ */
3081
+ async generateContentStream(request, singleRequestOptions) {
3082
+ const formattedParams = formatGenerateContentInput(request);
3083
+ const { stream, response } = await generateContentStream(this._apiSettings, this.model, {
3084
+ generationConfig: this.generationConfig,
3085
+ safetySettings: this.safetySettings,
3086
+ tools: this.tools,
3087
+ toolConfig: this.toolConfig,
3088
+ systemInstruction: this.systemInstruction,
3089
+ ...formattedParams
3090
+ }, this.chromeAdapter,
3091
+ // Merge request options
3092
+ {
3093
+ ...this.requestOptions,
3094
+ ...singleRequestOptions
3095
+ });
3096
+ return { stream, response };
3097
+ }
3098
+ /**
3099
+ * Gets a new {@link ChatSession} instance which can be used for
3100
+ * multi-turn chats.
3101
+ */
3102
+ startChat(startChatParams) {
3103
+ return new ChatSession(this._apiSettings, this.model, this.chromeAdapter, {
3104
+ tools: this.tools,
3105
+ toolConfig: this.toolConfig,
3106
+ systemInstruction: this.systemInstruction,
3107
+ generationConfig: this.generationConfig,
3108
+ safetySettings: this.safetySettings,
3109
+ /**
3110
+ * Overrides params inherited from GenerativeModel with those explicitly set in the
3111
+ * StartChatParams. For example, if startChatParams.generationConfig is set, it'll override
3112
+ * this.generationConfig.
3113
+ */
3114
+ ...startChatParams
3115
+ }, this.requestOptions);
3116
+ }
3117
+ /**
3118
+ * Counts the tokens in the provided request.
3119
+ */
3120
+ async countTokens(request, singleRequestOptions) {
3121
+ const formattedParams = formatGenerateContentInput(request);
3122
+ return countTokens(this._apiSettings, this.model, formattedParams, this.chromeAdapter,
3123
+ // Merge request options
3124
+ {
3125
+ ...this.requestOptions,
3126
+ ...singleRequestOptions
3127
+ });
3128
+ }
3129
+ }
3130
+ /**
3131
+ * Client-side validation of some common `GenerationConfig` pitfalls, in order
3132
+ * to save the developer a wasted request.
3133
+ */
3134
+ function validateGenerationConfig(generationConfig) {
3135
+ if (
3136
+ // != allows for null and undefined. 0 is considered "set" by the model
3137
+ generationConfig.thinkingConfig?.thinkingBudget != null &&
3138
+ generationConfig.thinkingConfig?.thinkingLevel) {
3139
+ throw new AIError(AIErrorCode.UNSUPPORTED, `Cannot set both thinkingBudget and thinkingLevel in a config.`);
3140
+ }
3141
+ }
3142
+
3143
+ /**
3144
+ * @license
3145
+ * Copyright 2025 Google LLC
3146
+ *
3147
+ * Licensed under the Apache License, Version 2.0 (the "License");
3148
+ * you may not use this file except in compliance with the License.
3149
+ * You may obtain a copy of the License at
3150
+ *
3151
+ * http://www.apache.org/licenses/LICENSE-2.0
3152
+ *
3153
+ * Unless required by applicable law or agreed to in writing, software
3154
+ * distributed under the License is distributed on an "AS IS" BASIS,
3155
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3156
+ * See the License for the specific language governing permissions and
3157
+ * limitations under the License.
3158
+ */
3159
+ /**
3160
+ * Represents an active, real-time, bidirectional conversation with the model.
3161
+ *
3162
+ * This class should only be instantiated by calling {@link LiveGenerativeModel.connect}.
3163
+ *
3164
+ * @beta
3165
+ */
3166
+ class LiveSession {
3167
+ /**
3168
+ * @internal
3169
+ */
3170
+ constructor(webSocketHandler, serverMessages) {
3171
+ this.webSocketHandler = webSocketHandler;
3172
+ this.serverMessages = serverMessages;
3173
+ /**
3174
+ * Indicates whether this Live session is closed.
3175
+ *
3176
+ * @beta
3177
+ */
3178
+ this.isClosed = false;
3179
+ /**
3180
+ * Indicates whether this Live session is being controlled by an `AudioConversationController`.
3181
+ *
3182
+ * @beta
3183
+ */
3184
+ this.inConversation = false;
3185
+ }
3186
+ /**
3187
+ * Sends content to the server.
3188
+ *
3189
+ * @param request - The message to send to the model.
3190
+ * @param turnComplete - Indicates if the turn is complete. Defaults to false.
3191
+ * @throws If this session has been closed.
3192
+ *
3193
+ * @beta
3194
+ */
3195
+ async send(request, turnComplete = true) {
3196
+ if (this.isClosed) {
3197
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
3198
+ }
3199
+ const newContent = formatNewContent(request);
3200
+ const message = {
3201
+ clientContent: {
3202
+ turns: [newContent],
3203
+ turnComplete
3204
+ }
3205
+ };
3206
+ this.webSocketHandler.send(JSON.stringify(message));
3207
+ }
3208
+ /**
3209
+ * Sends text to the server in realtime.
3210
+ *
3211
+ * @example
3212
+ * ```javascript
3213
+ * liveSession.sendTextRealtime("Hello, how are you?");
3214
+ * ```
3215
+ *
3216
+ * @param text - The text data to send.
3217
+ * @throws If this session has been closed.
3218
+ *
3219
+ * @beta
3220
+ */
3221
+ async sendTextRealtime(text) {
3222
+ if (this.isClosed) {
3223
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
3224
+ }
3225
+ const message = {
3226
+ realtimeInput: {
3227
+ text
3228
+ }
3229
+ };
3230
+ this.webSocketHandler.send(JSON.stringify(message));
3231
+ }
3232
+ /**
3233
+ * Sends audio data to the server in realtime.
3234
+ *
3235
+ * @remarks The server requires that the audio data is base64-encoded 16-bit PCM at 16kHz
3236
+ * little-endian.
3237
+ *
3238
+ * @example
3239
+ * ```javascript
3240
+ * // const pcmData = ... base64-encoded 16-bit PCM at 16kHz little-endian.
3241
+ * const blob = { mimeType: "audio/pcm", data: pcmData };
3242
+ * liveSession.sendAudioRealtime(blob);
3243
+ * ```
3244
+ *
3245
+ * @param blob - The base64-encoded PCM data to send to the server in realtime.
3246
+ * @throws If this session has been closed.
3247
+ *
3248
+ * @beta
3249
+ */
3250
+ async sendAudioRealtime(blob) {
3251
+ if (this.isClosed) {
3252
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
3253
+ }
3254
+ const message = {
3255
+ realtimeInput: {
3256
+ audio: blob
3257
+ }
3258
+ };
3259
+ this.webSocketHandler.send(JSON.stringify(message));
3260
+ }
3261
+ /**
3262
+ * Sends video data to the server in realtime.
3263
+ *
3264
+ * @remarks The server requires that the video is sent as individual video frames at 1 FPS. It
3265
+ * is recommended to set `mimeType` to `image/jpeg`.
3266
+ *
3267
+ * @example
3268
+ * ```javascript
3269
+ * // const videoFrame = ... base64-encoded JPEG data
3270
+ * const blob = { mimeType: "image/jpeg", data: videoFrame };
3271
+ * liveSession.sendVideoRealtime(blob);
3272
+ * ```
3273
+ * @param blob - The base64-encoded video data to send to the server in realtime.
3274
+ * @throws If this session has been closed.
3275
+ *
3276
+ * @beta
3277
+ */
3278
+ async sendVideoRealtime(blob) {
3279
+ if (this.isClosed) {
3280
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
3281
+ }
3282
+ const message = {
3283
+ realtimeInput: {
3284
+ video: blob
3285
+ }
3286
+ };
3287
+ this.webSocketHandler.send(JSON.stringify(message));
3288
+ }
3289
+ /**
3290
+ * Sends function responses to the server.
3291
+ *
3292
+ * @param functionResponses - The function responses to send.
3293
+ * @throws If this session has been closed.
3294
+ *
3295
+ * @beta
3296
+ */
3297
+ async sendFunctionResponses(functionResponses) {
3298
+ if (this.isClosed) {
3299
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
3300
+ }
3301
+ const message = {
3302
+ toolResponse: {
3303
+ functionResponses
3304
+ }
3305
+ };
3306
+ this.webSocketHandler.send(JSON.stringify(message));
3307
+ }
3308
+ /**
3309
+ * Yields messages received from the server.
3310
+ * This can only be used by one consumer at a time.
3311
+ *
3312
+ * @returns An `AsyncGenerator` that yields server messages as they arrive.
3313
+ * @throws If the session is already closed, or if we receive a response that we don't support.
3314
+ *
3315
+ * @beta
3316
+ */
3317
+ async *receive() {
3318
+ if (this.isClosed) {
3319
+ throw new AIError(AIErrorCode.SESSION_CLOSED, 'Cannot read from a Live session that is closed. Try starting a new Live session.');
3320
+ }
3321
+ for await (const message of this.serverMessages) {
3322
+ if (message && typeof message === 'object') {
3323
+ if (LiveResponseType.SERVER_CONTENT in message) {
3324
+ yield {
3325
+ type: 'serverContent',
3326
+ ...message
3327
+ .serverContent
3328
+ };
3329
+ }
3330
+ else if (LiveResponseType.TOOL_CALL in message) {
3331
+ yield {
3332
+ type: 'toolCall',
3333
+ ...message
3334
+ .toolCall
3335
+ };
3336
+ }
3337
+ else if (LiveResponseType.TOOL_CALL_CANCELLATION in message) {
3338
+ yield {
3339
+ type: 'toolCallCancellation',
3340
+ ...message.toolCallCancellation
3341
+ };
3342
+ }
3343
+ else if ('goAway' in message) {
3344
+ const notice = message.goAway;
3345
+ yield {
3346
+ type: LiveResponseType.GOING_AWAY_NOTICE,
3347
+ timeLeft: parseDuration(notice.timeLeft)
3348
+ };
3349
+ }
3350
+ else {
3351
+ logger.warn(`Received an unknown message type from the server: ${JSON.stringify(message)}`);
3352
+ }
3353
+ }
3354
+ else {
3355
+ logger.warn(`Received an invalid message from the server: ${JSON.stringify(message)}`);
3356
+ }
3357
+ }
3358
+ }
3359
+ /**
3360
+ * Closes this session.
3361
+ * All methods on this session will throw an error once this resolves.
3362
+ *
3363
+ * @beta
3364
+ */
3365
+ async close() {
3366
+ if (!this.isClosed) {
3367
+ this.isClosed = true;
3368
+ await this.webSocketHandler.close(1000, 'Client closed session.');
3369
+ }
3370
+ }
3371
+ /**
3372
+ * Sends realtime input to the server.
3373
+ *
3374
+ * @deprecated Use `sendTextRealtime()`, `sendAudioRealtime()`, and `sendVideoRealtime()` instead.
3375
+ *
3376
+ * @param mediaChunks - The media chunks to send.
3377
+ * @throws If this session has been closed.
3378
+ *
3379
+ * @beta
3380
+ */
3381
+ async sendMediaChunks(mediaChunks) {
3382
+ if (this.isClosed) {
3383
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
3384
+ }
3385
+ // The backend does not support sending more than one mediaChunk in one message.
3386
+ // Work around this limitation by sending mediaChunks in separate messages.
3387
+ mediaChunks.forEach(mediaChunk => {
3388
+ const message = {
3389
+ realtimeInput: { mediaChunks: [mediaChunk] }
3390
+ };
3391
+ this.webSocketHandler.send(JSON.stringify(message));
3392
+ });
3393
+ }
3394
+ /**
3395
+ * @deprecated Use `sendTextRealtime()`, `sendAudioRealtime()`, and `sendVideoRealtime()` instead.
3396
+ *
3397
+ * Sends a stream of {@link GenerativeContentBlob}.
3398
+ *
3399
+ * @param mediaChunkStream - The stream of {@link GenerativeContentBlob} to send.
3400
+ * @throws If this session has been closed.
3401
+ *
3402
+ * @beta
3403
+ */
3404
+ async sendMediaStream(mediaChunkStream) {
3405
+ if (this.isClosed) {
3406
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.');
3407
+ }
3408
+ const reader = mediaChunkStream.getReader();
3409
+ while (true) {
3410
+ try {
3411
+ const { done, value } = await reader.read();
3412
+ if (done) {
3413
+ break;
3414
+ }
3415
+ else if (!value) {
3416
+ throw new Error('Missing chunk in reader, but reader is not done.');
3417
+ }
3418
+ await this.sendMediaChunks([value]);
3419
+ }
3420
+ catch (e) {
3421
+ // Re-throw any errors that occur during stream consumption or sending.
3422
+ const message = e instanceof Error ? e.message : 'Error processing media stream.';
3423
+ throw new AIError(AIErrorCode.REQUEST_ERROR, message);
3424
+ }
3425
+ }
3426
+ }
3427
+ }
3428
+ /**
3429
+ * Parses a duration string (e.g. "3.000000001s") into a number of seconds.
3430
+ *
3431
+ * @param duration - The duration string to parse.
3432
+ * @returns The duration in seconds.
3433
+ */
3434
+ function parseDuration(duration) {
3435
+ if (!duration || !duration.endsWith('s')) {
3436
+ return 0;
3437
+ }
3438
+ return Number(duration.slice(0, -1)); // slice removes the trailing 's'.
3439
+ }
3440
+
3441
+ /**
3442
+ * @license
3443
+ * Copyright 2025 Google LLC
3444
+ *
3445
+ * Licensed under the Apache License, Version 2.0 (the "License");
3446
+ * you may not use this file except in compliance with the License.
3447
+ * You may obtain a copy of the License at
3448
+ *
3449
+ * http://www.apache.org/licenses/LICENSE-2.0
3450
+ *
3451
+ * Unless required by applicable law or agreed to in writing, software
3452
+ * distributed under the License is distributed on an "AS IS" BASIS,
3453
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3454
+ * See the License for the specific language governing permissions and
3455
+ * limitations under the License.
3456
+ */
3457
+ /**
3458
+ * Class for Live generative model APIs. The Live API enables low-latency, two-way multimodal
3459
+ * interactions with Gemini.
3460
+ *
3461
+ * This class should only be instantiated with {@link getLiveGenerativeModel}.
3462
+ *
3463
+ * @beta
3464
+ */
3465
+ class LiveGenerativeModel extends AIModel {
3466
+ /**
3467
+ * @internal
3468
+ */
3469
+ constructor(ai, modelParams,
3470
+ /**
3471
+ * @internal
3472
+ */
3473
+ _webSocketHandler) {
3474
+ super(ai, modelParams.model);
3475
+ this._webSocketHandler = _webSocketHandler;
3476
+ this.generationConfig = modelParams.generationConfig || {};
3477
+ this.tools = modelParams.tools;
3478
+ this.toolConfig = modelParams.toolConfig;
3479
+ this.systemInstruction = formatSystemInstruction(modelParams.systemInstruction);
3480
+ }
3481
+ /**
3482
+ * Starts a {@link LiveSession}.
3483
+ *
3484
+ * @returns A {@link LiveSession}.
3485
+ * @throws If the connection failed to be established with the server.
3486
+ *
3487
+ * @beta
3488
+ */
3489
+ async connect() {
3490
+ const url = new WebSocketUrl(this._apiSettings);
3491
+ await this._webSocketHandler.connect(url.toString());
3492
+ let fullModelPath;
3493
+ if (this._apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
3494
+ fullModelPath = `projects/${this._apiSettings.project}/${this.model}`;
3495
+ }
3496
+ else {
3497
+ fullModelPath = `projects/${this._apiSettings.project}/locations/${this._apiSettings.location}/${this.model}`;
3498
+ }
3499
+ // inputAudioTranscription and outputAudioTranscription are on the generation config in the public API,
3500
+ // but the backend expects them to be in the `setup` message.
3501
+ const { inputAudioTranscription, outputAudioTranscription, ...generationConfig } = this.generationConfig;
3502
+ const setupMessage = {
3503
+ setup: {
3504
+ model: fullModelPath,
3505
+ generationConfig,
3506
+ tools: this.tools,
3507
+ toolConfig: this.toolConfig,
3508
+ systemInstruction: this.systemInstruction,
3509
+ inputAudioTranscription,
3510
+ outputAudioTranscription
3511
+ }
3512
+ };
3513
+ try {
3514
+ // Begin listening for server messages, and begin the handshake by sending the 'setupMessage'
3515
+ const serverMessages = this._webSocketHandler.listen();
3516
+ this._webSocketHandler.send(JSON.stringify(setupMessage));
3517
+ // Verify we received the handshake response 'setupComplete'
3518
+ const firstMessage = (await serverMessages.next()).value;
3519
+ if (!firstMessage ||
3520
+ !(typeof firstMessage === 'object') ||
3521
+ !('setupComplete' in firstMessage)) {
3522
+ await this._webSocketHandler.close(1011, 'Handshake failure');
3523
+ throw new AIError(AIErrorCode.RESPONSE_ERROR, 'Server connection handshake failed. The server did not respond with a setupComplete message.');
3524
+ }
3525
+ return new LiveSession(this._webSocketHandler, serverMessages);
3526
+ }
3527
+ catch (e) {
3528
+ // Ensure connection is closed on any setup error
3529
+ await this._webSocketHandler.close();
3530
+ throw e;
3531
+ }
3532
+ }
3533
+ }
3534
+
3535
+ /**
3536
+ * @license
3537
+ * Copyright 2025 Google LLC
3538
+ *
3539
+ * Licensed under the Apache License, Version 2.0 (the "License");
3540
+ * you may not use this file except in compliance with the License.
3541
+ * You may obtain a copy of the License at
3542
+ *
3543
+ * http://www.apache.org/licenses/LICENSE-2.0
3544
+ *
3545
+ * Unless required by applicable law or agreed to in writing, software
3546
+ * distributed under the License is distributed on an "AS IS" BASIS,
3547
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3548
+ * See the License for the specific language governing permissions and
3549
+ * limitations under the License.
3550
+ */
3551
+ /**
3552
+ * Class for Imagen model APIs.
3553
+ *
3554
+ * This class provides methods for generating images using the Imagen model.
3555
+ *
3556
+ * @example
3557
+ * ```javascript
3558
+ * const imagen = new ImagenModel(
3559
+ * ai,
3560
+ * {
3561
+ * model: 'imagen-3.0-generate-002'
3562
+ * }
3563
+ * );
3564
+ *
3565
+ * const response = await imagen.generateImages('A photo of a cat');
3566
+ * if (response.images.length > 0) {
3567
+ * console.log(response.images[0].bytesBase64Encoded);
3568
+ * }
3569
+ * ```
3570
+ *
3571
+ * @public
3572
+ */
3573
+ class ImagenModel extends AIModel {
3574
+ /**
3575
+ * Constructs a new instance of the {@link ImagenModel} class.
3576
+ *
3577
+ * @param ai - an {@link AI} instance.
3578
+ * @param modelParams - Parameters to use when making requests to Imagen.
3579
+ * @param requestOptions - Additional options to use when making requests.
3580
+ *
3581
+ * @throws If the `apiKey` or `projectId` fields are missing in your
3582
+ * Firebase config.
3583
+ */
3584
+ constructor(ai, modelParams, requestOptions) {
3585
+ const { model, generationConfig, safetySettings } = modelParams;
3586
+ super(ai, model);
3587
+ this.requestOptions = requestOptions;
3588
+ this.generationConfig = generationConfig;
3589
+ this.safetySettings = safetySettings;
3590
+ }
3591
+ /**
3592
+ * Generates images using the Imagen model and returns them as
3593
+ * base64-encoded strings.
3594
+ *
3595
+ * @param prompt - A text prompt describing the image(s) to generate.
3596
+ * @returns A promise that resolves to an {@link ImagenGenerationResponse}
3597
+ * object containing the generated images.
3598
+ *
3599
+ * @throws If the request to generate images fails. This happens if the
3600
+ * prompt is blocked.
3601
+ *
3602
+ * @remarks
3603
+ * If the prompt was not blocked, but one or more of the generated images were filtered, the
3604
+ * returned object will have a `filteredReason` property.
3605
+ * If all images are filtered, the `images` array will be empty.
3606
+ *
3607
+ * @public
3608
+ */
3609
+ async generateImages(prompt, singleRequestOptions) {
3610
+ const body = createPredictRequestBody(prompt, {
3611
+ ...this.generationConfig,
3612
+ ...this.safetySettings
3613
+ });
3614
+ const response = await makeRequest({
3615
+ task: "predict" /* Task.PREDICT */,
3616
+ model: this.model,
3617
+ apiSettings: this._apiSettings,
3618
+ stream: false,
3619
+ // Merge request options. Single request options overwrite the model's request options.
3620
+ singleRequestOptions: {
3621
+ ...this.requestOptions,
3622
+ ...singleRequestOptions
3623
+ }
3624
+ }, JSON.stringify(body));
3625
+ return handlePredictResponse(response);
3626
+ }
3627
+ /**
3628
+ * Generates images to Cloud Storage for Firebase using the Imagen model.
3629
+ *
3630
+ * @internal This method is temporarily internal.
3631
+ *
3632
+ * @param prompt - A text prompt describing the image(s) to generate.
3633
+ * @param gcsURI - The URI of file stored in a Cloud Storage for Firebase bucket.
3634
+ * This should be a directory. For example, `gs://my-bucket/my-directory/`.
3635
+ * @returns A promise that resolves to an {@link ImagenGenerationResponse}
3636
+ * object containing the URLs of the generated images.
3637
+ *
3638
+ * @throws If the request fails to generate images fails. This happens if
3639
+ * the prompt is blocked.
3640
+ *
3641
+ * @remarks
3642
+ * If the prompt was not blocked, but one or more of the generated images were filtered, the
3643
+ * returned object will have a `filteredReason` property.
3644
+ * If all images are filtered, the `images` array will be empty.
3645
+ */
3646
+ async generateImagesGCS(prompt, gcsURI, singleRequestOptions) {
3647
+ const body = createPredictRequestBody(prompt, {
3648
+ gcsURI,
3649
+ ...this.generationConfig,
3650
+ ...this.safetySettings
3651
+ });
3652
+ const response = await makeRequest({
3653
+ task: "predict" /* Task.PREDICT */,
3654
+ model: this.model,
3655
+ apiSettings: this._apiSettings,
3656
+ stream: false,
3657
+ // Merge request options. Single request options overwrite the model's request options.
3658
+ singleRequestOptions: {
3659
+ ...this.requestOptions,
3660
+ ...singleRequestOptions
3661
+ }
3662
+ }, JSON.stringify(body));
3663
+ return handlePredictResponse(response);
3664
+ }
3665
+ }
3666
+
3667
+ /**
3668
+ * @license
3669
+ * Copyright 2025 Google LLC
3670
+ *
3671
+ * Licensed under the Apache License, Version 2.0 (the "License");
3672
+ * you may not use this file except in compliance with the License.
3673
+ * You may obtain a copy of the License at
3674
+ *
3675
+ * http://www.apache.org/licenses/LICENSE-2.0
3676
+ *
3677
+ * Unless required by applicable law or agreed to in writing, software
3678
+ * distributed under the License is distributed on an "AS IS" BASIS,
3679
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3680
+ * See the License for the specific language governing permissions and
3681
+ * limitations under the License.
3682
+ */
3683
+ /**
3684
+ * A wrapper for the native `WebSocket` available in both Browsers and Node >= 22.
3685
+ *
3686
+ * @internal
3687
+ */
3688
+ class WebSocketHandlerImpl {
3689
+ constructor() {
3690
+ if (typeof WebSocket === 'undefined') {
3691
+ throw new AIError(AIErrorCode.UNSUPPORTED, 'The WebSocket API is not available in this environment. ' +
3692
+ 'The "Live" feature is not supported here. It is supported in ' +
3693
+ 'modern browser windows, Web Workers with WebSocket support, and Node >= 22.');
3694
+ }
3695
+ }
3696
+ connect(url) {
3697
+ return new Promise((resolve, reject) => {
3698
+ this.ws = new WebSocket(url);
3699
+ this.ws.binaryType = 'blob'; // Only important to set in Node
3700
+ this.ws.addEventListener('open', () => resolve(), { once: true });
3701
+ this.ws.addEventListener('error', () => reject(new AIError(AIErrorCode.FETCH_ERROR, `Error event raised on WebSocket`)), { once: true });
3702
+ this.ws.addEventListener('close', (closeEvent) => {
3703
+ if (closeEvent.reason) {
3704
+ logger.warn(`WebSocket connection closed by server. Reason: '${closeEvent.reason}'`);
3705
+ }
3706
+ });
3707
+ });
3708
+ }
3709
+ send(data) {
3710
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
3711
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'WebSocket is not open.');
3712
+ }
3713
+ this.ws.send(data);
3714
+ }
3715
+ async *listen() {
3716
+ if (!this.ws) {
3717
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'WebSocket is not connected.');
3718
+ }
3719
+ const messageQueue = [];
3720
+ const errorQueue = [];
3721
+ let resolvePromise = null;
3722
+ let isClosed = false;
3723
+ const messageListener = async (event) => {
3724
+ let data;
3725
+ if (event.data instanceof Blob) {
3726
+ data = await event.data.text();
3727
+ }
3728
+ else if (typeof event.data === 'string') {
3729
+ data = event.data;
3730
+ }
3731
+ else {
3732
+ errorQueue.push(new AIError(AIErrorCode.PARSE_FAILED, `Failed to parse WebSocket response. Expected data to be a Blob or string, but was ${typeof event.data}.`));
3733
+ if (resolvePromise) {
3734
+ resolvePromise();
3735
+ resolvePromise = null;
3736
+ }
3737
+ return;
3738
+ }
3739
+ try {
3740
+ const obj = JSON.parse(data);
3741
+ messageQueue.push(obj);
3742
+ }
3743
+ catch (e) {
3744
+ const err = e;
3745
+ errorQueue.push(new AIError(AIErrorCode.PARSE_FAILED, `Error parsing WebSocket message to JSON: ${err.message}`));
3746
+ }
3747
+ if (resolvePromise) {
3748
+ resolvePromise();
3749
+ resolvePromise = null;
3750
+ }
3751
+ };
3752
+ const errorListener = () => {
3753
+ errorQueue.push(new AIError(AIErrorCode.FETCH_ERROR, 'WebSocket connection error.'));
3754
+ if (resolvePromise) {
3755
+ resolvePromise();
3756
+ resolvePromise = null;
3757
+ }
3758
+ };
3759
+ const closeListener = (event) => {
3760
+ if (event.reason) {
3761
+ logger.warn(`WebSocket connection closed by the server with reason: ${event.reason}`);
3762
+ }
3763
+ isClosed = true;
3764
+ if (resolvePromise) {
3765
+ resolvePromise();
3766
+ resolvePromise = null;
3767
+ }
3768
+ // Clean up listeners to prevent memory leaks
3769
+ this.ws?.removeEventListener('message', messageListener);
3770
+ this.ws?.removeEventListener('close', closeListener);
3771
+ this.ws?.removeEventListener('error', errorListener);
3772
+ };
3773
+ this.ws.addEventListener('message', messageListener);
3774
+ this.ws.addEventListener('close', closeListener);
3775
+ this.ws.addEventListener('error', errorListener);
3776
+ while (!isClosed) {
3777
+ if (errorQueue.length > 0) {
3778
+ const error = errorQueue.shift();
3779
+ throw error;
3780
+ }
3781
+ if (messageQueue.length > 0) {
3782
+ yield messageQueue.shift();
3783
+ }
3784
+ else {
3785
+ await new Promise(resolve => {
3786
+ resolvePromise = resolve;
3787
+ });
3788
+ }
3789
+ }
3790
+ // If the loop terminated because isClosed is true, check for any final errors
3791
+ if (errorQueue.length > 0) {
3792
+ const error = errorQueue.shift();
3793
+ throw error;
3794
+ }
3795
+ }
3796
+ close(code, reason) {
3797
+ return new Promise(resolve => {
3798
+ if (!this.ws) {
3799
+ return resolve();
3800
+ }
3801
+ this.ws.addEventListener('close', () => resolve(), { once: true });
3802
+ // Calling 'close' during these states results in an error.
3803
+ if (this.ws.readyState === WebSocket.CLOSED ||
3804
+ this.ws.readyState === WebSocket.CONNECTING) {
3805
+ return resolve();
3806
+ }
3807
+ if (this.ws.readyState !== WebSocket.CLOSING) {
3808
+ this.ws.close(code, reason);
3809
+ }
3810
+ });
3811
+ }
3812
+ }
3813
+
3814
+ /**
3815
+ * @license
3816
+ * Copyright 2025 Google LLC
3817
+ *
3818
+ * Licensed under the Apache License, Version 2.0 (the "License");
3819
+ * you may not use this file except in compliance with the License.
3820
+ * You may obtain a copy of the License at
3821
+ *
3822
+ * http://www.apache.org/licenses/LICENSE-2.0
3823
+ *
3824
+ * Unless required by applicable law or agreed to in writing, software
3825
+ * distributed under the License is distributed on an "AS IS" BASIS,
3826
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3827
+ * See the License for the specific language governing permissions and
3828
+ * limitations under the License.
3829
+ */
3830
+ /**
3831
+ * {@link GenerativeModel} APIs that execute on a server-side template.
3832
+ *
3833
+ * This class should only be instantiated with {@link getTemplateGenerativeModel}.
3834
+ *
3835
+ * @beta
3836
+ */
3837
+ class TemplateGenerativeModel {
3838
+ /**
3839
+ * @hideconstructor
3840
+ */
3841
+ constructor(ai, requestOptions) {
3842
+ this.requestOptions = requestOptions || {};
3843
+ this._apiSettings = initApiSettings(ai);
3844
+ }
3845
+ /**
3846
+ * Makes a single non-streaming call to the model and returns an object
3847
+ * containing a single {@link GenerateContentResponse}.
3848
+ *
3849
+ * @param templateId - The ID of the server-side template to execute.
3850
+ * @param templateVariables - A key-value map of variables to populate the
3851
+ * template with.
3852
+ *
3853
+ * @beta
3854
+ */
3855
+ async generateContent(templateId, templateVariables, singleRequestOptions) {
3856
+ return templateGenerateContent(this._apiSettings, templateId, { inputs: templateVariables }, {
3857
+ ...this.requestOptions,
3858
+ ...singleRequestOptions
3859
+ });
3860
+ }
3861
+ /**
3862
+ * Makes a single streaming call to the model and returns an object
3863
+ * containing an iterable stream that iterates over all chunks in the
3864
+ * streaming response as well as a promise that returns the final aggregated
3865
+ * response.
3866
+ *
3867
+ * @param templateId - The ID of the server-side template to execute.
3868
+ * @param templateVariables - A key-value map of variables to populate the
3869
+ * template with.
3870
+ *
3871
+ * @beta
3872
+ */
3873
+ async generateContentStream(templateId, templateVariables, singleRequestOptions) {
3874
+ return templateGenerateContentStream(this._apiSettings, templateId, { inputs: templateVariables }, {
3875
+ ...this.requestOptions,
3876
+ ...singleRequestOptions
3877
+ });
3878
+ }
3879
+ }
3880
+
3881
+ /**
3882
+ * @license
3883
+ * Copyright 2025 Google LLC
3884
+ *
3885
+ * Licensed under the Apache License, Version 2.0 (the "License");
3886
+ * you may not use this file except in compliance with the License.
3887
+ * You may obtain a copy of the License at
3888
+ *
3889
+ * http://www.apache.org/licenses/LICENSE-2.0
3890
+ *
3891
+ * Unless required by applicable law or agreed to in writing, software
3892
+ * distributed under the License is distributed on an "AS IS" BASIS,
3893
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3894
+ * See the License for the specific language governing permissions and
3895
+ * limitations under the License.
3896
+ */
3897
+ /**
3898
+ * Class for Imagen model APIs that execute on a server-side template.
3899
+ *
3900
+ * This class should only be instantiated with {@link getTemplateImagenModel}.
3901
+ *
3902
+ * @beta
3903
+ */
3904
+ class TemplateImagenModel {
3905
+ /**
3906
+ * @hideconstructor
3907
+ */
3908
+ constructor(ai, requestOptions) {
3909
+ this.requestOptions = requestOptions || {};
3910
+ this._apiSettings = initApiSettings(ai);
3911
+ }
3912
+ /**
3913
+ * Makes a single call to the model and returns an object containing a single
3914
+ * {@link ImagenGenerationResponse}.
3915
+ *
3916
+ * @param templateId - The ID of the server-side template to execute.
3917
+ * @param templateVariables - A key-value map of variables to populate the
3918
+ * template with.
3919
+ *
3920
+ * @beta
3921
+ */
3922
+ async generateImages(templateId, templateVariables, singleRequestOptions) {
3923
+ const response = await makeRequest({
3924
+ task: "templatePredict" /* ServerPromptTemplateTask.TEMPLATE_PREDICT */,
3925
+ templateId,
3926
+ apiSettings: this._apiSettings,
3927
+ stream: false,
3928
+ singleRequestOptions: {
3929
+ ...this.requestOptions,
3930
+ ...singleRequestOptions
3931
+ }
3932
+ }, JSON.stringify({ inputs: templateVariables }));
3933
+ return handlePredictResponse(response);
3934
+ }
3935
+ }
3936
+
3937
+ /**
3938
+ * @license
3939
+ * Copyright 2024 Google LLC
3940
+ *
3941
+ * Licensed under the Apache License, Version 2.0 (the "License");
3942
+ * you may not use this file except in compliance with the License.
3943
+ * You may obtain a copy of the License at
3944
+ *
3945
+ * http://www.apache.org/licenses/LICENSE-2.0
3946
+ *
3947
+ * Unless required by applicable law or agreed to in writing, software
3948
+ * distributed under the License is distributed on an "AS IS" BASIS,
3949
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3950
+ * See the License for the specific language governing permissions and
3951
+ * limitations under the License.
3952
+ */
3953
+ /**
3954
+ * Parent class encompassing all Schema types, with static methods that
3955
+ * allow building specific Schema types. This class can be converted with
3956
+ * `JSON.stringify()` into a JSON string accepted by Vertex AI REST endpoints.
3957
+ * (This string conversion is automatically done when calling SDK methods.)
3958
+ * @public
3959
+ */
3960
+ class Schema {
3961
+ constructor(schemaParams) {
3962
+ // TODO(dlarocque): Enforce this with union types
3963
+ if (!schemaParams.type && !schemaParams.anyOf) {
3964
+ throw new AIError(AIErrorCode.INVALID_SCHEMA, "A schema must have either a 'type' or an 'anyOf' array of sub-schemas.");
3965
+ }
3966
+ // eslint-disable-next-line guard-for-in
3967
+ for (const paramKey in schemaParams) {
3968
+ this[paramKey] = schemaParams[paramKey];
3969
+ }
3970
+ // Ensure these are explicitly set to avoid TS errors.
3971
+ this.type = schemaParams.type;
3972
+ this.format = schemaParams.hasOwnProperty('format')
3973
+ ? schemaParams.format
3974
+ : undefined;
3975
+ this.nullable = schemaParams.hasOwnProperty('nullable')
3976
+ ? !!schemaParams.nullable
3977
+ : false;
3978
+ }
3979
+ /**
3980
+ * Defines how this Schema should be serialized as JSON.
3981
+ * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior
3982
+ * @internal
3983
+ */
3984
+ toJSON() {
3985
+ const obj = {
3986
+ type: this.type
3987
+ };
3988
+ for (const prop in this) {
3989
+ if (this.hasOwnProperty(prop) && this[prop] !== undefined) {
3990
+ if (prop !== 'required' || this.type === SchemaType.OBJECT) {
3991
+ obj[prop] = this[prop];
3992
+ }
3993
+ }
3994
+ }
3995
+ return obj;
3996
+ }
3997
+ static array(arrayParams) {
3998
+ return new ArraySchema(arrayParams, arrayParams.items);
3999
+ }
4000
+ static object(objectParams) {
4001
+ return new ObjectSchema(objectParams, objectParams.properties, objectParams.optionalProperties);
4002
+ }
4003
+ // eslint-disable-next-line id-blacklist
4004
+ static string(stringParams) {
4005
+ return new StringSchema(stringParams);
4006
+ }
4007
+ static enumString(stringParams) {
4008
+ return new StringSchema(stringParams, stringParams.enum);
4009
+ }
4010
+ static integer(integerParams) {
4011
+ return new IntegerSchema(integerParams);
4012
+ }
4013
+ // eslint-disable-next-line id-blacklist
4014
+ static number(numberParams) {
4015
+ return new NumberSchema(numberParams);
4016
+ }
4017
+ // eslint-disable-next-line id-blacklist
4018
+ static boolean(booleanParams) {
4019
+ return new BooleanSchema(booleanParams);
4020
+ }
4021
+ static anyOf(anyOfParams) {
4022
+ return new AnyOfSchema(anyOfParams);
4023
+ }
4024
+ }
4025
+ /**
4026
+ * Schema class for "integer" types.
4027
+ * @public
4028
+ */
4029
+ class IntegerSchema extends Schema {
4030
+ constructor(schemaParams) {
4031
+ super({
4032
+ type: SchemaType.INTEGER,
4033
+ ...schemaParams
4034
+ });
4035
+ }
4036
+ }
4037
+ /**
4038
+ * Schema class for "number" types.
4039
+ * @public
4040
+ */
4041
+ class NumberSchema extends Schema {
4042
+ constructor(schemaParams) {
4043
+ super({
4044
+ type: SchemaType.NUMBER,
4045
+ ...schemaParams
4046
+ });
4047
+ }
4048
+ }
4049
+ /**
4050
+ * Schema class for "boolean" types.
4051
+ * @public
4052
+ */
4053
+ class BooleanSchema extends Schema {
4054
+ constructor(schemaParams) {
4055
+ super({
4056
+ type: SchemaType.BOOLEAN,
4057
+ ...schemaParams
4058
+ });
4059
+ }
4060
+ }
4061
+ /**
4062
+ * Schema class for "string" types. Can be used with or without
4063
+ * enum values.
4064
+ * @public
4065
+ */
4066
+ class StringSchema extends Schema {
4067
+ constructor(schemaParams, enumValues) {
4068
+ super({
4069
+ type: SchemaType.STRING,
4070
+ ...schemaParams
4071
+ });
4072
+ this.enum = enumValues;
4073
+ }
4074
+ /**
4075
+ * @internal
4076
+ */
4077
+ toJSON() {
4078
+ const obj = super.toJSON();
4079
+ if (this.enum) {
4080
+ obj['enum'] = this.enum;
4081
+ }
4082
+ return obj;
4083
+ }
4084
+ }
4085
+ /**
4086
+ * Schema class for "array" types.
4087
+ * The `items` param should refer to the type of item that can be a member
4088
+ * of the array.
4089
+ * @public
4090
+ */
4091
+ class ArraySchema extends Schema {
4092
+ constructor(schemaParams, items) {
4093
+ super({
4094
+ type: SchemaType.ARRAY,
4095
+ ...schemaParams
4096
+ });
4097
+ this.items = items;
4098
+ }
4099
+ /**
4100
+ * @internal
4101
+ */
4102
+ toJSON() {
4103
+ const obj = super.toJSON();
4104
+ obj.items = this.items.toJSON();
4105
+ return obj;
4106
+ }
4107
+ }
4108
+ /**
4109
+ * Schema class for "object" types.
4110
+ * The `properties` param must be a map of `Schema` objects.
4111
+ * @public
4112
+ */
4113
+ class ObjectSchema extends Schema {
4114
+ constructor(schemaParams, properties, optionalProperties = []) {
4115
+ super({
4116
+ type: SchemaType.OBJECT,
4117
+ ...schemaParams
4118
+ });
4119
+ this.properties = properties;
4120
+ this.optionalProperties = optionalProperties;
4121
+ }
4122
+ /**
4123
+ * @internal
4124
+ */
4125
+ toJSON() {
4126
+ const obj = super.toJSON();
4127
+ obj.properties = { ...this.properties };
4128
+ const required = [];
4129
+ if (this.optionalProperties) {
4130
+ for (const propertyKey of this.optionalProperties) {
4131
+ if (!this.properties.hasOwnProperty(propertyKey)) {
4132
+ throw new AIError(AIErrorCode.INVALID_SCHEMA, `Property "${propertyKey}" specified in "optionalProperties" does not exist.`);
4133
+ }
4134
+ }
4135
+ }
4136
+ for (const propertyKey in this.properties) {
4137
+ if (this.properties.hasOwnProperty(propertyKey)) {
4138
+ obj.properties[propertyKey] = this.properties[propertyKey].toJSON();
4139
+ if (!this.optionalProperties.includes(propertyKey)) {
4140
+ required.push(propertyKey);
4141
+ }
4142
+ }
4143
+ }
4144
+ if (required.length > 0) {
4145
+ obj.required = required;
4146
+ }
4147
+ delete obj.optionalProperties;
4148
+ return obj;
4149
+ }
4150
+ }
4151
+ /**
4152
+ * Schema class representing a value that can conform to any of the provided sub-schemas. This is
4153
+ * useful when a field can accept multiple distinct types or structures.
4154
+ * @public
4155
+ */
4156
+ class AnyOfSchema extends Schema {
4157
+ constructor(schemaParams) {
4158
+ if (schemaParams.anyOf.length === 0) {
4159
+ throw new AIError(AIErrorCode.INVALID_SCHEMA, "The 'anyOf' array must not be empty.");
4160
+ }
4161
+ super({
4162
+ ...schemaParams,
4163
+ type: undefined // anyOf schemas do not have an explicit type
4164
+ });
4165
+ this.anyOf = schemaParams.anyOf;
4166
+ }
4167
+ /**
4168
+ * @internal
4169
+ */
4170
+ toJSON() {
4171
+ const obj = super.toJSON();
4172
+ // Ensure the 'anyOf' property contains serialized SchemaRequest objects.
4173
+ if (this.anyOf && Array.isArray(this.anyOf)) {
4174
+ obj.anyOf = this.anyOf.map(s => s.toJSON());
4175
+ }
4176
+ return obj;
4177
+ }
4178
+ }
4179
+
4180
+ /**
4181
+ * @license
4182
+ * Copyright 2025 Google LLC
4183
+ *
4184
+ * Licensed under the Apache License, Version 2.0 (the "License");
4185
+ * you may not use this file except in compliance with the License.
4186
+ * You may obtain a copy of the License at
4187
+ *
4188
+ * http://www.apache.org/licenses/LICENSE-2.0
4189
+ *
4190
+ * Unless required by applicable law or agreed to in writing, software
4191
+ * distributed under the License is distributed on an "AS IS" BASIS,
4192
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4193
+ * See the License for the specific language governing permissions and
4194
+ * limitations under the License.
4195
+ */
4196
+ /**
4197
+ * Defines the image format for images generated by Imagen.
4198
+ *
4199
+ * Use this class to specify the desired format (JPEG or PNG) and compression quality
4200
+ * for images generated by Imagen. This is typically included as part of
4201
+ * {@link ImagenModelParams}.
4202
+ *
4203
+ * @example
4204
+ * ```javascript
4205
+ * const imagenModelParams = {
4206
+ * // ... other ImagenModelParams
4207
+ * imageFormat: ImagenImageFormat.jpeg(75) // JPEG with a compression level of 75.
4208
+ * }
4209
+ * ```
4210
+ *
4211
+ * @public
4212
+ */
4213
+ class ImagenImageFormat {
4214
+ constructor() {
4215
+ this.mimeType = 'image/png';
4216
+ }
4217
+ /**
4218
+ * Creates an {@link ImagenImageFormat} for a JPEG image.
4219
+ *
4220
+ * @param compressionQuality - The level of compression (a number between 0 and 100).
4221
+ * @returns An {@link ImagenImageFormat} object for a JPEG image.
4222
+ *
4223
+ * @public
4224
+ */
4225
+ static jpeg(compressionQuality) {
4226
+ if (compressionQuality &&
4227
+ (compressionQuality < 0 || compressionQuality > 100)) {
4228
+ logger.warn(`Invalid JPEG compression quality of ${compressionQuality} specified; the supported range is [0, 100].`);
4229
+ }
4230
+ return { mimeType: 'image/jpeg', compressionQuality };
4231
+ }
4232
+ /**
4233
+ * Creates an {@link ImagenImageFormat} for a PNG image.
4234
+ *
4235
+ * @returns An {@link ImagenImageFormat} object for a PNG image.
4236
+ *
4237
+ * @public
4238
+ */
4239
+ static png() {
4240
+ return { mimeType: 'image/png' };
4241
+ }
4242
+ }
4243
+
4244
+ /**
4245
+ * @license
4246
+ * Copyright 2025 Google LLC
4247
+ *
4248
+ * Licensed under the Apache License, Version 2.0 (the "License");
4249
+ * you may not use this file except in compliance with the License.
4250
+ * You may obtain a copy of the License at
4251
+ *
4252
+ * http://www.apache.org/licenses/LICENSE-2.0
4253
+ *
4254
+ * Unless required by applicable law or agreed to in writing, software
4255
+ * distributed under the License is distributed on an "AS IS" BASIS,
4256
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4257
+ * See the License for the specific language governing permissions and
4258
+ * limitations under the License.
4259
+ */
4260
+ const SERVER_INPUT_SAMPLE_RATE = 16000;
4261
+ const SERVER_OUTPUT_SAMPLE_RATE = 24000;
4262
+ const AUDIO_PROCESSOR_NAME = 'audio-processor';
4263
+ /**
4264
+ * The JS for an `AudioWorkletProcessor`.
4265
+ * This processor is responsible for taking raw audio from the microphone,
4266
+ * converting it to the required 16-bit 16kHz PCM, and posting it back to the main thread.
4267
+ *
4268
+ * See: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor
4269
+ *
4270
+ * It is defined as a string here so that it can be converted into a `Blob`
4271
+ * and loaded at runtime.
4272
+ */
4273
+ const audioProcessorWorkletString = `
4274
+ class AudioProcessor extends AudioWorkletProcessor {
4275
+ constructor(options) {
4276
+ super();
4277
+ this.targetSampleRate = options.processorOptions.targetSampleRate;
4278
+ // 'sampleRate' is a global variable available inside the AudioWorkletGlobalScope,
4279
+ // representing the native sample rate of the AudioContext.
4280
+ this.inputSampleRate = sampleRate;
4281
+ }
4282
+
4283
+ /**
4284
+ * This method is called by the browser's audio engine for each block of audio data.
4285
+ * Input is a single input, with a single channel (input[0][0]).
4286
+ */
4287
+ process(inputs) {
4288
+ const input = inputs[0];
4289
+ if (input && input.length > 0 && input[0].length > 0) {
4290
+ const pcmData = input[0]; // Float32Array of raw audio samples.
4291
+
4292
+ // Simple linear interpolation for resampling.
4293
+ const resampled = new Float32Array(Math.round(pcmData.length * this.targetSampleRate / this.inputSampleRate));
4294
+ const ratio = pcmData.length / resampled.length;
4295
+ for (let i = 0; i < resampled.length; i++) {
4296
+ resampled[i] = pcmData[Math.floor(i * ratio)];
4297
+ }
4298
+
4299
+ // Convert Float32 (-1, 1) samples to Int16 (-32768, 32767)
4300
+ const resampledInt16 = new Int16Array(resampled.length);
4301
+ for (let i = 0; i < resampled.length; i++) {
4302
+ const sample = Math.max(-1, Math.min(1, resampled[i]));
4303
+ if (sample < 0) {
4304
+ resampledInt16[i] = sample * 32768;
4305
+ } else {
4306
+ resampledInt16[i] = sample * 32767;
4307
+ }
4308
+ }
4309
+
4310
+ this.port.postMessage(resampledInt16);
4311
+ }
4312
+ // Return true to keep the processor alive and processing the next audio block.
4313
+ return true;
4314
+ }
4315
+ }
4316
+
4317
+ // Register the processor with a name that can be used to instantiate it from the main thread.
4318
+ registerProcessor('${AUDIO_PROCESSOR_NAME}', AudioProcessor);
4319
+ `;
4320
+ /**
4321
+ * Encapsulates the core logic of an audio conversation.
4322
+ *
4323
+ * @internal
4324
+ */
4325
+ class AudioConversationRunner {
4326
+ constructor(liveSession, options, deps) {
4327
+ this.liveSession = liveSession;
4328
+ this.options = options;
4329
+ this.deps = deps;
4330
+ /** A flag to indicate if the conversation has been stopped. */
4331
+ this.isStopped = false;
4332
+ /** A deferred that contains a promise that is resolved when stop() is called, to unblock the receive loop. */
4333
+ this.stopDeferred = new util.Deferred();
4334
+ /** A FIFO queue of 24kHz, 16-bit PCM audio chunks received from the server. */
4335
+ this.playbackQueue = [];
4336
+ /** Tracks scheduled audio sources. Used to cancel scheduled audio when the model is interrupted. */
4337
+ this.scheduledSources = [];
4338
+ /** A high-precision timeline pointer for scheduling gapless audio playback. */
4339
+ this.nextStartTime = 0;
4340
+ /** A mutex to prevent the playback processing loop from running multiple times concurrently. */
4341
+ this.isPlaybackLoopRunning = false;
4342
+ this.liveSession.inConversation = true;
4343
+ // Start listening for messages from the server.
4344
+ this.receiveLoopPromise = this.runReceiveLoop().finally(() => this.cleanup());
4345
+ // Set up the handler for receiving processed audio data from the worklet.
4346
+ // Message data has been resampled to 16kHz 16-bit PCM.
4347
+ this.deps.workletNode.port.onmessage = event => {
4348
+ if (this.isStopped) {
4349
+ return;
4350
+ }
4351
+ const pcm16 = event.data;
4352
+ const base64 = btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(pcm16.buffer))));
4353
+ const chunk = {
4354
+ mimeType: 'audio/pcm',
4355
+ data: base64
4356
+ };
4357
+ void this.liveSession.sendAudioRealtime(chunk);
4358
+ };
4359
+ }
4360
+ /**
4361
+ * Stops the conversation and unblocks the main receive loop.
4362
+ */
4363
+ async stop() {
4364
+ if (this.isStopped) {
4365
+ return;
4366
+ }
4367
+ this.isStopped = true;
4368
+ this.stopDeferred.resolve(); // Unblock the receive loop
4369
+ await this.receiveLoopPromise; // Wait for the loop and cleanup to finish
4370
+ }
4371
+ /**
4372
+ * Cleans up all audio resources (nodes, stream tracks, context) and marks the
4373
+ * session as no longer in a conversation.
4374
+ */
4375
+ cleanup() {
4376
+ this.interruptPlayback(); // Ensure all audio is stopped on final cleanup.
4377
+ this.deps.workletNode.port.onmessage = null;
4378
+ this.deps.workletNode.disconnect();
4379
+ this.deps.sourceNode.disconnect();
4380
+ this.deps.mediaStream.getTracks().forEach(track => track.stop());
4381
+ if (this.deps.audioContext.state !== 'closed') {
4382
+ void this.deps.audioContext.close();
4383
+ }
4384
+ this.liveSession.inConversation = false;
4385
+ }
4386
+ /**
4387
+ * Adds audio data to the queue and ensures the playback loop is running.
4388
+ */
4389
+ enqueueAndPlay(audioData) {
4390
+ this.playbackQueue.push(audioData);
4391
+ // Will no-op if it's already running.
4392
+ void this.processPlaybackQueue();
4393
+ }
4394
+ /**
4395
+ * Stops all current and pending audio playback and clears the queue. This is
4396
+ * called when the server indicates the model's speech was interrupted with
4397
+ * `LiveServerContent.modelTurn.interrupted`.
4398
+ */
4399
+ interruptPlayback() {
4400
+ // Stop all sources that have been scheduled. The onended event will fire for each,
4401
+ // which will clean up the scheduledSources array.
4402
+ [...this.scheduledSources].forEach(source => source.stop(0));
4403
+ // Clear the internal buffer of unprocessed audio chunks.
4404
+ this.playbackQueue.length = 0;
4405
+ // Reset the playback clock to start fresh.
4406
+ this.nextStartTime = this.deps.audioContext.currentTime;
4407
+ }
4408
+ /**
4409
+ * Processes the playback queue in a loop, scheduling each chunk in a gapless sequence.
4410
+ */
4411
+ async processPlaybackQueue() {
4412
+ if (this.isPlaybackLoopRunning) {
4413
+ return;
4414
+ }
4415
+ this.isPlaybackLoopRunning = true;
4416
+ while (this.playbackQueue.length > 0 && !this.isStopped) {
4417
+ const pcmRawBuffer = this.playbackQueue.shift();
4418
+ try {
4419
+ const pcm16 = new Int16Array(pcmRawBuffer);
4420
+ const frameCount = pcm16.length;
4421
+ const audioBuffer = this.deps.audioContext.createBuffer(1, frameCount, SERVER_OUTPUT_SAMPLE_RATE);
4422
+ // Convert 16-bit PCM to 32-bit PCM, required by the Web Audio API.
4423
+ const channelData = audioBuffer.getChannelData(0);
4424
+ for (let i = 0; i < frameCount; i++) {
4425
+ channelData[i] = pcm16[i] / 32768; // Normalize to Float32 range [-1.0, 1.0]
4426
+ }
4427
+ const source = this.deps.audioContext.createBufferSource();
4428
+ source.buffer = audioBuffer;
4429
+ source.connect(this.deps.audioContext.destination);
4430
+ // Track the source and set up a handler to remove it from tracking when it finishes.
4431
+ this.scheduledSources.push(source);
4432
+ source.onended = () => {
4433
+ this.scheduledSources = this.scheduledSources.filter(s => s !== source);
4434
+ };
4435
+ // To prevent gaps, schedule the next chunk to start either now (if we're catching up)
4436
+ // or exactly when the previous chunk is scheduled to end.
4437
+ this.nextStartTime = Math.max(this.deps.audioContext.currentTime, this.nextStartTime);
4438
+ source.start(this.nextStartTime);
4439
+ // Update the schedule for the *next* chunk.
4440
+ this.nextStartTime += audioBuffer.duration;
4441
+ }
4442
+ catch (e) {
4443
+ logger.error('Error playing audio:', e);
4444
+ }
4445
+ }
4446
+ this.isPlaybackLoopRunning = false;
4447
+ }
4448
+ /**
4449
+ * The main loop that listens for and processes messages from the server.
4450
+ */
4451
+ async runReceiveLoop() {
4452
+ const messageGenerator = this.liveSession.receive();
4453
+ while (!this.isStopped) {
4454
+ const result = await Promise.race([
4455
+ messageGenerator.next(),
4456
+ this.stopDeferred.promise
4457
+ ]);
4458
+ if (this.isStopped || !result || result.done) {
4459
+ break;
4460
+ }
4461
+ const message = result.value;
4462
+ if (message.type === 'serverContent') {
4463
+ const serverContent = message;
4464
+ if (serverContent.interrupted) {
4465
+ this.interruptPlayback();
4466
+ }
4467
+ const audioPart = serverContent.modelTurn?.parts.find(part => part.inlineData?.mimeType.startsWith('audio/'));
4468
+ if (audioPart?.inlineData) {
4469
+ const audioData = Uint8Array.from(atob(audioPart.inlineData.data), c => c.charCodeAt(0)).buffer;
4470
+ this.enqueueAndPlay(audioData);
4471
+ }
4472
+ }
4473
+ else if (message.type === 'toolCall') {
4474
+ if (!this.options.functionCallingHandler) {
4475
+ logger.warn('Received tool call message, but StartAudioConversationOptions.functionCallingHandler is undefined. Ignoring tool call.');
4476
+ }
4477
+ else {
4478
+ try {
4479
+ const functionResponse = await this.options.functionCallingHandler(message.functionCalls);
4480
+ if (!this.isStopped) {
4481
+ void this.liveSession.sendFunctionResponses([functionResponse]);
4482
+ }
4483
+ }
4484
+ catch (e) {
4485
+ throw new AIError(AIErrorCode.ERROR, `Function calling handler failed: ${e.message}`);
4486
+ }
4487
+ }
4488
+ }
4489
+ }
4490
+ }
4491
+ }
4492
+ /**
4493
+ * Starts a real-time, bidirectional audio conversation with the model. This helper function manages
4494
+ * the complexities of microphone access, audio recording, playback, and interruptions.
4495
+ *
4496
+ * @remarks Important: This function must be called in response to a user gesture
4497
+ * (for example, a button click) to comply with {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy | browser autoplay policies}.
4498
+ *
4499
+ * @example
4500
+ * ```javascript
4501
+ * const liveSession = await model.connect();
4502
+ * let conversationController;
4503
+ *
4504
+ * // This function must be called from within a click handler.
4505
+ * async function startConversation() {
4506
+ * try {
4507
+ * conversationController = await startAudioConversation(liveSession);
4508
+ * } catch (e) {
4509
+ * // Handle AI-specific errors
4510
+ * if (e instanceof AIError) {
4511
+ * console.error("AI Error:", e.message);
4512
+ * }
4513
+ * // Handle microphone permission and hardware errors
4514
+ * else if (e instanceof DOMException) {
4515
+ * console.error("Microphone Error:", e.message);
4516
+ * }
4517
+ * // Handle other unexpected errors
4518
+ * else {
4519
+ * console.error("An unexpected error occurred:", e);
4520
+ * }
4521
+ * }
4522
+ * }
4523
+ *
4524
+ * // Later, to stop the conversation:
4525
+ * // if (conversationController) {
4526
+ * // await conversationController.stop();
4527
+ * // }
4528
+ * ```
4529
+ *
4530
+ * @param liveSession - An active {@link LiveSession} instance.
4531
+ * @param options - Configuration options for the audio conversation.
4532
+ * @returns A `Promise` that resolves with an {@link AudioConversationController}.
4533
+ * @throws `AIError` if the environment does not support required Web APIs (`UNSUPPORTED`), if a conversation is already active (`REQUEST_ERROR`), the session is closed (`SESSION_CLOSED`), or if an unexpected initialization error occurs (`ERROR`).
4534
+ * @throws `DOMException` Thrown by `navigator.mediaDevices.getUserMedia()` if issues occur with microphone access, such as permissions being denied (`NotAllowedError`) or no compatible hardware being found (`NotFoundError`). See the {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#exceptions | MDN documentation} for a full list of exceptions.
4535
+ *
4536
+ * @beta
4537
+ */
4538
+ async function startAudioConversation(liveSession, options = {}) {
4539
+ if (liveSession.isClosed) {
4540
+ throw new AIError(AIErrorCode.SESSION_CLOSED, 'Cannot start audio conversation on a closed LiveSession.');
4541
+ }
4542
+ if (liveSession.inConversation) {
4543
+ throw new AIError(AIErrorCode.REQUEST_ERROR, 'An audio conversation is already in progress for this session.');
4544
+ }
4545
+ // Check for necessary Web API support.
4546
+ if (typeof AudioWorkletNode === 'undefined' ||
4547
+ typeof AudioContext === 'undefined' ||
4548
+ typeof navigator === 'undefined' ||
4549
+ !navigator.mediaDevices) {
4550
+ throw new AIError(AIErrorCode.UNSUPPORTED, 'Audio conversation is not supported in this environment. It requires the Web Audio API and AudioWorklet support.');
4551
+ }
4552
+ let audioContext;
4553
+ try {
4554
+ // 1. Set up the audio context. This must be in response to a user gesture.
4555
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy
4556
+ audioContext = new AudioContext();
4557
+ if (audioContext.state === 'suspended') {
4558
+ await audioContext.resume();
4559
+ }
4560
+ // 2. Prompt for microphone access and get the media stream.
4561
+ // This can throw a variety of permission or hardware-related errors.
4562
+ const mediaStream = await navigator.mediaDevices.getUserMedia({
4563
+ audio: true
4564
+ });
4565
+ // 3. Load the AudioWorklet processor.
4566
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet
4567
+ const workletBlob = new Blob([audioProcessorWorkletString], {
4568
+ type: 'application/javascript'
4569
+ });
4570
+ const workletURL = URL.createObjectURL(workletBlob);
4571
+ await audioContext.audioWorklet.addModule(workletURL);
4572
+ // 4. Create the audio graph: Microphone -> Source Node -> Worklet Node
4573
+ const sourceNode = audioContext.createMediaStreamSource(mediaStream);
4574
+ const workletNode = new AudioWorkletNode(audioContext, AUDIO_PROCESSOR_NAME, {
4575
+ processorOptions: { targetSampleRate: SERVER_INPUT_SAMPLE_RATE }
4576
+ });
4577
+ sourceNode.connect(workletNode);
4578
+ // 5. Instantiate and return the runner which manages the conversation.
4579
+ const runner = new AudioConversationRunner(liveSession, options, {
4580
+ audioContext,
4581
+ mediaStream,
4582
+ sourceNode,
4583
+ workletNode
4584
+ });
4585
+ return { stop: () => runner.stop() };
4586
+ }
4587
+ catch (e) {
4588
+ // Ensure the audio context is closed on any setup error.
4589
+ if (audioContext && audioContext.state !== 'closed') {
4590
+ void audioContext.close();
4591
+ }
4592
+ // Re-throw specific, known error types directly. The user may want to handle `DOMException`
4593
+ // errors differently (for example, if permission to access audio device was denied).
4594
+ if (e instanceof AIError || e instanceof DOMException) {
4595
+ throw e;
4596
+ }
4597
+ // Wrap any other unexpected errors in a standard AIError.
4598
+ throw new AIError(AIErrorCode.ERROR, `Failed to initialize audio recording: ${e.message}`);
4599
+ }
4600
+ }
4601
+
4602
+ /**
4603
+ * @license
4604
+ * Copyright 2024 Google LLC
4605
+ *
4606
+ * Licensed under the Apache License, Version 2.0 (the "License");
4607
+ * you may not use this file except in compliance with the License.
4608
+ * You may obtain a copy of the License at
4609
+ *
4610
+ * http://www.apache.org/licenses/LICENSE-2.0
4611
+ *
4612
+ * Unless required by applicable law or agreed to in writing, software
4613
+ * distributed under the License is distributed on an "AS IS" BASIS,
4614
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4615
+ * See the License for the specific language governing permissions and
4616
+ * limitations under the License.
4617
+ */
4618
+ /**
4619
+ * Returns the default {@link AI} instance that is associated with the provided
4620
+ * {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new instance with the
4621
+ * default settings.
4622
+ *
4623
+ * @example
4624
+ * ```javascript
4625
+ * const ai = getAI(app);
4626
+ * ```
4627
+ *
4628
+ * @example
4629
+ * ```javascript
4630
+ * // Get an AI instance configured to use the Gemini Developer API (via Google AI).
4631
+ * const ai = getAI(app, { backend: new GoogleAIBackend() });
4632
+ * ```
4633
+ *
4634
+ * @example
4635
+ * ```javascript
4636
+ * // Get an AI instance configured to use the Vertex AI Gemini API.
4637
+ * const ai = getAI(app, { backend: new VertexAIBackend() });
4638
+ * ```
4639
+ *
4640
+ * @param app - The {@link @firebase/app#FirebaseApp} to use.
4641
+ * @param options - {@link AIOptions} that configure the AI instance.
4642
+ * @returns The default {@link AI} instance for the given {@link @firebase/app#FirebaseApp}.
4643
+ *
4644
+ * @public
4645
+ */
4646
+ function getAI(app$1 = app.getApp(), options) {
4647
+ app$1 = util.getModularInstance(app$1);
4648
+ // Dependencies
4649
+ const AIProvider = app._getProvider(app$1, AI_TYPE);
4650
+ const backend = options?.backend ?? new GoogleAIBackend();
4651
+ const finalOptions = {
4652
+ useLimitedUseAppCheckTokens: options?.useLimitedUseAppCheckTokens ?? false
4653
+ };
4654
+ const identifier = encodeInstanceIdentifier(backend);
4655
+ const aiInstance = AIProvider.getImmediate({
4656
+ identifier
4657
+ });
4658
+ aiInstance.options = finalOptions;
4659
+ return aiInstance;
4660
+ }
4661
+ /**
4662
+ * Returns a {@link GenerativeModel} class with methods for inference
4663
+ * and other functionality.
4664
+ *
4665
+ * @public
4666
+ */
4667
+ function getGenerativeModel(ai, modelParams, requestOptions) {
4668
+ // Uses the existence of HybridParams.mode to clarify the type of the modelParams input.
4669
+ const hybridParams = modelParams;
4670
+ let inCloudParams;
4671
+ if (hybridParams.mode) {
4672
+ inCloudParams = hybridParams.inCloudParams || {
4673
+ model: DEFAULT_HYBRID_IN_CLOUD_MODEL
4674
+ };
4675
+ }
4676
+ else {
4677
+ inCloudParams = modelParams;
4678
+ }
4679
+ if (!inCloudParams.model) {
4680
+ throw new AIError(AIErrorCode.NO_MODEL, `Must provide a model name. Example: getGenerativeModel({ model: 'my-model-name' })`);
4681
+ }
4682
+ /**
4683
+ * An AIService registered by index.node.ts will not have a
4684
+ * chromeAdapterFactory() method.
4685
+ */
4686
+ const chromeAdapter = ai.chromeAdapterFactory?.(hybridParams.mode, typeof window === 'undefined' ? undefined : window, hybridParams.onDeviceParams);
4687
+ const generativeModel = new GenerativeModel(ai, inCloudParams, requestOptions, chromeAdapter);
4688
+ generativeModel._apiSettings.inferenceMode = hybridParams.mode;
4689
+ return generativeModel;
4690
+ }
4691
+ /**
4692
+ * Returns an {@link ImagenModel} class with methods for using Imagen.
4693
+ *
4694
+ * Only Imagen 3 models (named `imagen-3.0-*`) are supported.
4695
+ *
4696
+ * @param ai - An {@link AI} instance.
4697
+ * @param modelParams - Parameters to use when making Imagen requests.
4698
+ * @param requestOptions - Additional options to use when making requests.
4699
+ *
4700
+ * @throws If the `apiKey` or `projectId` fields are missing in your
4701
+ * Firebase config.
4702
+ *
4703
+ * @public
4704
+ */
4705
+ function getImagenModel(ai, modelParams, requestOptions) {
4706
+ if (!modelParams.model) {
4707
+ throw new AIError(AIErrorCode.NO_MODEL, `Must provide a model name. Example: getImagenModel({ model: 'my-model-name' })`);
4708
+ }
4709
+ return new ImagenModel(ai, modelParams, requestOptions);
4710
+ }
4711
+ /**
4712
+ * Returns a {@link LiveGenerativeModel} class for real-time, bidirectional communication.
4713
+ *
4714
+ * The Live API is only supported in modern browser windows and Node >= 22.
4715
+ *
4716
+ * @param ai - An {@link AI} instance.
4717
+ * @param modelParams - Parameters to use when setting up a {@link LiveSession}.
4718
+ * @throws If the `apiKey` or `projectId` fields are missing in your
4719
+ * Firebase config.
4720
+ *
4721
+ * @beta
4722
+ */
4723
+ function getLiveGenerativeModel(ai, modelParams) {
4724
+ if (!modelParams.model) {
4725
+ throw new AIError(AIErrorCode.NO_MODEL, `Must provide a model name for getLiveGenerativeModel. Example: getLiveGenerativeModel(ai, { model: 'my-model-name' })`);
4726
+ }
4727
+ const webSocketHandler = new WebSocketHandlerImpl();
4728
+ return new LiveGenerativeModel(ai, modelParams, webSocketHandler);
4729
+ }
4730
+ /**
4731
+ * Returns a {@link TemplateGenerativeModel} class for executing server-side
4732
+ * templates.
4733
+ *
4734
+ * @param ai - An {@link AI} instance.
4735
+ * @param requestOptions - Additional options to use when making requests.
4736
+ *
4737
+ * @beta
4738
+ */
4739
+ function getTemplateGenerativeModel(ai, requestOptions) {
4740
+ return new TemplateGenerativeModel(ai, requestOptions);
4741
+ }
4742
+ /**
4743
+ * Returns a {@link TemplateImagenModel} class for executing server-side
4744
+ * Imagen templates.
4745
+ *
4746
+ * @param ai - An {@link AI} instance.
4747
+ * @param requestOptions - Additional options to use when making requests.
4748
+ *
4749
+ * @beta
4750
+ */
4751
+ function getTemplateImagenModel(ai, requestOptions) {
4752
+ return new TemplateImagenModel(ai, requestOptions);
4753
+ }
4754
+
4755
+ /**
4756
+ * The Firebase AI Web SDK.
4757
+ *
4758
+ * @packageDocumentation
4759
+ */
4760
+ function registerAI() {
4761
+ app._registerComponent(new component.Component(AI_TYPE, factory, "PUBLIC" /* ComponentType.PUBLIC */).setMultipleInstances(true));
4762
+ app.registerVersion(name, version);
4763
+ // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation
4764
+ app.registerVersion(name, version, 'cjs2020');
4765
+ }
4766
+ registerAI();
4767
+
4768
+ exports.AIError = AIError;
4769
+ exports.AIErrorCode = AIErrorCode;
4770
+ exports.AIModel = AIModel;
4771
+ exports.AnyOfSchema = AnyOfSchema;
4772
+ exports.ArraySchema = ArraySchema;
4773
+ exports.Backend = Backend;
4774
+ exports.BackendType = BackendType;
4775
+ exports.BlockReason = BlockReason;
4776
+ exports.BooleanSchema = BooleanSchema;
4777
+ exports.ChatSession = ChatSession;
4778
+ exports.FinishReason = FinishReason;
4779
+ exports.FunctionCallingMode = FunctionCallingMode;
4780
+ exports.GenerativeModel = GenerativeModel;
4781
+ exports.GoogleAIBackend = GoogleAIBackend;
4782
+ exports.HarmBlockMethod = HarmBlockMethod;
4783
+ exports.HarmBlockThreshold = HarmBlockThreshold;
4784
+ exports.HarmCategory = HarmCategory;
4785
+ exports.HarmProbability = HarmProbability;
4786
+ exports.HarmSeverity = HarmSeverity;
4787
+ exports.ImagenAspectRatio = ImagenAspectRatio;
4788
+ exports.ImagenImageFormat = ImagenImageFormat;
4789
+ exports.ImagenModel = ImagenModel;
4790
+ exports.ImagenPersonFilterLevel = ImagenPersonFilterLevel;
4791
+ exports.ImagenSafetyFilterLevel = ImagenSafetyFilterLevel;
4792
+ exports.InferenceMode = InferenceMode;
4793
+ exports.InferenceSource = InferenceSource;
4794
+ exports.IntegerSchema = IntegerSchema;
4795
+ exports.Language = Language;
4796
+ exports.LiveGenerativeModel = LiveGenerativeModel;
4797
+ exports.LiveResponseType = LiveResponseType;
4798
+ exports.LiveSession = LiveSession;
4799
+ exports.Modality = Modality;
4800
+ exports.NumberSchema = NumberSchema;
4801
+ exports.ObjectSchema = ObjectSchema;
4802
+ exports.Outcome = Outcome;
4803
+ exports.POSSIBLE_ROLES = POSSIBLE_ROLES;
4804
+ exports.ResponseModality = ResponseModality;
4805
+ exports.Schema = Schema;
4806
+ exports.SchemaType = SchemaType;
4807
+ exports.StringSchema = StringSchema;
4808
+ exports.TemplateGenerativeModel = TemplateGenerativeModel;
4809
+ exports.TemplateImagenModel = TemplateImagenModel;
4810
+ exports.ThinkingLevel = ThinkingLevel;
4811
+ exports.URLRetrievalStatus = URLRetrievalStatus;
4812
+ exports.VertexAIBackend = VertexAIBackend;
4813
+ exports.getAI = getAI;
4814
+ exports.getGenerativeModel = getGenerativeModel;
4815
+ exports.getImagenModel = getImagenModel;
4816
+ exports.getLiveGenerativeModel = getLiveGenerativeModel;
4817
+ exports.getTemplateGenerativeModel = getTemplateGenerativeModel;
4818
+ exports.getTemplateImagenModel = getTemplateImagenModel;
4819
+ exports.startAudioConversation = startAudioConversation;
4820
+ //# sourceMappingURL=index.cjs.js.map