diff options
| author | altaf-creator <dev@altafcreator.com> | 2025-11-09 11:15:19 +0800 |
|---|---|---|
| committer | altaf-creator <dev@altafcreator.com> | 2025-11-09 11:15:19 +0800 |
| commit | 8eff962cab608341a6f2fedc640a0e32d96f26e2 (patch) | |
| tree | 05534d1a720ddc3691d346c69b4972555820a061 /frontend-old/node_modules/@firebase/ai/dist/index.node.cjs.js | |
pain
Diffstat (limited to 'frontend-old/node_modules/@firebase/ai/dist/index.node.cjs.js')
| -rw-r--r-- | frontend-old/node_modules/@firebase/ai/dist/index.node.cjs.js | 3971 |
1 files changed, 3971 insertions, 0 deletions
diff --git a/frontend-old/node_modules/@firebase/ai/dist/index.node.cjs.js b/frontend-old/node_modules/@firebase/ai/dist/index.node.cjs.js new file mode 100644 index 0000000..da7e499 --- /dev/null +++ b/frontend-old/node_modules/@firebase/ai/dist/index.node.cjs.js @@ -0,0 +1,3971 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +var app = require('@firebase/app'); +var component = require('@firebase/component'); +var util = require('@firebase/util'); +var logger$1 = require('@firebase/logger'); + +var name = "@firebase/ai"; +var version = "2.5.0"; + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const AI_TYPE = 'AI'; +const DEFAULT_LOCATION = 'us-central1'; +const DEFAULT_DOMAIN = 'firebasevertexai.googleapis.com'; +const DEFAULT_API_VERSION = 'v1beta'; +const PACKAGE_VERSION = version; +const LANGUAGE_TAG = 'gl-js'; +const DEFAULT_FETCH_TIMEOUT_MS = 180 * 1000; +/** + * Defines the name of the default in-cloud model to use for hybrid inference. + */ +const DEFAULT_HYBRID_IN_CLOUD_MODEL = 'gemini-2.0-flash-lite'; + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Possible roles. + * @public + */ +const POSSIBLE_ROLES = ['user', 'model', 'function', 'system']; +/** + * Harm categories that would cause prompts or candidates to be blocked. + * @public + */ +const HarmCategory = { + HARM_CATEGORY_HATE_SPEECH: 'HARM_CATEGORY_HATE_SPEECH', + HARM_CATEGORY_SEXUALLY_EXPLICIT: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', + HARM_CATEGORY_HARASSMENT: 'HARM_CATEGORY_HARASSMENT', + HARM_CATEGORY_DANGEROUS_CONTENT: 'HARM_CATEGORY_DANGEROUS_CONTENT' +}; +/** + * Threshold above which a prompt or candidate will be blocked. + * @public + */ +const HarmBlockThreshold = { + /** + * Content with `NEGLIGIBLE` will be allowed. + */ + BLOCK_LOW_AND_ABOVE: 'BLOCK_LOW_AND_ABOVE', + /** + * Content with `NEGLIGIBLE` and `LOW` will be allowed. + */ + BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE', + /** + * Content with `NEGLIGIBLE`, `LOW`, and `MEDIUM` will be allowed. + */ + BLOCK_ONLY_HIGH: 'BLOCK_ONLY_HIGH', + /** + * All content will be allowed. + */ + BLOCK_NONE: 'BLOCK_NONE', + /** + * All content will be allowed. This is the same as `BLOCK_NONE`, but the metadata corresponding + * to the {@link (HarmCategory:type)} will not be present in the response. + */ + OFF: 'OFF' +}; +/** + * This property is not supported in the Gemini Developer API ({@link GoogleAIBackend}). + * + * @public + */ +const HarmBlockMethod = { + /** + * The harm block method uses both probability and severity scores. + */ + SEVERITY: 'SEVERITY', + /** + * The harm block method uses the probability score. + */ + PROBABILITY: 'PROBABILITY' +}; +/** + * Probability that a prompt or candidate matches a harm category. + * @public + */ +const HarmProbability = { + /** + * Content has a negligible chance of being unsafe. + */ + NEGLIGIBLE: 'NEGLIGIBLE', + /** + * Content has a low chance of being unsafe. + */ + LOW: 'LOW', + /** + * Content has a medium chance of being unsafe. + */ + MEDIUM: 'MEDIUM', + /** + * Content has a high chance of being unsafe. + */ + HIGH: 'HIGH' +}; +/** + * Harm severity levels. + * @public + */ +const HarmSeverity = { + /** + * Negligible level of harm severity. + */ + HARM_SEVERITY_NEGLIGIBLE: 'HARM_SEVERITY_NEGLIGIBLE', + /** + * Low level of harm severity. + */ + HARM_SEVERITY_LOW: 'HARM_SEVERITY_LOW', + /** + * Medium level of harm severity. + */ + HARM_SEVERITY_MEDIUM: 'HARM_SEVERITY_MEDIUM', + /** + * High level of harm severity. + */ + HARM_SEVERITY_HIGH: 'HARM_SEVERITY_HIGH', + /** + * Harm severity is not supported. + * + * @remarks + * The GoogleAI backend does not support `HarmSeverity`, so this value is used as a fallback. + */ + HARM_SEVERITY_UNSUPPORTED: 'HARM_SEVERITY_UNSUPPORTED' +}; +/** + * Reason that a prompt was blocked. + * @public + */ +const BlockReason = { + /** + * Content was blocked by safety settings. + */ + SAFETY: 'SAFETY', + /** + * Content was blocked, but the reason is uncategorized. + */ + OTHER: 'OTHER', + /** + * Content was blocked because it contained terms from the terminology blocklist. + */ + BLOCKLIST: 'BLOCKLIST', + /** + * Content was blocked due to prohibited content. + */ + PROHIBITED_CONTENT: 'PROHIBITED_CONTENT' +}; +/** + * Reason that a candidate finished. + * @public + */ +const FinishReason = { + /** + * Natural stop point of the model or provided stop sequence. + */ + STOP: 'STOP', + /** + * The maximum number of tokens as specified in the request was reached. + */ + MAX_TOKENS: 'MAX_TOKENS', + /** + * The candidate content was flagged for safety reasons. + */ + SAFETY: 'SAFETY', + /** + * The candidate content was flagged for recitation reasons. + */ + RECITATION: 'RECITATION', + /** + * Unknown reason. + */ + OTHER: 'OTHER', + /** + * The candidate content contained forbidden terms. + */ + BLOCKLIST: 'BLOCKLIST', + /** + * The candidate content potentially contained prohibited content. + */ + PROHIBITED_CONTENT: 'PROHIBITED_CONTENT', + /** + * The candidate content potentially contained Sensitive Personally Identifiable Information (SPII). + */ + SPII: 'SPII', + /** + * The function call generated by the model was invalid. + */ + MALFORMED_FUNCTION_CALL: 'MALFORMED_FUNCTION_CALL' +}; +/** + * @public + */ +const FunctionCallingMode = { + /** + * Default model behavior; model decides to predict either a function call + * or a natural language response. + */ + AUTO: 'AUTO', + /** + * Model is constrained to always predicting a function call only. + * If `allowed_function_names` is set, the predicted function call will be + * limited to any one of `allowed_function_names`, else the predicted + * function call will be any one of the provided `function_declarations`. + */ + ANY: 'ANY', + /** + * Model will not predict any function call. Model behavior is same as when + * not passing any function declarations. + */ + NONE: 'NONE' +}; +/** + * Content part modality. + * @public + */ +const Modality = { + /** + * Unspecified modality. + */ + MODALITY_UNSPECIFIED: 'MODALITY_UNSPECIFIED', + /** + * Plain text. + */ + TEXT: 'TEXT', + /** + * Image. + */ + IMAGE: 'IMAGE', + /** + * Video. + */ + VIDEO: 'VIDEO', + /** + * Audio. + */ + AUDIO: 'AUDIO', + /** + * Document (for example, PDF). + */ + DOCUMENT: 'DOCUMENT' +}; +/** + * Generation modalities to be returned in generation responses. + * + * @beta + */ +const ResponseModality = { + /** + * Text. + * @beta + */ + TEXT: 'TEXT', + /** + * Image. + * @beta + */ + IMAGE: 'IMAGE', + /** + * Audio. + * @beta + */ + AUDIO: 'AUDIO' +}; +/** + * Determines whether inference happens on-device or in-cloud. + * + * @remarks + * <b>PREFER_ON_DEVICE:</b> Attempt to make inference calls using an + * on-device model. If on-device inference is not available, the SDK + * will fall back to using a cloud-hosted model. + * <br/> + * <b>ONLY_ON_DEVICE:</b> Only attempt to make inference calls using an + * on-device model. The SDK will not fall back to a cloud-hosted model. + * If on-device inference is not available, inference methods will throw. + * <br/> + * <b>ONLY_IN_CLOUD:</b> Only attempt to make inference calls using a + * cloud-hosted model. The SDK will not fall back to an on-device model. + * <br/> + * <b>PREFER_IN_CLOUD:</b> Attempt to make inference calls to a + * cloud-hosted model. If not available, the SDK will fall back to an + * on-device model. + * + * @beta + */ +const InferenceMode = { + 'PREFER_ON_DEVICE': 'prefer_on_device', + 'ONLY_ON_DEVICE': 'only_on_device', + 'ONLY_IN_CLOUD': 'only_in_cloud', + 'PREFER_IN_CLOUD': 'prefer_in_cloud' +}; +/** + * Indicates whether inference happened on-device or in-cloud. + * + * @beta + */ +const InferenceSource = { + 'ON_DEVICE': 'on_device', + 'IN_CLOUD': 'in_cloud' +}; +/** + * Represents the result of the code execution. + * + * @beta + */ +const Outcome = { + UNSPECIFIED: 'OUTCOME_UNSPECIFIED', + OK: 'OUTCOME_OK', + FAILED: 'OUTCOME_FAILED', + DEADLINE_EXCEEDED: 'OUTCOME_DEADLINE_EXCEEDED' +}; +/** + * The programming language of the code. + * + * @beta + */ +const Language = { + UNSPECIFIED: 'LANGUAGE_UNSPECIFIED', + PYTHON: 'PYTHON' +}; + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * The status of a URL retrieval. + * + * @remarks + * <b>URL_RETRIEVAL_STATUS_UNSPECIFIED:</b> Unspecified retrieval status. + * <br/> + * <b>URL_RETRIEVAL_STATUS_SUCCESS:</b> The URL retrieval was successful. + * <br/> + * <b>URL_RETRIEVAL_STATUS_ERROR:</b> The URL retrieval failed. + * <br/> + * <b>URL_RETRIEVAL_STATUS_PAYWALL:</b> The URL retrieval failed because the content is behind a paywall. + * <br/> + * <b>URL_RETRIEVAL_STATUS_UNSAFE:</b> The URL retrieval failed because the content is unsafe. + * <br/> + * + * @beta + */ +const URLRetrievalStatus = { + /** + * Unspecified retrieval status. + */ + URL_RETRIEVAL_STATUS_UNSPECIFIED: 'URL_RETRIEVAL_STATUS_UNSPECIFIED', + /** + * The URL retrieval was successful. + */ + URL_RETRIEVAL_STATUS_SUCCESS: 'URL_RETRIEVAL_STATUS_SUCCESS', + /** + * The URL retrieval failed. + */ + URL_RETRIEVAL_STATUS_ERROR: 'URL_RETRIEVAL_STATUS_ERROR', + /** + * The URL retrieval failed because the content is behind a paywall. + */ + URL_RETRIEVAL_STATUS_PAYWALL: 'URL_RETRIEVAL_STATUS_PAYWALL', + /** + * The URL retrieval failed because the content is unsafe. + */ + URL_RETRIEVAL_STATUS_UNSAFE: 'URL_RETRIEVAL_STATUS_UNSAFE' +}; +/** + * The types of responses that can be returned by {@link LiveSession.receive}. + * + * @beta + */ +const LiveResponseType = { + SERVER_CONTENT: 'serverContent', + TOOL_CALL: 'toolCall', + TOOL_CALL_CANCELLATION: 'toolCallCancellation' +}; + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Standardized error codes that {@link AIError} can have. + * + * @public + */ +const AIErrorCode = { + /** A generic error occurred. */ + ERROR: 'error', + /** An error occurred in a request. */ + REQUEST_ERROR: 'request-error', + /** An error occurred in a response. */ + RESPONSE_ERROR: 'response-error', + /** An error occurred while performing a fetch. */ + FETCH_ERROR: 'fetch-error', + /** An error occurred because an operation was attempted on a closed session. */ + SESSION_CLOSED: 'session-closed', + /** An error associated with a Content object. */ + INVALID_CONTENT: 'invalid-content', + /** An error due to the Firebase API not being enabled in the Console. */ + API_NOT_ENABLED: 'api-not-enabled', + /** An error due to invalid Schema input. */ + INVALID_SCHEMA: 'invalid-schema', + /** An error occurred due to a missing Firebase API key. */ + NO_API_KEY: 'no-api-key', + /** An error occurred due to a missing Firebase app ID. */ + NO_APP_ID: 'no-app-id', + /** An error occurred due to a model name not being specified during initialization. */ + NO_MODEL: 'no-model', + /** An error occurred due to a missing project ID. */ + NO_PROJECT_ID: 'no-project-id', + /** An error occurred while parsing. */ + PARSE_FAILED: 'parse-failed', + /** An error occurred due an attempt to use an unsupported feature. */ + UNSUPPORTED: 'unsupported' +}; + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains the list of OpenAPI data types + * as defined by the + * {@link https://swagger.io/docs/specification/data-models/data-types/ | OpenAPI specification} + * @public + */ +const SchemaType = { + /** String type. */ + STRING: 'string', + /** Number type. */ + NUMBER: 'number', + /** Integer type. */ + INTEGER: 'integer', + /** Boolean type. */ + BOOLEAN: 'boolean', + /** Array type. */ + ARRAY: 'array', + /** Object type. */ + OBJECT: 'object' +}; + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * A filter level controlling how aggressively to filter sensitive content. + * + * Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI + * are assessed against a list of safety filters, which include 'harmful categories' (for example, + * `violence`, `sexual`, `derogatory`, and `toxic`). This filter level controls how aggressively to + * filter out potentially harmful content from responses. See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } + * and the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters | Responsible AI and usage guidelines} + * for more details. + * + * @public + */ +const ImagenSafetyFilterLevel = { + /** + * The most aggressive filtering level; most strict blocking. + */ + BLOCK_LOW_AND_ABOVE: 'block_low_and_above', + /** + * Blocks some sensitive prompts and responses. + */ + BLOCK_MEDIUM_AND_ABOVE: 'block_medium_and_above', + /** + * Blocks few sensitive prompts and responses. + */ + BLOCK_ONLY_HIGH: 'block_only_high', + /** + * The least aggressive filtering level; blocks very few sensitive prompts and responses. + * + * Access to this feature is restricted and may require your case to be reviewed and approved by + * Cloud support. + */ + BLOCK_NONE: 'block_none' +}; +/** + * A filter level controlling whether generation of images containing people or faces is allowed. + * + * See the <a href="http://firebase.google.com/docs/vertex-ai/generate-images">personGeneration</a> + * documentation for more details. + * + * @public + */ +const ImagenPersonFilterLevel = { + /** + * Disallow generation of images containing people or faces; images of people are filtered out. + */ + BLOCK_ALL: 'dont_allow', + /** + * Allow generation of images containing adults only; images of children are filtered out. + * + * Generation of images containing people or faces may require your use case to be + * 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} + * for more details. + */ + ALLOW_ADULT: 'allow_adult', + /** + * Allow generation of images containing adults only; images of children are filtered out. + * + * Generation of images containing people or faces may require your use case to be + * 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} + * for more details. + */ + ALLOW_ALL: 'allow_all' +}; +/** + * Aspect ratios for Imagen images. + * + * To specify an aspect ratio for generated images, set the `aspectRatio` property in your + * {@link ImagenGenerationConfig}. + * + * See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } + * for more details and examples of the supported aspect ratios. + * + * @public + */ +const ImagenAspectRatio = { + /** + * Square (1:1) aspect ratio. + */ + 'SQUARE': '1:1', + /** + * Landscape (3:4) aspect ratio. + */ + 'LANDSCAPE_3x4': '3:4', + /** + * Portrait (4:3) aspect ratio. + */ + 'PORTRAIT_4x3': '4:3', + /** + * Landscape (16:9) aspect ratio. + */ + 'LANDSCAPE_16x9': '16:9', + /** + * Portrait (9:16) aspect ratio. + */ + 'PORTRAIT_9x16': '9:16' +}; + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * An enum-like object containing constants that represent the supported backends + * for the Firebase AI SDK. + * This determines which backend service (Vertex AI Gemini API or Gemini Developer API) + * the SDK will communicate with. + * + * These values are assigned to the `backendType` property within the specific backend + * configuration objects ({@link GoogleAIBackend} or {@link VertexAIBackend}) to identify + * which service to target. + * + * @public + */ +const BackendType = { + /** + * Identifies the backend service for the Vertex AI Gemini API provided through Google Cloud. + * Use this constant when creating a {@link VertexAIBackend} configuration. + */ + VERTEX_AI: 'VERTEX_AI', + /** + * Identifies the backend service for the Gemini Developer API ({@link https://ai.google/ | Google AI}). + * Use this constant when creating a {@link GoogleAIBackend} configuration. + */ + GOOGLE_AI: 'GOOGLE_AI' +}; // Using 'as const' makes the string values literal types + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Abstract base class representing the configuration for an AI service backend. + * This class should not be instantiated directly. Use its subclasses; {@link GoogleAIBackend} for + * the Gemini Developer API (via {@link https://ai.google/ | Google AI}), and + * {@link VertexAIBackend} for the Vertex AI Gemini API. + * + * @public + */ +class Backend { + /** + * Protected constructor for use by subclasses. + * @param type - The backend type. + */ + constructor(type) { + this.backendType = type; + } +} +/** + * Configuration class for the Gemini Developer API. + * + * Use this with {@link AIOptions} when initializing the AI service via + * {@link getAI | getAI()} to specify the Gemini Developer API as the backend. + * + * @public + */ +class GoogleAIBackend extends Backend { + /** + * Creates a configuration object for the Gemini Developer API backend. + */ + constructor() { + super(BackendType.GOOGLE_AI); + } +} +/** + * Configuration class for the Vertex AI Gemini API. + * + * Use this with {@link AIOptions} when initializing the AI service via + * {@link getAI | getAI()} to specify the Vertex AI Gemini API as the backend. + * + * @public + */ +class VertexAIBackend extends Backend { + /** + * Creates a configuration object for the Vertex AI backend. + * + * @param location - The region identifier, defaulting to `us-central1`; + * see {@link https://firebase.google.com/docs/vertex-ai/locations#available-locations | Vertex AI locations} + * for a list of supported locations. + */ + constructor(location = DEFAULT_LOCATION) { + super(BackendType.VERTEX_AI); + if (!location) { + this.location = DEFAULT_LOCATION; + } + else { + this.location = location; + } + } +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +class AIService { + constructor(app, backend, authProvider, appCheckProvider, chromeAdapterFactory) { + this.app = app; + this.backend = backend; + this.chromeAdapterFactory = chromeAdapterFactory; + const appCheck = appCheckProvider?.getImmediate({ optional: true }); + const auth = authProvider?.getImmediate({ optional: true }); + this.auth = auth || null; + this.appCheck = appCheck || null; + if (backend instanceof VertexAIBackend) { + this.location = backend.location; + } + else { + this.location = ''; + } + } + _delete() { + return Promise.resolve(); + } + set options(optionsToSet) { + this._options = optionsToSet; + } + get options() { + return this._options; + } +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Error class for the Firebase AI SDK. + * + * @public + */ +class AIError extends util.FirebaseError { + /** + * Constructs a new instance of the `AIError` class. + * + * @param code - The error code from {@link (AIErrorCode:type)}. + * @param message - A human-readable message describing the error. + * @param customErrorData - Optional error data. + */ + constructor(code, message, customErrorData) { + // Match error format used by FirebaseError from ErrorFactory + const service = AI_TYPE; + const fullCode = `${service}/${code}`; + const fullMessage = `${service}: ${message} (${fullCode})`; + super(code, fullMessage); + this.code = code; + this.customErrorData = customErrorData; + // FirebaseError initializes a stack trace, but it assumes the error is created from the error + // factory. Since we break this assumption, we set the stack trace to be originating from this + // constructor. + // This is only supported in V8. + if (Error.captureStackTrace) { + // Allows us to initialize the stack trace without including the constructor itself at the + // top level of the stack trace. + Error.captureStackTrace(this, AIError); + } + // Allows instanceof AIError in ES5/ES6 + // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work + // TODO(dlarocque): Replace this with `new.target`: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + // which we can now use since we no longer target ES5. + Object.setPrototypeOf(this, AIError.prototype); + // Since Error is an interface, we don't inherit toString and so we define it ourselves. + this.toString = () => fullMessage; + } +} + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Encodes a {@link Backend} into a string that will be used to uniquely identify {@link AI} + * instances by backend type. + * + * @internal + */ +function encodeInstanceIdentifier(backend) { + if (backend instanceof GoogleAIBackend) { + return `${AI_TYPE}/googleai`; + } + else if (backend instanceof VertexAIBackend) { + return `${AI_TYPE}/vertexai/${backend.location}`; + } + else { + throw new AIError(AIErrorCode.ERROR, `Invalid backend: ${JSON.stringify(backend.backendType)}`); + } +} +/** + * Decodes an instance identifier string into a {@link Backend}. + * + * @internal + */ +function decodeInstanceIdentifier(instanceIdentifier) { + const identifierParts = instanceIdentifier.split('/'); + if (identifierParts[0] !== AI_TYPE) { + throw new AIError(AIErrorCode.ERROR, `Invalid instance identifier, unknown prefix '${identifierParts[0]}'`); + } + const backendType = identifierParts[1]; + switch (backendType) { + case 'vertexai': + const location = identifierParts[2]; + if (!location) { + throw new AIError(AIErrorCode.ERROR, `Invalid instance identifier, unknown location '${instanceIdentifier}'`); + } + return new VertexAIBackend(location); + case 'googleai': + return new GoogleAIBackend(); + default: + throw new AIError(AIErrorCode.ERROR, `Invalid instance identifier string: '${instanceIdentifier}'`); + } +} + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Base class for Firebase AI model APIs. + * + * Instances of this class are associated with a specific Firebase AI {@link Backend} + * and provide methods for interacting with the configured generative model. + * + * @public + */ +class AIModel { + /** + * Constructs a new instance of the {@link AIModel} class. + * + * This constructor should only be called from subclasses that provide + * a model API. + * + * @param ai - an {@link AI} instance. + * @param modelName - The name of the model being used. It can be in one of the following formats: + * - `my-model` (short name, will resolve to `publishers/google/models/my-model`) + * - `models/my-model` (will resolve to `publishers/google/models/my-model`) + * - `publishers/my-publisher/models/my-model` (fully qualified model name) + * + * @throws If the `apiKey` or `projectId` fields are missing in your + * Firebase config. + * + * @internal + */ + constructor(ai, modelName) { + if (!ai.app?.options?.apiKey) { + 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.`); + } + else if (!ai.app?.options?.projectId) { + 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.`); + } + else if (!ai.app?.options?.appId) { + 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.`); + } + else { + this._apiSettings = { + apiKey: ai.app.options.apiKey, + project: ai.app.options.projectId, + appId: ai.app.options.appId, + automaticDataCollectionEnabled: ai.app.automaticDataCollectionEnabled, + location: ai.location, + backend: ai.backend + }; + if (app._isFirebaseServerApp(ai.app) && ai.app.settings.appCheckToken) { + const token = ai.app.settings.appCheckToken; + this._apiSettings.getAppCheckToken = () => { + return Promise.resolve({ token }); + }; + } + else if (ai.appCheck) { + if (ai.options?.useLimitedUseAppCheckTokens) { + this._apiSettings.getAppCheckToken = () => ai.appCheck.getLimitedUseToken(); + } + else { + this._apiSettings.getAppCheckToken = () => ai.appCheck.getToken(); + } + } + if (ai.auth) { + this._apiSettings.getAuthToken = () => ai.auth.getToken(); + } + this.model = AIModel.normalizeModelName(modelName, this._apiSettings.backend.backendType); + } + } + /** + * Normalizes the given model name to a fully qualified model resource name. + * + * @param modelName - The model name to normalize. + * @returns The fully qualified model resource name. + * + * @internal + */ + static normalizeModelName(modelName, backendType) { + if (backendType === BackendType.GOOGLE_AI) { + return AIModel.normalizeGoogleAIModelName(modelName); + } + else { + return AIModel.normalizeVertexAIModelName(modelName); + } + } + /** + * @internal + */ + static normalizeGoogleAIModelName(modelName) { + return `models/${modelName}`; + } + /** + * @internal + */ + static normalizeVertexAIModelName(modelName) { + let model; + if (modelName.includes('/')) { + if (modelName.startsWith('models/')) { + // Add 'publishers/google' if the user is only passing in 'models/model-name'. + model = `publishers/google/${modelName}`; + } + else { + // Any other custom format (e.g. tuned models) must be passed in correctly. + model = modelName; + } + } + else { + // If path is not included, assume it's a non-tuned model. + model = `publishers/google/models/${modelName}`; + } + return model; + } +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const logger = new logger$1.Logger('@firebase/vertexai'); + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var Task; +(function (Task) { + Task["GENERATE_CONTENT"] = "generateContent"; + Task["STREAM_GENERATE_CONTENT"] = "streamGenerateContent"; + Task["COUNT_TOKENS"] = "countTokens"; + Task["PREDICT"] = "predict"; +})(Task || (Task = {})); +class RequestUrl { + constructor(model, task, apiSettings, stream, requestOptions) { + this.model = model; + this.task = task; + this.apiSettings = apiSettings; + this.stream = stream; + this.requestOptions = requestOptions; + } + toString() { + const url = new URL(this.baseUrl); // Throws if the URL is invalid + url.pathname = `/${this.apiVersion}/${this.modelPath}:${this.task}`; + url.search = this.queryParams.toString(); + return url.toString(); + } + get baseUrl() { + return this.requestOptions?.baseUrl || `https://${DEFAULT_DOMAIN}`; + } + get apiVersion() { + return DEFAULT_API_VERSION; // TODO: allow user-set options if that feature becomes available + } + get modelPath() { + if (this.apiSettings.backend instanceof GoogleAIBackend) { + return `projects/${this.apiSettings.project}/${this.model}`; + } + else if (this.apiSettings.backend instanceof VertexAIBackend) { + return `projects/${this.apiSettings.project}/locations/${this.apiSettings.backend.location}/${this.model}`; + } + else { + throw new AIError(AIErrorCode.ERROR, `Invalid backend: ${JSON.stringify(this.apiSettings.backend)}`); + } + } + get queryParams() { + const params = new URLSearchParams(); + if (this.stream) { + params.set('alt', 'sse'); + } + return params; + } +} +class WebSocketUrl { + constructor(apiSettings) { + this.apiSettings = apiSettings; + } + toString() { + const url = new URL(`wss://${DEFAULT_DOMAIN}`); + url.pathname = this.pathname; + const queryParams = new URLSearchParams(); + queryParams.set('key', this.apiSettings.apiKey); + url.search = queryParams.toString(); + return url.toString(); + } + get pathname() { + if (this.apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + return 'ws/google.firebase.vertexai.v1beta.GenerativeService/BidiGenerateContent'; + } + else { + return `ws/google.firebase.vertexai.v1beta.LlmBidiService/BidiGenerateContent/locations/${this.apiSettings.location}`; + } + } +} +/** + * Log language and "fire/version" to x-goog-api-client + */ +function getClientHeaders() { + const loggingTags = []; + loggingTags.push(`${LANGUAGE_TAG}/${PACKAGE_VERSION}`); + loggingTags.push(`fire/${PACKAGE_VERSION}`); + return loggingTags.join(' '); +} +async function getHeaders(url) { + const headers = new Headers(); + headers.append('Content-Type', 'application/json'); + headers.append('x-goog-api-client', getClientHeaders()); + headers.append('x-goog-api-key', url.apiSettings.apiKey); + if (url.apiSettings.automaticDataCollectionEnabled) { + headers.append('X-Firebase-Appid', url.apiSettings.appId); + } + if (url.apiSettings.getAppCheckToken) { + const appCheckToken = await url.apiSettings.getAppCheckToken(); + if (appCheckToken) { + headers.append('X-Firebase-AppCheck', appCheckToken.token); + if (appCheckToken.error) { + logger.warn(`Unable to obtain a valid App Check token: ${appCheckToken.error.message}`); + } + } + } + if (url.apiSettings.getAuthToken) { + const authToken = await url.apiSettings.getAuthToken(); + if (authToken) { + headers.append('Authorization', `Firebase ${authToken.accessToken}`); + } + } + return headers; +} +async function constructRequest(model, task, apiSettings, stream, body, requestOptions) { + const url = new RequestUrl(model, task, apiSettings, stream, requestOptions); + return { + url: url.toString(), + fetchOptions: { + method: 'POST', + headers: await getHeaders(url), + body + } + }; +} +async function makeRequest(model, task, apiSettings, stream, body, requestOptions) { + const url = new RequestUrl(model, task, apiSettings, stream, requestOptions); + let response; + let fetchTimeoutId; + try { + const request = await constructRequest(model, task, apiSettings, stream, body, requestOptions); + // Timeout is 180s by default + const timeoutMillis = requestOptions?.timeout != null && requestOptions.timeout >= 0 + ? requestOptions.timeout + : DEFAULT_FETCH_TIMEOUT_MS; + const abortController = new AbortController(); + fetchTimeoutId = setTimeout(() => abortController.abort(), timeoutMillis); + request.fetchOptions.signal = abortController.signal; + response = await fetch(request.url, request.fetchOptions); + if (!response.ok) { + let message = ''; + let errorDetails; + try { + const json = await response.json(); + message = json.error.message; + if (json.error.details) { + message += ` ${JSON.stringify(json.error.details)}`; + errorDetails = json.error.details; + } + } + catch (e) { + // ignored + } + if (response.status === 403 && + errorDetails && + errorDetails.some((detail) => detail.reason === 'SERVICE_DISABLED') && + errorDetails.some((detail) => detail.links?.[0]?.description.includes('Google developers console API activation'))) { + throw new AIError(AIErrorCode.API_NOT_ENABLED, `The Firebase AI SDK requires the Firebase AI ` + + `API ('firebasevertexai.googleapis.com') to be enabled in your ` + + `Firebase project. Enable this API by visiting the Firebase Console ` + + `at https://console.firebase.google.com/project/${url.apiSettings.project}/genai/ ` + + `and clicking "Get started". If you enabled this API recently, ` + + `wait a few minutes for the action to propagate to our systems and ` + + `then retry.`, { + status: response.status, + statusText: response.statusText, + errorDetails + }); + } + throw new AIError(AIErrorCode.FETCH_ERROR, `Error fetching from ${url}: [${response.status} ${response.statusText}] ${message}`, { + status: response.status, + statusText: response.statusText, + errorDetails + }); + } + } + catch (e) { + let err = e; + if (e.code !== AIErrorCode.FETCH_ERROR && + e.code !== AIErrorCode.API_NOT_ENABLED && + e instanceof Error) { + err = new AIError(AIErrorCode.ERROR, `Error fetching from ${url.toString()}: ${e.message}`); + err.stack = e.stack; + } + throw err; + } + finally { + if (fetchTimeoutId) { + clearTimeout(fetchTimeoutId); + } + } + return response; +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Check that at least one candidate exists and does not have a bad + * finish reason. Warns if multiple candidates exist. + */ +function hasValidCandidates(response) { + if (response.candidates && response.candidates.length > 0) { + if (response.candidates.length > 1) { + logger.warn(`This response had ${response.candidates.length} ` + + `candidates. Returning text from the first candidate only. ` + + `Access response.candidates directly to use the other candidates.`); + } + if (hadBadFinishReason(response.candidates[0])) { + throw new AIError(AIErrorCode.RESPONSE_ERROR, `Response error: ${formatBlockErrorMessage(response)}. Response body stored in error.response`, { + response + }); + } + return true; + } + else { + return false; + } +} +/** + * Creates an EnhancedGenerateContentResponse object that has helper functions and + * other modifications that improve usability. + */ +function createEnhancedContentResponse(response, inferenceSource = InferenceSource.IN_CLOUD) { + /** + * The Vertex AI backend omits default values. + * This causes the `index` property to be omitted from the first candidate in the + * response, since it has index 0, and 0 is a default value. + * See: https://github.com/firebase/firebase-js-sdk/issues/8566 + */ + if (response.candidates && !response.candidates[0].hasOwnProperty('index')) { + response.candidates[0].index = 0; + } + const responseWithHelpers = addHelpers(response); + responseWithHelpers.inferenceSource = inferenceSource; + return responseWithHelpers; +} +/** + * Adds convenience helper methods to a response object, including stream + * chunks (as long as each chunk is a complete GenerateContentResponse JSON). + */ +function addHelpers(response) { + response.text = () => { + if (hasValidCandidates(response)) { + return getText(response, part => !part.thought); + } + else if (response.promptFeedback) { + throw new AIError(AIErrorCode.RESPONSE_ERROR, `Text not available. ${formatBlockErrorMessage(response)}`, { + response + }); + } + return ''; + }; + response.thoughtSummary = () => { + if (hasValidCandidates(response)) { + const result = getText(response, part => !!part.thought); + return result === '' ? undefined : result; + } + else if (response.promptFeedback) { + throw new AIError(AIErrorCode.RESPONSE_ERROR, `Thought summary not available. ${formatBlockErrorMessage(response)}`, { + response + }); + } + return undefined; + }; + response.inlineDataParts = () => { + if (hasValidCandidates(response)) { + return getInlineDataParts(response); + } + else if (response.promptFeedback) { + throw new AIError(AIErrorCode.RESPONSE_ERROR, `Data not available. ${formatBlockErrorMessage(response)}`, { + response + }); + } + return undefined; + }; + response.functionCalls = () => { + if (hasValidCandidates(response)) { + return getFunctionCalls(response); + } + else if (response.promptFeedback) { + throw new AIError(AIErrorCode.RESPONSE_ERROR, `Function call not available. ${formatBlockErrorMessage(response)}`, { + response + }); + } + return undefined; + }; + return response; +} +/** + * Returns all text from the first candidate's parts, filtering by whether + * `partFilter()` returns true. + * + * @param response - The `GenerateContentResponse` from which to extract text. + * @param partFilter - Only return `Part`s for which this returns true + */ +function getText(response, partFilter) { + const textStrings = []; + if (response.candidates?.[0].content?.parts) { + for (const part of response.candidates?.[0].content?.parts) { + if (part.text && partFilter(part)) { + textStrings.push(part.text); + } + } + } + if (textStrings.length > 0) { + return textStrings.join(''); + } + else { + return ''; + } +} +/** + * Returns every {@link FunctionCall} associated with first candidate. + */ +function getFunctionCalls(response) { + const functionCalls = []; + if (response.candidates?.[0].content?.parts) { + for (const part of response.candidates?.[0].content?.parts) { + if (part.functionCall) { + functionCalls.push(part.functionCall); + } + } + } + if (functionCalls.length > 0) { + return functionCalls; + } + else { + return undefined; + } +} +/** + * Returns every {@link InlineDataPart} in the first candidate if present. + * + * @internal + */ +function getInlineDataParts(response) { + const data = []; + if (response.candidates?.[0].content?.parts) { + for (const part of response.candidates?.[0].content?.parts) { + if (part.inlineData) { + data.push(part); + } + } + } + if (data.length > 0) { + return data; + } + else { + return undefined; + } +} +const badFinishReasons = [FinishReason.RECITATION, FinishReason.SAFETY]; +function hadBadFinishReason(candidate) { + return (!!candidate.finishReason && + badFinishReasons.some(reason => reason === candidate.finishReason)); +} +function formatBlockErrorMessage(response) { + let message = ''; + if ((!response.candidates || response.candidates.length === 0) && + response.promptFeedback) { + message += 'Response was blocked'; + if (response.promptFeedback?.blockReason) { + message += ` due to ${response.promptFeedback.blockReason}`; + } + if (response.promptFeedback?.blockReasonMessage) { + message += `: ${response.promptFeedback.blockReasonMessage}`; + } + } + else if (response.candidates?.[0]) { + const firstCandidate = response.candidates[0]; + if (hadBadFinishReason(firstCandidate)) { + message += `Candidate was blocked due to ${firstCandidate.finishReason}`; + if (firstCandidate.finishMessage) { + message += `: ${firstCandidate.finishMessage}`; + } + } + } + return message; +} +/** + * Convert a generic successful fetch response body to an Imagen response object + * that can be returned to the user. This converts the REST APIs response format to our + * APIs representation of a response. + * + * @internal + */ +async function handlePredictResponse(response) { + const responseJson = await response.json(); + const images = []; + let filteredReason = undefined; + // The backend should always send a non-empty array of predictions if the response was successful. + if (!responseJson.predictions || responseJson.predictions?.length === 0) { + 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.'); + } + for (const prediction of responseJson.predictions) { + if (prediction.raiFilteredReason) { + filteredReason = prediction.raiFilteredReason; + } + else if (prediction.mimeType && prediction.bytesBase64Encoded) { + images.push({ + mimeType: prediction.mimeType, + bytesBase64Encoded: prediction.bytesBase64Encoded + }); + } + else if (prediction.mimeType && prediction.gcsUri) { + images.push({ + mimeType: prediction.mimeType, + gcsURI: prediction.gcsUri + }); + } + else if (prediction.safetyAttributes) ; + else { + throw new AIError(AIErrorCode.RESPONSE_ERROR, `Unexpected element in 'predictions' array in response: '${JSON.stringify(prediction)}'`); + } + } + return { images, filteredReason }; +} + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * This SDK supports both the Vertex AI Gemini API and the Gemini Developer API (using Google AI). + * The public API prioritizes the format used by the Vertex AI Gemini API. + * We avoid having two sets of types by translating requests and responses between the two API formats. + * This translation allows developers to switch between the Vertex AI Gemini API and the Gemini Developer API + * with minimal code changes. + * + * In here are functions that map requests and responses between the two API formats. + * Requests in the Vertex AI format are mapped to the Google AI format before being sent. + * Responses from the Google AI backend are mapped back to the Vertex AI format before being returned to the user. + */ +/** + * Maps a Vertex AI {@link GenerateContentRequest} to a format that can be sent to Google AI. + * + * @param generateContentRequest The {@link GenerateContentRequest} to map. + * @returns A {@link GenerateContentResponse} that conforms to the Google AI format. + * + * @throws If the request contains properties that are unsupported by Google AI. + * + * @internal + */ +function mapGenerateContentRequest(generateContentRequest) { + generateContentRequest.safetySettings?.forEach(safetySetting => { + if (safetySetting.method) { + throw new AIError(AIErrorCode.UNSUPPORTED, 'SafetySetting.method is not supported in the the Gemini Developer API. Please remove this property.'); + } + }); + if (generateContentRequest.generationConfig?.topK) { + const roundedTopK = Math.round(generateContentRequest.generationConfig.topK); + if (roundedTopK !== generateContentRequest.generationConfig.topK) { + logger.warn('topK in GenerationConfig has been rounded to the nearest integer to match the format for requests to the Gemini Developer API.'); + generateContentRequest.generationConfig.topK = roundedTopK; + } + } + return generateContentRequest; +} +/** + * Maps a {@link GenerateContentResponse} from Google AI to the format of the + * {@link GenerateContentResponse} that we get from VertexAI that is exposed in the public API. + * + * @param googleAIResponse The {@link GenerateContentResponse} from Google AI. + * @returns A {@link GenerateContentResponse} that conforms to the public API's format. + * + * @internal + */ +function mapGenerateContentResponse(googleAIResponse) { + const generateContentResponse = { + candidates: googleAIResponse.candidates + ? mapGenerateContentCandidates(googleAIResponse.candidates) + : undefined, + prompt: googleAIResponse.promptFeedback + ? mapPromptFeedback(googleAIResponse.promptFeedback) + : undefined, + usageMetadata: googleAIResponse.usageMetadata + }; + return generateContentResponse; +} +/** + * Maps a Vertex AI {@link CountTokensRequest} to a format that can be sent to Google AI. + * + * @param countTokensRequest The {@link CountTokensRequest} to map. + * @param model The model to count tokens with. + * @returns A {@link CountTokensRequest} that conforms to the Google AI format. + * + * @internal + */ +function mapCountTokensRequest(countTokensRequest, model) { + const mappedCountTokensRequest = { + generateContentRequest: { + model, + ...countTokensRequest + } + }; + return mappedCountTokensRequest; +} +/** + * Maps a Google AI {@link GoogleAIGenerateContentCandidate} to a format that conforms + * to the Vertex AI API format. + * + * @param candidates The {@link GoogleAIGenerateContentCandidate} to map. + * @returns A {@link GenerateContentCandidate} that conforms to the Vertex AI format. + * + * @throws If any {@link Part} in the candidates has a `videoMetadata` property. + * + * @internal + */ +function mapGenerateContentCandidates(candidates) { + const mappedCandidates = []; + let mappedSafetyRatings; + if (mappedCandidates) { + candidates.forEach(candidate => { + // Map citationSources to citations. + let citationMetadata; + if (candidate.citationMetadata) { + citationMetadata = { + citations: candidate.citationMetadata.citationSources + }; + } + // Assign missing candidate SafetyRatings properties to their defaults if undefined. + if (candidate.safetyRatings) { + mappedSafetyRatings = candidate.safetyRatings.map(safetyRating => { + return { + ...safetyRating, + severity: safetyRating.severity ?? HarmSeverity.HARM_SEVERITY_UNSUPPORTED, + probabilityScore: safetyRating.probabilityScore ?? 0, + severityScore: safetyRating.severityScore ?? 0 + }; + }); + } + // videoMetadata is not supported. + // Throw early since developers may send a long video as input and only expect to pay + // for inference on a small portion of the video. + if (candidate.content?.parts?.some(part => part?.videoMetadata)) { + throw new AIError(AIErrorCode.UNSUPPORTED, 'Part.videoMetadata is not supported in the Gemini Developer API. Please remove this property.'); + } + const mappedCandidate = { + index: candidate.index, + content: candidate.content, + finishReason: candidate.finishReason, + finishMessage: candidate.finishMessage, + safetyRatings: mappedSafetyRatings, + citationMetadata, + groundingMetadata: candidate.groundingMetadata, + urlContextMetadata: candidate.urlContextMetadata + }; + mappedCandidates.push(mappedCandidate); + }); + } + return mappedCandidates; +} +function mapPromptFeedback(promptFeedback) { + // Assign missing SafetyRating properties to their defaults if undefined. + const mappedSafetyRatings = []; + promptFeedback.safetyRatings.forEach(safetyRating => { + mappedSafetyRatings.push({ + category: safetyRating.category, + probability: safetyRating.probability, + severity: safetyRating.severity ?? HarmSeverity.HARM_SEVERITY_UNSUPPORTED, + probabilityScore: safetyRating.probabilityScore ?? 0, + severityScore: safetyRating.severityScore ?? 0, + blocked: safetyRating.blocked + }); + }); + const mappedPromptFeedback = { + blockReason: promptFeedback.blockReason, + safetyRatings: mappedSafetyRatings, + blockReasonMessage: promptFeedback.blockReasonMessage + }; + return mappedPromptFeedback; +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/; +/** + * Process a response.body stream from the backend and return an + * iterator that provides one complete GenerateContentResponse at a time + * and a promise that resolves with a single aggregated + * GenerateContentResponse. + * + * @param response - Response from a fetch call + */ +function processStream(response, apiSettings, inferenceSource) { + const inputStream = response.body.pipeThrough(new TextDecoderStream('utf8', { fatal: true })); + const responseStream = getResponseStream(inputStream); + const [stream1, stream2] = responseStream.tee(); + return { + stream: generateResponseSequence(stream1, apiSettings, inferenceSource), + response: getResponsePromise(stream2, apiSettings, inferenceSource) + }; +} +async function getResponsePromise(stream, apiSettings, inferenceSource) { + const allResponses = []; + const reader = stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) { + let generateContentResponse = aggregateResponses(allResponses); + if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + generateContentResponse = mapGenerateContentResponse(generateContentResponse); + } + return createEnhancedContentResponse(generateContentResponse, inferenceSource); + } + allResponses.push(value); + } +} +async function* generateResponseSequence(stream, apiSettings, inferenceSource) { + const reader = stream.getReader(); + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + let enhancedResponse; + if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + enhancedResponse = createEnhancedContentResponse(mapGenerateContentResponse(value), inferenceSource); + } + else { + enhancedResponse = createEnhancedContentResponse(value, inferenceSource); + } + const firstCandidate = enhancedResponse.candidates?.[0]; + // Don't yield a response with no useful data for the developer. + if (!firstCandidate?.content?.parts && + !firstCandidate?.finishReason && + !firstCandidate?.citationMetadata && + !firstCandidate?.urlContextMetadata) { + continue; + } + yield enhancedResponse; + } +} +/** + * Reads a raw stream from the fetch response and join incomplete + * chunks, returning a new stream that provides a single complete + * GenerateContentResponse in each iteration. + */ +function getResponseStream(inputStream) { + const reader = inputStream.getReader(); + const stream = new ReadableStream({ + start(controller) { + let currentText = ''; + return pump(); + function pump() { + return reader.read().then(({ value, done }) => { + if (done) { + if (currentText.trim()) { + controller.error(new AIError(AIErrorCode.PARSE_FAILED, 'Failed to parse stream')); + return; + } + controller.close(); + return; + } + currentText += value; + let match = currentText.match(responseLineRE); + let parsedResponse; + while (match) { + try { + parsedResponse = JSON.parse(match[1]); + } + catch (e) { + controller.error(new AIError(AIErrorCode.PARSE_FAILED, `Error parsing JSON response: "${match[1]}`)); + return; + } + controller.enqueue(parsedResponse); + currentText = currentText.substring(match[0].length); + match = currentText.match(responseLineRE); + } + return pump(); + }); + } + } + }); + return stream; +} +/** + * Aggregates an array of `GenerateContentResponse`s into a single + * GenerateContentResponse. + */ +function aggregateResponses(responses) { + const lastResponse = responses[responses.length - 1]; + const aggregatedResponse = { + promptFeedback: lastResponse?.promptFeedback + }; + for (const response of responses) { + if (response.candidates) { + for (const candidate of response.candidates) { + // Index will be undefined if it's the first index (0), so we should use 0 if it's undefined. + // See: https://github.com/firebase/firebase-js-sdk/issues/8566 + const i = candidate.index || 0; + if (!aggregatedResponse.candidates) { + aggregatedResponse.candidates = []; + } + if (!aggregatedResponse.candidates[i]) { + aggregatedResponse.candidates[i] = { + index: candidate.index + }; + } + // Keep overwriting, the last one will be final + aggregatedResponse.candidates[i].citationMetadata = + candidate.citationMetadata; + aggregatedResponse.candidates[i].finishReason = candidate.finishReason; + aggregatedResponse.candidates[i].finishMessage = + candidate.finishMessage; + aggregatedResponse.candidates[i].safetyRatings = + candidate.safetyRatings; + aggregatedResponse.candidates[i].groundingMetadata = + candidate.groundingMetadata; + // The urlContextMetadata object is defined in the first chunk of the response stream. + // In all subsequent chunks, the urlContextMetadata object will be undefined. We need to + // make sure that we don't overwrite the first value urlContextMetadata object with undefined. + // FIXME: What happens if we receive a second, valid urlContextMetadata object? + const urlContextMetadata = candidate.urlContextMetadata; + if (typeof urlContextMetadata === 'object' && + urlContextMetadata !== null && + Object.keys(urlContextMetadata).length > 0) { + aggregatedResponse.candidates[i].urlContextMetadata = + urlContextMetadata; + } + /** + * Candidates should always have content and parts, but this handles + * possible malformed responses. + */ + if (candidate.content) { + // Skip a candidate without parts. + if (!candidate.content.parts) { + continue; + } + if (!aggregatedResponse.candidates[i].content) { + aggregatedResponse.candidates[i].content = { + role: candidate.content.role || 'user', + parts: [] + }; + } + for (const part of candidate.content.parts) { + const newPart = { ...part }; + // The backend can send empty text parts. If these are sent back + // (e.g. in chat history), the backend will respond with an error. + // To prevent this, ignore empty text parts. + if (part.text === '') { + continue; + } + if (Object.keys(newPart).length > 0) { + aggregatedResponse.candidates[i].content.parts.push(newPart); + } + } + } + } + } + } + return aggregatedResponse; +} + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const errorsCausingFallback = [ + // most network errors + AIErrorCode.FETCH_ERROR, + // fallback code for all other errors in makeRequest + AIErrorCode.ERROR, + // error due to API not being enabled in project + AIErrorCode.API_NOT_ENABLED +]; +/** + * Dispatches a request to the appropriate backend (on-device or in-cloud) + * based on the inference mode. + * + * @param request - The request to be sent. + * @param chromeAdapter - The on-device model adapter. + * @param onDeviceCall - The function to call for on-device inference. + * @param inCloudCall - The function to call for in-cloud inference. + * @returns The response from the backend. + */ +async function callCloudOrDevice(request, chromeAdapter, onDeviceCall, inCloudCall) { + if (!chromeAdapter) { + return { + response: await inCloudCall(), + inferenceSource: InferenceSource.IN_CLOUD + }; + } + switch (chromeAdapter.mode) { + case InferenceMode.ONLY_ON_DEVICE: + if (await chromeAdapter.isAvailable(request)) { + return { + response: await onDeviceCall(), + inferenceSource: InferenceSource.ON_DEVICE + }; + } + throw new AIError(AIErrorCode.UNSUPPORTED, 'Inference mode is ONLY_ON_DEVICE, but an on-device model is not available.'); + case InferenceMode.ONLY_IN_CLOUD: + return { + response: await inCloudCall(), + inferenceSource: InferenceSource.IN_CLOUD + }; + case InferenceMode.PREFER_IN_CLOUD: + try { + return { + response: await inCloudCall(), + inferenceSource: InferenceSource.IN_CLOUD + }; + } + catch (e) { + if (e instanceof AIError && errorsCausingFallback.includes(e.code)) { + return { + response: await onDeviceCall(), + inferenceSource: InferenceSource.ON_DEVICE + }; + } + throw e; + } + case InferenceMode.PREFER_ON_DEVICE: + if (await chromeAdapter.isAvailable(request)) { + return { + response: await onDeviceCall(), + inferenceSource: InferenceSource.ON_DEVICE + }; + } + return { + response: await inCloudCall(), + inferenceSource: InferenceSource.IN_CLOUD + }; + default: + throw new AIError(AIErrorCode.ERROR, `Unexpected infererence mode: ${chromeAdapter.mode}`); + } +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +async function generateContentStreamOnCloud(apiSettings, model, params, requestOptions) { + if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + params = mapGenerateContentRequest(params); + } + return makeRequest(model, Task.STREAM_GENERATE_CONTENT, apiSettings, + /* stream */ true, JSON.stringify(params), requestOptions); +} +async function generateContentStream(apiSettings, model, params, chromeAdapter, requestOptions) { + const callResult = await callCloudOrDevice(params, chromeAdapter, () => chromeAdapter.generateContentStream(params), () => generateContentStreamOnCloud(apiSettings, model, params, requestOptions)); + return processStream(callResult.response, apiSettings); // TODO: Map streaming responses +} +async function generateContentOnCloud(apiSettings, model, params, requestOptions) { + if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + params = mapGenerateContentRequest(params); + } + return makeRequest(model, Task.GENERATE_CONTENT, apiSettings, + /* stream */ false, JSON.stringify(params), requestOptions); +} +async function generateContent(apiSettings, model, params, chromeAdapter, requestOptions) { + const callResult = await callCloudOrDevice(params, chromeAdapter, () => chromeAdapter.generateContent(params), () => generateContentOnCloud(apiSettings, model, params, requestOptions)); + const generateContentResponse = await processGenerateContentResponse(callResult.response, apiSettings); + const enhancedResponse = createEnhancedContentResponse(generateContentResponse, callResult.inferenceSource); + return { + response: enhancedResponse + }; +} +async function processGenerateContentResponse(response, apiSettings) { + const responseJson = await response.json(); + if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + return mapGenerateContentResponse(responseJson); + } + else { + return responseJson; + } +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +function formatSystemInstruction(input) { + // null or undefined + if (input == null) { + return undefined; + } + else if (typeof input === 'string') { + return { role: 'system', parts: [{ text: input }] }; + } + else if (input.text) { + return { role: 'system', parts: [input] }; + } + else if (input.parts) { + if (!input.role) { + return { role: 'system', parts: input.parts }; + } + else { + return input; + } + } +} +function formatNewContent(request) { + let newParts = []; + if (typeof request === 'string') { + newParts = [{ text: request }]; + } + else { + for (const partOrString of request) { + if (typeof partOrString === 'string') { + newParts.push({ text: partOrString }); + } + else { + newParts.push(partOrString); + } + } + } + return assignRoleToPartsAndValidateSendMessageRequest(newParts); +} +/** + * When multiple Part types (i.e. FunctionResponsePart and TextPart) are + * passed in a single Part array, we may need to assign different roles to each + * part. Currently only FunctionResponsePart requires a role other than 'user'. + * @private + * @param parts Array of parts to pass to the model + * @returns Array of content items + */ +function assignRoleToPartsAndValidateSendMessageRequest(parts) { + const userContent = { role: 'user', parts: [] }; + const functionContent = { role: 'function', parts: [] }; + let hasUserContent = false; + let hasFunctionContent = false; + for (const part of parts) { + if ('functionResponse' in part) { + functionContent.parts.push(part); + hasFunctionContent = true; + } + else { + userContent.parts.push(part); + hasUserContent = true; + } + } + if (hasUserContent && hasFunctionContent) { + 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.'); + } + if (!hasUserContent && !hasFunctionContent) { + throw new AIError(AIErrorCode.INVALID_CONTENT, 'No Content is provided for sending chat message.'); + } + if (hasUserContent) { + return userContent; + } + return functionContent; +} +function formatGenerateContentInput(params) { + let formattedRequest; + if (params.contents) { + formattedRequest = params; + } + else { + // Array or string + const content = formatNewContent(params); + formattedRequest = { contents: [content] }; + } + if (params.systemInstruction) { + formattedRequest.systemInstruction = formatSystemInstruction(params.systemInstruction); + } + return formattedRequest; +} +/** + * Convert the user-defined parameters in {@link ImagenGenerationParams} to the format + * that is expected from the REST API. + * + * @internal + */ +function createPredictRequestBody(prompt, { gcsURI, imageFormat, addWatermark, numberOfImages = 1, negativePrompt, aspectRatio, safetyFilterLevel, personFilterLevel }) { + // Properties that are undefined will be omitted from the JSON string that is sent in the request. + const body = { + instances: [ + { + prompt + } + ], + parameters: { + storageUri: gcsURI, + negativePrompt, + sampleCount: numberOfImages, + aspectRatio, + outputOptions: imageFormat, + addWatermark, + safetyFilterLevel, + personGeneration: personFilterLevel, + includeRaiReason: true, + includeSafetyAttributes: true + } + }; + return body; +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// https://ai.google.dev/api/rest/v1beta/Content#part +const VALID_PART_FIELDS = [ + 'text', + 'inlineData', + 'functionCall', + 'functionResponse', + 'thought', + 'thoughtSignature' +]; +const VALID_PARTS_PER_ROLE = { + user: ['text', 'inlineData'], + function: ['functionResponse'], + model: ['text', 'functionCall', 'thought', 'thoughtSignature'], + // System instructions shouldn't be in history anyway. + system: ['text'] +}; +const VALID_PREVIOUS_CONTENT_ROLES = { + user: ['model'], + function: ['model'], + model: ['user', 'function'], + // System instructions shouldn't be in history. + system: [] +}; +function validateChatHistory(history) { + let prevContent = null; + for (const currContent of history) { + const { role, parts } = currContent; + if (!prevContent && role !== 'user') { + throw new AIError(AIErrorCode.INVALID_CONTENT, `First Content should be with role 'user', got ${role}`); + } + if (!POSSIBLE_ROLES.includes(role)) { + throw new AIError(AIErrorCode.INVALID_CONTENT, `Each item should include role field. Got ${role} but valid roles are: ${JSON.stringify(POSSIBLE_ROLES)}`); + } + if (!Array.isArray(parts)) { + throw new AIError(AIErrorCode.INVALID_CONTENT, `Content should have 'parts' property with an array of Parts`); + } + if (parts.length === 0) { + throw new AIError(AIErrorCode.INVALID_CONTENT, `Each Content should have at least one part`); + } + const countFields = { + text: 0, + inlineData: 0, + functionCall: 0, + functionResponse: 0, + thought: 0, + thoughtSignature: 0, + executableCode: 0, + codeExecutionResult: 0 + }; + for (const part of parts) { + for (const key of VALID_PART_FIELDS) { + if (key in part) { + countFields[key] += 1; + } + } + } + const validParts = VALID_PARTS_PER_ROLE[role]; + for (const key of VALID_PART_FIELDS) { + if (!validParts.includes(key) && countFields[key] > 0) { + throw new AIError(AIErrorCode.INVALID_CONTENT, `Content with role '${role}' can't contain '${key}' part`); + } + } + if (prevContent) { + const validPreviousContentRoles = VALID_PREVIOUS_CONTENT_ROLES[role]; + if (!validPreviousContentRoles.includes(prevContent.role)) { + throw new AIError(AIErrorCode.INVALID_CONTENT, `Content with role '${role}' can't follow '${prevContent.role}'. Valid previous roles: ${JSON.stringify(VALID_PREVIOUS_CONTENT_ROLES)}`); + } + } + prevContent = currContent; + } +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Do not log a message for this error. + */ +const SILENT_ERROR = 'SILENT_ERROR'; +/** + * ChatSession class that enables sending chat messages and stores + * history of sent and received messages so far. + * + * @public + */ +class ChatSession { + constructor(apiSettings, model, chromeAdapter, params, requestOptions) { + this.model = model; + this.chromeAdapter = chromeAdapter; + this.params = params; + this.requestOptions = requestOptions; + this._history = []; + this._sendPromise = Promise.resolve(); + this._apiSettings = apiSettings; + if (params?.history) { + validateChatHistory(params.history); + this._history = params.history; + } + } + /** + * Gets the chat history so far. Blocked prompts are not added to history. + * Neither blocked candidates nor the prompts that generated them are added + * to history. + */ + async getHistory() { + await this._sendPromise; + return this._history; + } + /** + * Sends a chat message and receives a non-streaming + * {@link GenerateContentResult} + */ + async sendMessage(request) { + await this._sendPromise; + const newContent = formatNewContent(request); + const generateContentRequest = { + safetySettings: this.params?.safetySettings, + generationConfig: this.params?.generationConfig, + tools: this.params?.tools, + toolConfig: this.params?.toolConfig, + systemInstruction: this.params?.systemInstruction, + contents: [...this._history, newContent] + }; + let finalResult = {}; + // Add onto the chain. + this._sendPromise = this._sendPromise + .then(() => generateContent(this._apiSettings, this.model, generateContentRequest, this.chromeAdapter, this.requestOptions)) + .then(result => { + if (result.response.candidates && + result.response.candidates.length > 0) { + this._history.push(newContent); + const responseContent = { + parts: result.response.candidates?.[0].content.parts || [], + // Response seems to come back without a role set. + role: result.response.candidates?.[0].content.role || 'model' + }; + this._history.push(responseContent); + } + else { + const blockErrorMessage = formatBlockErrorMessage(result.response); + if (blockErrorMessage) { + logger.warn(`sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`); + } + } + finalResult = result; + }); + await this._sendPromise; + return finalResult; + } + /** + * Sends a chat message and receives the response as a + * {@link GenerateContentStreamResult} containing an iterable stream + * and a response promise. + */ + async sendMessageStream(request) { + await this._sendPromise; + const newContent = formatNewContent(request); + const generateContentRequest = { + safetySettings: this.params?.safetySettings, + generationConfig: this.params?.generationConfig, + tools: this.params?.tools, + toolConfig: this.params?.toolConfig, + systemInstruction: this.params?.systemInstruction, + contents: [...this._history, newContent] + }; + const streamPromise = generateContentStream(this._apiSettings, this.model, generateContentRequest, this.chromeAdapter, this.requestOptions); + // Add onto the chain. + this._sendPromise = this._sendPromise + .then(() => streamPromise) + // This must be handled to avoid unhandled rejection, but jump + // to the final catch block with a label to not log this error. + .catch(_ignored => { + throw new Error(SILENT_ERROR); + }) + .then(streamResult => streamResult.response) + .then(response => { + if (response.candidates && response.candidates.length > 0) { + this._history.push(newContent); + const responseContent = { ...response.candidates[0].content }; + // Response seems to come back without a role set. + if (!responseContent.role) { + responseContent.role = 'model'; + } + this._history.push(responseContent); + } + else { + const blockErrorMessage = formatBlockErrorMessage(response); + if (blockErrorMessage) { + logger.warn(`sendMessageStream() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`); + } + } + }) + .catch(e => { + // Errors in streamPromise are already catchable by the user as + // streamPromise is returned. + // Avoid duplicating the error message in logs. + if (e.message !== SILENT_ERROR) { + // Users do not have access to _sendPromise to catch errors + // downstream from streamPromise, so they should not throw. + logger.error(e); + } + }); + return streamPromise; + } +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +async function countTokensOnCloud(apiSettings, model, params, requestOptions) { + let body = ''; + if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + const mappedParams = mapCountTokensRequest(params, model); + body = JSON.stringify(mappedParams); + } + else { + body = JSON.stringify(params); + } + const response = await makeRequest(model, Task.COUNT_TOKENS, apiSettings, false, body, requestOptions); + return response.json(); +} +async function countTokens(apiSettings, model, params, chromeAdapter, requestOptions) { + if (chromeAdapter?.mode === InferenceMode.ONLY_ON_DEVICE) { + throw new AIError(AIErrorCode.UNSUPPORTED, 'countTokens() is not supported for on-device models.'); + } + return countTokensOnCloud(apiSettings, model, params, requestOptions); +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Class for generative model APIs. + * @public + */ +class GenerativeModel extends AIModel { + constructor(ai, modelParams, requestOptions, chromeAdapter) { + super(ai, modelParams.model); + this.chromeAdapter = chromeAdapter; + this.generationConfig = modelParams.generationConfig || {}; + this.safetySettings = modelParams.safetySettings || []; + this.tools = modelParams.tools; + this.toolConfig = modelParams.toolConfig; + this.systemInstruction = formatSystemInstruction(modelParams.systemInstruction); + this.requestOptions = requestOptions || {}; + } + /** + * Makes a single non-streaming call to the model + * and returns an object containing a single {@link GenerateContentResponse}. + */ + async generateContent(request) { + const formattedParams = formatGenerateContentInput(request); + return generateContent(this._apiSettings, this.model, { + generationConfig: this.generationConfig, + safetySettings: this.safetySettings, + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction, + ...formattedParams + }, this.chromeAdapter, this.requestOptions); + } + /** + * Makes a single streaming call to the model + * and returns an object containing an iterable stream that iterates + * over all chunks in the streaming response as well as + * a promise that returns the final aggregated response. + */ + async generateContentStream(request) { + const formattedParams = formatGenerateContentInput(request); + return generateContentStream(this._apiSettings, this.model, { + generationConfig: this.generationConfig, + safetySettings: this.safetySettings, + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction, + ...formattedParams + }, this.chromeAdapter, this.requestOptions); + } + /** + * Gets a new {@link ChatSession} instance which can be used for + * multi-turn chats. + */ + startChat(startChatParams) { + return new ChatSession(this._apiSettings, this.model, this.chromeAdapter, { + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction, + generationConfig: this.generationConfig, + safetySettings: this.safetySettings, + /** + * Overrides params inherited from GenerativeModel with those explicitly set in the + * StartChatParams. For example, if startChatParams.generationConfig is set, it'll override + * this.generationConfig. + */ + ...startChatParams + }, this.requestOptions); + } + /** + * Counts the tokens in the provided request. + */ + async countTokens(request) { + const formattedParams = formatGenerateContentInput(request); + return countTokens(this._apiSettings, this.model, formattedParams, this.chromeAdapter); + } +} + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Represents an active, real-time, bidirectional conversation with the model. + * + * This class should only be instantiated by calling {@link LiveGenerativeModel.connect}. + * + * @beta + */ +class LiveSession { + /** + * @internal + */ + constructor(webSocketHandler, serverMessages) { + this.webSocketHandler = webSocketHandler; + this.serverMessages = serverMessages; + /** + * Indicates whether this Live session is closed. + * + * @beta + */ + this.isClosed = false; + /** + * Indicates whether this Live session is being controlled by an `AudioConversationController`. + * + * @beta + */ + this.inConversation = false; + } + /** + * Sends content to the server. + * + * @param request - The message to send to the model. + * @param turnComplete - Indicates if the turn is complete. Defaults to false. + * @throws If this session has been closed. + * + * @beta + */ + async send(request, turnComplete = true) { + if (this.isClosed) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.'); + } + const newContent = formatNewContent(request); + const message = { + clientContent: { + turns: [newContent], + turnComplete + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + /** + * Sends text to the server in realtime. + * + * @example + * ```javascript + * liveSession.sendTextRealtime("Hello, how are you?"); + * ``` + * + * @param text - The text data to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendTextRealtime(text) { + if (this.isClosed) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.'); + } + const message = { + realtimeInput: { + text + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + /** + * Sends audio data to the server in realtime. + * + * @remarks The server requires that the audio data is base64-encoded 16-bit PCM at 16kHz + * little-endian. + * + * @example + * ```javascript + * // const pcmData = ... base64-encoded 16-bit PCM at 16kHz little-endian. + * const blob = { mimeType: "audio/pcm", data: pcmData }; + * liveSession.sendAudioRealtime(blob); + * ``` + * + * @param blob - The base64-encoded PCM data to send to the server in realtime. + * @throws If this session has been closed. + * + * @beta + */ + async sendAudioRealtime(blob) { + if (this.isClosed) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.'); + } + const message = { + realtimeInput: { + audio: blob + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + /** + * Sends video data to the server in realtime. + * + * @remarks The server requires that the video is sent as individual video frames at 1 FPS. It + * is recommended to set `mimeType` to `image/jpeg`. + * + * @example + * ```javascript + * // const videoFrame = ... base64-encoded JPEG data + * const blob = { mimeType: "image/jpeg", data: videoFrame }; + * liveSession.sendVideoRealtime(blob); + * ``` + * @param blob - The base64-encoded video data to send to the server in realtime. + * @throws If this session has been closed. + * + * @beta + */ + async sendVideoRealtime(blob) { + if (this.isClosed) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.'); + } + const message = { + realtimeInput: { + video: blob + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + /** + * Sends function responses to the server. + * + * @param functionResponses - The function responses to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendFunctionResponses(functionResponses) { + if (this.isClosed) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.'); + } + const message = { + toolResponse: { + functionResponses + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + /** + * Yields messages received from the server. + * This can only be used by one consumer at a time. + * + * @returns An `AsyncGenerator` that yields server messages as they arrive. + * @throws If the session is already closed, or if we receive a response that we don't support. + * + * @beta + */ + async *receive() { + if (this.isClosed) { + throw new AIError(AIErrorCode.SESSION_CLOSED, 'Cannot read from a Live session that is closed. Try starting a new Live session.'); + } + for await (const message of this.serverMessages) { + if (message && typeof message === 'object') { + if (LiveResponseType.SERVER_CONTENT in message) { + yield { + type: 'serverContent', + ...message + .serverContent + }; + } + else if (LiveResponseType.TOOL_CALL in message) { + yield { + type: 'toolCall', + ...message + .toolCall + }; + } + else if (LiveResponseType.TOOL_CALL_CANCELLATION in message) { + yield { + type: 'toolCallCancellation', + ...message.toolCallCancellation + }; + } + else { + logger.warn(`Received an unknown message type from the server: ${JSON.stringify(message)}`); + } + } + else { + logger.warn(`Received an invalid message from the server: ${JSON.stringify(message)}`); + } + } + } + /** + * Closes this session. + * All methods on this session will throw an error once this resolves. + * + * @beta + */ + async close() { + if (!this.isClosed) { + this.isClosed = true; + await this.webSocketHandler.close(1000, 'Client closed session.'); + } + } + /** + * Sends realtime input to the server. + * + * @deprecated Use `sendTextRealtime()`, `sendAudioRealtime()`, and `sendVideoRealtime()` instead. + * + * @param mediaChunks - The media chunks to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendMediaChunks(mediaChunks) { + if (this.isClosed) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.'); + } + // The backend does not support sending more than one mediaChunk in one message. + // Work around this limitation by sending mediaChunks in separate messages. + mediaChunks.forEach(mediaChunk => { + const message = { + realtimeInput: { mediaChunks: [mediaChunk] } + }; + this.webSocketHandler.send(JSON.stringify(message)); + }); + } + /** + * @deprecated Use `sendTextRealtime()`, `sendAudioRealtime()`, and `sendVideoRealtime()` instead. + * + * Sends a stream of {@link GenerativeContentBlob}. + * + * @param mediaChunkStream - The stream of {@link GenerativeContentBlob} to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendMediaStream(mediaChunkStream) { + if (this.isClosed) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'This LiveSession has been closed and cannot be used.'); + } + const reader = mediaChunkStream.getReader(); + while (true) { + try { + const { done, value } = await reader.read(); + if (done) { + break; + } + else if (!value) { + throw new Error('Missing chunk in reader, but reader is not done.'); + } + await this.sendMediaChunks([value]); + } + catch (e) { + // Re-throw any errors that occur during stream consumption or sending. + const message = e instanceof Error ? e.message : 'Error processing media stream.'; + throw new AIError(AIErrorCode.REQUEST_ERROR, message); + } + } + } +} + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Class for Live generative model APIs. The Live API enables low-latency, two-way multimodal + * interactions with Gemini. + * + * This class should only be instantiated with {@link getLiveGenerativeModel}. + * + * @beta + */ +class LiveGenerativeModel extends AIModel { + /** + * @internal + */ + constructor(ai, modelParams, + /** + * @internal + */ + _webSocketHandler) { + super(ai, modelParams.model); + this._webSocketHandler = _webSocketHandler; + this.generationConfig = modelParams.generationConfig || {}; + this.tools = modelParams.tools; + this.toolConfig = modelParams.toolConfig; + this.systemInstruction = formatSystemInstruction(modelParams.systemInstruction); + } + /** + * Starts a {@link LiveSession}. + * + * @returns A {@link LiveSession}. + * @throws If the connection failed to be established with the server. + * + * @beta + */ + async connect() { + const url = new WebSocketUrl(this._apiSettings); + await this._webSocketHandler.connect(url.toString()); + let fullModelPath; + if (this._apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + fullModelPath = `projects/${this._apiSettings.project}/${this.model}`; + } + else { + fullModelPath = `projects/${this._apiSettings.project}/locations/${this._apiSettings.location}/${this.model}`; + } + // inputAudioTranscription and outputAudioTranscription are on the generation config in the public API, + // but the backend expects them to be in the `setup` message. + const { inputAudioTranscription, outputAudioTranscription, ...generationConfig } = this.generationConfig; + const setupMessage = { + setup: { + model: fullModelPath, + generationConfig, + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction, + inputAudioTranscription, + outputAudioTranscription + } + }; + try { + // Begin listening for server messages, and begin the handshake by sending the 'setupMessage' + const serverMessages = this._webSocketHandler.listen(); + this._webSocketHandler.send(JSON.stringify(setupMessage)); + // Verify we received the handshake response 'setupComplete' + const firstMessage = (await serverMessages.next()).value; + if (!firstMessage || + !(typeof firstMessage === 'object') || + !('setupComplete' in firstMessage)) { + await this._webSocketHandler.close(1011, 'Handshake failure'); + throw new AIError(AIErrorCode.RESPONSE_ERROR, 'Server connection handshake failed. The server did not respond with a setupComplete message.'); + } + return new LiveSession(this._webSocketHandler, serverMessages); + } + catch (e) { + // Ensure connection is closed on any setup error + await this._webSocketHandler.close(); + throw e; + } + } +} + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Class for Imagen model APIs. + * + * This class provides methods for generating images using the Imagen model. + * + * @example + * ```javascript + * const imagen = new ImagenModel( + * ai, + * { + * model: 'imagen-3.0-generate-002' + * } + * ); + * + * const response = await imagen.generateImages('A photo of a cat'); + * if (response.images.length > 0) { + * console.log(response.images[0].bytesBase64Encoded); + * } + * ``` + * + * @public + */ +class ImagenModel extends AIModel { + /** + * Constructs a new instance of the {@link ImagenModel} class. + * + * @param ai - an {@link AI} instance. + * @param modelParams - Parameters to use when making requests to Imagen. + * @param requestOptions - Additional options to use when making requests. + * + * @throws If the `apiKey` or `projectId` fields are missing in your + * Firebase config. + */ + constructor(ai, modelParams, requestOptions) { + const { model, generationConfig, safetySettings } = modelParams; + super(ai, model); + this.requestOptions = requestOptions; + this.generationConfig = generationConfig; + this.safetySettings = safetySettings; + } + /** + * Generates images using the Imagen model and returns them as + * base64-encoded strings. + * + * @param prompt - A text prompt describing the image(s) to generate. + * @returns A promise that resolves to an {@link ImagenGenerationResponse} + * object containing the generated images. + * + * @throws If the request to generate images fails. This happens if the + * prompt is blocked. + * + * @remarks + * If the prompt was not blocked, but one or more of the generated images were filtered, the + * returned object will have a `filteredReason` property. + * If all images are filtered, the `images` array will be empty. + * + * @public + */ + async generateImages(prompt) { + const body = createPredictRequestBody(prompt, { + ...this.generationConfig, + ...this.safetySettings + }); + const response = await makeRequest(this.model, Task.PREDICT, this._apiSettings, + /* stream */ false, JSON.stringify(body), this.requestOptions); + return handlePredictResponse(response); + } + /** + * Generates images to Cloud Storage for Firebase using the Imagen model. + * + * @internal This method is temporarily internal. + * + * @param prompt - A text prompt describing the image(s) to generate. + * @param gcsURI - The URI of file stored in a Cloud Storage for Firebase bucket. + * This should be a directory. For example, `gs://my-bucket/my-directory/`. + * @returns A promise that resolves to an {@link ImagenGenerationResponse} + * object containing the URLs of the generated images. + * + * @throws If the request fails to generate images fails. This happens if + * the prompt is blocked. + * + * @remarks + * If the prompt was not blocked, but one or more of the generated images were filtered, the + * returned object will have a `filteredReason` property. + * If all images are filtered, the `images` array will be empty. + */ + async generateImagesGCS(prompt, gcsURI) { + const body = createPredictRequestBody(prompt, { + gcsURI, + ...this.generationConfig, + ...this.safetySettings + }); + const response = await makeRequest(this.model, Task.PREDICT, this._apiSettings, + /* stream */ false, JSON.stringify(body), this.requestOptions); + return handlePredictResponse(response); + } +} + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * A wrapper for the native `WebSocket` available in both Browsers and Node >= 22. + * + * @internal + */ +class WebSocketHandlerImpl { + constructor() { + if (typeof WebSocket === 'undefined') { + throw new AIError(AIErrorCode.UNSUPPORTED, 'The WebSocket API is not available in this environment. ' + + 'The "Live" feature is not supported here. It is supported in ' + + 'modern browser windows, Web Workers with WebSocket support, and Node >= 22.'); + } + } + connect(url) { + return new Promise((resolve, reject) => { + this.ws = new WebSocket(url); + this.ws.binaryType = 'blob'; // Only important to set in Node + this.ws.addEventListener('open', () => resolve(), { once: true }); + this.ws.addEventListener('error', () => reject(new AIError(AIErrorCode.FETCH_ERROR, `Error event raised on WebSocket`)), { once: true }); + this.ws.addEventListener('close', (closeEvent) => { + if (closeEvent.reason) { + logger.warn(`WebSocket connection closed by server. Reason: '${closeEvent.reason}'`); + } + }); + }); + } + send(data) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'WebSocket is not open.'); + } + this.ws.send(data); + } + async *listen() { + if (!this.ws) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'WebSocket is not connected.'); + } + const messageQueue = []; + const errorQueue = []; + let resolvePromise = null; + let isClosed = false; + const messageListener = async (event) => { + let data; + if (event.data instanceof Blob) { + data = await event.data.text(); + } + else if (typeof event.data === 'string') { + data = event.data; + } + else { + 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}.`)); + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + return; + } + try { + const obj = JSON.parse(data); + messageQueue.push(obj); + } + catch (e) { + const err = e; + errorQueue.push(new AIError(AIErrorCode.PARSE_FAILED, `Error parsing WebSocket message to JSON: ${err.message}`)); + } + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + }; + const errorListener = () => { + errorQueue.push(new AIError(AIErrorCode.FETCH_ERROR, 'WebSocket connection error.')); + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + }; + const closeListener = (event) => { + if (event.reason) { + logger.warn(`WebSocket connection closed by the server with reason: ${event.reason}`); + } + isClosed = true; + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + // Clean up listeners to prevent memory leaks + this.ws?.removeEventListener('message', messageListener); + this.ws?.removeEventListener('close', closeListener); + this.ws?.removeEventListener('error', errorListener); + }; + this.ws.addEventListener('message', messageListener); + this.ws.addEventListener('close', closeListener); + this.ws.addEventListener('error', errorListener); + while (!isClosed) { + if (errorQueue.length > 0) { + const error = errorQueue.shift(); + throw error; + } + if (messageQueue.length > 0) { + yield messageQueue.shift(); + } + else { + await new Promise(resolve => { + resolvePromise = resolve; + }); + } + } + // If the loop terminated because isClosed is true, check for any final errors + if (errorQueue.length > 0) { + const error = errorQueue.shift(); + throw error; + } + } + close(code, reason) { + return new Promise(resolve => { + if (!this.ws) { + return resolve(); + } + this.ws.addEventListener('close', () => resolve(), { once: true }); + // Calling 'close' during these states results in an error. + if (this.ws.readyState === WebSocket.CLOSED || + this.ws.readyState === WebSocket.CONNECTING) { + return resolve(); + } + if (this.ws.readyState !== WebSocket.CLOSING) { + this.ws.close(code, reason); + } + }); + } +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Parent class encompassing all Schema types, with static methods that + * allow building specific Schema types. This class can be converted with + * `JSON.stringify()` into a JSON string accepted by Vertex AI REST endpoints. + * (This string conversion is automatically done when calling SDK methods.) + * @public + */ +class Schema { + constructor(schemaParams) { + // TODO(dlarocque): Enforce this with union types + if (!schemaParams.type && !schemaParams.anyOf) { + throw new AIError(AIErrorCode.INVALID_SCHEMA, "A schema must have either a 'type' or an 'anyOf' array of sub-schemas."); + } + // eslint-disable-next-line guard-for-in + for (const paramKey in schemaParams) { + this[paramKey] = schemaParams[paramKey]; + } + // Ensure these are explicitly set to avoid TS errors. + this.type = schemaParams.type; + this.format = schemaParams.hasOwnProperty('format') + ? schemaParams.format + : undefined; + this.nullable = schemaParams.hasOwnProperty('nullable') + ? !!schemaParams.nullable + : false; + } + /** + * Defines how this Schema should be serialized as JSON. + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior + * @internal + */ + toJSON() { + const obj = { + type: this.type + }; + for (const prop in this) { + if (this.hasOwnProperty(prop) && this[prop] !== undefined) { + if (prop !== 'required' || this.type === SchemaType.OBJECT) { + obj[prop] = this[prop]; + } + } + } + return obj; + } + static array(arrayParams) { + return new ArraySchema(arrayParams, arrayParams.items); + } + static object(objectParams) { + return new ObjectSchema(objectParams, objectParams.properties, objectParams.optionalProperties); + } + // eslint-disable-next-line id-blacklist + static string(stringParams) { + return new StringSchema(stringParams); + } + static enumString(stringParams) { + return new StringSchema(stringParams, stringParams.enum); + } + static integer(integerParams) { + return new IntegerSchema(integerParams); + } + // eslint-disable-next-line id-blacklist + static number(numberParams) { + return new NumberSchema(numberParams); + } + // eslint-disable-next-line id-blacklist + static boolean(booleanParams) { + return new BooleanSchema(booleanParams); + } + static anyOf(anyOfParams) { + return new AnyOfSchema(anyOfParams); + } +} +/** + * Schema class for "integer" types. + * @public + */ +class IntegerSchema extends Schema { + constructor(schemaParams) { + super({ + type: SchemaType.INTEGER, + ...schemaParams + }); + } +} +/** + * Schema class for "number" types. + * @public + */ +class NumberSchema extends Schema { + constructor(schemaParams) { + super({ + type: SchemaType.NUMBER, + ...schemaParams + }); + } +} +/** + * Schema class for "boolean" types. + * @public + */ +class BooleanSchema extends Schema { + constructor(schemaParams) { + super({ + type: SchemaType.BOOLEAN, + ...schemaParams + }); + } +} +/** + * Schema class for "string" types. Can be used with or without + * enum values. + * @public + */ +class StringSchema extends Schema { + constructor(schemaParams, enumValues) { + super({ + type: SchemaType.STRING, + ...schemaParams + }); + this.enum = enumValues; + } + /** + * @internal + */ + toJSON() { + const obj = super.toJSON(); + if (this.enum) { + obj['enum'] = this.enum; + } + return obj; + } +} +/** + * Schema class for "array" types. + * The `items` param should refer to the type of item that can be a member + * of the array. + * @public + */ +class ArraySchema extends Schema { + constructor(schemaParams, items) { + super({ + type: SchemaType.ARRAY, + ...schemaParams + }); + this.items = items; + } + /** + * @internal + */ + toJSON() { + const obj = super.toJSON(); + obj.items = this.items.toJSON(); + return obj; + } +} +/** + * Schema class for "object" types. + * The `properties` param must be a map of `Schema` objects. + * @public + */ +class ObjectSchema extends Schema { + constructor(schemaParams, properties, optionalProperties = []) { + super({ + type: SchemaType.OBJECT, + ...schemaParams + }); + this.properties = properties; + this.optionalProperties = optionalProperties; + } + /** + * @internal + */ + toJSON() { + const obj = super.toJSON(); + obj.properties = { ...this.properties }; + const required = []; + if (this.optionalProperties) { + for (const propertyKey of this.optionalProperties) { + if (!this.properties.hasOwnProperty(propertyKey)) { + throw new AIError(AIErrorCode.INVALID_SCHEMA, `Property "${propertyKey}" specified in "optionalProperties" does not exist.`); + } + } + } + for (const propertyKey in this.properties) { + if (this.properties.hasOwnProperty(propertyKey)) { + obj.properties[propertyKey] = this.properties[propertyKey].toJSON(); + if (!this.optionalProperties.includes(propertyKey)) { + required.push(propertyKey); + } + } + } + if (required.length > 0) { + obj.required = required; + } + delete obj.optionalProperties; + return obj; + } +} +/** + * Schema class representing a value that can conform to any of the provided sub-schemas. This is + * useful when a field can accept multiple distinct types or structures. + * @public + */ +class AnyOfSchema extends Schema { + constructor(schemaParams) { + if (schemaParams.anyOf.length === 0) { + throw new AIError(AIErrorCode.INVALID_SCHEMA, "The 'anyOf' array must not be empty."); + } + super({ + ...schemaParams, + type: undefined // anyOf schemas do not have an explicit type + }); + this.anyOf = schemaParams.anyOf; + } + /** + * @internal + */ + toJSON() { + const obj = super.toJSON(); + // Ensure the 'anyOf' property contains serialized SchemaRequest objects. + if (this.anyOf && Array.isArray(this.anyOf)) { + obj.anyOf = this.anyOf.map(s => s.toJSON()); + } + return obj; + } +} + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Defines the image format for images generated by Imagen. + * + * Use this class to specify the desired format (JPEG or PNG) and compression quality + * for images generated by Imagen. This is typically included as part of + * {@link ImagenModelParams}. + * + * @example + * ```javascript + * const imagenModelParams = { + * // ... other ImagenModelParams + * imageFormat: ImagenImageFormat.jpeg(75) // JPEG with a compression level of 75. + * } + * ``` + * + * @public + */ +class ImagenImageFormat { + constructor() { + this.mimeType = 'image/png'; + } + /** + * Creates an {@link ImagenImageFormat} for a JPEG image. + * + * @param compressionQuality - The level of compression (a number between 0 and 100). + * @returns An {@link ImagenImageFormat} object for a JPEG image. + * + * @public + */ + static jpeg(compressionQuality) { + if (compressionQuality && + (compressionQuality < 0 || compressionQuality > 100)) { + logger.warn(`Invalid JPEG compression quality of ${compressionQuality} specified; the supported range is [0, 100].`); + } + return { mimeType: 'image/jpeg', compressionQuality }; + } + /** + * Creates an {@link ImagenImageFormat} for a PNG image. + * + * @returns An {@link ImagenImageFormat} object for a PNG image. + * + * @public + */ + static png() { + return { mimeType: 'image/png' }; + } +} + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const SERVER_INPUT_SAMPLE_RATE = 16000; +const SERVER_OUTPUT_SAMPLE_RATE = 24000; +const AUDIO_PROCESSOR_NAME = 'audio-processor'; +/** + * The JS for an `AudioWorkletProcessor`. + * This processor is responsible for taking raw audio from the microphone, + * converting it to the required 16-bit 16kHz PCM, and posting it back to the main thread. + * + * See: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor + * + * It is defined as a string here so that it can be converted into a `Blob` + * and loaded at runtime. + */ +const audioProcessorWorkletString = ` + class AudioProcessor extends AudioWorkletProcessor { + constructor(options) { + super(); + this.targetSampleRate = options.processorOptions.targetSampleRate; + // 'sampleRate' is a global variable available inside the AudioWorkletGlobalScope, + // representing the native sample rate of the AudioContext. + this.inputSampleRate = sampleRate; + } + + /** + * This method is called by the browser's audio engine for each block of audio data. + * Input is a single input, with a single channel (input[0][0]). + */ + process(inputs) { + const input = inputs[0]; + if (input && input.length > 0 && input[0].length > 0) { + const pcmData = input[0]; // Float32Array of raw audio samples. + + // Simple linear interpolation for resampling. + const resampled = new Float32Array(Math.round(pcmData.length * this.targetSampleRate / this.inputSampleRate)); + const ratio = pcmData.length / resampled.length; + for (let i = 0; i < resampled.length; i++) { + resampled[i] = pcmData[Math.floor(i * ratio)]; + } + + // Convert Float32 (-1, 1) samples to Int16 (-32768, 32767) + const resampledInt16 = new Int16Array(resampled.length); + for (let i = 0; i < resampled.length; i++) { + const sample = Math.max(-1, Math.min(1, resampled[i])); + if (sample < 0) { + resampledInt16[i] = sample * 32768; + } else { + resampledInt16[i] = sample * 32767; + } + } + + this.port.postMessage(resampledInt16); + } + // Return true to keep the processor alive and processing the next audio block. + return true; + } + } + + // Register the processor with a name that can be used to instantiate it from the main thread. + registerProcessor('${AUDIO_PROCESSOR_NAME}', AudioProcessor); +`; +/** + * Encapsulates the core logic of an audio conversation. + * + * @internal + */ +class AudioConversationRunner { + constructor(liveSession, options, deps) { + this.liveSession = liveSession; + this.options = options; + this.deps = deps; + /** A flag to indicate if the conversation has been stopped. */ + this.isStopped = false; + /** A deferred that contains a promise that is resolved when stop() is called, to unblock the receive loop. */ + this.stopDeferred = new util.Deferred(); + /** A FIFO queue of 24kHz, 16-bit PCM audio chunks received from the server. */ + this.playbackQueue = []; + /** Tracks scheduled audio sources. Used to cancel scheduled audio when the model is interrupted. */ + this.scheduledSources = []; + /** A high-precision timeline pointer for scheduling gapless audio playback. */ + this.nextStartTime = 0; + /** A mutex to prevent the playback processing loop from running multiple times concurrently. */ + this.isPlaybackLoopRunning = false; + this.liveSession.inConversation = true; + // Start listening for messages from the server. + this.receiveLoopPromise = this.runReceiveLoop().finally(() => this.cleanup()); + // Set up the handler for receiving processed audio data from the worklet. + // Message data has been resampled to 16kHz 16-bit PCM. + this.deps.workletNode.port.onmessage = event => { + if (this.isStopped) { + return; + } + const pcm16 = event.data; + const base64 = btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(pcm16.buffer)))); + const chunk = { + mimeType: 'audio/pcm', + data: base64 + }; + void this.liveSession.sendAudioRealtime(chunk); + }; + } + /** + * Stops the conversation and unblocks the main receive loop. + */ + async stop() { + if (this.isStopped) { + return; + } + this.isStopped = true; + this.stopDeferred.resolve(); // Unblock the receive loop + await this.receiveLoopPromise; // Wait for the loop and cleanup to finish + } + /** + * Cleans up all audio resources (nodes, stream tracks, context) and marks the + * session as no longer in a conversation. + */ + cleanup() { + this.interruptPlayback(); // Ensure all audio is stopped on final cleanup. + this.deps.workletNode.port.onmessage = null; + this.deps.workletNode.disconnect(); + this.deps.sourceNode.disconnect(); + this.deps.mediaStream.getTracks().forEach(track => track.stop()); + if (this.deps.audioContext.state !== 'closed') { + void this.deps.audioContext.close(); + } + this.liveSession.inConversation = false; + } + /** + * Adds audio data to the queue and ensures the playback loop is running. + */ + enqueueAndPlay(audioData) { + this.playbackQueue.push(audioData); + // Will no-op if it's already running. + void this.processPlaybackQueue(); + } + /** + * Stops all current and pending audio playback and clears the queue. This is + * called when the server indicates the model's speech was interrupted with + * `LiveServerContent.modelTurn.interrupted`. + */ + interruptPlayback() { + // Stop all sources that have been scheduled. The onended event will fire for each, + // which will clean up the scheduledSources array. + [...this.scheduledSources].forEach(source => source.stop(0)); + // Clear the internal buffer of unprocessed audio chunks. + this.playbackQueue.length = 0; + // Reset the playback clock to start fresh. + this.nextStartTime = this.deps.audioContext.currentTime; + } + /** + * Processes the playback queue in a loop, scheduling each chunk in a gapless sequence. + */ + async processPlaybackQueue() { + if (this.isPlaybackLoopRunning) { + return; + } + this.isPlaybackLoopRunning = true; + while (this.playbackQueue.length > 0 && !this.isStopped) { + const pcmRawBuffer = this.playbackQueue.shift(); + try { + const pcm16 = new Int16Array(pcmRawBuffer); + const frameCount = pcm16.length; + const audioBuffer = this.deps.audioContext.createBuffer(1, frameCount, SERVER_OUTPUT_SAMPLE_RATE); + // Convert 16-bit PCM to 32-bit PCM, required by the Web Audio API. + const channelData = audioBuffer.getChannelData(0); + for (let i = 0; i < frameCount; i++) { + channelData[i] = pcm16[i] / 32768; // Normalize to Float32 range [-1.0, 1.0] + } + const source = this.deps.audioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(this.deps.audioContext.destination); + // Track the source and set up a handler to remove it from tracking when it finishes. + this.scheduledSources.push(source); + source.onended = () => { + this.scheduledSources = this.scheduledSources.filter(s => s !== source); + }; + // To prevent gaps, schedule the next chunk to start either now (if we're catching up) + // or exactly when the previous chunk is scheduled to end. + this.nextStartTime = Math.max(this.deps.audioContext.currentTime, this.nextStartTime); + source.start(this.nextStartTime); + // Update the schedule for the *next* chunk. + this.nextStartTime += audioBuffer.duration; + } + catch (e) { + logger.error('Error playing audio:', e); + } + } + this.isPlaybackLoopRunning = false; + } + /** + * The main loop that listens for and processes messages from the server. + */ + async runReceiveLoop() { + const messageGenerator = this.liveSession.receive(); + while (!this.isStopped) { + const result = await Promise.race([ + messageGenerator.next(), + this.stopDeferred.promise + ]); + if (this.isStopped || !result || result.done) { + break; + } + const message = result.value; + if (message.type === 'serverContent') { + const serverContent = message; + if (serverContent.interrupted) { + this.interruptPlayback(); + } + const audioPart = serverContent.modelTurn?.parts.find(part => part.inlineData?.mimeType.startsWith('audio/')); + if (audioPart?.inlineData) { + const audioData = Uint8Array.from(atob(audioPart.inlineData.data), c => c.charCodeAt(0)).buffer; + this.enqueueAndPlay(audioData); + } + } + else if (message.type === 'toolCall') { + if (!this.options.functionCallingHandler) { + logger.warn('Received tool call message, but StartAudioConversationOptions.functionCallingHandler is undefined. Ignoring tool call.'); + } + else { + try { + const functionResponse = await this.options.functionCallingHandler(message.functionCalls); + if (!this.isStopped) { + void this.liveSession.sendFunctionResponses([functionResponse]); + } + } + catch (e) { + throw new AIError(AIErrorCode.ERROR, `Function calling handler failed: ${e.message}`); + } + } + } + } + } +} +/** + * Starts a real-time, bidirectional audio conversation with the model. This helper function manages + * the complexities of microphone access, audio recording, playback, and interruptions. + * + * @remarks Important: This function must be called in response to a user gesture + * (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}. + * + * @example + * ```javascript + * const liveSession = await model.connect(); + * let conversationController; + * + * // This function must be called from within a click handler. + * async function startConversation() { + * try { + * conversationController = await startAudioConversation(liveSession); + * } catch (e) { + * // Handle AI-specific errors + * if (e instanceof AIError) { + * console.error("AI Error:", e.message); + * } + * // Handle microphone permission and hardware errors + * else if (e instanceof DOMException) { + * console.error("Microphone Error:", e.message); + * } + * // Handle other unexpected errors + * else { + * console.error("An unexpected error occurred:", e); + * } + * } + * } + * + * // Later, to stop the conversation: + * // if (conversationController) { + * // await conversationController.stop(); + * // } + * ``` + * + * @param liveSession - An active {@link LiveSession} instance. + * @param options - Configuration options for the audio conversation. + * @returns A `Promise` that resolves with an {@link AudioConversationController}. + * @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`). + * @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. + * + * @beta + */ +async function startAudioConversation(liveSession, options = {}) { + if (liveSession.isClosed) { + throw new AIError(AIErrorCode.SESSION_CLOSED, 'Cannot start audio conversation on a closed LiveSession.'); + } + if (liveSession.inConversation) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'An audio conversation is already in progress for this session.'); + } + // Check for necessary Web API support. + if (typeof AudioWorkletNode === 'undefined' || + typeof AudioContext === 'undefined' || + typeof navigator === 'undefined' || + !navigator.mediaDevices) { + throw new AIError(AIErrorCode.UNSUPPORTED, 'Audio conversation is not supported in this environment. It requires the Web Audio API and AudioWorklet support.'); + } + let audioContext; + try { + // 1. Set up the audio context. This must be in response to a user gesture. + // See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy + audioContext = new AudioContext(); + if (audioContext.state === 'suspended') { + await audioContext.resume(); + } + // 2. Prompt for microphone access and get the media stream. + // This can throw a variety of permission or hardware-related errors. + const mediaStream = await navigator.mediaDevices.getUserMedia({ + audio: true + }); + // 3. Load the AudioWorklet processor. + // See: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet + const workletBlob = new Blob([audioProcessorWorkletString], { + type: 'application/javascript' + }); + const workletURL = URL.createObjectURL(workletBlob); + await audioContext.audioWorklet.addModule(workletURL); + // 4. Create the audio graph: Microphone -> Source Node -> Worklet Node + const sourceNode = audioContext.createMediaStreamSource(mediaStream); + const workletNode = new AudioWorkletNode(audioContext, AUDIO_PROCESSOR_NAME, { + processorOptions: { targetSampleRate: SERVER_INPUT_SAMPLE_RATE } + }); + sourceNode.connect(workletNode); + // 5. Instantiate and return the runner which manages the conversation. + const runner = new AudioConversationRunner(liveSession, options, { + audioContext, + mediaStream, + sourceNode, + workletNode + }); + return { stop: () => runner.stop() }; + } + catch (e) { + // Ensure the audio context is closed on any setup error. + if (audioContext && audioContext.state !== 'closed') { + void audioContext.close(); + } + // Re-throw specific, known error types directly. The user may want to handle `DOMException` + // errors differently (for example, if permission to access audio device was denied). + if (e instanceof AIError || e instanceof DOMException) { + throw e; + } + // Wrap any other unexpected errors in a standard AIError. + throw new AIError(AIErrorCode.ERROR, `Failed to initialize audio recording: ${e.message}`); + } +} + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Returns the default {@link AI} instance that is associated with the provided + * {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new instance with the + * default settings. + * + * @example + * ```javascript + * const ai = getAI(app); + * ``` + * + * @example + * ```javascript + * // Get an AI instance configured to use the Gemini Developer API (via Google AI). + * const ai = getAI(app, { backend: new GoogleAIBackend() }); + * ``` + * + * @example + * ```javascript + * // Get an AI instance configured to use the Vertex AI Gemini API. + * const ai = getAI(app, { backend: new VertexAIBackend() }); + * ``` + * + * @param app - The {@link @firebase/app#FirebaseApp} to use. + * @param options - {@link AIOptions} that configure the AI instance. + * @returns The default {@link AI} instance for the given {@link @firebase/app#FirebaseApp}. + * + * @public + */ +function getAI(app$1 = app.getApp(), options) { + app$1 = util.getModularInstance(app$1); + // Dependencies + const AIProvider = app._getProvider(app$1, AI_TYPE); + const backend = options?.backend ?? new GoogleAIBackend(); + const finalOptions = { + useLimitedUseAppCheckTokens: options?.useLimitedUseAppCheckTokens ?? false + }; + const identifier = encodeInstanceIdentifier(backend); + const aiInstance = AIProvider.getImmediate({ + identifier + }); + aiInstance.options = finalOptions; + return aiInstance; +} +/** + * Returns a {@link GenerativeModel} class with methods for inference + * and other functionality. + * + * @public + */ +function getGenerativeModel(ai, modelParams, requestOptions) { + // Uses the existence of HybridParams.mode to clarify the type of the modelParams input. + const hybridParams = modelParams; + let inCloudParams; + if (hybridParams.mode) { + inCloudParams = hybridParams.inCloudParams || { + model: DEFAULT_HYBRID_IN_CLOUD_MODEL + }; + } + else { + inCloudParams = modelParams; + } + if (!inCloudParams.model) { + throw new AIError(AIErrorCode.NO_MODEL, `Must provide a model name. Example: getGenerativeModel({ model: 'my-model-name' })`); + } + /** + * An AIService registered by index.node.ts will not have a + * chromeAdapterFactory() method. + */ + const chromeAdapter = ai.chromeAdapterFactory?.(hybridParams.mode, typeof window === 'undefined' ? undefined : window, hybridParams.onDeviceParams); + return new GenerativeModel(ai, inCloudParams, requestOptions, chromeAdapter); +} +/** + * Returns an {@link ImagenModel} class with methods for using Imagen. + * + * Only Imagen 3 models (named `imagen-3.0-*`) are supported. + * + * @param ai - An {@link AI} instance. + * @param modelParams - Parameters to use when making Imagen requests. + * @param requestOptions - Additional options to use when making requests. + * + * @throws If the `apiKey` or `projectId` fields are missing in your + * Firebase config. + * + * @public + */ +function getImagenModel(ai, modelParams, requestOptions) { + if (!modelParams.model) { + throw new AIError(AIErrorCode.NO_MODEL, `Must provide a model name. Example: getImagenModel({ model: 'my-model-name' })`); + } + return new ImagenModel(ai, modelParams, requestOptions); +} +/** + * Returns a {@link LiveGenerativeModel} class for real-time, bidirectional communication. + * + * The Live API is only supported in modern browser windows and Node >= 22. + * + * @param ai - An {@link AI} instance. + * @param modelParams - Parameters to use when setting up a {@link LiveSession}. + * @throws If the `apiKey` or `projectId` fields are missing in your + * Firebase config. + * + * @beta + */ +function getLiveGenerativeModel(ai, modelParams) { + if (!modelParams.model) { + throw new AIError(AIErrorCode.NO_MODEL, `Must provide a model name for getLiveGenerativeModel. Example: getLiveGenerativeModel(ai, { model: 'my-model-name' })`); + } + const webSocketHandler = new WebSocketHandlerImpl(); + return new LiveGenerativeModel(ai, modelParams, webSocketHandler); +} + +/** + * The Firebase AI Web SDK. + * + * @packageDocumentation + */ +function registerAI() { + app._registerComponent(new component.Component(AI_TYPE, (container, { instanceIdentifier }) => { + if (!instanceIdentifier) { + throw new AIError(AIErrorCode.ERROR, 'AIService instance identifier is undefined.'); + } + const backend = decodeInstanceIdentifier(instanceIdentifier); + // getImmediate for FirebaseApp will always succeed + const app = container.getProvider('app').getImmediate(); + const auth = container.getProvider('auth-internal'); + const appCheckProvider = container.getProvider('app-check-internal'); + return new AIService(app, backend, auth, appCheckProvider); + }, "PUBLIC" /* ComponentType.PUBLIC */).setMultipleInstances(true)); + app.registerVersion(name, version, 'node'); + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation + app.registerVersion(name, version, 'cjs2020'); +} +registerAI(); + +exports.AIError = AIError; +exports.AIErrorCode = AIErrorCode; +exports.AIModel = AIModel; +exports.AnyOfSchema = AnyOfSchema; +exports.ArraySchema = ArraySchema; +exports.Backend = Backend; +exports.BackendType = BackendType; +exports.BlockReason = BlockReason; +exports.BooleanSchema = BooleanSchema; +exports.ChatSession = ChatSession; +exports.FinishReason = FinishReason; +exports.FunctionCallingMode = FunctionCallingMode; +exports.GenerativeModel = GenerativeModel; +exports.GoogleAIBackend = GoogleAIBackend; +exports.HarmBlockMethod = HarmBlockMethod; +exports.HarmBlockThreshold = HarmBlockThreshold; +exports.HarmCategory = HarmCategory; +exports.HarmProbability = HarmProbability; +exports.HarmSeverity = HarmSeverity; +exports.ImagenAspectRatio = ImagenAspectRatio; +exports.ImagenImageFormat = ImagenImageFormat; +exports.ImagenModel = ImagenModel; +exports.ImagenPersonFilterLevel = ImagenPersonFilterLevel; +exports.ImagenSafetyFilterLevel = ImagenSafetyFilterLevel; +exports.InferenceMode = InferenceMode; +exports.InferenceSource = InferenceSource; +exports.IntegerSchema = IntegerSchema; +exports.Language = Language; +exports.LiveGenerativeModel = LiveGenerativeModel; +exports.LiveResponseType = LiveResponseType; +exports.LiveSession = LiveSession; +exports.Modality = Modality; +exports.NumberSchema = NumberSchema; +exports.ObjectSchema = ObjectSchema; +exports.Outcome = Outcome; +exports.POSSIBLE_ROLES = POSSIBLE_ROLES; +exports.ResponseModality = ResponseModality; +exports.Schema = Schema; +exports.SchemaType = SchemaType; +exports.StringSchema = StringSchema; +exports.URLRetrievalStatus = URLRetrievalStatus; +exports.VertexAIBackend = VertexAIBackend; +exports.getAI = getAI; +exports.getGenerativeModel = getGenerativeModel; +exports.getImagenModel = getImagenModel; +exports.getLiveGenerativeModel = getLiveGenerativeModel; +exports.startAudioConversation = startAudioConversation; +//# sourceMappingURL=index.node.cjs.js.map |
