@ai-sdk/google 3.0.60 → 3.0.62

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.
@@ -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
  };