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