@ai-sdk/provider-utils 5.0.49 → 5.0.50
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/CHANGELOG.md +8 -0
- package/dist/index.d.ts +25 -2
- package/dist/index.js +272 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/streaming-tool-call-argument-state.ts +100 -0
- package/src/streaming-tool-call-tracker.ts +313 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/provider-utils",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.50",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@ai-sdk/provider": "4.0.
|
|
40
|
+
"@ai-sdk/provider": "4.0.19",
|
|
41
41
|
"@standard-schema/spec": "^1.1.0",
|
|
42
42
|
"@workflow/serde": "4.1.0",
|
|
43
43
|
"eventsource-parser": "^3.0.8",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
type ArgumentStructure =
|
|
2
|
+
| { kind: 'undetermined' }
|
|
3
|
+
| { kind: 'other' }
|
|
4
|
+
| {
|
|
5
|
+
kind: 'structured';
|
|
6
|
+
stack: Array<'{' | '['>;
|
|
7
|
+
inString: boolean;
|
|
8
|
+
escaped: boolean;
|
|
9
|
+
complete: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function startsWithStructuredValue(
|
|
13
|
+
value: string | null | undefined,
|
|
14
|
+
): boolean {
|
|
15
|
+
if (typeof value !== 'string') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const firstCharacter = value.trimStart()[0];
|
|
20
|
+
return firstCharacter === '{' || firstCharacter === '[';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Incrementally tracks whether streamed tool-call arguments contain a complete
|
|
25
|
+
* structured JSON value. This is intentionally structural rather than a JSON
|
|
26
|
+
* parse: a currently parsable scalar can still be the prefix of a later value.
|
|
27
|
+
*/
|
|
28
|
+
export class StreamingToolCallArgumentState {
|
|
29
|
+
private structure: ArgumentStructure = { kind: 'undetermined' };
|
|
30
|
+
|
|
31
|
+
constructor(initialValue = '') {
|
|
32
|
+
this.append(initialValue);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get hasCompleteStructuredValue(): boolean {
|
|
36
|
+
return (
|
|
37
|
+
this.structure.kind === 'structured' && this.structure.complete === true
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
append(delta: string): void {
|
|
42
|
+
let nextStructure = this.structure;
|
|
43
|
+
|
|
44
|
+
for (const character of delta) {
|
|
45
|
+
if (nextStructure.kind === 'undetermined') {
|
|
46
|
+
if (/\s/.test(character)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (character !== '{' && character !== '[') {
|
|
51
|
+
nextStructure = { kind: 'other' };
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
nextStructure = {
|
|
56
|
+
kind: 'structured',
|
|
57
|
+
stack: [character],
|
|
58
|
+
inString: false,
|
|
59
|
+
escaped: false,
|
|
60
|
+
complete: false,
|
|
61
|
+
};
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (nextStructure.kind !== 'structured' || nextStructure.complete) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (nextStructure.inString) {
|
|
70
|
+
if (nextStructure.escaped) {
|
|
71
|
+
nextStructure.escaped = false;
|
|
72
|
+
} else if (character === '\\') {
|
|
73
|
+
nextStructure.escaped = true;
|
|
74
|
+
} else if (character === '"') {
|
|
75
|
+
nextStructure.inString = false;
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (character === '"') {
|
|
81
|
+
nextStructure.inString = true;
|
|
82
|
+
} else if (character === '{' || character === '[') {
|
|
83
|
+
nextStructure.stack.push(character);
|
|
84
|
+
} else if (character === '}' || character === ']') {
|
|
85
|
+
const expectedOpening = character === '}' ? '{' : '[';
|
|
86
|
+
if (nextStructure.stack.at(-1) !== expectedOpening) {
|
|
87
|
+
nextStructure = { kind: 'other' };
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
nextStructure.stack.pop();
|
|
92
|
+
if (nextStructure.stack.length === 0) {
|
|
93
|
+
nextStructure.complete = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.structure = nextStructure;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
type SharedV4ProviderMetadata,
|
|
5
5
|
} from '@ai-sdk/provider';
|
|
6
6
|
import { generateId as defaultGenerateId } from './generate-id';
|
|
7
|
+
import {
|
|
8
|
+
startsWithStructuredValue,
|
|
9
|
+
StreamingToolCallArgumentState,
|
|
10
|
+
} from './streaming-tool-call-argument-state';
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Minimal interface for a streaming tool call delta from an OpenAI-compatible API.
|
|
@@ -23,6 +27,7 @@ export interface StreamingToolCallTrackerOptions<
|
|
|
23
27
|
> {
|
|
24
28
|
/**
|
|
25
29
|
* ID generator function for tool call IDs.
|
|
30
|
+
* Blank or repeated outputs are converted to usable unique IDs.
|
|
26
31
|
* Defaults to the standard generateId.
|
|
27
32
|
*/
|
|
28
33
|
generateId?: () => string;
|
|
@@ -55,12 +60,20 @@ export interface StreamingToolCallTrackerOptions<
|
|
|
55
60
|
|
|
56
61
|
interface TrackedToolCall {
|
|
57
62
|
id: string;
|
|
63
|
+
index?: number;
|
|
64
|
+
sequence: number;
|
|
58
65
|
type: 'function';
|
|
59
66
|
function: { name: string; arguments: string };
|
|
67
|
+
argumentState: StreamingToolCallArgumentState;
|
|
60
68
|
hasFinished: boolean;
|
|
61
69
|
metadata?: SharedV4ProviderMetadata;
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
type ToolCallResolution =
|
|
73
|
+
| { kind: 'existing'; toolCall: TrackedToolCall }
|
|
74
|
+
| { kind: 'new' }
|
|
75
|
+
| { kind: 'ambiguous' };
|
|
76
|
+
|
|
64
77
|
type StreamingToolCallTrackerController = Pick<
|
|
65
78
|
TransformStreamDefaultController<LanguageModelV4StreamPart>,
|
|
66
79
|
'enqueue'
|
|
@@ -72,15 +85,17 @@ type StreamingToolCallTrackerController = Pick<
|
|
|
72
85
|
* emits tool-input-start/delta/end and tool-call events, and finalizes
|
|
73
86
|
* unfinished tool calls on flush.
|
|
74
87
|
*
|
|
75
|
-
* Used by openai, openai-compatible, groq, deepseek,
|
|
88
|
+
* Used by openai, openai-compatible, groq, deepseek, alibaba, mistral, and
|
|
89
|
+
* moonshotai providers.
|
|
76
90
|
*/
|
|
77
91
|
export class StreamingToolCallTracker<
|
|
78
92
|
DELTA extends StreamingToolCallDelta = StreamingToolCallDelta,
|
|
79
93
|
> {
|
|
80
|
-
private toolCalls =
|
|
81
|
-
private toolCallsById = new Map<string, TrackedToolCall
|
|
82
|
-
private toolCallsByIndex = new Map<number, TrackedToolCall
|
|
83
|
-
private
|
|
94
|
+
private toolCalls: TrackedToolCall[] = [];
|
|
95
|
+
private toolCallsById = new Map<string, Set<TrackedToolCall>>();
|
|
96
|
+
private toolCallsByIndex = new Map<number, Set<TrackedToolCall>>();
|
|
97
|
+
private usedToolCallIds = new Set<string>();
|
|
98
|
+
private nextGeneratedIdSuffixes = new Map<string, number>();
|
|
84
99
|
private readonly controller: StreamingToolCallTrackerController;
|
|
85
100
|
private readonly _generateId: () => string;
|
|
86
101
|
private readonly typeValidation: 'none' | 'if-present' | 'required';
|
|
@@ -108,24 +123,51 @@ export class StreamingToolCallTracker<
|
|
|
108
123
|
* events as appropriate.
|
|
109
124
|
*/
|
|
110
125
|
processDelta(toolCallDelta: DELTA): void {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
const wireName = toolCallDelta.function?.name;
|
|
127
|
+
const hasBlankName =
|
|
128
|
+
typeof wireName === 'string' && wireName.trim().length === 0;
|
|
129
|
+
const wireId = this.getNonBlankString(toolCallDelta.id);
|
|
130
|
+
const name = this.getNonBlankString(wireName);
|
|
131
|
+
const { index } = toolCallDelta;
|
|
132
|
+
|
|
133
|
+
const resolution = this.resolveToolCall({
|
|
134
|
+
wireId,
|
|
135
|
+
index,
|
|
136
|
+
name,
|
|
137
|
+
hasExplicitCallStart:
|
|
138
|
+
name != null &&
|
|
139
|
+
startsWithStructuredValue(toolCallDelta.function?.arguments),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (resolution.kind === 'ambiguous') {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let toolCall: TrackedToolCall;
|
|
147
|
+
if (resolution.kind === 'new') {
|
|
148
|
+
// Blank names cannot start a usable call, but some providers repeat a
|
|
149
|
+
// blank name on continuations. Correlate those continuations first, then
|
|
150
|
+
// ignore only the unmatched blank-name delta.
|
|
151
|
+
if (hasBlankName) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
toolCall = this.processNewToolCall(toolCallDelta, {
|
|
156
|
+
wireId,
|
|
157
|
+
index,
|
|
158
|
+
name,
|
|
159
|
+
});
|
|
121
160
|
} else {
|
|
161
|
+
toolCall = resolution.toolCall;
|
|
162
|
+
if (wireId != null) {
|
|
163
|
+
this.associateWireId(toolCall, wireId);
|
|
164
|
+
}
|
|
122
165
|
this.processExistingToolCall(toolCall, toolCallDelta);
|
|
123
166
|
}
|
|
124
167
|
|
|
125
168
|
if (index != null) {
|
|
126
|
-
this.
|
|
169
|
+
this.associateIndex(toolCall, index);
|
|
127
170
|
}
|
|
128
|
-
this.latestToolCall = toolCall;
|
|
129
171
|
}
|
|
130
172
|
|
|
131
173
|
/**
|
|
@@ -133,14 +175,191 @@ export class StreamingToolCallTracker<
|
|
|
133
175
|
* flush handler to ensure all tool calls are properly completed.
|
|
134
176
|
*/
|
|
135
177
|
flush(): void {
|
|
136
|
-
|
|
178
|
+
// Index order is reliable only when every call has an index. For mixed
|
|
179
|
+
// streams, keep insertion order rather than moving all index-less calls
|
|
180
|
+
// behind indexed calls.
|
|
181
|
+
const toolCalls = this.toolCalls.every(toolCall => toolCall.index != null)
|
|
182
|
+
? [...this.toolCalls].sort(
|
|
183
|
+
(a, b) => a.index! - b.index! || a.sequence - b.sequence,
|
|
184
|
+
)
|
|
185
|
+
: this.toolCalls;
|
|
186
|
+
|
|
187
|
+
for (const toolCall of toolCalls) {
|
|
137
188
|
if (!toolCall.hasFinished) {
|
|
138
189
|
this.finishToolCall(toolCall);
|
|
139
190
|
}
|
|
140
191
|
}
|
|
141
192
|
}
|
|
142
193
|
|
|
143
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Correlation precedence for streamed deltas:
|
|
196
|
+
*
|
|
197
|
+
* | ID evidence | index/name evidence | start evidence | resolution |
|
|
198
|
+
* | --- | --- | --- | --- |
|
|
199
|
+
* | known | matching | any | matching call, new call, or ambiguity |
|
|
200
|
+
* | known | conflicting | named | new call |
|
|
201
|
+
* | unseen | matching | structured start | new call |
|
|
202
|
+
* | unseen | matching | continuation | matching call or ambiguity |
|
|
203
|
+
* | absent | matching | any | matching call, new call, or ambiguity |
|
|
204
|
+
* | absent | absent | named | new call |
|
|
205
|
+
* | absent | absent | unnamed | sole unfinished call, new call, or ambiguity |
|
|
206
|
+
*/
|
|
207
|
+
private resolveToolCall({
|
|
208
|
+
wireId,
|
|
209
|
+
index,
|
|
210
|
+
name,
|
|
211
|
+
hasExplicitCallStart,
|
|
212
|
+
}: {
|
|
213
|
+
wireId: string | undefined;
|
|
214
|
+
index: number | null | undefined;
|
|
215
|
+
name: string | undefined;
|
|
216
|
+
hasExplicitCallStart: boolean;
|
|
217
|
+
}): ToolCallResolution {
|
|
218
|
+
const indexedToolCalls =
|
|
219
|
+
index != null ? this.toolCallsByIndex.get(index) : undefined;
|
|
220
|
+
const matchingIndexedToolCalls = this.filterToolCallsByName(
|
|
221
|
+
indexedToolCalls,
|
|
222
|
+
name,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
if (wireId != null) {
|
|
226
|
+
const toolCallsWithId = this.toolCallsById.get(wireId);
|
|
227
|
+
|
|
228
|
+
if (toolCallsWithId != null) {
|
|
229
|
+
if (index != null) {
|
|
230
|
+
const matchingToolCalls = matchingIndexedToolCalls.filter(toolCall =>
|
|
231
|
+
toolCallsWithId.has(toolCall),
|
|
232
|
+
);
|
|
233
|
+
const matchingToolCall = this.resolveMatchingToolCall(
|
|
234
|
+
matchingToolCalls,
|
|
235
|
+
hasExplicitCallStart,
|
|
236
|
+
);
|
|
237
|
+
if (matchingToolCall.kind !== 'new') {
|
|
238
|
+
return matchingToolCall;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// A named delta with a distinct index starts a new call even when its
|
|
242
|
+
// wire ID and function name repeat. Providers may reuse IDs across
|
|
243
|
+
// parallel calls, so the ID/name pair cannot override index evidence.
|
|
244
|
+
if (name != null) {
|
|
245
|
+
return { kind: 'new' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Conflicting labels on a continuation cannot be resolved safely.
|
|
249
|
+
if (indexedToolCalls != null) {
|
|
250
|
+
return { kind: 'ambiguous' };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return this.resolveMatchingToolCall([...toolCallsWithId], false);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (name != null) {
|
|
257
|
+
const matchingToolCalls = [...toolCallsWithId].filter(
|
|
258
|
+
toolCall => toolCall.function.name === name,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return this.resolveMatchingToolCall(
|
|
262
|
+
matchingToolCalls,
|
|
263
|
+
hasExplicitCallStart,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return this.resolveMatchingToolCall([...toolCallsWithId], false);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (matchingIndexedToolCalls.length > 0) {
|
|
271
|
+
// A previously unseen ID plus a named structured argument start is
|
|
272
|
+
// stronger evidence of a distinct call than a reused index/name. This
|
|
273
|
+
// also keeps interleaved same-name calls separate while still allowing
|
|
274
|
+
// IDs to change on ordinary continuation fragments.
|
|
275
|
+
return hasExplicitCallStart
|
|
276
|
+
? { kind: 'new' }
|
|
277
|
+
: this.resolveMatchingToolCall(matchingIndexedToolCalls, false);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { kind: 'new' };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (indexedToolCalls != null) {
|
|
284
|
+
// Repeated names are valid on continuations. A different name at the
|
|
285
|
+
// same index is evidence of a new call from a provider that reuses
|
|
286
|
+
// indices across parallel calls.
|
|
287
|
+
return this.resolveMatchingToolCall(
|
|
288
|
+
matchingIndexedToolCalls,
|
|
289
|
+
hasExplicitCallStart,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (name != null) {
|
|
294
|
+
return { kind: 'new' };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const unfinishedToolCalls = this.toolCalls.filter(
|
|
298
|
+
toolCall => !toolCall.hasFinished,
|
|
299
|
+
);
|
|
300
|
+
if (unfinishedToolCalls.length === 1) {
|
|
301
|
+
return { kind: 'existing', toolCall: unfinishedToolCalls[0] };
|
|
302
|
+
}
|
|
303
|
+
return unfinishedToolCalls.length > 1
|
|
304
|
+
? { kind: 'ambiguous' }
|
|
305
|
+
: { kind: 'new' };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private filterToolCallsByName(
|
|
309
|
+
toolCalls: Set<TrackedToolCall> | undefined,
|
|
310
|
+
name: string | undefined,
|
|
311
|
+
): TrackedToolCall[] {
|
|
312
|
+
if (toolCalls == null) {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return [...toolCalls].filter(
|
|
317
|
+
toolCall => name == null || toolCall.function.name === name,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private resolveMatchingToolCall(
|
|
322
|
+
toolCalls: TrackedToolCall[],
|
|
323
|
+
hasExplicitCallStart: boolean,
|
|
324
|
+
): ToolCallResolution {
|
|
325
|
+
if (toolCalls.length === 0) {
|
|
326
|
+
return { kind: 'new' };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!hasExplicitCallStart) {
|
|
330
|
+
return toolCalls.length === 1
|
|
331
|
+
? { kind: 'existing', toolCall: toolCalls[0] }
|
|
332
|
+
: { kind: 'ambiguous' };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// A repeated name can occur on continuations. A fresh structured
|
|
336
|
+
// argument prefix is evidence of another call only after the matching call
|
|
337
|
+
// has completed its own structured argument payload.
|
|
338
|
+
const continuableToolCalls = toolCalls.filter(
|
|
339
|
+
toolCall => !toolCall.argumentState.hasCompleteStructuredValue,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (continuableToolCalls.length === 1) {
|
|
343
|
+
return { kind: 'existing', toolCall: continuableToolCalls[0] };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return continuableToolCalls.length > 1
|
|
347
|
+
? { kind: 'ambiguous' }
|
|
348
|
+
: { kind: 'new' };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private processNewToolCall(
|
|
352
|
+
toolCallDelta: DELTA,
|
|
353
|
+
{
|
|
354
|
+
wireId,
|
|
355
|
+
index,
|
|
356
|
+
name,
|
|
357
|
+
}: {
|
|
358
|
+
wireId: string | undefined;
|
|
359
|
+
index: number | null | undefined;
|
|
360
|
+
name: string | undefined;
|
|
361
|
+
},
|
|
362
|
+
): TrackedToolCall {
|
|
144
363
|
if (this.typeValidation === 'required') {
|
|
145
364
|
if (toolCallDelta.type !== 'function') {
|
|
146
365
|
throw new InvalidResponseDataError({
|
|
@@ -157,41 +376,40 @@ export class StreamingToolCallTracker<
|
|
|
157
376
|
}
|
|
158
377
|
}
|
|
159
378
|
|
|
160
|
-
if (
|
|
161
|
-
throw new InvalidResponseDataError({
|
|
162
|
-
data: toolCallDelta,
|
|
163
|
-
message: `Expected 'id' to be a string.`,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (toolCallDelta.function?.name == null) {
|
|
379
|
+
if (name == null) {
|
|
168
380
|
throw new InvalidResponseDataError({
|
|
169
381
|
data: toolCallDelta,
|
|
170
382
|
message: `Expected 'function.name' to be a string.`,
|
|
171
383
|
});
|
|
172
384
|
}
|
|
173
385
|
|
|
386
|
+
const id = this.createToolCallId(wireId);
|
|
387
|
+
|
|
174
388
|
this.controller.enqueue({
|
|
175
389
|
type: 'tool-input-start',
|
|
176
|
-
id
|
|
177
|
-
toolName:
|
|
390
|
+
id,
|
|
391
|
+
toolName: name,
|
|
178
392
|
});
|
|
179
393
|
|
|
180
394
|
const metadata = this.extractMetadata?.(toolCallDelta);
|
|
181
395
|
|
|
396
|
+
const initialArguments = toolCallDelta.function?.arguments ?? '';
|
|
182
397
|
const toolCall: TrackedToolCall = {
|
|
183
|
-
id
|
|
398
|
+
id,
|
|
399
|
+
index: index ?? undefined,
|
|
400
|
+
sequence: this.toolCalls.length,
|
|
184
401
|
type: 'function',
|
|
185
402
|
function: {
|
|
186
|
-
name
|
|
187
|
-
arguments:
|
|
403
|
+
name,
|
|
404
|
+
arguments: initialArguments,
|
|
188
405
|
},
|
|
406
|
+
argumentState: new StreamingToolCallArgumentState(initialArguments),
|
|
189
407
|
hasFinished: false,
|
|
190
408
|
metadata,
|
|
191
409
|
};
|
|
192
|
-
this.toolCalls.
|
|
193
|
-
if (
|
|
194
|
-
this.
|
|
410
|
+
this.toolCalls.push(toolCall);
|
|
411
|
+
if (wireId != null) {
|
|
412
|
+
this.associateWireId(toolCall, wireId);
|
|
195
413
|
}
|
|
196
414
|
|
|
197
415
|
// Emit initial delta if arguments already present
|
|
@@ -210,6 +428,64 @@ export class StreamingToolCallTracker<
|
|
|
210
428
|
return toolCall;
|
|
211
429
|
}
|
|
212
430
|
|
|
431
|
+
private associateWireId(toolCall: TrackedToolCall, wireId: string): void {
|
|
432
|
+
let toolCallsWithId = this.toolCallsById.get(wireId);
|
|
433
|
+
if (toolCallsWithId == null) {
|
|
434
|
+
toolCallsWithId = new Set();
|
|
435
|
+
this.toolCallsById.set(wireId, toolCallsWithId);
|
|
436
|
+
}
|
|
437
|
+
toolCallsWithId.add(toolCall);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private associateIndex(toolCall: TrackedToolCall, index: number): void {
|
|
441
|
+
let toolCallsWithIndex = this.toolCallsByIndex.get(index);
|
|
442
|
+
if (toolCallsWithIndex == null) {
|
|
443
|
+
toolCallsWithIndex = new Set();
|
|
444
|
+
this.toolCallsByIndex.set(index, toolCallsWithIndex);
|
|
445
|
+
}
|
|
446
|
+
toolCallsWithIndex.add(toolCall);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private createToolCallId(wireId: string | undefined): string {
|
|
450
|
+
if (wireId != null && !this.usedToolCallIds.has(wireId)) {
|
|
451
|
+
this.usedToolCallIds.add(wireId);
|
|
452
|
+
return wireId;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const generatedId =
|
|
456
|
+
this.getNonBlankString(this._generateId()) ?? 'tool-call';
|
|
457
|
+
|
|
458
|
+
if (!this.usedToolCallIds.has(generatedId)) {
|
|
459
|
+
this.usedToolCallIds.add(generatedId);
|
|
460
|
+
return generatedId;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Resume after the last suffix checked for this generated value. This
|
|
464
|
+
// keeps deterministic generators bounded without repeatedly rescanning
|
|
465
|
+
// the same occupied suffixes.
|
|
466
|
+
const initialSuffix = this.nextGeneratedIdSuffixes.get(generatedId) ?? 1;
|
|
467
|
+
const maximumSuffix = initialSuffix + this.usedToolCallIds.size;
|
|
468
|
+
for (let suffix = initialSuffix; suffix <= maximumSuffix; suffix++) {
|
|
469
|
+
const suffixedId = `${generatedId}-${suffix}`;
|
|
470
|
+
if (!this.usedToolCallIds.has(suffixedId)) {
|
|
471
|
+
this.usedToolCallIds.add(suffixedId);
|
|
472
|
+
this.nextGeneratedIdSuffixes.set(generatedId, suffix + 1);
|
|
473
|
+
return suffixedId;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// The bounded search above is guaranteed to return by the pigeonhole
|
|
478
|
+
// principle. This guards against future changes invalidating that
|
|
479
|
+
// invariant without restoring an unbounded retry loop.
|
|
480
|
+
throw new Error('Failed to create a unique tool call ID.');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private getNonBlankString(
|
|
484
|
+
value: string | null | undefined,
|
|
485
|
+
): string | undefined {
|
|
486
|
+
return value != null && value.trim().length > 0 ? value : undefined;
|
|
487
|
+
}
|
|
488
|
+
|
|
213
489
|
private processExistingToolCall(
|
|
214
490
|
toolCall: TrackedToolCall,
|
|
215
491
|
toolCallDelta: DELTA,
|
|
@@ -219,6 +495,7 @@ export class StreamingToolCallTracker<
|
|
|
219
495
|
}
|
|
220
496
|
|
|
221
497
|
if (toolCallDelta.function?.arguments != null) {
|
|
498
|
+
toolCall.argumentState.append(toolCallDelta.function.arguments);
|
|
222
499
|
toolCall.function.arguments += toolCallDelta.function.arguments;
|
|
223
500
|
|
|
224
501
|
this.controller.enqueue({
|
|
@@ -241,7 +518,7 @@ export class StreamingToolCallTracker<
|
|
|
241
518
|
|
|
242
519
|
this.controller.enqueue({
|
|
243
520
|
type: 'tool-call',
|
|
244
|
-
toolCallId: toolCall.id
|
|
521
|
+
toolCallId: toolCall.id,
|
|
245
522
|
toolName: toolCall.function.name,
|
|
246
523
|
input: toolCall.function.arguments,
|
|
247
524
|
...(providerMetadata ? { providerMetadata } : {}),
|