@ai-sdk/provider-utils 5.0.54 → 5.0.56
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 +27 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -30
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/download-blob.ts +4 -1
- package/src/map-reasoning-to-provider.ts +1 -0
- package/src/parse-json-event-stream.ts +3 -1
- package/src/post-to-api.ts +1 -1
- package/src/response-handler.ts +56 -11
- package/src/streaming-tool-call-tracker.ts +28 -23
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.56",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@ai-sdk/provider": "4.0.
|
|
45
|
+
"@ai-sdk/provider": "4.0.24",
|
|
46
46
|
"@standard-schema/spec": "^1.1.0",
|
|
47
47
|
"@workflow/serde": "4.1.0",
|
|
48
48
|
"eventsource-parser": "^3.0.8",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@vercel/ai-tsconfig": "0.0.0",
|
|
54
54
|
"msw": "2.7.0",
|
|
55
55
|
"tsdown": "^0.23.0",
|
|
56
|
-
"typescript": "
|
|
56
|
+
"typescript": "6.0.3",
|
|
57
57
|
"zod": "3.25.76"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
package/src/download-blob.ts
CHANGED
|
@@ -45,7 +45,10 @@ export async function downloadBlob(
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
const contentType = response.headers.get('content-type') ?? undefined;
|
|
48
|
-
return new Blob(
|
|
48
|
+
return new Blob(
|
|
49
|
+
[data as Uint8Array<ArrayBuffer>],
|
|
50
|
+
contentType ? { type: contentType } : undefined,
|
|
51
|
+
);
|
|
49
52
|
} catch (error) {
|
|
50
53
|
if (DownloadError.isInstance(error)) {
|
|
51
54
|
throw error;
|
|
@@ -16,7 +16,9 @@ export function parseJsonEventStream<T>({
|
|
|
16
16
|
schema: FlexibleSchema<T>;
|
|
17
17
|
}): ReadableStream<ParseResult<T>> {
|
|
18
18
|
return stream
|
|
19
|
-
.pipeThrough(
|
|
19
|
+
.pipeThrough(
|
|
20
|
+
new TextDecoderStream() as ReadableWritablePair<string, Uint8Array>,
|
|
21
|
+
)
|
|
20
22
|
.pipeThrough(new EventSourceParserStream())
|
|
21
23
|
.pipeThrough(
|
|
22
24
|
new TransformStream<EventSourceMessage, ParseResult<T>>({
|
package/src/post-to-api.ts
CHANGED
|
@@ -102,7 +102,7 @@ export const postToApi = async <T>({
|
|
|
102
102
|
`ai-sdk-provider-utils/${VERSION}`,
|
|
103
103
|
getRuntimeEnvironmentUserAgent(),
|
|
104
104
|
),
|
|
105
|
-
body: body.content,
|
|
105
|
+
body: body.content as string | FormData | Uint8Array<ArrayBuffer> | Blob,
|
|
106
106
|
signal: abortSignal,
|
|
107
107
|
});
|
|
108
108
|
|
package/src/response-handler.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
APICallError,
|
|
3
|
+
EmptyResponseBodyError,
|
|
4
|
+
InvalidArgumentError,
|
|
5
|
+
} from '@ai-sdk/provider';
|
|
6
|
+
import { DownloadError } from './download-error';
|
|
2
7
|
import { extractResponseHeaders } from './extract-response-headers';
|
|
3
8
|
import { handleFetchError } from './handle-fetch-error';
|
|
4
9
|
import { isAbortError } from './is-abort-error';
|
|
@@ -19,6 +24,8 @@ export type ResponseHandler<RETURN_TYPE> = (options: {
|
|
|
19
24
|
|
|
20
25
|
const textDecoder = new TextDecoder();
|
|
21
26
|
|
|
27
|
+
const DEFAULT_MAX_JSON_LINE_BYTES = 64 * 1024 * 1024;
|
|
28
|
+
|
|
22
29
|
function wrapResponseBodyStream({
|
|
23
30
|
stream,
|
|
24
31
|
url,
|
|
@@ -225,9 +232,20 @@ export const createJsonResponseHandler =
|
|
|
225
232
|
};
|
|
226
233
|
};
|
|
227
234
|
|
|
228
|
-
export const createJsonLinesResponseHandler =
|
|
229
|
-
|
|
230
|
-
|
|
235
|
+
export const createJsonLinesResponseHandler = <T>(
|
|
236
|
+
responseSchema: FlexibleSchema<T>,
|
|
237
|
+
{
|
|
238
|
+
maxLineBytes = DEFAULT_MAX_JSON_LINE_BYTES,
|
|
239
|
+
}: { maxLineBytes?: number } = {},
|
|
240
|
+
): ResponseHandler<AsyncGenerator<T>> => {
|
|
241
|
+
if (!Number.isSafeInteger(maxLineBytes) || maxLineBytes <= 0) {
|
|
242
|
+
throw new InvalidArgumentError({
|
|
243
|
+
argument: 'maxLineBytes',
|
|
244
|
+
message: 'maxLineBytes must be a positive safe integer.',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return async ({ response, url }) => {
|
|
231
249
|
const responseHeaders = extractResponseHeaders(response);
|
|
232
250
|
|
|
233
251
|
if (response.body == null) {
|
|
@@ -239,20 +257,28 @@ export const createJsonLinesResponseHandler =
|
|
|
239
257
|
value: parseJsonLines({
|
|
240
258
|
stream: response.body,
|
|
241
259
|
schema: responseSchema,
|
|
260
|
+
url,
|
|
261
|
+
maxLineBytes,
|
|
242
262
|
}),
|
|
243
263
|
};
|
|
244
264
|
};
|
|
265
|
+
};
|
|
245
266
|
|
|
246
267
|
async function* parseJsonLines<T>({
|
|
247
268
|
stream,
|
|
248
269
|
schema,
|
|
270
|
+
url,
|
|
271
|
+
maxLineBytes,
|
|
249
272
|
}: {
|
|
250
273
|
stream: ReadableStream<Uint8Array>;
|
|
251
274
|
schema: FlexibleSchema<T>;
|
|
275
|
+
url: string;
|
|
276
|
+
maxLineBytes: number;
|
|
252
277
|
}): AsyncGenerator<T> {
|
|
253
278
|
const reader = stream.getReader();
|
|
254
279
|
const decoder = new TextDecoder();
|
|
255
280
|
let buffer = '';
|
|
281
|
+
let lineBytes = 0;
|
|
256
282
|
let finished = false;
|
|
257
283
|
|
|
258
284
|
try {
|
|
@@ -265,18 +291,37 @@ async function* parseJsonLines<T>({
|
|
|
265
291
|
break;
|
|
266
292
|
}
|
|
267
293
|
|
|
268
|
-
|
|
294
|
+
let offset = 0;
|
|
295
|
+
while (offset < value.length) {
|
|
296
|
+
const lineEnd = value.indexOf(10, offset);
|
|
297
|
+
const segmentEnd = lineEnd === -1 ? value.length : lineEnd;
|
|
269
298
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
299
|
+
// Bound each line before decoding it, excluding the newline byte.
|
|
300
|
+
lineBytes += segmentEnd - offset;
|
|
301
|
+
if (lineBytes > maxLineBytes) {
|
|
302
|
+
throw new DownloadError({
|
|
303
|
+
message: `JSON Lines response exceeded maximum line size of ${maxLineBytes} bytes.`,
|
|
304
|
+
url,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const decodeEnd = lineEnd === -1 ? segmentEnd : lineEnd + 1;
|
|
309
|
+
buffer += decoder.decode(value.subarray(offset, decodeEnd), {
|
|
310
|
+
stream: true,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (lineEnd === -1) {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const line = buffer.slice(0, -1).replace(/\r$/, '');
|
|
318
|
+
buffer = '';
|
|
319
|
+
lineBytes = 0;
|
|
320
|
+
offset = decodeEnd;
|
|
274
321
|
|
|
275
322
|
if (line.trim().length > 0) {
|
|
276
323
|
yield await parseJSON({ text: line, schema });
|
|
277
324
|
}
|
|
278
|
-
|
|
279
|
-
lineEnd = buffer.indexOf('\n');
|
|
280
325
|
}
|
|
281
326
|
}
|
|
282
327
|
|
|
@@ -128,6 +128,7 @@ export class StreamingToolCallTracker<
|
|
|
128
128
|
typeof wireName === 'string' && wireName.trim().length === 0;
|
|
129
129
|
const wireId = this.getNonBlankString(toolCallDelta.id);
|
|
130
130
|
const name = this.getNonBlankString(wireName);
|
|
131
|
+
const argumentsDelta = toolCallDelta.function?.arguments;
|
|
131
132
|
const { index } = toolCallDelta;
|
|
132
133
|
|
|
133
134
|
const resolution = this.resolveToolCall({
|
|
@@ -135,8 +136,9 @@ export class StreamingToolCallTracker<
|
|
|
135
136
|
index,
|
|
136
137
|
name,
|
|
137
138
|
hasExplicitCallStart:
|
|
138
|
-
name != null &&
|
|
139
|
-
|
|
139
|
+
name != null && startsWithStructuredValue(argumentsDelta),
|
|
140
|
+
hasEmptyArguments:
|
|
141
|
+
argumentsDelta == null || argumentsDelta.trim().length === 0,
|
|
140
142
|
});
|
|
141
143
|
|
|
142
144
|
if (resolution.kind === 'ambiguous') {
|
|
@@ -199,6 +201,7 @@ export class StreamingToolCallTracker<
|
|
|
199
201
|
* | known | matching | any | matching call, new call, or ambiguity |
|
|
200
202
|
* | known | conflicting | named | new call |
|
|
201
203
|
* | unseen | matching | structured start | new call |
|
|
204
|
+
* | unseen | matching | named empty arguments | incomplete call, new call, or ambiguity |
|
|
202
205
|
* | unseen | matching | continuation | matching call or ambiguity |
|
|
203
206
|
* | absent | matching | any | matching call, new call, or ambiguity |
|
|
204
207
|
* | absent | absent | named | new call |
|
|
@@ -209,11 +212,13 @@ export class StreamingToolCallTracker<
|
|
|
209
212
|
index,
|
|
210
213
|
name,
|
|
211
214
|
hasExplicitCallStart,
|
|
215
|
+
hasEmptyArguments,
|
|
212
216
|
}: {
|
|
213
217
|
wireId: string | undefined;
|
|
214
218
|
index: number | null | undefined;
|
|
215
219
|
name: string | undefined;
|
|
216
220
|
hasExplicitCallStart: boolean;
|
|
221
|
+
hasEmptyArguments: boolean;
|
|
217
222
|
}): ToolCallResolution {
|
|
218
223
|
const indexedToolCalls =
|
|
219
224
|
index != null ? this.toolCallsByIndex.get(index) : undefined;
|
|
@@ -268,13 +273,16 @@ export class StreamingToolCallTracker<
|
|
|
268
273
|
}
|
|
269
274
|
|
|
270
275
|
if (matchingIndexedToolCalls.length > 0) {
|
|
271
|
-
// A
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
276
|
+
// A named structured start with an unseen ID identifies a new call.
|
|
277
|
+
// Empty arguments can also occur when a continuation changes its ID.
|
|
278
|
+
// Keep the incomplete matching call in that case, and start a new call
|
|
279
|
+
// only after the matching structured arguments are complete.
|
|
275
280
|
return hasExplicitCallStart
|
|
276
281
|
? { kind: 'new' }
|
|
277
|
-
: this.resolveMatchingToolCall(
|
|
282
|
+
: this.resolveMatchingToolCall(
|
|
283
|
+
matchingIndexedToolCalls,
|
|
284
|
+
name != null && hasEmptyArguments,
|
|
285
|
+
);
|
|
278
286
|
}
|
|
279
287
|
|
|
280
288
|
return { kind: 'new' };
|
|
@@ -297,12 +305,7 @@ export class StreamingToolCallTracker<
|
|
|
297
305
|
const unfinishedToolCalls = this.toolCalls.filter(
|
|
298
306
|
toolCall => !toolCall.hasFinished,
|
|
299
307
|
);
|
|
300
|
-
|
|
301
|
-
return { kind: 'existing', toolCall: unfinishedToolCalls[0] };
|
|
302
|
-
}
|
|
303
|
-
return unfinishedToolCalls.length > 1
|
|
304
|
-
? { kind: 'ambiguous' }
|
|
305
|
-
: { kind: 'new' };
|
|
308
|
+
return this.resolveMatchingToolCall(unfinishedToolCalls, false);
|
|
306
309
|
}
|
|
307
310
|
|
|
308
311
|
private filterToolCallsByName(
|
|
@@ -326,26 +329,28 @@ export class StreamingToolCallTracker<
|
|
|
326
329
|
return { kind: 'new' };
|
|
327
330
|
}
|
|
328
331
|
|
|
329
|
-
if (!hasExplicitCallStart) {
|
|
330
|
-
return toolCalls.length === 1
|
|
331
|
-
? { kind: 'existing', toolCall: toolCalls[0] }
|
|
332
|
-
: { kind: 'ambiguous' };
|
|
333
|
-
}
|
|
334
|
-
|
|
335
332
|
// A repeated name can occur on continuations. A fresh structured
|
|
336
333
|
// argument prefix is evidence of another call only after the matching call
|
|
337
|
-
// has completed its own structured argument payload.
|
|
334
|
+
// has completed its own structured argument payload. When labels are
|
|
335
|
+
// missing, a sole call without a complete structured payload is the only
|
|
336
|
+
// viable continuation target.
|
|
338
337
|
const continuableToolCalls = toolCalls.filter(
|
|
339
338
|
toolCall => !toolCall.argumentState.hasCompleteStructuredValue,
|
|
340
339
|
);
|
|
341
340
|
|
|
341
|
+
if (!hasExplicitCallStart && toolCalls.length === 1) {
|
|
342
|
+
return { kind: 'existing', toolCall: toolCalls[0] };
|
|
343
|
+
}
|
|
344
|
+
|
|
342
345
|
if (continuableToolCalls.length === 1) {
|
|
343
346
|
return { kind: 'existing', toolCall: continuableToolCalls[0] };
|
|
344
347
|
}
|
|
345
348
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
+
if (continuableToolCalls.length > 1 || !hasExplicitCallStart) {
|
|
350
|
+
return { kind: 'ambiguous' };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { kind: 'new' };
|
|
349
354
|
}
|
|
350
355
|
|
|
351
356
|
private processNewToolCall(
|