@ai-sdk/provider-utils 5.0.11 → 5.0.13

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.11",
3
+ "version": "5.0.13",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -35,7 +35,7 @@
35
35
  "@standard-schema/spec": "^1.1.0",
36
36
  "@workflow/serde": "4.1.0",
37
37
  "eventsource-parser": "^3.0.8",
38
- "@ai-sdk/provider": "4.0.3"
38
+ "@ai-sdk/provider": "4.0.4"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "22.19.19",
@@ -63,7 +63,7 @@ const documentMediaTypeSignatures = [
63
63
  },
64
64
  ] as const;
65
65
 
66
- const audioMediaTypeSignatures = [
66
+ const audioMediaTypeSignaturesWithoutMp4 = [
67
67
  {
68
68
  mediaType: 'audio/mpeg' as const,
69
69
  bytesPrefix: [0xff, 0xfb],
@@ -117,16 +117,29 @@ const audioMediaTypeSignatures = [
117
117
  mediaType: 'audio/aac' as const,
118
118
  bytesPrefix: [0x40, 0x15, 0x00, 0x00],
119
119
  },
120
- {
121
- mediaType: 'audio/mp4' as const,
122
- bytesPrefix: [0x66, 0x74, 0x79, 0x70],
123
- },
124
120
  {
125
121
  mediaType: 'audio/webm',
126
122
  bytesPrefix: [0x1a, 0x45, 0xdf, 0xa3],
127
123
  },
128
124
  ] as const;
129
125
 
126
+ const audioMediaTypeSignatures = [
127
+ ...audioMediaTypeSignaturesWithoutMp4,
128
+ {
129
+ mediaType: 'audio/mp4' as const,
130
+ bytesPrefix: [
131
+ 0x00,
132
+ 0x00,
133
+ 0x00,
134
+ null,
135
+ 0x66,
136
+ 0x74,
137
+ 0x79,
138
+ 0x70, // ftyp
139
+ ],
140
+ },
141
+ ] as const;
142
+
130
143
  const videoMediaTypeSignatures = [
131
144
  {
132
145
  mediaType: 'video/mp4' as const,
@@ -166,31 +179,54 @@ const videoMediaTypeSignatures = [
166
179
  },
167
180
  ] as const;
168
181
 
169
- const stripID3 = (data: Uint8Array | string) => {
170
- const bytes =
171
- typeof data === 'string' ? convertBase64ToUint8Array(data) : data;
182
+ const DEFAULT_SNIFF_BYTES = 18;
183
+
184
+ // Longest signature prefix in the tables above (e.g. image/avif = 12 bytes).
185
+ const MAX_SIGNATURE_BYTES = 12;
186
+
187
+ // Largest ID3v2 tag (10-byte header + body) skipped to reach the audio frame.
188
+ // Covers typical tags including embedded cover art while keeping the decode
189
+ // bounded and O(1) in the attachment size. Exported for boundary tests.
190
+ export const MAX_ID3_TAG_BYTES = 128 * 1024;
191
+
192
+ // Total prefix decoded when an ID3 tag is present: the tag plus room for the
193
+ // trailing signature, so a tag right at the size limit stays detectable.
194
+ const ID3_SCAN_BYTES = MAX_ID3_TAG_BYTES + MAX_SIGNATURE_BYTES;
195
+
196
+ // Decode/view exactly the first `maxBytes` bytes from the front of the input.
197
+ // The base64 and raw-byte paths yield the same length, so detection does not
198
+ // depend on the input's representation.
199
+ function decodePrefix(data: Uint8Array | string, maxBytes: number): Uint8Array {
200
+ if (typeof data !== 'string') {
201
+ return data.length > maxBytes ? data.subarray(0, maxBytes) : data;
202
+ }
203
+ // base64: 4 chars -> 3 bytes. Decode whole 4-char groups, then trim the 0-2
204
+ // extra bytes so the result matches the raw-byte path exactly.
205
+ const maxChars = Math.ceil(maxBytes / 3) * 4;
206
+ const bytes = convertBase64ToUint8Array(
207
+ data.substring(0, Math.min(data.length, maxChars)),
208
+ );
209
+ return bytes.length > maxBytes ? bytes.subarray(0, maxBytes) : bytes;
210
+ }
211
+
212
+ function hasID3(bytes: Uint8Array): boolean {
213
+ return (
214
+ bytes.length > 10 &&
215
+ bytes[0] === 0x49 && // 'I'
216
+ bytes[1] === 0x44 && // 'D'
217
+ bytes[2] === 0x33 // '3'
218
+ );
219
+ }
220
+
221
+ const stripID3 = (bytes: Uint8Array): Uint8Array => {
172
222
  const id3Size =
173
223
  ((bytes[6] & 0x7f) << 21) |
174
224
  ((bytes[7] & 0x7f) << 14) |
175
225
  ((bytes[8] & 0x7f) << 7) |
176
226
  (bytes[9] & 0x7f);
177
-
178
- // The raw MP3 starts here
179
- return bytes.slice(id3Size + 10);
227
+ return bytes.subarray(id3Size + 10);
180
228
  };
181
229
 
182
- function stripID3TagsIfPresent(data: Uint8Array | string): Uint8Array | string {
183
- const hasId3 =
184
- (typeof data === 'string' && data.startsWith('SUQz')) ||
185
- (typeof data !== 'string' &&
186
- data.length > 10 &&
187
- data[0] === 0x49 && // 'I'
188
- data[1] === 0x44 && // 'D'
189
- data[2] === 0x33); // '3'
190
-
191
- return hasId3 ? stripID3(data) : data;
192
- }
193
-
194
230
  type MediaTypeSignatures = ReadonlyArray<{
195
231
  readonly mediaType: string;
196
232
  readonly bytesPrefix: ReadonlyArray<number | null>;
@@ -203,15 +239,13 @@ function detectMediaTypeBySignatures<T extends MediaTypeSignatures>({
203
239
  data: Uint8Array | string;
204
240
  signatures: T;
205
241
  }): T[number]['mediaType'] | undefined {
206
- const processedData = stripID3TagsIfPresent(data);
242
+ let bytes = decodePrefix(data, DEFAULT_SNIFF_BYTES);
207
243
 
208
- // Convert the first ~18 bytes (24 base64 chars) for consistent detection logic:
209
- const bytes =
210
- typeof processedData === 'string'
211
- ? convertBase64ToUint8Array(
212
- processedData.substring(0, Math.min(processedData.length, 24)),
213
- )
214
- : processedData;
244
+ // ID3v2-tagged MP3s carry the audio frame after the tag; scan a bounded
245
+ // prefix past it rather than decoding the whole input.
246
+ if (hasID3(bytes)) {
247
+ bytes = stripID3(decodePrefix(data, ID3_SCAN_BYTES));
248
+ }
215
249
 
216
250
  for (const signature of signatures) {
217
251
  if (
@@ -259,7 +293,9 @@ export function detectMediaType({
259
293
  signatures: [
260
294
  ...imageMediaTypeSignatures,
261
295
  ...documentMediaTypeSignatures,
262
- ...audioMediaTypeSignatures,
296
+ // MP4 containers cannot be distinguished as audio or video by ftyp alone.
297
+ // Preserve the generic detection result as video/mp4.
298
+ ...audioMediaTypeSignaturesWithoutMp4,
263
299
  ...videoMediaTypeSignatures,
264
300
  ],
265
301
  });