@firebase/ai 1.2.2-canary.f92069a21 → 1.2.2-eap-ai-hybridinference.c16cbf1a3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import { Logger } from '@firebase/logger';
5
5
  import { __asyncGenerator, __await } from 'tslib';
6
6
 
7
7
  var name = "@firebase/ai";
8
- var version = "1.2.2-canary.f92069a21";
8
+ var version = "1.2.2-eap-ai-hybridinference.c16cbf1a3";
9
9
 
10
10
  /**
11
11
  * @license
@@ -1624,20 +1624,38 @@ function aggregateResponses(responses) {
1624
1624
  * See the License for the specific language governing permissions and
1625
1625
  * limitations under the License.
1626
1626
  */
1627
- async function generateContentStream(apiSettings, model, params, requestOptions) {
1627
+ async function generateContentStreamOnCloud(apiSettings, model, params, requestOptions) {
1628
1628
  if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
1629
1629
  params = mapGenerateContentRequest(params);
1630
1630
  }
1631
- const response = await makeRequest(model, Task.STREAM_GENERATE_CONTENT, apiSettings,
1631
+ return makeRequest(model, Task.STREAM_GENERATE_CONTENT, apiSettings,
1632
1632
  /* stream */ true, JSON.stringify(params), requestOptions);
1633
+ }
1634
+ async function generateContentStream(apiSettings, model, params, chromeAdapter, requestOptions) {
1635
+ let response;
1636
+ if (await chromeAdapter.isAvailable(params)) {
1637
+ response = await chromeAdapter.generateContentStream(params);
1638
+ }
1639
+ else {
1640
+ response = await generateContentStreamOnCloud(apiSettings, model, params, requestOptions);
1641
+ }
1633
1642
  return processStream(response, apiSettings); // TODO: Map streaming responses
1634
1643
  }
1635
- async function generateContent(apiSettings, model, params, requestOptions) {
1644
+ async function generateContentOnCloud(apiSettings, model, params, requestOptions) {
1636
1645
  if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
1637
1646
  params = mapGenerateContentRequest(params);
1638
1647
  }
1639
- const response = await makeRequest(model, Task.GENERATE_CONTENT, apiSettings,
1648
+ return makeRequest(model, Task.GENERATE_CONTENT, apiSettings,
1640
1649
  /* stream */ false, JSON.stringify(params), requestOptions);
1650
+ }
1651
+ async function generateContent(apiSettings, model, params, chromeAdapter, requestOptions) {
1652
+ let response;
1653
+ if (await chromeAdapter.isAvailable(params)) {
1654
+ response = await chromeAdapter.generateContent(params);
1655
+ }
1656
+ else {
1657
+ response = await generateContentOnCloud(apiSettings, model, params, requestOptions);
1658
+ }
1641
1659
  const generateContentResponse = await processGenerateContentResponse(response, apiSettings);
1642
1660
  const enhancedResponse = createEnhancedContentResponse(generateContentResponse);
1643
1661
  return {
@@ -1894,8 +1912,9 @@ const SILENT_ERROR = 'SILENT_ERROR';
1894
1912
  * @public
1895
1913
  */
1896
1914
  class ChatSession {
1897
- constructor(apiSettings, model, params, requestOptions) {
1915
+ constructor(apiSettings, model, chromeAdapter, params, requestOptions) {
1898
1916
  this.model = model;
1917
+ this.chromeAdapter = chromeAdapter;
1899
1918
  this.params = params;
1900
1919
  this.requestOptions = requestOptions;
1901
1920
  this._history = [];
@@ -1934,7 +1953,7 @@ class ChatSession {
1934
1953
  let finalResult = {};
1935
1954
  // Add onto the chain.
1936
1955
  this._sendPromise = this._sendPromise
1937
- .then(() => generateContent(this._apiSettings, this.model, generateContentRequest, this.requestOptions))
1956
+ .then(() => generateContent(this._apiSettings, this.model, generateContentRequest, this.chromeAdapter, this.requestOptions))
1938
1957
  .then(result => {
1939
1958
  var _a, _b;
1940
1959
  if (result.response.candidates &&
@@ -1975,7 +1994,7 @@ class ChatSession {
1975
1994
  systemInstruction: (_e = this.params) === null || _e === void 0 ? void 0 : _e.systemInstruction,
1976
1995
  contents: [...this._history, newContent]
1977
1996
  };
1978
- const streamPromise = generateContentStream(this._apiSettings, this.model, generateContentRequest, this.requestOptions);
1997
+ const streamPromise = generateContentStream(this._apiSettings, this.model, generateContentRequest, this.chromeAdapter, this.requestOptions);
1979
1998
  // Add onto the chain.
1980
1999
  this._sendPromise = this._sendPromise
1981
2000
  .then(() => streamPromise)
@@ -2032,7 +2051,7 @@ class ChatSession {
2032
2051
  * See the License for the specific language governing permissions and
2033
2052
  * limitations under the License.
2034
2053
  */
2035
- async function countTokens(apiSettings, model, params, requestOptions) {
2054
+ async function countTokensOnCloud(apiSettings, model, params, requestOptions) {
2036
2055
  let body = '';
2037
2056
  if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) {
2038
2057
  const mappedParams = mapCountTokensRequest(params, model);
@@ -2044,6 +2063,12 @@ async function countTokens(apiSettings, model, params, requestOptions) {
2044
2063
  const response = await makeRequest(model, Task.COUNT_TOKENS, apiSettings, false, body, requestOptions);
2045
2064
  return response.json();
2046
2065
  }
2066
+ async function countTokens(apiSettings, model, params, chromeAdapter, requestOptions) {
2067
+ if (await chromeAdapter.isAvailable(params)) {
2068
+ return (await chromeAdapter.countTokens(params)).json();
2069
+ }
2070
+ return countTokensOnCloud(apiSettings, model, params, requestOptions);
2071
+ }
2047
2072
 
2048
2073
  /**
2049
2074
  * @license
@@ -2066,8 +2091,9 @@ async function countTokens(apiSettings, model, params, requestOptions) {
2066
2091
  * @public
2067
2092
  */
2068
2093
  class GenerativeModel extends AIModel {
2069
- constructor(ai, modelParams, requestOptions) {
2094
+ constructor(ai, modelParams, chromeAdapter, requestOptions) {
2070
2095
  super(ai, modelParams.model);
2096
+ this.chromeAdapter = chromeAdapter;
2071
2097
  this.generationConfig = modelParams.generationConfig || {};
2072
2098
  this.safetySettings = modelParams.safetySettings || [];
2073
2099
  this.tools = modelParams.tools;
@@ -2081,7 +2107,7 @@ class GenerativeModel extends AIModel {
2081
2107
  */
2082
2108
  async generateContent(request) {
2083
2109
  const formattedParams = formatGenerateContentInput(request);
2084
- return generateContent(this._apiSettings, this.model, Object.assign({ generationConfig: this.generationConfig, safetySettings: this.safetySettings, tools: this.tools, toolConfig: this.toolConfig, systemInstruction: this.systemInstruction }, formattedParams), this.requestOptions);
2110
+ return generateContent(this._apiSettings, this.model, Object.assign({ generationConfig: this.generationConfig, safetySettings: this.safetySettings, tools: this.tools, toolConfig: this.toolConfig, systemInstruction: this.systemInstruction }, formattedParams), this.chromeAdapter, this.requestOptions);
2085
2111
  }
2086
2112
  /**
2087
2113
  * Makes a single streaming call to the model
@@ -2091,23 +2117,27 @@ class GenerativeModel extends AIModel {
2091
2117
  */
2092
2118
  async generateContentStream(request) {
2093
2119
  const formattedParams = formatGenerateContentInput(request);
2094
- return generateContentStream(this._apiSettings, this.model, Object.assign({ generationConfig: this.generationConfig, safetySettings: this.safetySettings, tools: this.tools, toolConfig: this.toolConfig, systemInstruction: this.systemInstruction }, formattedParams), this.requestOptions);
2120
+ return generateContentStream(this._apiSettings, this.model, Object.assign({ generationConfig: this.generationConfig, safetySettings: this.safetySettings, tools: this.tools, toolConfig: this.toolConfig, systemInstruction: this.systemInstruction }, formattedParams), this.chromeAdapter, this.requestOptions);
2095
2121
  }
2096
2122
  /**
2097
2123
  * Gets a new {@link ChatSession} instance which can be used for
2098
2124
  * multi-turn chats.
2099
2125
  */
2100
2126
  startChat(startChatParams) {
2101
- return new ChatSession(this._apiSettings, this.model, Object.assign({ tools: this.tools, toolConfig: this.toolConfig, systemInstruction: this.systemInstruction, generationConfig: this.generationConfig, safetySettings: this.safetySettings }, startChatParams), this.requestOptions);
2127
+ return new ChatSession(this._apiSettings, this.model, this.chromeAdapter, Object.assign({ tools: this.tools, toolConfig: this.toolConfig, systemInstruction: this.systemInstruction, generationConfig: this.generationConfig, safetySettings: this.safetySettings }, startChatParams), this.requestOptions);
2102
2128
  }
2103
2129
  /**
2104
2130
  * Counts the tokens in the provided request.
2105
2131
  */
2106
2132
  async countTokens(request) {
2107
2133
  const formattedParams = formatGenerateContentInput(request);
2108
- return countTokens(this._apiSettings, this.model, formattedParams);
2134
+ return countTokens(this._apiSettings, this.model, formattedParams, this.chromeAdapter);
2109
2135
  }
2110
2136
  }
2137
+ /**
2138
+ * Defines the name of the default in-cloud model to use for hybrid inference.
2139
+ */
2140
+ GenerativeModel.DEFAULT_HYBRID_IN_CLOUD_MODEL = 'gemini-2.0-flash-lite';
2111
2141
 
2112
2142
  /**
2113
2143
  * @license
@@ -2216,6 +2246,278 @@ class ImagenModel extends AIModel {
2216
2246
  }
2217
2247
  }
2218
2248
 
2249
+ /**
2250
+ * @license
2251
+ * Copyright 2025 Google LLC
2252
+ *
2253
+ * Licensed under the Apache License, Version 2.0 (the "License");
2254
+ * you may not use this file except in compliance with the License.
2255
+ * You may obtain a copy of the License at
2256
+ *
2257
+ * http://www.apache.org/licenses/LICENSE-2.0
2258
+ *
2259
+ * Unless required by applicable law or agreed to in writing, software
2260
+ * distributed under the License is distributed on an "AS IS" BASIS,
2261
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2262
+ * See the License for the specific language governing permissions and
2263
+ * limitations under the License.
2264
+ */
2265
+ var Availability;
2266
+ (function (Availability) {
2267
+ Availability["unavailable"] = "unavailable";
2268
+ Availability["downloadable"] = "downloadable";
2269
+ Availability["downloading"] = "downloading";
2270
+ Availability["available"] = "available";
2271
+ })(Availability || (Availability = {}));
2272
+
2273
+ /**
2274
+ * @license
2275
+ * Copyright 2025 Google LLC
2276
+ *
2277
+ * Licensed under the Apache License, Version 2.0 (the "License");
2278
+ * you may not use this file except in compliance with the License.
2279
+ * You may obtain a copy of the License at
2280
+ *
2281
+ * http://www.apache.org/licenses/LICENSE-2.0
2282
+ *
2283
+ * Unless required by applicable law or agreed to in writing, software
2284
+ * distributed under the License is distributed on an "AS IS" BASIS,
2285
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2286
+ * See the License for the specific language governing permissions and
2287
+ * limitations under the License.
2288
+ */
2289
+ /**
2290
+ * Defines an inference "backend" that uses Chrome's on-device model,
2291
+ * and encapsulates logic for detecting when on-device is possible.
2292
+ */
2293
+ class ChromeAdapter {
2294
+ constructor(languageModelProvider, mode, onDeviceParams = {
2295
+ createOptions: {
2296
+ // Defaults to support image inputs for convenience.
2297
+ expectedInputs: [{ type: 'image' }]
2298
+ }
2299
+ }) {
2300
+ this.languageModelProvider = languageModelProvider;
2301
+ this.mode = mode;
2302
+ this.onDeviceParams = onDeviceParams;
2303
+ this.isDownloading = false;
2304
+ }
2305
+ /**
2306
+ * Checks if a given request can be made on-device.
2307
+ *
2308
+ * <ol>Encapsulates a few concerns:
2309
+ * <li>the mode</li>
2310
+ * <li>API existence</li>
2311
+ * <li>prompt formatting</li>
2312
+ * <li>model availability, including triggering download if necessary</li>
2313
+ * </ol>
2314
+ *
2315
+ * <p>Pros: callers needn't be concerned with details of on-device availability.</p>
2316
+ * <p>Cons: this method spans a few concerns and splits request validation from usage.
2317
+ * If instance variables weren't already part of the API, we could consider a better
2318
+ * separation of concerns.</p>
2319
+ */
2320
+ async isAvailable(request) {
2321
+ if (this.mode === 'only_in_cloud') {
2322
+ logger.debug(`On-device inference unavailable because mode is "only_in_cloud".`);
2323
+ return false;
2324
+ }
2325
+ // Triggers out-of-band download so model will eventually become available.
2326
+ const availability = await this.downloadIfAvailable();
2327
+ if (this.mode === 'only_on_device') {
2328
+ return true;
2329
+ }
2330
+ // Applies prefer_on_device logic.
2331
+ if (availability !== Availability.available) {
2332
+ logger.debug(`On-device inference unavailable because availability is "${availability}".`);
2333
+ return false;
2334
+ }
2335
+ if (!ChromeAdapter.isOnDeviceRequest(request)) {
2336
+ logger.debug(`On-device inference unavailable because request is incompatible.`);
2337
+ return false;
2338
+ }
2339
+ return true;
2340
+ }
2341
+ /**
2342
+ * Generates content on device.
2343
+ *
2344
+ * <p>This is comparable to {@link GenerativeModel.generateContent} for generating content in
2345
+ * Cloud.</p>
2346
+ * @param request a standard Vertex {@link GenerateContentRequest}
2347
+ * @returns {@link Response}, so we can reuse common response formatting.
2348
+ */
2349
+ async generateContent(request) {
2350
+ const session = await this.createSession();
2351
+ // TODO: support multiple content objects when Chrome supports
2352
+ // sequence<LanguageModelMessage>
2353
+ const contents = await Promise.all(request.contents[0].parts.map(ChromeAdapter.toLanguageModelMessageContent));
2354
+ const text = await session.prompt(contents, this.onDeviceParams.promptOptions);
2355
+ return ChromeAdapter.toResponse(text);
2356
+ }
2357
+ /**
2358
+ * Generates content stream on device.
2359
+ *
2360
+ * <p>This is comparable to {@link GenerativeModel.generateContentStream} for generating content in
2361
+ * Cloud.</p>
2362
+ * @param request a standard Vertex {@link GenerateContentRequest}
2363
+ * @returns {@link Response}, so we can reuse common response formatting.
2364
+ */
2365
+ async generateContentStream(request) {
2366
+ const session = await this.createSession();
2367
+ // TODO: support multiple content objects when Chrome supports
2368
+ // sequence<LanguageModelMessage>
2369
+ const contents = await Promise.all(request.contents[0].parts.map(ChromeAdapter.toLanguageModelMessageContent));
2370
+ const stream = await session.promptStreaming(contents, this.onDeviceParams.promptOptions);
2371
+ return ChromeAdapter.toStreamResponse(stream);
2372
+ }
2373
+ async countTokens(_request) {
2374
+ throw new AIError("request-error" /* AIErrorCode.REQUEST_ERROR */, 'Count Tokens is not yet available for on-device model.');
2375
+ }
2376
+ /**
2377
+ * Asserts inference for the given request can be performed by an on-device model.
2378
+ */
2379
+ static isOnDeviceRequest(request) {
2380
+ // Returns false if the prompt is empty.
2381
+ if (request.contents.length === 0) {
2382
+ logger.debug('Empty prompt rejected for on-device inference.');
2383
+ return false;
2384
+ }
2385
+ for (const content of request.contents) {
2386
+ // Returns false if the request contains multiple roles, eg a chat history.
2387
+ // TODO: remove this guard once LanguageModelMessage is supported.
2388
+ if (content.role !== 'user') {
2389
+ logger.debug(`Non-user role "${content.role}" rejected for on-device inference.`);
2390
+ return false;
2391
+ }
2392
+ // Returns false if request contains an image with an unsupported mime type.
2393
+ for (const part of content.parts) {
2394
+ if (part.inlineData &&
2395
+ ChromeAdapter.SUPPORTED_MIME_TYPES.indexOf(part.inlineData.mimeType) === -1) {
2396
+ logger.debug(`Unsupported mime type "${part.inlineData.mimeType}" rejected for on-device inference.`);
2397
+ return false;
2398
+ }
2399
+ }
2400
+ }
2401
+ return true;
2402
+ }
2403
+ /**
2404
+ * Encapsulates logic to get availability and download a model if one is downloadable.
2405
+ */
2406
+ async downloadIfAvailable() {
2407
+ var _a;
2408
+ const availability = await ((_a = this.languageModelProvider) === null || _a === void 0 ? void 0 : _a.availability(this.onDeviceParams.createOptions));
2409
+ if (availability === Availability.downloadable) {
2410
+ this.download();
2411
+ }
2412
+ return availability;
2413
+ }
2414
+ /**
2415
+ * Triggers out-of-band download of an on-device model.
2416
+ *
2417
+ * <p>Chrome only downloads models as needed. Chrome knows a model is needed when code calls
2418
+ * LanguageModel.create.</p>
2419
+ *
2420
+ * <p>Since Chrome manages the download, the SDK can only avoid redundant download requests by
2421
+ * tracking if a download has previously been requested.</p>
2422
+ */
2423
+ download() {
2424
+ var _a;
2425
+ if (this.isDownloading) {
2426
+ return;
2427
+ }
2428
+ this.isDownloading = true;
2429
+ this.downloadPromise = (_a = this.languageModelProvider) === null || _a === void 0 ? void 0 : _a.create(this.onDeviceParams.createOptions).then(() => {
2430
+ this.isDownloading = false;
2431
+ });
2432
+ }
2433
+ /**
2434
+ * Converts a Vertex Part object to a Chrome LanguageModelMessageContent object.
2435
+ */
2436
+ static async toLanguageModelMessageContent(part) {
2437
+ if (part.text) {
2438
+ return {
2439
+ type: 'text',
2440
+ content: part.text
2441
+ };
2442
+ }
2443
+ else if (part.inlineData) {
2444
+ const formattedImageContent = await fetch(`data:${part.inlineData.mimeType};base64,${part.inlineData.data}`);
2445
+ const imageBlob = await formattedImageContent.blob();
2446
+ const imageBitmap = await createImageBitmap(imageBlob);
2447
+ return {
2448
+ type: 'image',
2449
+ content: imageBitmap
2450
+ };
2451
+ }
2452
+ // Assumes contents have been verified to contain only a single TextPart.
2453
+ // TODO: support other input types
2454
+ throw new Error('Not yet implemented');
2455
+ }
2456
+ /**
2457
+ * Abstracts Chrome session creation.
2458
+ *
2459
+ * <p>Chrome uses a multi-turn session for all inference. Vertex uses single-turn for all
2460
+ * inference. To map the Vertex API to Chrome's API, the SDK creates a new session for all
2461
+ * inference.</p>
2462
+ *
2463
+ * <p>Chrome will remove a model from memory if it's no longer in use, so this method ensures a
2464
+ * new session is created before an old session is destroyed.</p>
2465
+ */
2466
+ async createSession() {
2467
+ if (!this.languageModelProvider) {
2468
+ throw new AIError("request-error" /* AIErrorCode.REQUEST_ERROR */, 'Chrome AI requested for unsupported browser version.');
2469
+ }
2470
+ const newSession = await this.languageModelProvider.create(this.onDeviceParams.createOptions);
2471
+ if (this.oldSession) {
2472
+ this.oldSession.destroy();
2473
+ }
2474
+ // Holds session reference, so model isn't unloaded from memory.
2475
+ this.oldSession = newSession;
2476
+ return newSession;
2477
+ }
2478
+ /**
2479
+ * Formats string returned by Chrome as a {@link Response} returned by Vertex.
2480
+ */
2481
+ static toResponse(text) {
2482
+ return {
2483
+ json: async () => ({
2484
+ candidates: [
2485
+ {
2486
+ content: {
2487
+ parts: [{ text }]
2488
+ }
2489
+ }
2490
+ ]
2491
+ })
2492
+ };
2493
+ }
2494
+ /**
2495
+ * Formats string stream returned by Chrome as SSE returned by Vertex.
2496
+ */
2497
+ static toStreamResponse(stream) {
2498
+ const encoder = new TextEncoder();
2499
+ return {
2500
+ body: stream.pipeThrough(new TransformStream({
2501
+ transform(chunk, controller) {
2502
+ const json = JSON.stringify({
2503
+ candidates: [
2504
+ {
2505
+ content: {
2506
+ role: 'model',
2507
+ parts: [{ text: chunk }]
2508
+ }
2509
+ }
2510
+ ]
2511
+ });
2512
+ controller.enqueue(encoder.encode(`data: ${json}\n\n`));
2513
+ }
2514
+ }))
2515
+ };
2516
+ }
2517
+ }
2518
+ // Visible for testing
2519
+ ChromeAdapter.SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png'];
2520
+
2219
2521
  /**
2220
2522
  * @license
2221
2523
  * Copyright 2024 Google LLC
@@ -2571,10 +2873,21 @@ function getAI(app = getApp(), options = { backend: new GoogleAIBackend() }) {
2571
2873
  * @public
2572
2874
  */
2573
2875
  function getGenerativeModel(ai, modelParams, requestOptions) {
2574
- if (!modelParams.model) {
2876
+ // Uses the existence of HybridParams.mode to clarify the type of the modelParams input.
2877
+ const hybridParams = modelParams;
2878
+ let inCloudParams;
2879
+ if (hybridParams.mode) {
2880
+ inCloudParams = hybridParams.inCloudParams || {
2881
+ model: GenerativeModel.DEFAULT_HYBRID_IN_CLOUD_MODEL
2882
+ };
2883
+ }
2884
+ else {
2885
+ inCloudParams = modelParams;
2886
+ }
2887
+ if (!inCloudParams.model) {
2575
2888
  throw new AIError("no-model" /* AIErrorCode.NO_MODEL */, `Must provide a model name. Example: getGenerativeModel({ model: 'my-model-name' })`);
2576
2889
  }
2577
- return new GenerativeModel(ai, modelParams, requestOptions);
2890
+ return new GenerativeModel(ai, inCloudParams, new ChromeAdapter(window.LanguageModel, hybridParams.mode, hybridParams.onDeviceParams), requestOptions);
2578
2891
  }
2579
2892
  /**
2580
2893
  * Returns an {@link ImagenModel} class with methods for using Imagen.