@ai-sdk/google 3.0.60 → 3.0.61

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/google",
3
- "version": "3.0.60",
3
+ "version": "3.0.61",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -44,8 +44,8 @@
44
44
  "tsup": "^8",
45
45
  "typescript": "5.8.3",
46
46
  "zod": "3.25.76",
47
- "@ai-sdk/test-server": "1.0.3",
48
- "@vercel/ai-tsconfig": "0.0.0"
47
+ "@vercel/ai-tsconfig": "0.0.0",
48
+ "@ai-sdk/test-server": "1.0.3"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "zod": "^3.25.76 || ^4.1.8"
@@ -44,6 +44,7 @@ import {
44
44
  GoogleGenerativeAIProviderMetadata,
45
45
  } from './google-generative-ai-prompt';
46
46
  import { prepareTools } from './google-prepare-tools';
47
+ import { GoogleJSONAccumulator, PartialArg } from './google-json-accumulator';
47
48
  import { mapGoogleGenerativeAIFinishReason } from './map-google-generative-ai-finish-reason';
48
49
 
49
50
  type GoogleGenerativeAIConfig = {
@@ -119,12 +120,14 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
119
120
  }
120
121
 
121
122
  // Add warning if Vertex rag tools are used with a non-Vertex Google provider
123
+ const isVertexProvider = this.config.provider.startsWith('google.vertex.');
124
+
122
125
  if (
123
126
  tools?.some(
124
127
  tool =>
125
128
  tool.type === 'provider' && tool.id === 'google.vertex_rag_store',
126
129
  ) &&
127
- !this.config.provider.startsWith('google.vertex.')
130
+ !isVertexProvider
128
131
  ) {
129
132
  warnings.push({
130
133
  type: 'other',
@@ -135,6 +138,16 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
135
138
  });
136
139
  }
137
140
 
141
+ if (googleOptions?.streamFunctionCallArguments && !isVertexProvider) {
142
+ warnings.push({
143
+ type: 'other',
144
+ message:
145
+ "'streamFunctionCallArguments' is only supported on the Vertex AI API " +
146
+ 'and will be ignored with the current Google provider ' +
147
+ `(${this.config.provider}). See https://docs.cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling#streaming-fc`,
148
+ });
149
+ }
150
+
138
151
  const isGemmaModel = this.modelId.toLowerCase().startsWith('gemma-');
139
152
  const supportsFunctionResponseParts = this.modelId.startsWith('gemini-3');
140
153
 
@@ -157,6 +170,28 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
157
170
  modelId: this.modelId,
158
171
  });
159
172
 
173
+ const streamFunctionCallArguments = isVertexProvider
174
+ ? (googleOptions?.streamFunctionCallArguments ?? true)
175
+ : undefined;
176
+
177
+ const toolConfig =
178
+ googleToolConfig ||
179
+ streamFunctionCallArguments ||
180
+ googleOptions?.retrievalConfig
181
+ ? {
182
+ ...googleToolConfig,
183
+ ...(streamFunctionCallArguments && {
184
+ functionCallingConfig: {
185
+ ...googleToolConfig?.functionCallingConfig,
186
+ streamFunctionCallArguments: true as const,
187
+ },
188
+ }),
189
+ ...(googleOptions?.retrievalConfig && {
190
+ retrievalConfig: googleOptions.retrievalConfig,
191
+ }),
192
+ }
193
+ : undefined;
194
+
160
195
  return {
161
196
  args: {
162
197
  generationConfig: {
@@ -200,12 +235,7 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
200
235
  systemInstruction: isGemmaModel ? undefined : systemInstruction,
201
236
  safetySettings: googleOptions?.safetySettings,
202
237
  tools: googleTools,
203
- toolConfig: googleOptions?.retrievalConfig
204
- ? {
205
- ...googleToolConfig,
206
- retrievalConfig: googleOptions.retrievalConfig,
207
- }
208
- : googleToolConfig,
238
+ toolConfig,
209
239
  cachedContent: googleOptions?.cachedContent,
210
240
  labels: googleOptions?.labels,
211
241
  serviceTier: googleOptions?.serviceTier,
@@ -301,7 +331,11 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
301
331
  providerMetadata: thoughtSignatureMetadata,
302
332
  });
303
333
  }
304
- } else if ('functionCall' in part) {
334
+ } else if (
335
+ 'functionCall' in part &&
336
+ part.functionCall.name != null &&
337
+ part.functionCall.args != null
338
+ ) {
305
339
  content.push({
306
340
  type: 'tool-call' as const,
307
341
  toolCallId: this.config.generateId(),
@@ -478,6 +512,13 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
478
512
  // Associates a server-side tool response with its preceding call (tool combination).
479
513
  let lastServerToolCallId: string | undefined;
480
514
 
515
+ const activeStreamingToolCalls: Array<{
516
+ toolCallId: string;
517
+ toolName: string;
518
+ accumulator: GoogleJSONAccumulator;
519
+ providerMetadata?: SharedV3ProviderMetadata;
520
+ }> = [];
521
+
481
522
  return {
482
523
  stream: response.pipeThrough(
483
524
  new TransformStream<
@@ -735,40 +776,152 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
735
776
  }
736
777
  }
737
778
 
738
- const toolCallDeltas = getToolCallsFromParts({
739
- parts: content.parts,
740
- generateId,
741
- providerOptionsName,
742
- });
779
+ // Handle streaming and complete function calls
780
+ for (const part of parts) {
781
+ if (!('functionCall' in part)) continue;
782
+
783
+ const providerMeta = part.thoughtSignature
784
+ ? {
785
+ [providerOptionsName]: {
786
+ thoughtSignature: part.thoughtSignature,
787
+ },
788
+ }
789
+ : undefined;
790
+
791
+ const isStreamingChunk =
792
+ part.functionCall.partialArgs != null ||
793
+ (part.functionCall.name != null &&
794
+ part.functionCall.willContinue === true);
795
+ const isTerminalChunk =
796
+ part.functionCall.name == null &&
797
+ part.functionCall.args == null &&
798
+ part.functionCall.partialArgs == null &&
799
+ part.functionCall.willContinue == null;
800
+ const isCompleteCall =
801
+ part.functionCall.name != null &&
802
+ part.functionCall.args != null &&
803
+ part.functionCall.partialArgs == null;
804
+
805
+ if (isStreamingChunk) {
806
+ if (
807
+ part.functionCall.name != null &&
808
+ part.functionCall.willContinue === true
809
+ ) {
810
+ const toolCallId = generateId();
811
+ const accumulator = new GoogleJSONAccumulator();
812
+ activeStreamingToolCalls.push({
813
+ toolCallId,
814
+ toolName: part.functionCall.name,
815
+ accumulator,
816
+ providerMetadata: providerMeta,
817
+ });
818
+
819
+ controller.enqueue({
820
+ type: 'tool-input-start',
821
+ id: toolCallId,
822
+ toolName: part.functionCall.name,
823
+ providerMetadata: providerMeta,
824
+ });
825
+
826
+ if (part.functionCall.partialArgs != null) {
827
+ const { textDelta } = accumulator.processPartialArgs(
828
+ part.functionCall.partialArgs as PartialArg[],
829
+ );
830
+ if (textDelta.length > 0) {
831
+ controller.enqueue({
832
+ type: 'tool-input-delta',
833
+ id: toolCallId,
834
+ delta: textDelta,
835
+ providerMetadata: providerMeta,
836
+ });
837
+ }
838
+ }
839
+ } else if (
840
+ part.functionCall.partialArgs != null &&
841
+ activeStreamingToolCalls.length > 0
842
+ ) {
843
+ const active =
844
+ activeStreamingToolCalls[
845
+ activeStreamingToolCalls.length - 1
846
+ ];
847
+ const { textDelta } = active.accumulator.processPartialArgs(
848
+ part.functionCall.partialArgs as PartialArg[],
849
+ );
850
+ if (textDelta.length > 0) {
851
+ controller.enqueue({
852
+ type: 'tool-input-delta',
853
+ id: active.toolCallId,
854
+ delta: textDelta,
855
+ providerMetadata: providerMeta,
856
+ });
857
+ }
858
+ }
859
+ } else if (
860
+ isTerminalChunk &&
861
+ activeStreamingToolCalls.length > 0
862
+ ) {
863
+ const active = activeStreamingToolCalls.pop()!;
864
+ const { finalJSON, closingDelta } =
865
+ active.accumulator.finalize();
866
+
867
+ if (closingDelta.length > 0) {
868
+ controller.enqueue({
869
+ type: 'tool-input-delta',
870
+ id: active.toolCallId,
871
+ delta: closingDelta,
872
+ providerMetadata: active.providerMetadata,
873
+ });
874
+ }
875
+
876
+ controller.enqueue({
877
+ type: 'tool-input-end',
878
+ id: active.toolCallId,
879
+ providerMetadata: active.providerMetadata,
880
+ });
881
+
882
+ controller.enqueue({
883
+ type: 'tool-call',
884
+ toolCallId: active.toolCallId,
885
+ toolName: active.toolName,
886
+ input: finalJSON,
887
+ providerMetadata: active.providerMetadata,
888
+ });
889
+
890
+ hasToolCalls = true;
891
+ } else if (isCompleteCall) {
892
+ const toolCallId = generateId();
893
+ const toolName = part.functionCall.name!;
894
+ const args =
895
+ typeof part.functionCall.args === 'string'
896
+ ? part.functionCall.args
897
+ : JSON.stringify(part.functionCall.args ?? {});
743
898
 
744
- if (toolCallDeltas != null) {
745
- for (const toolCall of toolCallDeltas) {
746
899
  controller.enqueue({
747
900
  type: 'tool-input-start',
748
- id: toolCall.toolCallId,
749
- toolName: toolCall.toolName,
750
- providerMetadata: toolCall.providerMetadata,
901
+ id: toolCallId,
902
+ toolName,
903
+ providerMetadata: providerMeta,
751
904
  });
752
905
 
753
906
  controller.enqueue({
754
907
  type: 'tool-input-delta',
755
- id: toolCall.toolCallId,
756
- delta: toolCall.args,
757
- providerMetadata: toolCall.providerMetadata,
908
+ id: toolCallId,
909
+ delta: args,
910
+ providerMetadata: providerMeta,
758
911
  });
759
912
 
760
913
  controller.enqueue({
761
914
  type: 'tool-input-end',
762
- id: toolCall.toolCallId,
763
- providerMetadata: toolCall.providerMetadata,
915
+ id: toolCallId,
916
+ providerMetadata: providerMeta,
764
917
  });
765
918
 
766
919
  controller.enqueue({
767
920
  type: 'tool-call',
768
- toolCallId: toolCall.toolCallId,
769
- toolName: toolCall.toolName,
770
- input: toolCall.args,
771
- providerMetadata: toolCall.providerMetadata,
921
+ toolCallId,
922
+ toolName,
923
+ input: args,
924
+ providerMetadata: providerMeta,
772
925
  });
773
926
 
774
927
  hasToolCalls = true;
@@ -1040,6 +1193,15 @@ export const getGroundingMetadataSchema = () =>
1040
1193
  .nullish(),
1041
1194
  });
1042
1195
 
1196
+ const partialArgSchema = z.object({
1197
+ jsonPath: z.string(),
1198
+ stringValue: z.string().nullish(),
1199
+ numberValue: z.number().nullish(),
1200
+ boolValue: z.boolean().nullish(),
1201
+ nullValue: z.unknown().nullish(),
1202
+ willContinue: z.boolean().nullish(),
1203
+ });
1204
+
1043
1205
  const getContentSchema = () =>
1044
1206
  z.object({
1045
1207
  parts: z
@@ -1048,8 +1210,10 @@ const getContentSchema = () =>
1048
1210
  // note: order matters since text can be fully empty
1049
1211
  z.object({
1050
1212
  functionCall: z.object({
1051
- name: z.string(),
1052
- args: z.unknown(),
1213
+ name: z.string().nullish(),
1214
+ args: z.unknown().nullish(),
1215
+ partialArgs: z.array(partialArgSchema).nullish(),
1216
+ willContinue: z.boolean().nullish(),
1053
1217
  }),
1054
1218
  thoughtSignature: z.string().nullish(),
1055
1219
  }),
@@ -189,6 +189,17 @@ export const googleLanguageModelOptions = lazySchema(() =>
189
189
  })
190
190
  .optional(),
191
191
 
192
+ /**
193
+ * Optional. When set to true, function call arguments will be streamed
194
+ * incrementally via partialArgs in streaming responses. Only supported
195
+ * on the Vertex AI API (not the Gemini API).
196
+ *
197
+ * @default true
198
+ *
199
+ * https://docs.cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling#streaming-fc
200
+ */
201
+ streamFunctionCallArguments: z.boolean().optional(),
202
+
192
203
  /**
193
204
  * Optional. The service tier to use for the request.
194
205
  */
@@ -0,0 +1,336 @@
1
+ export type PartialArg = {
2
+ jsonPath: string;
3
+ stringValue?: string | null;
4
+ numberValue?: number | null;
5
+ boolValue?: boolean | null;
6
+ nullValue?: unknown;
7
+ willContinue?: boolean | null;
8
+ };
9
+
10
+ type PathSegment = string | number;
11
+
12
+ type StackEntry = {
13
+ segment: PathSegment;
14
+ isArray: boolean;
15
+ childCount: number;
16
+ };
17
+
18
+ /**
19
+ * Incrementally builds a JSON object from Google's streaming `partialArgs`
20
+ * chunks emitted during tool-call function calling. Tracks both the structured
21
+ * object and a running JSON text representation so callers can emit text deltas
22
+ * that, when concatenated, form valid nested JSON matching JSON.stringify output.
23
+ *
24
+ * Input: [{jsonPath:"$.location",stringValue:"Boston"}]
25
+ * Output: '{"location":"Boston"', then finalize() → closingDelta='}'
26
+ */
27
+ export class GoogleJSONAccumulator {
28
+ private accumulatedArgs: Record<string, unknown> = {};
29
+ private jsonText = '';
30
+
31
+ /**
32
+ * Stack representing the currently "open" containers in the JSON output.
33
+ * Entry 0 is always the root `{` object once the first value is written.
34
+ */
35
+ private pathStack: StackEntry[] = [];
36
+
37
+ /**
38
+ * Whether a string value is currently "open" (willContinue was true),
39
+ * meaning the closing quote has not yet been emitted.
40
+ */
41
+ private stringOpen = false;
42
+
43
+ /**
44
+ * Input: [{jsonPath:"$.brightness",numberValue:50}]
45
+ * Output: { currentJSON:{brightness:50}, textDelta:'{"brightness":50' }
46
+ */
47
+ processPartialArgs(partialArgs: PartialArg[]): {
48
+ currentJSON: Record<string, unknown>;
49
+ textDelta: string;
50
+ } {
51
+ let delta = '';
52
+
53
+ for (const arg of partialArgs) {
54
+ const rawPath = arg.jsonPath.replace(/^\$\./, '');
55
+ if (!rawPath) continue;
56
+
57
+ const segments = parsePath(rawPath);
58
+
59
+ const existingValue = getNestedValue(this.accumulatedArgs, segments);
60
+ const isStringContinuation =
61
+ arg.stringValue != null && existingValue !== undefined;
62
+
63
+ if (isStringContinuation) {
64
+ const escaped = JSON.stringify(arg.stringValue).slice(1, -1);
65
+ setNestedValue(
66
+ this.accumulatedArgs,
67
+ segments,
68
+ (existingValue as string) + arg.stringValue,
69
+ );
70
+ delta += escaped;
71
+ continue;
72
+ }
73
+
74
+ const resolved = resolvePartialArgValue(arg);
75
+ if (resolved == null) continue;
76
+
77
+ setNestedValue(this.accumulatedArgs, segments, resolved.value);
78
+ delta += this.emitNavigationTo(segments, arg, resolved.json);
79
+ }
80
+
81
+ this.jsonText += delta;
82
+
83
+ return {
84
+ currentJSON: this.accumulatedArgs,
85
+ textDelta: delta,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Input: jsonText='{"brightness":50', accumulatedArgs={brightness:50}
91
+ * Output: { finalJSON:'{"brightness":50}', closingDelta:'}' }
92
+ */
93
+ finalize(): { finalJSON: string; closingDelta: string } {
94
+ const finalArgs = JSON.stringify(this.accumulatedArgs);
95
+ const closingDelta = finalArgs.slice(this.jsonText.length);
96
+ return { finalJSON: finalArgs, closingDelta };
97
+ }
98
+
99
+ /**
100
+ * Input: pathStack=[] (first call) or pathStack=[root,...] (subsequent calls)
101
+ * Output: '{' (first call) or '' (subsequent calls)
102
+ */
103
+ private ensureRoot(): string {
104
+ if (this.pathStack.length === 0) {
105
+ this.pathStack.push({ segment: '', isArray: false, childCount: 0 });
106
+ return '{';
107
+ }
108
+ return '';
109
+ }
110
+
111
+ /**
112
+ * Emits the JSON text fragment needed to navigate from the current open
113
+ * path to the new leaf at `targetSegments`, then writes the value.
114
+ *
115
+ * Input: targetSegments=["recipe","name"], arg={jsonPath:"$.recipe.name",stringValue:"Lasagna"}, valueJson='"Lasagna"'
116
+ * Output: '{"recipe":{"name":"Lasagna"'
117
+ */
118
+ private emitNavigationTo(
119
+ targetSegments: PathSegment[],
120
+ arg: PartialArg,
121
+ valueJson: string,
122
+ ): string {
123
+ let fragment = '';
124
+
125
+ if (this.stringOpen) {
126
+ fragment += '"';
127
+ this.stringOpen = false;
128
+ }
129
+
130
+ fragment += this.ensureRoot();
131
+
132
+ const targetContainerSegments = targetSegments.slice(0, -1);
133
+ const leafSegment = targetSegments[targetSegments.length - 1];
134
+
135
+ const commonDepth = this.findCommonStackDepth(targetContainerSegments);
136
+
137
+ fragment += this.closeDownTo(commonDepth);
138
+ fragment += this.openDownTo(targetContainerSegments, leafSegment);
139
+ fragment += this.emitLeaf(leafSegment, arg, valueJson);
140
+
141
+ return fragment;
142
+ }
143
+
144
+ /**
145
+ * Returns the stack depth to preserve when navigating to a new target
146
+ * container path. Always >= 1 (the root is never popped).
147
+ *
148
+ * Input: stack=[root,"recipe","ingredients",0], target=["recipe","ingredients",1]
149
+ * Output: 3 (keep root+"recipe"+"ingredients")
150
+ */
151
+ private findCommonStackDepth(targetContainer: PathSegment[]): number {
152
+ const maxDepth = Math.min(
153
+ this.pathStack.length - 1,
154
+ targetContainer.length,
155
+ );
156
+ let common = 0;
157
+ for (let i = 0; i < maxDepth; i++) {
158
+ if (this.pathStack[i + 1].segment === targetContainer[i]) {
159
+ common++;
160
+ } else {
161
+ break;
162
+ }
163
+ }
164
+ return common + 1;
165
+ }
166
+
167
+ /**
168
+ * Closes containers from the current stack depth back down to `targetDepth`.
169
+ *
170
+ * Input: this.pathStack=[root,"recipe","ingredients",0], targetDepth=3
171
+ * Output: '}'
172
+ */
173
+ private closeDownTo(targetDepth: number): string {
174
+ let fragment = '';
175
+ while (this.pathStack.length > targetDepth) {
176
+ const entry = this.pathStack.pop()!;
177
+ fragment += entry.isArray ? ']' : '}';
178
+ }
179
+ return fragment;
180
+ }
181
+
182
+ /**
183
+ * Opens containers from the current stack depth down to the full target
184
+ * container path, emitting opening `{`, `[`, keys, and commas as needed.
185
+ * `leafSegment` is used to determine if the innermost container is an array.
186
+ *
187
+ * Input: this.pathStack=[root], targetContainer=["recipe","ingredients"], leafSegment=0
188
+ * Output: '"recipe":{"ingredients":['
189
+ */
190
+ private openDownTo(
191
+ targetContainer: PathSegment[],
192
+ leafSegment: PathSegment,
193
+ ): string {
194
+ let fragment = '';
195
+
196
+ const startIdx = this.pathStack.length - 1;
197
+
198
+ for (let i = startIdx; i < targetContainer.length; i++) {
199
+ const seg = targetContainer[i];
200
+ const parentEntry = this.pathStack[this.pathStack.length - 1];
201
+
202
+ if (parentEntry.childCount > 0) {
203
+ fragment += ',';
204
+ }
205
+ parentEntry.childCount++;
206
+
207
+ if (typeof seg === 'string') {
208
+ fragment += `${JSON.stringify(seg)}:`;
209
+ }
210
+
211
+ const childSeg =
212
+ i + 1 < targetContainer.length ? targetContainer[i + 1] : leafSegment;
213
+ const isArray = typeof childSeg === 'number';
214
+
215
+ fragment += isArray ? '[' : '{';
216
+
217
+ this.pathStack.push({ segment: seg, isArray, childCount: 0 });
218
+ }
219
+
220
+ return fragment;
221
+ }
222
+
223
+ /**
224
+ * Emits the comma, key, and value for a leaf entry in the current container.
225
+ *
226
+ * Input: leafSegment="name", arg={stringValue:"Lasagna"}, valueJson='"Lasagna"'
227
+ * Output: '"name":"Lasagna"' (or ',"name":"Lasagna"' if container.childCount > 0)
228
+ */
229
+ private emitLeaf(
230
+ leafSegment: PathSegment,
231
+ arg: PartialArg,
232
+ valueJson: string,
233
+ ): string {
234
+ let fragment = '';
235
+ const container = this.pathStack[this.pathStack.length - 1];
236
+
237
+ if (container.childCount > 0) {
238
+ fragment += ',';
239
+ }
240
+ container.childCount++;
241
+
242
+ if (typeof leafSegment === 'string') {
243
+ fragment += `${JSON.stringify(leafSegment)}:`;
244
+ }
245
+
246
+ if (arg.stringValue != null && arg.willContinue) {
247
+ fragment += valueJson.slice(0, -1);
248
+ this.stringOpen = true;
249
+ } else {
250
+ fragment += valueJson;
251
+ }
252
+
253
+ return fragment;
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Splits a dotted/bracketed JSON path like `recipe.ingredients[0].name` into segments.
259
+ *
260
+ * Input: "recipe.ingredients[0].name"
261
+ * Output: ["recipe", "ingredients", 0, "name"]
262
+ */
263
+ function parsePath(rawPath: string): Array<string | number> {
264
+ const segments: Array<string | number> = [];
265
+ for (const part of rawPath.split('.')) {
266
+ const bracketIdx = part.indexOf('[');
267
+ if (bracketIdx === -1) {
268
+ segments.push(part);
269
+ } else {
270
+ if (bracketIdx > 0) segments.push(part.slice(0, bracketIdx));
271
+ for (const m of part.matchAll(/\[(\d+)\]/g)) {
272
+ segments.push(parseInt(m[1], 10));
273
+ }
274
+ }
275
+ }
276
+ return segments;
277
+ }
278
+
279
+ /**
280
+ * Traverses a nested object along the given path segments and returns the leaf value.
281
+ *
282
+ * Input: ({recipe:{name:"Lasagna"}}, ["recipe","name"])
283
+ * Output: "Lasagna"
284
+ */
285
+ function getNestedValue(
286
+ obj: Record<string, unknown>,
287
+ segments: Array<string | number>,
288
+ ): unknown {
289
+ let current: unknown = obj;
290
+ for (const seg of segments) {
291
+ if (current == null || typeof current !== 'object') return undefined;
292
+ current = (current as Record<string | number, unknown>)[seg];
293
+ }
294
+ return current;
295
+ }
296
+
297
+ /**
298
+ * Sets a value at a nested path, creating intermediate objects or arrays as needed.
299
+ *
300
+ * Input: obj={}, segments=["recipe","ingredients",0,"name"], value="Noodles"
301
+ * Output: {recipe:{ingredients:[{name:"Noodles"}]}}
302
+ */
303
+ function setNestedValue(
304
+ obj: Record<string, unknown>,
305
+ segments: Array<string | number>,
306
+ value: unknown,
307
+ ): void {
308
+ let current: Record<string | number, unknown> = obj;
309
+ for (let i = 0; i < segments.length - 1; i++) {
310
+ const seg = segments[i];
311
+ const nextSeg = segments[i + 1];
312
+ if (current[seg] == null) {
313
+ current[seg] = typeof nextSeg === 'number' ? [] : {};
314
+ }
315
+ current = current[seg] as Record<string | number, unknown>;
316
+ }
317
+ current[segments[segments.length - 1]] = value;
318
+ }
319
+
320
+ /**
321
+ * Extracts the first non-null typed value from a partial arg and returns it with its JSON representation.
322
+ *
323
+ * Input: arg={stringValue:"Boston"} or arg={numberValue:50}
324
+ * Output: {value:"Boston", json:'"Boston"'} or {value:50, json:'50'}
325
+ */
326
+ function resolvePartialArgValue(arg: {
327
+ stringValue?: string | null;
328
+ numberValue?: number | null;
329
+ boolValue?: boolean | null;
330
+ nullValue?: unknown;
331
+ }): { value: unknown; json: string } | undefined {
332
+ const value = arg.stringValue ?? arg.numberValue ?? arg.boolValue;
333
+ if (value != null) return { value, json: JSON.stringify(value) };
334
+ if ('nullValue' in arg) return { value: null, json: 'null' };
335
+ return undefined;
336
+ }
@@ -33,6 +33,7 @@ export function prepareTools({
33
33
  functionCallingConfig?: {
34
34
  mode: 'AUTO' | 'NONE' | 'ANY' | 'VALIDATED';
35
35
  allowedFunctionNames?: string[];
36
+ streamFunctionCallArguments?: boolean;
36
37
  };
37
38
  includeServerSideToolInvocations?: boolean;
38
39
  };