@ai-sdk/provider-utils 4.0.55 → 4.0.57
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 +14 -0
- package/dist/index.d.mts +107 -8
- package/dist/index.d.ts +107 -8
- package/dist/index.js +410 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +410 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/fetch-with-validated-redirects.ts +90 -3
- package/src/index.ts +9 -1
- package/src/safe-node-fetch.ts +20 -25
- package/src/streaming-tool-call-argument-state.ts +102 -0
- package/src/streaming-tool-call-tracker.ts +465 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InvalidResponseDataError,
|
|
3
|
+
type LanguageModelV3StreamPart,
|
|
4
|
+
type SharedV3ProviderMetadata,
|
|
5
|
+
} from '@ai-sdk/provider';
|
|
6
|
+
import { generateId as defaultGenerateId } from './generate-id';
|
|
7
|
+
import {
|
|
8
|
+
startsWithStructuredValue,
|
|
9
|
+
StreamingToolCallArgumentState,
|
|
10
|
+
} from './streaming-tool-call-argument-state';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Minimal interface for a streaming tool call delta from an OpenAI-compatible API.
|
|
14
|
+
*/
|
|
15
|
+
export interface StreamingToolCallDelta {
|
|
16
|
+
index?: number | null;
|
|
17
|
+
id?: string | null;
|
|
18
|
+
type?: string | null;
|
|
19
|
+
function?: {
|
|
20
|
+
name?: string | null;
|
|
21
|
+
arguments?: string | null;
|
|
22
|
+
} | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StreamingToolCallTrackerOptions<
|
|
26
|
+
DELTA extends StreamingToolCallDelta = StreamingToolCallDelta,
|
|
27
|
+
> {
|
|
28
|
+
/**
|
|
29
|
+
* ID generator function for tool call IDs.
|
|
30
|
+
* Blank or repeated outputs are converted to usable unique IDs.
|
|
31
|
+
* Defaults to the standard generateId.
|
|
32
|
+
*/
|
|
33
|
+
generateId?: () => string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How to validate the `type` field on new tool call deltas.
|
|
37
|
+
* - `'none'`: no validation (default)
|
|
38
|
+
* - `'if-present'`: throw if type is present and not `'function'`
|
|
39
|
+
* - `'required'`: throw if type is not exactly `'function'`
|
|
40
|
+
*/
|
|
41
|
+
typeValidation?: 'none' | 'if-present' | 'required';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract provider-specific metadata from a tool call delta.
|
|
45
|
+
* Called once when a new tool call is detected.
|
|
46
|
+
*/
|
|
47
|
+
extractMetadata?: (delta: DELTA) => SharedV3ProviderMetadata | undefined;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the `providerMetadata` object for a `tool-call` event.
|
|
51
|
+
*/
|
|
52
|
+
buildToolCallProviderMetadata?: (
|
|
53
|
+
metadata: SharedV3ProviderMetadata | undefined,
|
|
54
|
+
) => SharedV3ProviderMetadata | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface TrackedToolCall {
|
|
58
|
+
id: string;
|
|
59
|
+
index?: number;
|
|
60
|
+
sequence: number;
|
|
61
|
+
type: 'function';
|
|
62
|
+
function: { name: string; arguments: string };
|
|
63
|
+
argumentState: StreamingToolCallArgumentState;
|
|
64
|
+
hasFinished: boolean;
|
|
65
|
+
metadata?: SharedV3ProviderMetadata;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type ToolCallResolution =
|
|
69
|
+
| { kind: 'existing'; toolCall: TrackedToolCall }
|
|
70
|
+
| { kind: 'new' }
|
|
71
|
+
| { kind: 'ambiguous' };
|
|
72
|
+
|
|
73
|
+
type StreamingToolCallTrackerController = Pick<
|
|
74
|
+
TransformStreamDefaultController<LanguageModelV3StreamPart>,
|
|
75
|
+
'enqueue'
|
|
76
|
+
>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Tracks streaming tool call state across multiple deltas from an
|
|
80
|
+
* OpenAI-compatible chat completion stream.
|
|
81
|
+
*/
|
|
82
|
+
export class StreamingToolCallTracker<
|
|
83
|
+
DELTA extends StreamingToolCallDelta = StreamingToolCallDelta,
|
|
84
|
+
> {
|
|
85
|
+
private toolCalls: TrackedToolCall[] = [];
|
|
86
|
+
private toolCallsById = new Map<string, Set<TrackedToolCall>>();
|
|
87
|
+
private toolCallsByIndex = new Map<number, Set<TrackedToolCall>>();
|
|
88
|
+
private usedToolCallIds = new Set<string>();
|
|
89
|
+
private nextGeneratedIdSuffixes = new Map<string, number>();
|
|
90
|
+
private readonly controller: StreamingToolCallTrackerController;
|
|
91
|
+
private readonly _generateId: () => string;
|
|
92
|
+
private readonly typeValidation: 'none' | 'if-present' | 'required';
|
|
93
|
+
private readonly extractMetadata?: (
|
|
94
|
+
delta: DELTA,
|
|
95
|
+
) => SharedV3ProviderMetadata | undefined;
|
|
96
|
+
private readonly buildToolCallProviderMetadata?: (
|
|
97
|
+
metadata: SharedV3ProviderMetadata | undefined,
|
|
98
|
+
) => SharedV3ProviderMetadata | undefined;
|
|
99
|
+
|
|
100
|
+
constructor(
|
|
101
|
+
controller: StreamingToolCallTrackerController,
|
|
102
|
+
options: StreamingToolCallTrackerOptions<DELTA> = {},
|
|
103
|
+
) {
|
|
104
|
+
this.controller = controller;
|
|
105
|
+
this._generateId = options.generateId ?? defaultGenerateId;
|
|
106
|
+
this.typeValidation = options.typeValidation ?? 'none';
|
|
107
|
+
this.extractMetadata = options.extractMetadata;
|
|
108
|
+
this.buildToolCallProviderMetadata = options.buildToolCallProviderMetadata;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
processDelta(toolCallDelta: DELTA): void {
|
|
112
|
+
const wireName = toolCallDelta.function?.name;
|
|
113
|
+
const hasBlankName =
|
|
114
|
+
typeof wireName === 'string' && wireName.trim().length === 0;
|
|
115
|
+
const wireId = this.getNonBlankString(toolCallDelta.id);
|
|
116
|
+
const name = this.getNonBlankString(wireName);
|
|
117
|
+
const { index } = toolCallDelta;
|
|
118
|
+
|
|
119
|
+
const resolution = this.resolveToolCall({
|
|
120
|
+
wireId,
|
|
121
|
+
index,
|
|
122
|
+
name,
|
|
123
|
+
hasExplicitCallStart:
|
|
124
|
+
name != null &&
|
|
125
|
+
startsWithStructuredValue(toolCallDelta.function?.arguments),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (resolution.kind === 'ambiguous') {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let toolCall: TrackedToolCall;
|
|
133
|
+
if (resolution.kind === 'new') {
|
|
134
|
+
// Blank names cannot start usable calls. They can occur on
|
|
135
|
+
// continuations, which are correlated before unmatched starts are
|
|
136
|
+
// ignored so they cannot abort a stream containing valid calls.
|
|
137
|
+
if (hasBlankName) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
toolCall = this.processNewToolCall(toolCallDelta, {
|
|
141
|
+
wireId,
|
|
142
|
+
index,
|
|
143
|
+
name,
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
toolCall = resolution.toolCall;
|
|
147
|
+
if (wireId != null) {
|
|
148
|
+
this.associateWireId(toolCall, wireId);
|
|
149
|
+
}
|
|
150
|
+
this.processExistingToolCall(toolCall, toolCallDelta);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (index != null) {
|
|
154
|
+
this.associateIndex(toolCall, index);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
flush(): void {
|
|
159
|
+
// Index order is reliable only when every call has an index. For mixed
|
|
160
|
+
// streams, keep insertion order rather than moving index-less calls.
|
|
161
|
+
const toolCalls = this.toolCalls.every(toolCall => toolCall.index != null)
|
|
162
|
+
? [...this.toolCalls].sort(
|
|
163
|
+
(a, b) => a.index! - b.index! || a.sequence - b.sequence,
|
|
164
|
+
)
|
|
165
|
+
: this.toolCalls;
|
|
166
|
+
|
|
167
|
+
for (const toolCall of toolCalls) {
|
|
168
|
+
if (!toolCall.hasFinished) {
|
|
169
|
+
this.finishToolCall(toolCall);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private resolveToolCall({
|
|
175
|
+
wireId,
|
|
176
|
+
index,
|
|
177
|
+
name,
|
|
178
|
+
hasExplicitCallStart,
|
|
179
|
+
}: {
|
|
180
|
+
wireId: string | undefined;
|
|
181
|
+
index: number | null | undefined;
|
|
182
|
+
name: string | undefined;
|
|
183
|
+
hasExplicitCallStart: boolean;
|
|
184
|
+
}): ToolCallResolution {
|
|
185
|
+
const indexedToolCalls =
|
|
186
|
+
index != null ? this.toolCallsByIndex.get(index) : undefined;
|
|
187
|
+
const matchingIndexedToolCalls = this.filterToolCallsByName(
|
|
188
|
+
indexedToolCalls,
|
|
189
|
+
name,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (wireId != null) {
|
|
193
|
+
const toolCallsWithId = this.toolCallsById.get(wireId);
|
|
194
|
+
|
|
195
|
+
if (toolCallsWithId != null) {
|
|
196
|
+
if (index != null) {
|
|
197
|
+
const matchingToolCalls = matchingIndexedToolCalls.filter(toolCall =>
|
|
198
|
+
toolCallsWithId.has(toolCall),
|
|
199
|
+
);
|
|
200
|
+
const matchingToolCall = this.resolveMatchingToolCall(
|
|
201
|
+
matchingToolCalls,
|
|
202
|
+
hasExplicitCallStart,
|
|
203
|
+
);
|
|
204
|
+
if (matchingToolCall.kind !== 'new') {
|
|
205
|
+
return matchingToolCall;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (name != null) {
|
|
209
|
+
return { kind: 'new' };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (indexedToolCalls != null) {
|
|
213
|
+
return { kind: 'ambiguous' };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return this.resolveMatchingToolCall([...toolCallsWithId], false);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (name != null) {
|
|
220
|
+
const matchingToolCalls = [...toolCallsWithId].filter(
|
|
221
|
+
toolCall => toolCall.function.name === name,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return this.resolveMatchingToolCall(
|
|
225
|
+
matchingToolCalls,
|
|
226
|
+
hasExplicitCallStart,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return this.resolveMatchingToolCall([...toolCallsWithId], false);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (matchingIndexedToolCalls.length > 0) {
|
|
234
|
+
return hasExplicitCallStart
|
|
235
|
+
? { kind: 'new' }
|
|
236
|
+
: this.resolveMatchingToolCall(matchingIndexedToolCalls, false);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { kind: 'new' };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (indexedToolCalls != null) {
|
|
243
|
+
return this.resolveMatchingToolCall(
|
|
244
|
+
matchingIndexedToolCalls,
|
|
245
|
+
hasExplicitCallStart,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (name != null) {
|
|
250
|
+
return { kind: 'new' };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const unfinishedToolCalls = this.toolCalls.filter(
|
|
254
|
+
toolCall => !toolCall.hasFinished,
|
|
255
|
+
);
|
|
256
|
+
if (unfinishedToolCalls.length === 1) {
|
|
257
|
+
return { kind: 'existing', toolCall: unfinishedToolCalls[0] };
|
|
258
|
+
}
|
|
259
|
+
return unfinishedToolCalls.length > 1
|
|
260
|
+
? { kind: 'ambiguous' }
|
|
261
|
+
: { kind: 'new' };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private filterToolCallsByName(
|
|
265
|
+
toolCalls: Set<TrackedToolCall> | undefined,
|
|
266
|
+
name: string | undefined,
|
|
267
|
+
): TrackedToolCall[] {
|
|
268
|
+
if (toolCalls == null) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return [...toolCalls].filter(
|
|
273
|
+
toolCall => name == null || toolCall.function.name === name,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private resolveMatchingToolCall(
|
|
278
|
+
toolCalls: TrackedToolCall[],
|
|
279
|
+
hasExplicitCallStart: boolean,
|
|
280
|
+
): ToolCallResolution {
|
|
281
|
+
if (toolCalls.length === 0) {
|
|
282
|
+
return { kind: 'new' };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!hasExplicitCallStart) {
|
|
286
|
+
return toolCalls.length === 1
|
|
287
|
+
? { kind: 'existing', toolCall: toolCalls[0] }
|
|
288
|
+
: { kind: 'ambiguous' };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const continuableToolCalls = toolCalls.filter(
|
|
292
|
+
toolCall => !toolCall.argumentState.hasCompleteStructuredValue,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (continuableToolCalls.length === 1) {
|
|
296
|
+
return { kind: 'existing', toolCall: continuableToolCalls[0] };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return continuableToolCalls.length > 1
|
|
300
|
+
? { kind: 'ambiguous' }
|
|
301
|
+
: { kind: 'new' };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private processNewToolCall(
|
|
305
|
+
toolCallDelta: DELTA,
|
|
306
|
+
{
|
|
307
|
+
wireId,
|
|
308
|
+
index,
|
|
309
|
+
name,
|
|
310
|
+
}: {
|
|
311
|
+
wireId: string | undefined;
|
|
312
|
+
index: number | null | undefined;
|
|
313
|
+
name: string | undefined;
|
|
314
|
+
},
|
|
315
|
+
): TrackedToolCall {
|
|
316
|
+
if (this.typeValidation === 'required') {
|
|
317
|
+
if (toolCallDelta.type !== 'function') {
|
|
318
|
+
throw new InvalidResponseDataError({
|
|
319
|
+
data: toolCallDelta,
|
|
320
|
+
message: `Expected 'function' type.`,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
} else if (this.typeValidation === 'if-present') {
|
|
324
|
+
if (toolCallDelta.type != null && toolCallDelta.type !== 'function') {
|
|
325
|
+
throw new InvalidResponseDataError({
|
|
326
|
+
data: toolCallDelta,
|
|
327
|
+
message: `Expected 'function' type.`,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (name == null) {
|
|
333
|
+
throw new InvalidResponseDataError({
|
|
334
|
+
data: toolCallDelta,
|
|
335
|
+
message: `Expected 'function.name' to be a string.`,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const id = this.createToolCallId(wireId);
|
|
340
|
+
|
|
341
|
+
this.controller.enqueue({
|
|
342
|
+
type: 'tool-input-start',
|
|
343
|
+
id,
|
|
344
|
+
toolName: name,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const toolCall: TrackedToolCall = {
|
|
348
|
+
id,
|
|
349
|
+
index: index ?? undefined,
|
|
350
|
+
sequence: this.toolCalls.length,
|
|
351
|
+
type: 'function',
|
|
352
|
+
function: {
|
|
353
|
+
name,
|
|
354
|
+
arguments: toolCallDelta.function?.arguments ?? '',
|
|
355
|
+
},
|
|
356
|
+
argumentState: new StreamingToolCallArgumentState(
|
|
357
|
+
toolCallDelta.function?.arguments ?? '',
|
|
358
|
+
),
|
|
359
|
+
hasFinished: false,
|
|
360
|
+
metadata: this.extractMetadata?.(toolCallDelta),
|
|
361
|
+
};
|
|
362
|
+
this.toolCalls.push(toolCall);
|
|
363
|
+
if (wireId != null) {
|
|
364
|
+
this.associateWireId(toolCall, wireId);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (toolCall.function.arguments.length > 0) {
|
|
368
|
+
this.controller.enqueue({
|
|
369
|
+
type: 'tool-input-delta',
|
|
370
|
+
id: toolCall.id,
|
|
371
|
+
delta: toolCall.function.arguments,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return toolCall;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private associateWireId(toolCall: TrackedToolCall, wireId: string): void {
|
|
379
|
+
let toolCallsWithId = this.toolCallsById.get(wireId);
|
|
380
|
+
if (toolCallsWithId == null) {
|
|
381
|
+
toolCallsWithId = new Set();
|
|
382
|
+
this.toolCallsById.set(wireId, toolCallsWithId);
|
|
383
|
+
}
|
|
384
|
+
toolCallsWithId.add(toolCall);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private associateIndex(toolCall: TrackedToolCall, index: number): void {
|
|
388
|
+
let toolCallsWithIndex = this.toolCallsByIndex.get(index);
|
|
389
|
+
if (toolCallsWithIndex == null) {
|
|
390
|
+
toolCallsWithIndex = new Set();
|
|
391
|
+
this.toolCallsByIndex.set(index, toolCallsWithIndex);
|
|
392
|
+
}
|
|
393
|
+
toolCallsWithIndex.add(toolCall);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private createToolCallId(wireId: string | undefined): string {
|
|
397
|
+
if (wireId != null && !this.usedToolCallIds.has(wireId)) {
|
|
398
|
+
this.usedToolCallIds.add(wireId);
|
|
399
|
+
return wireId;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const generatedId =
|
|
403
|
+
this.getNonBlankString(this._generateId()) ?? 'tool-call';
|
|
404
|
+
|
|
405
|
+
if (!this.usedToolCallIds.has(generatedId)) {
|
|
406
|
+
this.usedToolCallIds.add(generatedId);
|
|
407
|
+
return generatedId;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const initialSuffix = this.nextGeneratedIdSuffixes.get(generatedId) ?? 1;
|
|
411
|
+
const maximumSuffix = initialSuffix + this.usedToolCallIds.size;
|
|
412
|
+
for (let suffix = initialSuffix; suffix <= maximumSuffix; suffix++) {
|
|
413
|
+
const suffixedId = `${generatedId}-${suffix}`;
|
|
414
|
+
if (!this.usedToolCallIds.has(suffixedId)) {
|
|
415
|
+
this.usedToolCallIds.add(suffixedId);
|
|
416
|
+
this.nextGeneratedIdSuffixes.set(generatedId, suffix + 1);
|
|
417
|
+
return suffixedId;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
throw new Error('Failed to create a unique tool call ID.');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private getNonBlankString(
|
|
425
|
+
value: string | null | undefined,
|
|
426
|
+
): string | undefined {
|
|
427
|
+
return value != null && value.trim().length > 0 ? value : undefined;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private processExistingToolCall(
|
|
431
|
+
toolCall: TrackedToolCall,
|
|
432
|
+
toolCallDelta: DELTA,
|
|
433
|
+
): void {
|
|
434
|
+
if (!toolCall.hasFinished && toolCallDelta.function?.arguments != null) {
|
|
435
|
+
toolCall.argumentState.append(toolCallDelta.function.arguments);
|
|
436
|
+
toolCall.function.arguments += toolCallDelta.function.arguments;
|
|
437
|
+
this.controller.enqueue({
|
|
438
|
+
type: 'tool-input-delta',
|
|
439
|
+
id: toolCall.id,
|
|
440
|
+
delta: toolCallDelta.function.arguments,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private finishToolCall(toolCall: TrackedToolCall): void {
|
|
446
|
+
this.controller.enqueue({
|
|
447
|
+
type: 'tool-input-end',
|
|
448
|
+
id: toolCall.id,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const providerMetadata = this.buildToolCallProviderMetadata?.(
|
|
452
|
+
toolCall.metadata,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
this.controller.enqueue({
|
|
456
|
+
type: 'tool-call',
|
|
457
|
+
toolCallId: toolCall.id,
|
|
458
|
+
toolName: toolCall.function.name,
|
|
459
|
+
input: toolCall.function.arguments,
|
|
460
|
+
...(providerMetadata ? { providerMetadata } : {}),
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
toolCall.hasFinished = true;
|
|
464
|
+
}
|
|
465
|
+
}
|