@ai-sdk/provider-utils 5.0.54 → 5.0.55

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/provider-utils",
3
- "version": "5.0.54",
3
+ "version": "5.0.55",
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.22",
45
+ "@ai-sdk/provider": "4.0.23",
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": "5.8.3",
56
+ "typescript": "6.0.3",
57
57
  "zod": "3.25.76"
58
58
  },
59
59
  "peerDependencies": {
@@ -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([data], contentType ? { type: contentType } : undefined);
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;
@@ -63,6 +63,7 @@ const DEFAULT_REASONING_BUDGET_PERCENTAGES: Record<ReasoningLevel, number> = {
63
63
  medium: 0.3,
64
64
  high: 0.6,
65
65
  xhigh: 0.9,
66
+ max: 0.95,
66
67
  };
67
68
 
68
69
  /**
@@ -16,7 +16,9 @@ export function parseJsonEventStream<T>({
16
16
  schema: FlexibleSchema<T>;
17
17
  }): ReadableStream<ParseResult<T>> {
18
18
  return stream
19
- .pipeThrough(new TextDecoderStream())
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>>({
@@ -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
 
@@ -1,4 +1,9 @@
1
- import { APICallError, EmptyResponseBodyError } from '@ai-sdk/provider';
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
- <T>(responseSchema: FlexibleSchema<T>): ResponseHandler<AsyncGenerator<T>> =>
230
- async ({ response }) => {
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
- buffer += decoder.decode(value, { stream: true });
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
- let lineEnd = buffer.indexOf('\n');
271
- while (lineEnd !== -1) {
272
- const line = buffer.slice(0, lineEnd).replace(/\r$/, '');
273
- buffer = buffer.slice(lineEnd + 1);
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
- startsWithStructuredValue(toolCallDelta.function?.arguments),
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 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.
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(matchingIndexedToolCalls, false);
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
- if (unfinishedToolCalls.length === 1) {
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
- return continuableToolCalls.length > 1
347
- ? { kind: 'ambiguous' }
348
- : { kind: 'new' };
349
+ if (continuableToolCalls.length > 1 || !hasExplicitCallStart) {
350
+ return { kind: 'ambiguous' };
351
+ }
352
+
353
+ return { kind: 'new' };
349
354
  }
350
355
 
351
356
  private processNewToolCall(