@ai-sdk/provider-utils 5.0.30 → 5.0.33

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.30",
3
+ "version": "5.0.33",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -0,0 +1,53 @@
1
+ const marker = Symbol.for('vercel.ai.providerStreamError');
2
+
3
+ export type ProviderStreamError = {
4
+ readonly message: string;
5
+ readonly type?: string;
6
+ readonly code?: string | number;
7
+ readonly statusCode?: number;
8
+ readonly isRetryable?: boolean;
9
+ readonly data: unknown;
10
+ };
11
+
12
+ /**
13
+ * Adds provider-owned status and retry metadata to a stream error payload
14
+ * without requiring provider packages to depend on AI SDK Core.
15
+ */
16
+ export function createProviderStreamError({
17
+ message,
18
+ type,
19
+ code,
20
+ statusCode,
21
+ isRetryable,
22
+ data,
23
+ }: {
24
+ message: string;
25
+ type?: string;
26
+ code?: string | number;
27
+ statusCode?: number;
28
+ isRetryable?: boolean;
29
+ data: unknown;
30
+ }): ProviderStreamError {
31
+ const error = {
32
+ message,
33
+ type,
34
+ code,
35
+ statusCode,
36
+ isRetryable,
37
+ data,
38
+ };
39
+
40
+ Object.defineProperty(error, marker, { value: true });
41
+
42
+ return error;
43
+ }
44
+
45
+ export function isProviderStreamError(
46
+ error: unknown,
47
+ ): error is ProviderStreamError {
48
+ return (
49
+ typeof error === 'object' &&
50
+ error != null &&
51
+ (error as Record<symbol, unknown>)[marker] === true
52
+ );
53
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Symbol for exposing the UTF-8 input byte budget of an embedding model.
3
+ *
4
+ * This capability is experimental and intentionally lives outside the versioned
5
+ * embedding model specification.
6
+ */
7
+ export const EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL = Symbol.for(
8
+ 'vercel.ai.embeddingModel.maxInputBytesPerCall',
9
+ );
@@ -3,7 +3,7 @@ import { isAbortError } from './is-abort-error';
3
3
 
4
4
  const FETCH_FAILED_ERROR_MESSAGES = ['fetch failed', 'failed to fetch'];
5
5
 
6
- const BUN_ERROR_CODES = [
6
+ const RETRYABLE_NETWORK_ERROR_CODES = new Set([
7
7
  'ConnectionRefused',
8
8
  'ConnectionClosed',
9
9
  'FailedToOpenSocket',
@@ -11,19 +11,33 @@ const BUN_ERROR_CODES = [
11
11
  'ECONNREFUSED',
12
12
  'ETIMEDOUT',
13
13
  'EPIPE',
14
- ];
14
+ 'UND_ERR_SOCKET',
15
+ 'UND_ERR_HEADERS_TIMEOUT',
16
+ 'UND_ERR_BODY_TIMEOUT',
17
+ 'UND_ERR_CONNECT_TIMEOUT',
18
+ ]);
15
19
 
16
- function isBunNetworkError(error: unknown): error is Error & { code?: string } {
17
- if (!(error instanceof Error)) {
18
- return false;
19
- }
20
+ function findNetworkError(
21
+ error: unknown,
22
+ ): (Error & { code?: unknown }) | undefined {
23
+ const visited = new Set<Error>();
24
+ let current = error;
25
+
26
+ while (current instanceof Error && !visited.has(current)) {
27
+ visited.add(current);
20
28
 
21
- const code = (error as any).code;
22
- if (typeof code === 'string' && BUN_ERROR_CODES.includes(code)) {
23
- return true;
29
+ const errorWithCode = current as Error & { code?: unknown };
30
+ if (
31
+ typeof errorWithCode.code === 'string' &&
32
+ RETRYABLE_NETWORK_ERROR_CODES.has(errorWithCode.code)
33
+ ) {
34
+ return errorWithCode;
35
+ }
36
+
37
+ current = (current as Error & { cause?: unknown }).cause;
24
38
  }
25
39
 
26
- return false;
40
+ return undefined;
27
41
  }
28
42
 
29
43
  export function handleFetchError({
@@ -58,9 +72,27 @@ export function handleFetchError({
58
72
  }
59
73
  }
60
74
 
61
- if (isBunNetworkError(error)) {
75
+ const networkError = findNetworkError(error);
76
+
77
+ if (networkError != null) {
78
+ if (APICallError.isInstance(error)) {
79
+ return new APICallError({
80
+ message: error.message,
81
+ cause: error.cause,
82
+ url: error.url,
83
+ requestBodyValues: error.requestBodyValues,
84
+ statusCode: error.statusCode,
85
+ responseHeaders: error.responseHeaders,
86
+ responseBody: error.responseBody,
87
+ data: error.data,
88
+ isRetryable: true,
89
+ });
90
+ }
91
+
62
92
  return new APICallError({
63
- message: `Cannot connect to API: ${error.message}`,
93
+ message: `Cannot connect to API: ${
94
+ error instanceof Error ? error.message : networkError.message
95
+ }`,
64
96
  cause: error,
65
97
  url,
66
98
  requestBodyValues,
package/src/index.ts CHANGED
@@ -15,6 +15,11 @@ export {
15
15
  createToolNameMapping,
16
16
  type ToolNameMapping,
17
17
  } from './create-tool-name-mapping';
18
+ export {
19
+ createProviderStreamError,
20
+ isProviderStreamError,
21
+ type ProviderStreamError,
22
+ } from './create-provider-stream-error';
18
23
  export * from './delay';
19
24
  export { DelayedPromise } from './delayed-promise';
20
25
  export {
@@ -24,6 +29,7 @@ export {
24
29
  } from './detect-media-type';
25
30
  export { downloadBlob } from './download-blob';
26
31
  export { DownloadError } from './download-error';
32
+ export { EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL as EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL } from './embedding-model-capabilities';
27
33
  export { fetchWithValidatedRedirects } from './fetch-with-validated-redirects';
28
34
  export { extractLines } from './extract-lines';
29
35
  export * from './extract-response-headers';
@@ -54,6 +60,7 @@ export {
54
60
  export { type MaybePromiseLike } from './maybe-promise-like';
55
61
  export { mediaTypeToExtension } from './media-type-to-extension';
56
62
  export { normalizeHeaders } from './normalize-headers';
63
+ export { normalizeBatchRequestCounts } from './normalize-batch-request-counts';
57
64
  export * from './parse-json';
58
65
  export { parseJsonEventStream } from './parse-json-event-stream';
59
66
  export { parseProviderOptions } from './parse-provider-options';
@@ -0,0 +1,42 @@
1
+ import type { Experimental_BatchV4Status as BatchV4Status } from '@ai-sdk/provider';
2
+
3
+ /**
4
+ * Normalizes complete batch request counts.
5
+ *
6
+ * Returns `undefined` when any count is missing, is not a non-negative safe
7
+ * integer, or when the item counts do not add up to the total.
8
+ */
9
+ export function normalizeBatchRequestCounts({
10
+ total,
11
+ pending,
12
+ completed,
13
+ failed,
14
+ }: {
15
+ total: number | null | undefined;
16
+ pending: number | null | undefined;
17
+ completed: number | null | undefined;
18
+ failed: number | null | undefined;
19
+ }): BatchV4Status['requestCounts'] | undefined {
20
+ if (
21
+ isNonNegativeSafeInteger(total) &&
22
+ isNonNegativeSafeInteger(pending) &&
23
+ isNonNegativeSafeInteger(completed) &&
24
+ isNonNegativeSafeInteger(failed) &&
25
+ pending + completed + failed === total
26
+ ) {
27
+ return {
28
+ total,
29
+ pending,
30
+ completed,
31
+ failed,
32
+ };
33
+ }
34
+
35
+ return undefined;
36
+ }
37
+
38
+ function isNonNegativeSafeInteger(
39
+ value: number | null | undefined,
40
+ ): value is number {
41
+ return value != null && Number.isSafeInteger(value) && value >= 0;
42
+ }
@@ -1,5 +1,7 @@
1
1
  import { APICallError, EmptyResponseBodyError } from '@ai-sdk/provider';
2
2
  import { extractResponseHeaders } from './extract-response-headers';
3
+ import { handleFetchError } from './handle-fetch-error';
4
+ import { isAbortError } from './is-abort-error';
3
5
  import { parseJSON, safeParseJSON, type ParseResult } from './parse-json';
4
6
  import { parseJsonEventStream } from './parse-json-event-stream';
5
7
  import { readResponseWithSizeLimit } from './read-response-with-size-limit';
@@ -17,6 +19,74 @@ export type ResponseHandler<RETURN_TYPE> = (options: {
17
19
 
18
20
  const textDecoder = new TextDecoder();
19
21
 
22
+ function wrapResponseBodyStream({
23
+ stream,
24
+ url,
25
+ requestBodyValues,
26
+ statusCode,
27
+ responseHeaders,
28
+ }: {
29
+ stream: ReadableStream<Uint8Array>;
30
+ url: string;
31
+ requestBodyValues: unknown;
32
+ statusCode: number;
33
+ responseHeaders: Record<string, string>;
34
+ }): ReadableStream<Uint8Array> {
35
+ const reader = stream.getReader();
36
+ let readerReleased = false;
37
+
38
+ const releaseReader = () => {
39
+ if (!readerReleased) {
40
+ reader.releaseLock();
41
+ readerReleased = true;
42
+ }
43
+ };
44
+
45
+ return new ReadableStream<Uint8Array>({
46
+ async pull(controller) {
47
+ try {
48
+ const { done, value } = await reader.read();
49
+
50
+ if (done) {
51
+ releaseReader();
52
+ controller.close();
53
+ } else {
54
+ controller.enqueue(value);
55
+ }
56
+ } catch (error) {
57
+ releaseReader();
58
+
59
+ if (isAbortError(error)) {
60
+ controller.error(error);
61
+ return;
62
+ }
63
+
64
+ controller.error(
65
+ handleFetchError({
66
+ error: new APICallError({
67
+ message: 'Failed to process successful response',
68
+ cause: error,
69
+ statusCode,
70
+ url,
71
+ responseHeaders,
72
+ requestBodyValues,
73
+ }),
74
+ url,
75
+ requestBodyValues,
76
+ }),
77
+ );
78
+ }
79
+ },
80
+ async cancel(reason) {
81
+ try {
82
+ await reader.cancel(reason);
83
+ } finally {
84
+ releaseReader();
85
+ }
86
+ },
87
+ });
88
+ }
89
+
20
90
  async function readResponseBodyAsText({
21
91
  response,
22
92
  url,
@@ -102,7 +172,7 @@ export const createEventSourceResponseHandler =
102
172
  <T>(
103
173
  chunkSchema: FlexibleSchema<T>,
104
174
  ): ResponseHandler<ReadableStream<ParseResult<T>>> =>
105
- async ({ response }: { response: Response }) => {
175
+ async ({ response, url, requestBodyValues }) => {
106
176
  const responseHeaders = extractResponseHeaders(response);
107
177
 
108
178
  if (response.body == null) {
@@ -112,7 +182,13 @@ export const createEventSourceResponseHandler =
112
182
  return {
113
183
  responseHeaders,
114
184
  value: parseJsonEventStream({
115
- stream: response.body,
185
+ stream: wrapResponseBodyStream({
186
+ stream: response.body,
187
+ url,
188
+ requestBodyValues,
189
+ statusCode: response.status,
190
+ responseHeaders,
191
+ }),
116
192
  schema: chunkSchema,
117
193
  }),
118
194
  };
@@ -149,6 +225,73 @@ export const createJsonResponseHandler =
149
225
  };
150
226
  };
151
227
 
228
+ export const createJsonLinesResponseHandler =
229
+ <T>(responseSchema: FlexibleSchema<T>): ResponseHandler<AsyncGenerator<T>> =>
230
+ async ({ response }) => {
231
+ const responseHeaders = extractResponseHeaders(response);
232
+
233
+ if (response.body == null) {
234
+ throw new EmptyResponseBodyError({});
235
+ }
236
+
237
+ return {
238
+ responseHeaders,
239
+ value: parseJsonLines({
240
+ stream: response.body,
241
+ schema: responseSchema,
242
+ }),
243
+ };
244
+ };
245
+
246
+ async function* parseJsonLines<T>({
247
+ stream,
248
+ schema,
249
+ }: {
250
+ stream: ReadableStream<Uint8Array>;
251
+ schema: FlexibleSchema<T>;
252
+ }): AsyncGenerator<T> {
253
+ const reader = stream.getReader();
254
+ const decoder = new TextDecoder();
255
+ let buffer = '';
256
+ let finished = false;
257
+
258
+ try {
259
+ while (true) {
260
+ const { done, value } = await reader.read();
261
+
262
+ if (done) {
263
+ finished = true;
264
+ buffer += decoder.decode();
265
+ break;
266
+ }
267
+
268
+ buffer += decoder.decode(value, { stream: true });
269
+
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);
274
+
275
+ if (line.trim().length > 0) {
276
+ yield await parseJSON({ text: line, schema });
277
+ }
278
+
279
+ lineEnd = buffer.indexOf('\n');
280
+ }
281
+ }
282
+
283
+ const finalLine = buffer.replace(/\r$/, '');
284
+ if (finalLine.trim().length > 0) {
285
+ yield await parseJSON({ text: finalLine, schema });
286
+ }
287
+ } finally {
288
+ if (!finished) {
289
+ await reader.cancel().catch(() => {});
290
+ }
291
+ reader.releaseLock();
292
+ }
293
+ }
294
+
152
295
  export const createBinaryResponseHandler =
153
296
  (): ResponseHandler<Uint8Array> =>
154
297
  async ({ response, url, requestBodyValues }) => {
@@ -14,6 +14,11 @@ export type ToolApprovalRequest = {
14
14
  */
15
15
  toolCallId: string;
16
16
 
17
+ /**
18
+ * Reason why the tool call requires approval.
19
+ */
20
+ reason?: string;
21
+
17
22
  /**
18
23
  * Flag indicating whether the tool was automatically approved or denied.
19
24
  *