@ai-sdk/provider-utils 4.0.13 → 4.0.15
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 +17 -0
- package/dist/index.d.mts +40 -3
- package/dist/index.d.ts +40 -3
- package/dist/index.js +72 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +70 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/download-blob.ts +23 -4
- package/src/index.ts +4 -0
- package/src/read-response-with-size-limit.ts +97 -0
- package/src/response-handler.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/provider-utils",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.15",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@standard-schema/spec": "^1.1.0",
|
|
37
37
|
"eventsource-parser": "^3.0.6",
|
|
38
|
-
"@ai-sdk/provider": "3.0.
|
|
38
|
+
"@ai-sdk/provider": "3.0.8"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "20.17.24",
|
package/src/download-blob.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import { DownloadError } from './download-error';
|
|
2
|
+
import {
|
|
3
|
+
readResponseWithSizeLimit,
|
|
4
|
+
DEFAULT_MAX_DOWNLOAD_SIZE,
|
|
5
|
+
} from './read-response-with-size-limit';
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Download a file from a URL and return it as a Blob.
|
|
5
9
|
*
|
|
6
10
|
* @param url - The URL to download from.
|
|
11
|
+
* @param options - Optional settings for the download.
|
|
12
|
+
* @param options.maxBytes - Maximum allowed download size in bytes. Defaults to 100 MiB.
|
|
13
|
+
* @param options.abortSignal - An optional abort signal to cancel the download.
|
|
7
14
|
* @returns A Promise that resolves to the downloaded Blob.
|
|
8
15
|
*
|
|
9
|
-
* @throws DownloadError if the download fails.
|
|
16
|
+
* @throws DownloadError if the download fails or exceeds maxBytes.
|
|
10
17
|
*/
|
|
11
|
-
export async function downloadBlob(
|
|
18
|
+
export async function downloadBlob(
|
|
19
|
+
url: string,
|
|
20
|
+
options?: { maxBytes?: number; abortSignal?: AbortSignal },
|
|
21
|
+
): Promise<Blob> {
|
|
12
22
|
try {
|
|
13
|
-
const response = await fetch(url
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
signal: options?.abortSignal,
|
|
25
|
+
});
|
|
14
26
|
|
|
15
27
|
if (!response.ok) {
|
|
16
28
|
throw new DownloadError({
|
|
@@ -20,7 +32,14 @@ export async function downloadBlob(url: string): Promise<Blob> {
|
|
|
20
32
|
});
|
|
21
33
|
}
|
|
22
34
|
|
|
23
|
-
|
|
35
|
+
const data = await readResponseWithSizeLimit({
|
|
36
|
+
response,
|
|
37
|
+
url,
|
|
38
|
+
maxBytes: options?.maxBytes ?? DEFAULT_MAX_DOWNLOAD_SIZE,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const contentType = response.headers.get('content-type') ?? undefined;
|
|
42
|
+
return new Blob([data], contentType ? { type: contentType } : undefined);
|
|
24
43
|
} catch (error) {
|
|
25
44
|
if (DownloadError.isInstance(error)) {
|
|
26
45
|
throw error;
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,10 @@ export { convertImageModelFileToDataUri } from './convert-image-model-file-to-da
|
|
|
11
11
|
export { convertToFormData } from './convert-to-form-data';
|
|
12
12
|
export { downloadBlob } from './download-blob';
|
|
13
13
|
export { DownloadError } from './download-error';
|
|
14
|
+
export {
|
|
15
|
+
readResponseWithSizeLimit,
|
|
16
|
+
DEFAULT_MAX_DOWNLOAD_SIZE,
|
|
17
|
+
} from './read-response-with-size-limit';
|
|
14
18
|
export * from './fetch-function';
|
|
15
19
|
export { createIdGenerator, generateId, type IdGenerator } from './generate-id';
|
|
16
20
|
export * from './get-error-message';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { DownloadError } from './download-error';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default maximum download size: 2 GiB.
|
|
5
|
+
*
|
|
6
|
+
* `fetch().arrayBuffer()` has ~2x peak memory overhead (undici buffers the
|
|
7
|
+
* body internally, then creates the JS ArrayBuffer), so very large downloads
|
|
8
|
+
* risk exceeding the default V8 heap limit on 64-bit systems and terminating
|
|
9
|
+
* the process with an out-of-memory error.
|
|
10
|
+
*
|
|
11
|
+
* Setting this limit converts an unrecoverable OOM crash into a catchable
|
|
12
|
+
* `DownloadError`.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_MAX_DOWNLOAD_SIZE = 2 * 1024 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads a fetch Response body with a size limit to prevent memory exhaustion.
|
|
18
|
+
*
|
|
19
|
+
* Checks the Content-Length header for early rejection, then reads the body
|
|
20
|
+
* incrementally via ReadableStream and aborts with a DownloadError when the
|
|
21
|
+
* limit is exceeded.
|
|
22
|
+
*
|
|
23
|
+
* @param response - The fetch Response to read.
|
|
24
|
+
* @param url - The URL being downloaded (used in error messages).
|
|
25
|
+
* @param maxBytes - Maximum allowed bytes. Defaults to DEFAULT_MAX_DOWNLOAD_SIZE.
|
|
26
|
+
* @returns A Uint8Array containing the response body.
|
|
27
|
+
* @throws DownloadError if the response exceeds maxBytes.
|
|
28
|
+
*/
|
|
29
|
+
export async function readResponseWithSizeLimit({
|
|
30
|
+
response,
|
|
31
|
+
url,
|
|
32
|
+
maxBytes = DEFAULT_MAX_DOWNLOAD_SIZE,
|
|
33
|
+
}: {
|
|
34
|
+
response: Response;
|
|
35
|
+
url: string;
|
|
36
|
+
maxBytes?: number;
|
|
37
|
+
}): Promise<Uint8Array> {
|
|
38
|
+
// Early rejection based on Content-Length header
|
|
39
|
+
const contentLength = response.headers.get('content-length');
|
|
40
|
+
if (contentLength != null) {
|
|
41
|
+
const length = parseInt(contentLength, 10);
|
|
42
|
+
if (!isNaN(length) && length > maxBytes) {
|
|
43
|
+
throw new DownloadError({
|
|
44
|
+
url,
|
|
45
|
+
message: `Download of ${url} exceeded maximum size of ${maxBytes} bytes (Content-Length: ${length}).`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const body = response.body;
|
|
51
|
+
|
|
52
|
+
// Handle missing body (empty responses)
|
|
53
|
+
if (body == null) {
|
|
54
|
+
return new Uint8Array(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const reader = body.getReader();
|
|
58
|
+
const chunks: Uint8Array[] = [];
|
|
59
|
+
let totalBytes = 0;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
while (true) {
|
|
63
|
+
const { done, value } = await reader.read();
|
|
64
|
+
|
|
65
|
+
if (done) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
totalBytes += value.length;
|
|
70
|
+
|
|
71
|
+
if (totalBytes > maxBytes) {
|
|
72
|
+
throw new DownloadError({
|
|
73
|
+
url,
|
|
74
|
+
message: `Download of ${url} exceeded maximum size of ${maxBytes} bytes.`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
chunks.push(value);
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
try {
|
|
82
|
+
await reader.cancel();
|
|
83
|
+
} finally {
|
|
84
|
+
reader.releaseLock();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Concatenate chunks into a single Uint8Array
|
|
89
|
+
const result = new Uint8Array(totalBytes);
|
|
90
|
+
let offset = 0;
|
|
91
|
+
for (const chunk of chunks) {
|
|
92
|
+
result.set(chunk, offset);
|
|
93
|
+
offset += chunk.length;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
package/src/response-handler.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { APICallError, EmptyResponseBodyError } from '@ai-sdk/provider';
|
|
2
|
-
import { ZodType } from 'zod/v4';
|
|
3
2
|
import { extractResponseHeaders } from './extract-response-headers';
|
|
4
3
|
import { parseJSON, ParseResult, safeParseJSON } from './parse-json';
|
|
5
4
|
import { parseJsonEventStream } from './parse-json-event-stream';
|