@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.
@@ -325,6 +325,7 @@ export declare class ChatSession {
325
325
  * to history.
326
326
  */
327
327
  getHistory(): Promise<Content[]>;
328
+ /* Excluded from this release type: _formatRequest */
328
329
  /**
329
330
  * Sends a chat message and receives a non-streaming
330
331
  * {@link GenerateContentResult}
@@ -336,6 +337,8 @@ export declare class ChatSession {
336
337
  * and a response promise.
337
338
  */
338
339
  sendMessageStream(request: string | Array<string | Part>, singleRequestOptions?: SingleRequestOptions): Promise<GenerateContentStreamResult>;
340
+ /* Excluded from this release type: _getCallableFunctionCalls */
341
+ /* Excluded from this release type: _callFunctionsAsNeeded */
339
342
  }
340
343
 
341
344
  /**
@@ -802,6 +805,11 @@ export declare interface FunctionDeclaration {
802
805
  * case-sensitive. For a function with no parameters, this can be left unset.
803
806
  */
804
807
  parameters?: ObjectSchema | ObjectSchemaRequest;
808
+ /**
809
+ * Reference to an actual function to call. Specifying this will cause the
810
+ * function to be called automatically when requested by the model.
811
+ */
812
+ functionReference?: Function;
805
813
  }
806
814
 
807
815
  /**
@@ -844,6 +852,7 @@ export declare interface FunctionResponse {
844
852
  id?: string;
845
853
  name: string;
846
854
  response: object;
855
+ parts?: Part[];
847
856
  }
848
857
 
849
858
  /**
@@ -2530,6 +2539,15 @@ export declare interface RequestOptions {
2530
2539
  * (used regardless of your chosen Gemini API provider).
2531
2540
  */
2532
2541
  baseUrl?: string;
2542
+ /**
2543
+ * Limits amount of sequential function calls the SDK can make during automatic
2544
+ * function calling, in order to prevent infinite loops. If not specified,
2545
+ * this value defaults to 10.
2546
+ *
2547
+ * When it reaches this limit, it will return the last response received
2548
+ * from the model, whether it is a text response or further function calls.
2549
+ */
2550
+ maxSequentalFunctionCalls?: number;
2533
2551
  }
2534
2552
 
2535
2553
  /**
package/dist/ai.d.ts CHANGED
@@ -371,6 +371,12 @@ export declare class ChatSession {
371
371
  * to history.
372
372
  */
373
373
  getHistory(): Promise<Content[]>;
374
+ /**
375
+ * Format Content into a request for generateContent or
376
+ * generateContentStream.
377
+ * @internal
378
+ */
379
+ _formatRequest(incomingContent: Content, tempHistory: Content[]): GenerateContentRequest;
374
380
  /**
375
381
  * Sends a chat message and receives a non-streaming
376
382
  * {@link GenerateContentResult}
@@ -382,6 +388,21 @@ export declare class ChatSession {
382
388
  * and a response promise.
383
389
  */
384
390
  sendMessageStream(request: string | Array<string | Part>, singleRequestOptions?: SingleRequestOptions): Promise<GenerateContentStreamResult>;
391
+ /**
392
+ * Get function calls that the SDK has references to actually call.
393
+ * This is all-or-nothing. If the model is requesting multiple
394
+ * function calls, all of them must have references in order for
395
+ * automatic function calling to work.
396
+ *
397
+ * @internal
398
+ */
399
+ _getCallableFunctionCalls(response?: GenerateContentResponse): FunctionCall[] | undefined;
400
+ /**
401
+ * Call user-defined functions if requested by the model, and return
402
+ * the response that should be sent to the model.
403
+ * @internal
404
+ */
405
+ _callFunctionsAsNeeded(functionCalls: FunctionCall[]): Promise<FunctionResponsePart[]>;
385
406
  }
386
407
 
387
408
  /**
@@ -866,6 +887,11 @@ export declare interface FunctionDeclaration {
866
887
  * case-sensitive. For a function with no parameters, this can be left unset.
867
888
  */
868
889
  parameters?: ObjectSchema | ObjectSchemaRequest;
890
+ /**
891
+ * Reference to an actual function to call. Specifying this will cause the
892
+ * function to be called automatically when requested by the model.
893
+ */
894
+ functionReference?: Function;
869
895
  }
870
896
 
871
897
  /**
@@ -908,6 +934,7 @@ export declare interface FunctionResponse {
908
934
  id?: string;
909
935
  name: string;
910
936
  response: object;
937
+ parts?: Part[];
911
938
  }
912
939
 
913
940
  /**
@@ -2676,6 +2703,15 @@ export declare interface RequestOptions {
2676
2703
  * (used regardless of your chosen Gemini API provider).
2677
2704
  */
2678
2705
  baseUrl?: string;
2706
+ /**
2707
+ * Limits amount of sequential function calls the SDK can make during automatic
2708
+ * function calling, in order to prevent infinite loops. If not specified,
2709
+ * this value defaults to 10.
2710
+ *
2711
+ * When it reaches this limit, it will return the last response received
2712
+ * from the model, whether it is a text response or further function calls.
2713
+ */
2714
+ maxSequentalFunctionCalls?: number;
2679
2715
  }
2680
2716
 
2681
2717
  /**
@@ -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
@@ -1776,6 +1776,9 @@ function getText(response, partFilter) {
1776
1776
  * Returns every {@link FunctionCall} associated with first candidate.
1777
1777
  */
1778
1778
  function getFunctionCalls(response) {
1779
+ if (!response) {
1780
+ return undefined;
1781
+ }
1779
1782
  const functionCalls = [];
1780
1783
  if (response.candidates?.[0].content?.parts) {
1781
1784
  for (const part of response.candidates?.[0].content?.parts) {
@@ -2071,15 +2074,44 @@ const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/;
2071
2074
  *
2072
2075
  * @param response - Response from a fetch call
2073
2076
  */
2074
- function processStream(response, apiSettings, inferenceSource) {
2077
+ async function processStream(response, apiSettings, inferenceSource) {
2075
2078
  const inputStream = response.body.pipeThrough(new TextDecoderStream('utf8', { fatal: true }));
2076
2079
  const responseStream = getResponseStream(inputStream);
2077
2080
  // We split the stream so the user can iterate over partial results (stream1)
2078
2081
  // while we aggregate the full result for history/final response (stream2).
2079
2082
  const [stream1, stream2] = responseStream.tee();
2083
+ const { response: internalResponse, firstValue } = await processStreamInternal(stream2, apiSettings, inferenceSource);
2080
2084
  return {
2081
2085
  stream: generateResponseSequence(stream1, apiSettings, inferenceSource),
2082
- response: getResponsePromise(stream2, apiSettings, inferenceSource)
2086
+ response: internalResponse,
2087
+ firstValue
2088
+ };
2089
+ }
2090
+ /**
2091
+ * Consumes streams teed from the input stream for internal needs.
2092
+ * The streams need to be teed because each stream can only be consumed
2093
+ * by one reader.
2094
+ *
2095
+ * "streamForPeek"
2096
+ * This tee is used to peek at the first value for relevant information
2097
+ * that we need to evaluate before returning the stream handle to the
2098
+ * client. For example, we need to check if the response is a function
2099
+ * call that may need to be handled by automatic function calling before
2100
+ * returning a response to the client.
2101
+ *
2102
+ * "streamForAggregation"
2103
+ * We iterate through this tee independently from the user and aggregate
2104
+ * it into a single response when the stream is complete. We need this
2105
+ * aggregate object to add to chat history when using ChatSession. It's
2106
+ * also provided to the user if they want it.
2107
+ */
2108
+ async function processStreamInternal(stream, apiSettings, inferenceSource) {
2109
+ const [streamForPeek, streamForAggregation] = stream.tee();
2110
+ const reader = streamForPeek.getReader();
2111
+ const { value } = await reader.read();
2112
+ return {
2113
+ firstValue: value,
2114
+ response: getResponsePromise(streamForAggregation, apiSettings, inferenceSource)
2083
2115
  };
2084
2116
  }
2085
2117
  async function getResponsePromise(stream, apiSettings, inferenceSource) {
@@ -2650,6 +2682,11 @@ function validateChatHistory(history) {
2650
2682
  * by the user, preventing duplicate console logs.
2651
2683
  */
2652
2684
  const SILENT_ERROR = 'SILENT_ERROR';
2685
+ /**
2686
+ * Prevent infinite loop if the model continues to request sequential
2687
+ * function calls during automatic function calling.
2688
+ */
2689
+ const DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS = 10;
2653
2690
  /**
2654
2691
  * ChatSession class that enables sending chat messages and stores
2655
2692
  * history of sent and received messages so far.
@@ -2684,48 +2721,89 @@ class ChatSession {
2684
2721
  return this._history;
2685
2722
  }
2686
2723
  /**
2687
- * Sends a chat message and receives a non-streaming
2688
- * {@link GenerateContentResult}
2724
+ * Format Content into a request for generateContent or
2725
+ * generateContentStream.
2726
+ * @internal
2689
2727
  */
2690
- async sendMessage(request, singleRequestOptions) {
2691
- await this._sendPromise;
2692
- const newContent = formatNewContent(request);
2693
- const generateContentRequest = {
2728
+ _formatRequest(incomingContent, tempHistory) {
2729
+ return {
2694
2730
  safetySettings: this.params?.safetySettings,
2695
2731
  generationConfig: this.params?.generationConfig,
2696
2732
  tools: this.params?.tools,
2697
2733
  toolConfig: this.params?.toolConfig,
2698
2734
  systemInstruction: this.params?.systemInstruction,
2699
- contents: [...this._history, newContent]
2735
+ contents: [...this._history, ...tempHistory, incomingContent]
2700
2736
  };
2737
+ }
2738
+ /**
2739
+ * Sends a chat message and receives a non-streaming
2740
+ * {@link GenerateContentResult}
2741
+ */
2742
+ async sendMessage(request, singleRequestOptions) {
2701
2743
  let finalResult = {};
2702
- this._sendPromise = this._sendPromise
2703
- .then(() => generateContent(this._apiSettings, this.model, generateContentRequest, this.chromeAdapter, {
2704
- ...this.requestOptions,
2705
- ...singleRequestOptions
2706
- }))
2707
- .then(result => {
2708
- // TODO: Make this update atomic. If creating `responseContent` throws,
2709
- // history will contain the user message but not the response, causing
2710
- // validation errors on the next request.
2711
- if (result.response.candidates &&
2712
- result.response.candidates.length > 0) {
2713
- this._history.push(newContent);
2714
- const responseContent = {
2715
- parts: result.response.candidates?.[0].content.parts || [],
2716
- role: result.response.candidates?.[0].content.role || 'model'
2717
- };
2718
- this._history.push(responseContent);
2719
- }
2720
- else {
2721
- const blockErrorMessage = formatBlockErrorMessage(result.response);
2722
- if (blockErrorMessage) {
2723
- logger.warn(`sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`);
2744
+ await this._sendPromise;
2745
+ /**
2746
+ * Temporarily store multiple turns for cases like automatic function
2747
+ * calling, only writing them to official history when the entire
2748
+ * sequence has completed successfully.
2749
+ */
2750
+ const tempHistory = [];
2751
+ this._sendPromise = this._sendPromise.then(async () => {
2752
+ let functionCalls;
2753
+ let functionCallTurnCount = 0;
2754
+ const functionCallMaxTurns = this.requestOptions?.maxSequentalFunctionCalls ??
2755
+ DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS;
2756
+ // Repeats until model returns a response with no function calls
2757
+ // or until `functionCallMaxTurns` is met or exceeded.
2758
+ do {
2759
+ let formattedContent;
2760
+ if (functionCalls) {
2761
+ functionCallTurnCount++;
2762
+ const functionResponseParts = await this._callFunctionsAsNeeded(functionCalls);
2763
+ formattedContent = formatNewContent(functionResponseParts);
2764
+ }
2765
+ else {
2766
+ formattedContent = formatNewContent(request);
2724
2767
  }
2768
+ const formattedRequest = this._formatRequest(formattedContent, tempHistory);
2769
+ tempHistory.push(formattedContent);
2770
+ const result = await generateContent(this._apiSettings, this.model, formattedRequest, this.chromeAdapter, {
2771
+ ...this.requestOptions,
2772
+ ...singleRequestOptions
2773
+ });
2774
+ if (result) {
2775
+ finalResult = result;
2776
+ functionCalls = this._getCallableFunctionCalls(result.response);
2777
+ if (result.response.candidates &&
2778
+ result.response.candidates.length > 0) {
2779
+ // TODO: Make this update atomic. If creating `responseContent` throws,
2780
+ // history will contain the user message but not the response, causing
2781
+ // validation errors on the next request.
2782
+ const responseContent = {
2783
+ parts: result.response.candidates?.[0].content.parts || [],
2784
+ // Response seems to come back without a role set.
2785
+ role: result.response.candidates?.[0].content.role || 'model'
2786
+ };
2787
+ tempHistory.push(responseContent);
2788
+ }
2789
+ else {
2790
+ const blockErrorMessage = formatBlockErrorMessage(result.response);
2791
+ if (blockErrorMessage) {
2792
+ logger.warn(`sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`);
2793
+ }
2794
+ }
2795
+ }
2796
+ else {
2797
+ functionCalls = undefined;
2798
+ }
2799
+ } while (functionCalls && functionCallTurnCount < functionCallMaxTurns);
2800
+ if (functionCalls && functionCallTurnCount >= functionCallMaxTurns) {
2801
+ logger.warn(`Automatic function calling exceeded the limit of` +
2802
+ ` ${functionCallMaxTurns} function calls. Returning last model response.`);
2725
2803
  }
2726
- finalResult = result;
2727
2804
  });
2728
2805
  await this._sendPromise;
2806
+ this._history = this._history.concat(tempHistory);
2729
2807
  return finalResult;
2730
2808
  }
2731
2809
  /**
@@ -2735,23 +2813,62 @@ class ChatSession {
2735
2813
  */
2736
2814
  async sendMessageStream(request, singleRequestOptions) {
2737
2815
  await this._sendPromise;
2738
- const newContent = formatNewContent(request);
2739
- const generateContentRequest = {
2740
- safetySettings: this.params?.safetySettings,
2741
- generationConfig: this.params?.generationConfig,
2742
- tools: this.params?.tools,
2743
- toolConfig: this.params?.toolConfig,
2744
- systemInstruction: this.params?.systemInstruction,
2745
- contents: [...this._history, newContent]
2816
+ /**
2817
+ * Temporarily store multiple turns for cases like automatic function
2818
+ * calling, only writing them to official history when the entire
2819
+ * sequence has completed successfully.
2820
+ */
2821
+ const tempHistory = [];
2822
+ const callGenerateContentStream = async () => {
2823
+ let functionCalls;
2824
+ let functionCallTurnCount = 0;
2825
+ const functionCallMaxTurns = this.requestOptions?.maxSequentalFunctionCalls ??
2826
+ DEFAULT_MAX_SEQUENTIAL_FUNCTION_CALLS;
2827
+ let result;
2828
+ // Repeats until model returns a response with no function calls
2829
+ // or until `functionCallMaxTurns` is met or exceeded.
2830
+ do {
2831
+ let formattedContent;
2832
+ if (functionCalls) {
2833
+ functionCallTurnCount++;
2834
+ const functionResponseParts = await this._callFunctionsAsNeeded(functionCalls);
2835
+ formattedContent = formatNewContent(functionResponseParts);
2836
+ }
2837
+ else {
2838
+ formattedContent = formatNewContent(request);
2839
+ }
2840
+ tempHistory.push(formattedContent);
2841
+ const formattedRequest = this._formatRequest(formattedContent, tempHistory);
2842
+ result = await generateContentStream(this._apiSettings, this.model, formattedRequest, this.chromeAdapter, {
2843
+ ...this.requestOptions,
2844
+ ...singleRequestOptions
2845
+ });
2846
+ functionCalls = this._getCallableFunctionCalls(result.firstValue);
2847
+ if (functionCalls &&
2848
+ result.firstValue &&
2849
+ result.firstValue.candidates &&
2850
+ result.firstValue.candidates.length > 0) {
2851
+ const responseContent = {
2852
+ ...result.firstValue.candidates[0].content
2853
+ };
2854
+ if (!responseContent.role) {
2855
+ responseContent.role = 'model';
2856
+ }
2857
+ tempHistory.push(responseContent);
2858
+ }
2859
+ } while (functionCalls && functionCallTurnCount < functionCallMaxTurns);
2860
+ if (functionCalls && functionCallTurnCount >= functionCallMaxTurns) {
2861
+ logger.warn(`Automatic function calling exceeded the limit of` +
2862
+ ` ${functionCallMaxTurns} function calls. Returning last model response.`);
2863
+ }
2864
+ return { stream: result.stream, response: result.response };
2746
2865
  };
2747
- const streamPromise = generateContentStream(this._apiSettings, this.model, generateContentRequest, this.chromeAdapter, {
2748
- ...this.requestOptions,
2749
- ...singleRequestOptions
2750
- });
2751
- // We hook into the chain to update history, but we don't block the
2752
- // return of `streamPromise` to the user.
2866
+ const streamPromise = callGenerateContentStream();
2867
+ // Add onto the chain.
2753
2868
  this._sendPromise = this._sendPromise
2754
- .then(() => streamPromise)
2869
+ .then(async () => streamPromise)
2870
+ // This must be handled to avoid unhandled rejection, but jump
2871
+ // to the final catch block with a label to not log this error.
2755
2872
  .catch(_ignored => {
2756
2873
  // If the initial fetch fails, the user's `streamPromise` rejects.
2757
2874
  // We swallow the error here to prevent double logging in the final catch.
@@ -2764,7 +2881,7 @@ class ChatSession {
2764
2881
  // TODO: Move response validation logic upstream to `stream-reader` so
2765
2882
  // errors propagate to the user's `result.response` promise.
2766
2883
  if (response.candidates && response.candidates.length > 0) {
2767
- this._history.push(newContent);
2884
+ this._history = this._history.concat(tempHistory);
2768
2885
  // TODO: Validate that `response.candidates[0].content` is not null.
2769
2886
  const responseContent = { ...response.candidates[0].content };
2770
2887
  if (!responseContent.role) {
@@ -2787,6 +2904,75 @@ class ChatSession {
2787
2904
  });
2788
2905
  return streamPromise;
2789
2906
  }
2907
+ /**
2908
+ * Get function calls that the SDK has references to actually call.
2909
+ * This is all-or-nothing. If the model is requesting multiple
2910
+ * function calls, all of them must have references in order for
2911
+ * automatic function calling to work.
2912
+ *
2913
+ * @internal
2914
+ */
2915
+ _getCallableFunctionCalls(response) {
2916
+ const functionDeclarationsTool = this.params?.tools?.find(tool => tool.functionDeclarations);
2917
+ if (!functionDeclarationsTool?.functionDeclarations) {
2918
+ return;
2919
+ }
2920
+ const functionCalls = getFunctionCalls(response);
2921
+ if (!functionCalls) {
2922
+ return;
2923
+ }
2924
+ for (const functionCall of functionCalls) {
2925
+ const hasFunctionReference = functionDeclarationsTool.functionDeclarations?.some(declaration => declaration.name === functionCall.name &&
2926
+ typeof declaration.functionReference === 'function');
2927
+ if (!hasFunctionReference) {
2928
+ return;
2929
+ }
2930
+ }
2931
+ return functionCalls;
2932
+ }
2933
+ /**
2934
+ * Call user-defined functions if requested by the model, and return
2935
+ * the response that should be sent to the model.
2936
+ * @internal
2937
+ */
2938
+ async _callFunctionsAsNeeded(functionCalls) {
2939
+ const activeCallList = new Map();
2940
+ const promiseList = [];
2941
+ const functionDeclarationsTool = this.params?.tools?.find(tool => tool.functionDeclarations);
2942
+ if (functionDeclarationsTool &&
2943
+ functionDeclarationsTool.functionDeclarations) {
2944
+ for (const functionCall of functionCalls) {
2945
+ const functionDeclaration = functionDeclarationsTool.functionDeclarations.find(declaration => declaration.name === functionCall.name);
2946
+ if (functionDeclaration?.functionReference) {
2947
+ const results = Promise.resolve(functionDeclaration.functionReference(functionCall.args)).catch(e => {
2948
+ const wrappedError = new AIError(AIErrorCode.ERROR, `Error in user-defined function "${functionDeclaration.name}": ${e.message}`);
2949
+ wrappedError.stack = e.stack;
2950
+ throw wrappedError;
2951
+ });
2952
+ activeCallList.set(functionCall.name, {
2953
+ id: functionCall.id,
2954
+ results
2955
+ });
2956
+ promiseList.push(results);
2957
+ }
2958
+ }
2959
+ // Wait for promises to finish.
2960
+ await Promise.all(promiseList);
2961
+ const functionResponseParts = [];
2962
+ for (const [name, callData] of activeCallList) {
2963
+ functionResponseParts.push({
2964
+ functionResponse: {
2965
+ name,
2966
+ response: await callData.results
2967
+ }
2968
+ });
2969
+ }
2970
+ return functionResponseParts;
2971
+ }
2972
+ else {
2973
+ throw new AIError(AIErrorCode.REQUEST_ERROR, `No function declarations were provided in "tools".`);
2974
+ }
2975
+ }
2790
2976
  }
2791
2977
 
2792
2978
  /**
@@ -2890,7 +3076,7 @@ class GenerativeModel extends AIModel {
2890
3076
  */
2891
3077
  async generateContentStream(request, singleRequestOptions) {
2892
3078
  const formattedParams = formatGenerateContentInput(request);
2893
- return generateContentStream(this._apiSettings, this.model, {
3079
+ const { stream, response } = await generateContentStream(this._apiSettings, this.model, {
2894
3080
  generationConfig: this.generationConfig,
2895
3081
  safetySettings: this.safetySettings,
2896
3082
  tools: this.tools,
@@ -2903,6 +3089,7 @@ class GenerativeModel extends AIModel {
2903
3089
  ...this.requestOptions,
2904
3090
  ...singleRequestOptions
2905
3091
  });
3092
+ return { stream, response };
2906
3093
  }
2907
3094
  /**
2908
3095
  * Gets a new {@link ChatSession} instance which can be used for