ai 6.0.232 → 6.0.234

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.
@@ -13,6 +13,7 @@ import { writeToServerResponse } from '../util/write-to-server-response';
13
13
  * @param options.statusText - Optional HTTP status text.
14
14
  * @param options.headers - Optional response headers.
15
15
  * @param options.textStream - The text stream to pipe.
16
+ * @returns A promise that resolves when the stream has been written.
16
17
  */
17
18
  export function pipeTextStreamToResponse({
18
19
  response,
@@ -23,8 +24,8 @@ export function pipeTextStreamToResponse({
23
24
  }: {
24
25
  response: ServerResponse;
25
26
  textStream: ReadableStream<string>;
26
- } & ResponseInit): void {
27
- writeToServerResponse({
27
+ } & ResponseInit): Promise<void> {
28
+ return writeToServerResponse({
28
29
  response,
29
30
  status,
30
31
  statusText,
@@ -104,13 +104,43 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
104
104
  new TransformStream<UIMessageChunk, InferUIMessageChunk<UI_MESSAGE>>({
105
105
  async transform(chunk, controller) {
106
106
  await runUpdateMessageJob(async ({ state, write }) => {
107
+ function getCurrentStepParts() {
108
+ const parts = state.message.parts;
109
+ let currentStepStartIndex = parts.length - 1;
110
+
111
+ while (
112
+ currentStepStartIndex >= 0 &&
113
+ parts[currentStepStartIndex].type !== 'step-start'
114
+ ) {
115
+ currentStepStartIndex--;
116
+ }
117
+
118
+ return parts.slice(currentStepStartIndex + 1);
119
+ }
120
+
121
+ function getCurrentStepToolInvocations() {
122
+ return getCurrentStepParts().filter(isToolUIPart);
123
+ }
124
+
107
125
  function getToolInvocation(toolCallId: string) {
108
- const toolInvocations = state.message.parts.filter(isToolUIPart);
126
+ const toolInvocations = getCurrentStepToolInvocations();
109
127
 
110
- const toolInvocation = toolInvocations.find(
128
+ let toolInvocation = toolInvocations.find(
111
129
  invocation => invocation.toolCallId === toolCallId,
112
130
  );
113
131
 
132
+ if (toolInvocation == null) {
133
+ const parts = state.message.parts;
134
+
135
+ for (let i = parts.length - 1; i >= 0; i--) {
136
+ const part = parts[i];
137
+ if (isToolUIPart(part) && part.toolCallId === toolCallId) {
138
+ toolInvocation = part;
139
+ break;
140
+ }
141
+ }
142
+ }
143
+
114
144
  if (toolInvocation == null) {
115
145
  throw new UIMessageStreamError({
116
146
  chunkType: 'tool-invocation',
@@ -159,12 +189,15 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
159
189
  providerMetadata?: ProviderMetadata;
160
190
  }
161
191
  ),
192
+ existingPart?: ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
162
193
  ) {
163
- const part = state.message.parts.find(
164
- part =>
165
- isStaticToolUIPart(part) &&
166
- part.toolCallId === options.toolCallId,
167
- ) as ToolUIPart<InferUIMessageTools<UI_MESSAGE>> | undefined;
194
+ const part =
195
+ existingPart ??
196
+ (getCurrentStepParts().find(
197
+ part =>
198
+ isStaticToolUIPart(part) &&
199
+ part.toolCallId === options.toolCallId,
200
+ ) as ToolUIPart<InferUIMessageTools<UI_MESSAGE>> | undefined);
168
201
 
169
202
  const anyOptions = options as any;
170
203
  const anyPart = part as any;
@@ -266,12 +299,15 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
266
299
  providerMetadata?: ProviderMetadata;
267
300
  }
268
301
  ),
302
+ existingPart?: DynamicToolUIPart,
269
303
  ) {
270
- const part = state.message.parts.find(
271
- part =>
272
- part.type === 'dynamic-tool' &&
273
- part.toolCallId === options.toolCallId,
274
- ) as DynamicToolUIPart | undefined;
304
+ const part =
305
+ existingPart ??
306
+ (getCurrentStepParts().find(
307
+ part =>
308
+ part.type === 'dynamic-tool' &&
309
+ part.toolCallId === options.toolCallId,
310
+ ) as DynamicToolUIPart | undefined);
275
311
 
276
312
  const anyOptions = options as any;
277
313
  const anyPart = part as any;
@@ -510,7 +546,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
510
546
 
511
547
  case 'tool-input-start': {
512
548
  const toolInvocations =
513
- state.message.parts.filter(isStaticToolUIPart);
549
+ getCurrentStepParts().filter(isStaticToolUIPart);
514
550
 
515
551
  // add the partial tool call to the map
516
552
  state.partialToolCalls[chunk.toolCallId] = {
@@ -635,7 +671,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
635
671
  // When a part already exists for this toolCallId (e.g. from
636
672
  // tool-input-start), honour its type so we update in place
637
673
  // instead of creating a duplicate with a mismatched type.
638
- const existingPart = state.message.parts
674
+ const existingPart = getCurrentStepParts()
639
675
  .filter(isToolUIPart)
640
676
  .find(p => p.toolCallId === chunk.toolCallId);
641
677
  const isDynamic =
@@ -696,31 +732,37 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
696
732
  const toolInvocation = getToolInvocation(chunk.toolCallId);
697
733
 
698
734
  if (toolInvocation.type === 'dynamic-tool') {
699
- updateDynamicToolPart({
700
- toolCallId: chunk.toolCallId,
701
- toolName: toolInvocation.toolName,
702
- state: 'output-available',
703
- input: (toolInvocation as any).input,
704
- output: chunk.output,
705
- preliminary: chunk.preliminary,
706
- providerExecuted: chunk.providerExecuted,
707
- providerMetadata: chunk.providerMetadata,
708
- title: toolInvocation.title,
709
- toolMetadata: toolInvocation.toolMetadata,
710
- });
735
+ updateDynamicToolPart(
736
+ {
737
+ toolCallId: chunk.toolCallId,
738
+ toolName: toolInvocation.toolName,
739
+ state: 'output-available',
740
+ input: (toolInvocation as any).input,
741
+ output: chunk.output,
742
+ preliminary: chunk.preliminary,
743
+ providerExecuted: chunk.providerExecuted,
744
+ providerMetadata: chunk.providerMetadata,
745
+ title: toolInvocation.title,
746
+ toolMetadata: toolInvocation.toolMetadata,
747
+ },
748
+ toolInvocation,
749
+ );
711
750
  } else {
712
- updateToolPart({
713
- toolCallId: chunk.toolCallId,
714
- toolName: getStaticToolName(toolInvocation),
715
- state: 'output-available',
716
- input: (toolInvocation as any).input,
717
- output: chunk.output,
718
- providerExecuted: chunk.providerExecuted,
719
- preliminary: chunk.preliminary,
720
- providerMetadata: chunk.providerMetadata,
721
- title: toolInvocation.title,
722
- toolMetadata: toolInvocation.toolMetadata,
723
- });
751
+ updateToolPart(
752
+ {
753
+ toolCallId: chunk.toolCallId,
754
+ toolName: getStaticToolName(toolInvocation),
755
+ state: 'output-available',
756
+ input: (toolInvocation as any).input,
757
+ output: chunk.output,
758
+ providerExecuted: chunk.providerExecuted,
759
+ preliminary: chunk.preliminary,
760
+ providerMetadata: chunk.providerMetadata,
761
+ title: toolInvocation.title,
762
+ toolMetadata: toolInvocation.toolMetadata,
763
+ },
764
+ toolInvocation as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
765
+ );
724
766
  }
725
767
 
726
768
  write();
@@ -731,30 +773,36 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
731
773
  const toolInvocation = getToolInvocation(chunk.toolCallId);
732
774
 
733
775
  if (toolInvocation.type === 'dynamic-tool') {
734
- updateDynamicToolPart({
735
- toolCallId: chunk.toolCallId,
736
- toolName: toolInvocation.toolName,
737
- state: 'output-error',
738
- input: (toolInvocation as any).input,
739
- errorText: chunk.errorText,
740
- providerExecuted: chunk.providerExecuted,
741
- providerMetadata: chunk.providerMetadata,
742
- title: toolInvocation.title,
743
- toolMetadata: toolInvocation.toolMetadata,
744
- });
776
+ updateDynamicToolPart(
777
+ {
778
+ toolCallId: chunk.toolCallId,
779
+ toolName: toolInvocation.toolName,
780
+ state: 'output-error',
781
+ input: (toolInvocation as any).input,
782
+ errorText: chunk.errorText,
783
+ providerExecuted: chunk.providerExecuted,
784
+ providerMetadata: chunk.providerMetadata,
785
+ title: toolInvocation.title,
786
+ toolMetadata: toolInvocation.toolMetadata,
787
+ },
788
+ toolInvocation,
789
+ );
745
790
  } else {
746
- updateToolPart({
747
- toolCallId: chunk.toolCallId,
748
- toolName: getStaticToolName(toolInvocation),
749
- state: 'output-error',
750
- input: (toolInvocation as any).input,
751
- rawInput: (toolInvocation as any).rawInput,
752
- errorText: chunk.errorText,
753
- providerExecuted: chunk.providerExecuted,
754
- providerMetadata: chunk.providerMetadata,
755
- title: toolInvocation.title,
756
- toolMetadata: toolInvocation.toolMetadata,
757
- });
791
+ updateToolPart(
792
+ {
793
+ toolCallId: chunk.toolCallId,
794
+ toolName: getStaticToolName(toolInvocation),
795
+ state: 'output-error',
796
+ input: (toolInvocation as any).input,
797
+ rawInput: (toolInvocation as any).rawInput,
798
+ errorText: chunk.errorText,
799
+ providerExecuted: chunk.providerExecuted,
800
+ providerMetadata: chunk.providerMetadata,
801
+ title: toolInvocation.title,
802
+ toolMetadata: toolInvocation.toolMetadata,
803
+ },
804
+ toolInvocation as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
805
+ );
758
806
  }
759
807
 
760
808
  write();
@@ -16,6 +16,7 @@ import type { UIMessageStreamResponseInit } from './ui-message-stream-response-i
16
16
  * @param options.headers - Additional HTTP headers to include in the response.
17
17
  * @param options.stream - The UI message chunk stream to send.
18
18
  * @param options.consumeSseStream - Optional callback to consume a copy of the SSE stream independently.
19
+ * @returns A promise that resolves when the stream has been written.
19
20
  */
20
21
  export function pipeUIMessageStreamToResponse({
21
22
  response,
@@ -27,7 +28,7 @@ export function pipeUIMessageStreamToResponse({
27
28
  }: {
28
29
  response: ServerResponse;
29
30
  stream: ReadableStream<UIMessageChunk>;
30
- } & UIMessageStreamResponseInit): void {
31
+ } & UIMessageStreamResponseInit): Promise<void> {
31
32
  let sseStream = stream.pipeThrough(new JsonToSseTransformStream());
32
33
 
33
34
  // when the consumeSseStream is provided, we need to tee the stream
@@ -39,7 +40,7 @@ export function pipeUIMessageStreamToResponse({
39
40
  consumeSseStream({ stream: stream2 }); // no await (do not block the response)
40
41
  }
41
42
 
42
- writeToServerResponse({
43
+ return writeToServerResponse({
43
44
  response,
44
45
  status,
45
46
  statusText,
@@ -112,7 +112,16 @@ export const audioMediaTypeSignatures = [
112
112
  },
113
113
  {
114
114
  mediaType: 'audio/mp4' as const,
115
- bytesPrefix: [0x66, 0x74, 0x79, 0x70],
115
+ bytesPrefix: [
116
+ 0x00,
117
+ 0x00,
118
+ 0x00,
119
+ null,
120
+ 0x66,
121
+ 0x74,
122
+ 0x79,
123
+ 0x70, // ftyp
124
+ ],
116
125
  },
117
126
  {
118
127
  mediaType: 'audio/webm',
@@ -159,31 +168,54 @@ export const videoMediaTypeSignatures = [
159
168
  },
160
169
  ] as const;
161
170
 
162
- const stripID3 = (data: Uint8Array | string) => {
163
- const bytes =
164
- typeof data === 'string' ? convertBase64ToUint8Array(data) : data;
171
+ const DEFAULT_SNIFF_BYTES = 18;
172
+
173
+ // Longest signature prefix in the tables above (e.g. image/avif = 12 bytes).
174
+ const MAX_SIGNATURE_BYTES = 12;
175
+
176
+ // Largest ID3v2 tag (10-byte header + body) skipped to reach the audio frame.
177
+ // Covers typical tags including embedded cover art while keeping the decode
178
+ // bounded and O(1) in the attachment size. Exported for boundary tests.
179
+ export const MAX_ID3_TAG_BYTES = 128 * 1024;
180
+
181
+ // Total prefix decoded when an ID3 tag is present: the tag plus room for the
182
+ // trailing signature, so a tag right at the size limit stays detectable.
183
+ const ID3_SCAN_BYTES = MAX_ID3_TAG_BYTES + MAX_SIGNATURE_BYTES;
184
+
185
+ // Decode/view exactly the first `maxBytes` bytes from the front of the input.
186
+ // The base64 and raw-byte paths yield the same length, so detection does not
187
+ // depend on the input's representation.
188
+ function decodePrefix(data: Uint8Array | string, maxBytes: number): Uint8Array {
189
+ if (typeof data !== 'string') {
190
+ return data.length > maxBytes ? data.subarray(0, maxBytes) : data;
191
+ }
192
+ // base64: 4 chars -> 3 bytes. Decode whole 4-char groups, then trim the 0-2
193
+ // extra bytes so the result matches the raw-byte path exactly.
194
+ const maxChars = Math.ceil(maxBytes / 3) * 4;
195
+ const bytes = convertBase64ToUint8Array(
196
+ data.substring(0, Math.min(data.length, maxChars)),
197
+ );
198
+ return bytes.length > maxBytes ? bytes.subarray(0, maxBytes) : bytes;
199
+ }
200
+
201
+ function hasID3(bytes: Uint8Array): boolean {
202
+ return (
203
+ bytes.length > 10 &&
204
+ bytes[0] === 0x49 && // 'I'
205
+ bytes[1] === 0x44 && // 'D'
206
+ bytes[2] === 0x33 // '3'
207
+ );
208
+ }
209
+
210
+ const stripID3 = (bytes: Uint8Array): Uint8Array => {
165
211
  const id3Size =
166
212
  ((bytes[6] & 0x7f) << 21) |
167
213
  ((bytes[7] & 0x7f) << 14) |
168
214
  ((bytes[8] & 0x7f) << 7) |
169
215
  (bytes[9] & 0x7f);
170
-
171
- // The raw MP3 starts here
172
- return bytes.slice(id3Size + 10);
216
+ return bytes.subarray(id3Size + 10);
173
217
  };
174
218
 
175
- function stripID3TagsIfPresent(data: Uint8Array | string): Uint8Array | string {
176
- const hasId3 =
177
- (typeof data === 'string' && data.startsWith('SUQz')) ||
178
- (typeof data !== 'string' &&
179
- data.length > 10 &&
180
- data[0] === 0x49 && // 'I'
181
- data[1] === 0x44 && // 'D'
182
- data[2] === 0x33); // '3'
183
-
184
- return hasId3 ? stripID3(data) : data;
185
- }
186
-
187
219
  /**
188
220
  * Detect the media IANA media type of a file using a list of signatures.
189
221
  *
@@ -201,15 +233,13 @@ export function detectMediaType({
201
233
  | typeof imageMediaTypeSignatures
202
234
  | typeof videoMediaTypeSignatures;
203
235
  }): (typeof signatures)[number]['mediaType'] | undefined {
204
- const processedData = stripID3TagsIfPresent(data);
236
+ let bytes = decodePrefix(data, DEFAULT_SNIFF_BYTES);
205
237
 
206
- // Convert the first ~18 bytes (24 base64 chars) for consistent detection logic:
207
- const bytes =
208
- typeof processedData === 'string'
209
- ? convertBase64ToUint8Array(
210
- processedData.substring(0, Math.min(processedData.length, 24)),
211
- )
212
- : processedData;
238
+ // ID3v2-tagged MP3s carry the audio frame after the tag; scan a bounded
239
+ // prefix past it rather than decoding the whole input.
240
+ if (hasID3(bytes)) {
241
+ bytes = stripID3(decodePrefix(data, ID3_SCAN_BYTES));
242
+ }
213
243
 
214
244
  for (const signature of signatures) {
215
245
  if (
@@ -15,7 +15,7 @@ export function writeToServerResponse({
15
15
  statusText?: string;
16
16
  headers?: Record<string, string | number | string[]>;
17
17
  stream: ReadableStream<Uint8Array>;
18
- }): void {
18
+ }): Promise<void> {
19
19
  const statusCode = status ?? 200;
20
20
  if (statusText !== undefined) {
21
21
  response.writeHead(statusCode, statusText, headers);
@@ -45,5 +45,5 @@ export function writeToServerResponse({
45
45
  }
46
46
  };
47
47
 
48
- read();
48
+ return read();
49
49
  }