@firebase/ai 2.8.0 → 2.9.0-20260224183151

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import { FirebaseError, Deferred, getModularInstance } from '@firebase/util';
4
4
  import { Logger } from '@firebase/logger';
5
5
 
6
6
  var name = "@firebase/ai";
7
- var version = "2.8.0";
7
+ var version = "2.9.0-20260224183151";
8
8
 
9
9
  /**
10
10
  * @license
@@ -1468,6 +1468,9 @@ function getText(response, partFilter) {
1468
1468
  * Returns every {@link FunctionCall} associated with first candidate.
1469
1469
  */
1470
1470
  function getFunctionCalls(response) {
1471
+ if (!response) {
1472
+ return undefined;
1473
+ }
1471
1474
  const functionCalls = [];
1472
1475
  if (response.candidates?.[0].content?.parts) {
1473
1476
  for (const part of response.candidates?.[0].content?.parts) {
@@ -1763,15 +1766,44 @@ const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/;
1763
1766
  *
1764
1767
  * @param response - Response from a fetch call
1765
1768
  */
1766
- function processStream(response, apiSettings, inferenceSource) {
1769
+ async function processStream(response, apiSettings, inferenceSource) {
1767
1770
  const inputStream = response.body.pipeThrough(new TextDecoderStream('utf8', { fatal: true }));
1768
1771
  const responseStream = getResponseStream(inputStream);
1769
1772
  // We split the stream so the user can iterate over partial results (stream1)
1770
1773
  // while we aggregate the full result for history/final response (stream2).
1771
1774
  const [stream1, stream2] = responseStream.tee();
1775
+ const { response: internalResponse, firstValue } = await processStreamInternal(stream2, apiSettings, inferenceSource);
1772
1776
  return {
1773
1777
  stream: generateResponseSequence(stream1, apiSettings, inferenceSource),
1774
- response: getResponsePromise(stream2, apiSettings, inferenceSource)
1778
+ response: internalResponse,
1779
+ firstValue
1780
+ };
1781
+ }
1782
+ /**
1783
+ * Consumes streams teed from the input stream for internal needs.
1784
+ * The streams need to be teed because each stream can only be consumed
1785
+ * by one reader.
1786
+ *
1787
+ * "streamForPeek"
1788
+ * This tee is used to peek at the first value for relevant information
1789
+ * that we need to evaluate before returning the stream handle to the
1790
+ * client. For example, we need to check if the response is a function
1791
+ * call that may need to be handled by automatic function calling before
1792
+ * returning a response to the client.
1793
+ *
1794
+ * "streamForAggregation"
1795
+ * We iterate through this tee independently from the user and aggregate
1796
+ * it into a single response when the stream is complete. We need this
1797
+ * aggregate object to add to chat history when using ChatSession. It's
1798
+ * also provided to the user if they want it.
1799
+ */
1800
+ async function processStreamInternal(stream, apiSettings, inferenceSource) {
1801
+ const [streamForPeek, streamForAggregation] = stream.tee();
1802
+ const reader = streamForPeek.getReader();
1803
+ const { value } = await reader.read();
1804
+ return {
1805
+ firstValue: value,
1806
+ response: getResponsePromise(streamForAggregation, apiSettings, inferenceSource)
1775
1807
  };
1776
1808
  }
1777
1809
  async function getResponsePromise(stream, apiSettings, inferenceSource) {
@@ -2342,6 +2374,11 @@ function validateChatHistory(history) {
2342
2374
  * by the user, preventing duplicate console logs.
2343
2375
  */
2344
2376
  const SILENT_ERROR = 'SILENT_ERROR';
2377
+ /**
2378
+ * Prevent infinite loop if the model continues to request sequential
2379
+ * function calls during automatic function calling.
2380
+ */
2381
+ const DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS = 10;
2345
2382
  /**
2346
2383
  * ChatSession class that enables sending chat messages and stores
2347
2384
  * history of sent and received messages so far.
@@ -2376,48 +2413,89 @@ class ChatSession {
2376
2413
  return this._history;
2377
2414
  }
2378
2415
  /**
2379
- * Sends a chat message and receives a non-streaming
2380
- * {@link GenerateContentResult}
2416
+ * Format Content into a request for generateContent or
2417
+ * generateContentStream.
2418
+ * @internal
2381
2419
  */
2382
- async sendMessage(request, singleRequestOptions) {
2383
- await this._sendPromise;
2384
- const newContent = formatNewContent(request);
2385
- const generateContentRequest = {
2420
+ _formatRequest(incomingContent, tempHistory) {
2421
+ return {
2386
2422
  safetySettings: this.params?.safetySettings,
2387
2423
  generationConfig: this.params?.generationConfig,
2388
2424
  tools: this.params?.tools,
2389
2425
  toolConfig: this.params?.toolConfig,
2390
2426
  systemInstruction: this.params?.systemInstruction,
2391
- contents: [...this._history, newContent]
2427
+ contents: [...this._history, ...tempHistory, incomingContent]
2392
2428
  };
2429
+ }
2430
+ /**
2431
+ * Sends a chat message and receives a non-streaming
2432
+ * {@link GenerateContentResult}
2433
+ */
2434
+ async sendMessage(request, singleRequestOptions) {
2393
2435
  let finalResult = {};
2394
- this._sendPromise = this._sendPromise
2395
- .then(() => generateContent(this._apiSettings, this.model, generateContentRequest, this.chromeAdapter, {
2396
- ...this.requestOptions,
2397
- ...singleRequestOptions
2398
- }))
2399
- .then(result => {
2400
- // TODO: Make this update atomic. If creating `responseContent` throws,
2401
- // history will contain the user message but not the response, causing
2402
- // validation errors on the next request.
2403
- if (result.response.candidates &&
2404
- result.response.candidates.length > 0) {
2405
- this._history.push(newContent);
2406
- const responseContent = {
2407
- parts: result.response.candidates?.[0].content.parts || [],
2408
- role: result.response.candidates?.[0].content.role || 'model'
2409
- };
2410
- this._history.push(responseContent);
2411
- }
2412
- else {
2413
- const blockErrorMessage = formatBlockErrorMessage(result.response);
2414
- if (blockErrorMessage) {
2415
- logger.warn(`sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`);
2436
+ await this._sendPromise;
2437
+ /**
2438
+ * Temporarily store multiple turns for cases like automatic function
2439
+ * calling, only writing them to official history when the entire
2440
+ * sequence has completed successfully.
2441
+ */
2442
+ const tempHistory = [];
2443
+ this._sendPromise = this._sendPromise.then(async () => {
2444
+ let functionCalls;
2445
+ let functionCallTurnCount = 0;
2446
+ const functionCallMaxTurns = this.requestOptions?.maxSequentalFunctionCalls ??
2447
+ DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS;
2448
+ // Repeats until model returns a response with no function calls
2449
+ // or until `functionCallMaxTurns` is met or exceeded.
2450
+ do {
2451
+ let formattedContent;
2452
+ if (functionCalls) {
2453
+ functionCallTurnCount++;
2454
+ const functionResponseParts = await this._callFunctionsAsNeeded(functionCalls);
2455
+ formattedContent = formatNewContent(functionResponseParts);
2416
2456
  }
2457
+ else {
2458
+ formattedContent = formatNewContent(request);
2459
+ }
2460
+ const formattedRequest = this._formatRequest(formattedContent, tempHistory);
2461
+ tempHistory.push(formattedContent);
2462
+ const result = await generateContent(this._apiSettings, this.model, formattedRequest, this.chromeAdapter, {
2463
+ ...this.requestOptions,
2464
+ ...singleRequestOptions
2465
+ });
2466
+ if (result) {
2467
+ finalResult = result;
2468
+ functionCalls = this._getCallableFunctionCalls(result.response);
2469
+ if (result.response.candidates &&
2470
+ result.response.candidates.length > 0) {
2471
+ // TODO: Make this update atomic. If creating `responseContent` throws,
2472
+ // history will contain the user message but not the response, causing
2473
+ // validation errors on the next request.
2474
+ const responseContent = {
2475
+ parts: result.response.candidates?.[0].content.parts || [],
2476
+ // Response seems to come back without a role set.
2477
+ role: result.response.candidates?.[0].content.role || 'model'
2478
+ };
2479
+ tempHistory.push(responseContent);
2480
+ }
2481
+ else {
2482
+ const blockErrorMessage = formatBlockErrorMessage(result.response);
2483
+ if (blockErrorMessage) {
2484
+ logger.warn(`sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`);
2485
+ }
2486
+ }
2487
+ }
2488
+ else {
2489
+ functionCalls = undefined;
2490
+ }
2491
+ } while (functionCalls && functionCallTurnCount < functionCallMaxTurns);
2492
+ if (functionCalls && functionCallTurnCount >= functionCallMaxTurns) {
2493
+ logger.warn(`Automatic function calling exceeded the limit of` +
2494
+ ` ${functionCallMaxTurns} function calls. Returning last model response.`);
2417
2495
  }
2418
- finalResult = result;
2419
2496
  });
2420
2497
  await this._sendPromise;
2498
+ this._history = this._history.concat(tempHistory);
2421
2499
  return finalResult;
2422
2500
  }
2423
2501
  /**
@@ -2427,23 +2505,62 @@ class ChatSession {
2427
2505
  */
2428
2506
  async sendMessageStream(request, singleRequestOptions) {
2429
2507
  await this._sendPromise;
2430
- const newContent = formatNewContent(request);
2431
- const generateContentRequest = {
2432
- safetySettings: this.params?.safetySettings,
2433
- generationConfig: this.params?.generationConfig,
2434
- tools: this.params?.tools,
2435
- toolConfig: this.params?.toolConfig,
2436
- systemInstruction: this.params?.systemInstruction,
2437
- contents: [...this._history, newContent]
2508
+ /**
2509
+ * Temporarily store multiple turns for cases like automatic function
2510
+ * calling, only writing them to official history when the entire
2511
+ * sequence has completed successfully.
2512
+ */
2513
+ const tempHistory = [];
2514
+ const callGenerateContentStream = async () => {
2515
+ let functionCalls;
2516
+ let functionCallTurnCount = 0;
2517
+ const functionCallMaxTurns = this.requestOptions?.maxSequentalFunctionCalls ??
2518
+ DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS;
2519
+ let result;
2520
+ // Repeats until model returns a response with no function calls
2521
+ // or until `functionCallMaxTurns` is met or exceeded.
2522
+ do {
2523
+ let formattedContent;
2524
+ if (functionCalls) {
2525
+ functionCallTurnCount++;
2526
+ const functionResponseParts = await this._callFunctionsAsNeeded(functionCalls);
2527
+ formattedContent = formatNewContent(functionResponseParts);
2528
+ }
2529
+ else {
2530
+ formattedContent = formatNewContent(request);
2531
+ }
2532
+ tempHistory.push(formattedContent);
2533
+ const formattedRequest = this._formatRequest(formattedContent, tempHistory);
2534
+ result = await generateContentStream(this._apiSettings, this.model, formattedRequest, this.chromeAdapter, {
2535
+ ...this.requestOptions,
2536
+ ...singleRequestOptions
2537
+ });
2538
+ functionCalls = this._getCallableFunctionCalls(result.firstValue);
2539
+ if (functionCalls &&
2540
+ result.firstValue &&
2541
+ result.firstValue.candidates &&
2542
+ result.firstValue.candidates.length > 0) {
2543
+ const responseContent = {
2544
+ ...result.firstValue.candidates[0].content
2545
+ };
2546
+ if (!responseContent.role) {
2547
+ responseContent.role = 'model';
2548
+ }
2549
+ tempHistory.push(responseContent);
2550
+ }
2551
+ } while (functionCalls && functionCallTurnCount < functionCallMaxTurns);
2552
+ if (functionCalls && functionCallTurnCount >= functionCallMaxTurns) {
2553
+ logger.warn(`Automatic function calling exceeded the limit of` +
2554
+ ` ${functionCallMaxTurns} function calls. Returning last model response.`);
2555
+ }
2556
+ return { stream: result.stream, response: result.response };
2438
2557
  };
2439
- const streamPromise = generateContentStream(this._apiSettings, this.model, generateContentRequest, this.chromeAdapter, {
2440
- ...this.requestOptions,
2441
- ...singleRequestOptions
2442
- });
2443
- // We hook into the chain to update history, but we don't block the
2444
- // return of `streamPromise` to the user.
2558
+ const streamPromise = callGenerateContentStream();
2559
+ // Add onto the chain.
2445
2560
  this._sendPromise = this._sendPromise
2446
- .then(() => streamPromise)
2561
+ .then(async () => streamPromise)
2562
+ // This must be handled to avoid unhandled rejection, but jump
2563
+ // to the final catch block with a label to not log this error.
2447
2564
  .catch(_ignored => {
2448
2565
  // If the initial fetch fails, the user's `streamPromise` rejects.
2449
2566
  // We swallow the error here to prevent double logging in the final catch.
@@ -2456,7 +2573,7 @@ class ChatSession {
2456
2573
  // TODO: Move response validation logic upstream to `stream-reader` so
2457
2574
  // errors propagate to the user's `result.response` promise.
2458
2575
  if (response.candidates && response.candidates.length > 0) {
2459
- this._history.push(newContent);
2576
+ this._history = this._history.concat(tempHistory);
2460
2577
  // TODO: Validate that `response.candidates[0].content` is not null.
2461
2578
  const responseContent = { ...response.candidates[0].content };
2462
2579
  if (!responseContent.role) {
@@ -2479,6 +2596,75 @@ class ChatSession {
2479
2596
  });
2480
2597
  return streamPromise;
2481
2598
  }
2599
+ /**
2600
+ * Get function calls that the SDK has references to actually call.
2601
+ * This is all-or-nothing. If the model is requesting multiple
2602
+ * function calls, all of them must have references in order for
2603
+ * automatic function calling to work.
2604
+ *
2605
+ * @internal
2606
+ */
2607
+ _getCallableFunctionCalls(response) {
2608
+ const functionDeclarationsTool = this.params?.tools?.find(tool => tool.functionDeclarations);
2609
+ if (!functionDeclarationsTool?.functionDeclarations) {
2610
+ return;
2611
+ }
2612
+ const functionCalls = getFunctionCalls(response);
2613
+ if (!functionCalls) {
2614
+ return;
2615
+ }
2616
+ for (const functionCall of functionCalls) {
2617
+ const hasFunctionReference = functionDeclarationsTool.functionDeclarations?.some(declaration => declaration.name === functionCall.name &&
2618
+ typeof declaration.functionReference === 'function');
2619
+ if (!hasFunctionReference) {
2620
+ return;
2621
+ }
2622
+ }
2623
+ return functionCalls;
2624
+ }
2625
+ /**
2626
+ * Call user-defined functions if requested by the model, and return
2627
+ * the response that should be sent to the model.
2628
+ * @internal
2629
+ */
2630
+ async _callFunctionsAsNeeded(functionCalls) {
2631
+ const activeCallList = new Map();
2632
+ const promiseList = [];
2633
+ const functionDeclarationsTool = this.params?.tools?.find(tool => tool.functionDeclarations);
2634
+ if (functionDeclarationsTool &&
2635
+ functionDeclarationsTool.functionDeclarations) {
2636
+ for (const functionCall of functionCalls) {
2637
+ const functionDeclaration = functionDeclarationsTool.functionDeclarations.find(declaration => declaration.name === functionCall.name);
2638
+ if (functionDeclaration?.functionReference) {
2639
+ const results = Promise.resolve(functionDeclaration.functionReference(functionCall.args)).catch(e => {
2640
+ const wrappedError = new AIError(AIErrorCode.ERROR, `Error in user-defined function "${functionDeclaration.name}": ${e.message}`);
2641
+ wrappedError.stack = e.stack;
2642
+ throw wrappedError;
2643
+ });
2644
+ activeCallList.set(functionCall.name, {
2645
+ id: functionCall.id,
2646
+ results
2647
+ });
2648
+ promiseList.push(results);
2649
+ }
2650
+ }
2651
+ // Wait for promises to finish.
2652
+ await Promise.all(promiseList);
2653
+ const functionResponseParts = [];
2654
+ for (const [name, callData] of activeCallList) {
2655
+ functionResponseParts.push({
2656
+ functionResponse: {
2657
+ name,
2658
+ response: await callData.results
2659
+ }
2660
+ });
2661
+ }
2662
+ return functionResponseParts;
2663
+ }
2664
+ else {
2665
+ throw new AIError(AIErrorCode.REQUEST_ERROR, `No function declarations were provided in "tools".`);
2666
+ }
2667
+ }
2482
2668
  }
2483
2669
 
2484
2670
  /**
@@ -2582,7 +2768,7 @@ class GenerativeModel extends AIModel {
2582
2768
  */
2583
2769
  async generateContentStream(request, singleRequestOptions) {
2584
2770
  const formattedParams = formatGenerateContentInput(request);
2585
- return generateContentStream(this._apiSettings, this.model, {
2771
+ const { stream, response } = await generateContentStream(this._apiSettings, this.model, {
2586
2772
  generationConfig: this.generationConfig,
2587
2773
  safetySettings: this.safetySettings,
2588
2774
  tools: this.tools,
@@ -2595,6 +2781,7 @@ class GenerativeModel extends AIModel {
2595
2781
  ...this.requestOptions,
2596
2782
  ...singleRequestOptions
2597
2783
  });
2784
+ return { stream, response };
2598
2785
  }
2599
2786
  /**
2600
2787
  * Gets a new {@link ChatSession} instance which can be used for