ai 7.0.54 → 7.0.56

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.
@@ -11,6 +11,7 @@ import type {
11
11
  import {
12
12
  convertBase64ToUint8Array,
13
13
  delay as defaultDelay,
14
+ generateId,
14
15
  withUserAgentSuffix,
15
16
  type DataContent,
16
17
  detectMediaType,
@@ -553,13 +554,23 @@ async function executeStartStatusFlow({
553
554
  }
554
555
  }
555
556
 
556
- // 2. Start the generation
557
- const startResult = await retry(() =>
558
- model.doStart!({
559
- ...callOptions,
560
- webhookUrl,
561
- }),
557
+ // 2. Start the generation. `doStart` is billable: mint one idempotency token
558
+ // per logical start, outside the retry closure; a caller-supplied key wins.
559
+ const callerIdempotencyKey = Object.entries(callOptions.headers ?? {}).find(
560
+ ([key, value]) =>
561
+ key.toLowerCase() === 'idempotency-key' && value !== undefined,
562
562
  );
563
+ const startCallOptions = {
564
+ ...callOptions,
565
+ headers: {
566
+ ...callOptions.headers,
567
+ ...(callerIdempotencyKey
568
+ ? {}
569
+ : { 'idempotency-key': `aisdk_vid_${generateId()}` }),
570
+ },
571
+ webhookUrl,
572
+ };
573
+ const startResult = await retry(() => model.doStart!(startCallOptions));
563
574
 
564
575
  const allWarnings = [...earlyWarnings, ...startResult.warnings];
565
576
  let operationProviderMetadata =
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ export {
32
32
 
33
33
  // directory exports
34
34
  export * from './agent';
35
+ export * from './batch';
35
36
  export * from './embed';
36
37
  export * from './error';
37
38
  export * from './generate-image';
@@ -64,6 +64,7 @@ export interface ChatTransport<UI_MESSAGE extends UIMessage> {
64
64
  *
65
65
  * @param options - Configuration object containing:
66
66
  * @param options.chatId - Unique identifier for the chat session to reconnect to
67
+ * @param options.abortSignal - Signal to abort the reconnection request if needed
67
68
  * @param options.headers - Additional HTTP headers to include in the reconnection request
68
69
  * @param options.body - Additional JSON properties to include in the request body
69
70
  * @param options.metadata - Custom metadata to attach to the request
@@ -78,6 +79,8 @@ export interface ChatTransport<UI_MESSAGE extends UIMessage> {
78
79
  options: {
79
80
  /** Unique identifier for the chat session to reconnect to */
80
81
  chatId: string;
82
+ /** Signal to abort the reconnection request if needed */
83
+ abortSignal?: AbortSignal;
81
84
  } & ChatRequestOptions,
82
85
  ) => Promise<ReadableStream<UIMessageChunk> | null>;
83
86
  }
package/src/ui/chat.ts CHANGED
@@ -135,6 +135,10 @@ type ActiveResponse<UI_MESSAGE extends UIMessage> = {
135
135
  abortController: AbortController;
136
136
  };
137
137
 
138
+ type ActiveResumeRequest = {
139
+ abortController: AbortController;
140
+ };
141
+
138
142
  export interface ChatState<UI_MESSAGE extends UIMessage> {
139
143
  status: ChatStatus;
140
144
 
@@ -254,6 +258,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
254
258
  private sendAutomaticallyWhen?: ChatInit<UI_MESSAGE>['sendAutomaticallyWhen'];
255
259
 
256
260
  private activeResponse: ActiveResponse<UI_MESSAGE> | undefined = undefined;
261
+ private activeResumeRequest: ActiveResumeRequest | undefined = undefined;
257
262
  private jobExecutor = new SerialJobExecutor();
258
263
 
259
264
  constructor({
@@ -584,11 +589,8 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
584
589
  * Abort the current request immediately, keep the generated tokens if any.
585
590
  */
586
591
  stop = async () => {
587
- if (this.status !== 'streaming' && this.status !== 'submitted') return;
588
-
589
- if (this.activeResponse?.abortController) {
590
- this.activeResponse.abortController.abort();
591
- }
592
+ this.activeResumeRequest?.abortController.abort();
593
+ this.activeResponse?.abortController.abort();
592
594
  };
593
595
 
594
596
  private async shouldSendAutomatically(): Promise<boolean> {
@@ -616,6 +618,25 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
616
618
  trigger: 'submit-message' | 'resume-stream' | 'regenerate-message';
617
619
  messageId?: string;
618
620
  } & ChatRequestOptions) {
621
+ const abortController = new AbortController();
622
+ const activeResumeRequest =
623
+ trigger === 'resume-stream' ? { abortController } : undefined;
624
+
625
+ if (activeResumeRequest) {
626
+ this.activeResumeRequest?.abortController.abort();
627
+ this.activeResumeRequest = activeResumeRequest;
628
+ }
629
+
630
+ const isCurrentRequest = () =>
631
+ activeResumeRequest == null ||
632
+ this.activeResumeRequest === activeResumeRequest;
633
+
634
+ const clearActiveResumeRequest = () => {
635
+ if (this.activeResumeRequest === activeResumeRequest) {
636
+ this.activeResumeRequest = undefined;
637
+ }
638
+ };
639
+
619
640
  // For resume-stream, check if there's an active stream before
620
641
  // changing status. This avoids a brief flash of 'submitted' status
621
642
  // when there is no stream to resume (e.g. on page load).
@@ -624,21 +645,49 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
624
645
  try {
625
646
  const reconnect = await this.transport.reconnectToStream({
626
647
  chatId: this.id,
648
+ abortSignal: abortController.signal,
627
649
  metadata,
628
650
  headers,
629
651
  body,
630
652
  });
631
653
 
654
+ if (abortController.signal.aborted || !isCurrentRequest()) {
655
+ await reconnect?.cancel().catch(() => {});
656
+ if (isCurrentRequest()) {
657
+ this.setStatus({ status: 'ready' });
658
+ }
659
+ clearActiveResumeRequest();
660
+ return;
661
+ }
662
+
632
663
  if (reconnect == null) {
664
+ this.setStatus({ status: 'ready' });
665
+ clearActiveResumeRequest();
633
666
  return; // no active stream found, so we do not resume
634
667
  }
635
668
 
636
669
  resumeStream = reconnect;
637
670
  } catch (err) {
671
+ if (
672
+ abortController.signal.aborted ||
673
+ (err as { name?: string }).name === 'AbortError'
674
+ ) {
675
+ if (isCurrentRequest()) {
676
+ this.setStatus({ status: 'ready' });
677
+ }
678
+ clearActiveResumeRequest();
679
+ return;
680
+ }
681
+
682
+ if (!isCurrentRequest()) {
683
+ return;
684
+ }
685
+
638
686
  if (this.onError && err instanceof Error) {
639
687
  this.onError(err);
640
688
  }
641
689
  this.setStatus({ status: 'error', error: err as Error });
690
+ clearActiveResumeRequest();
642
691
  return;
643
692
  }
644
693
  }
@@ -661,7 +710,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
661
710
  : this.state.snapshot(lastMessage),
662
711
  messageId: this.generateId(),
663
712
  }),
664
- abortController: new AbortController(),
713
+ abortController,
665
714
  } as ActiveResponse<UI_MESSAGE>;
666
715
 
667
716
  activeResponse = response;
@@ -696,10 +745,18 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
696
745
  }) => Promise<void>,
697
746
  ) =>
698
747
  // serialize the job execution to avoid race conditions:
699
- this.jobExecutor.run(() =>
700
- job({
748
+ this.jobExecutor.run(() => {
749
+ if (response.abortController.signal.aborted) {
750
+ return Promise.resolve();
751
+ }
752
+
753
+ return job({
701
754
  state: response.state,
702
755
  write: () => {
756
+ if (response.abortController.signal.aborted) {
757
+ return;
758
+ }
759
+
703
760
  // streaming is set on first write (before it should be "submitted")
704
761
  this.setStatus({ status: 'streaming' });
705
762
 
@@ -715,8 +772,8 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
715
772
  this.state.pushMessage(response.state.message);
716
773
  }
717
774
  },
718
- }),
719
- );
775
+ });
776
+ });
720
777
 
721
778
  await consumeStream({
722
779
  stream: processUIMessageStream({
@@ -730,17 +787,33 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
730
787
  throw error;
731
788
  },
732
789
  }),
790
+ abortSignal: response.abortController.signal,
733
791
  onError: error => {
734
792
  throw error;
735
793
  },
736
794
  });
737
795
 
738
- this.setStatus({ status: 'ready' });
796
+ if (isAbort) {
797
+ if (isCurrentRequest()) {
798
+ this.setStatus({ status: 'ready' });
799
+ }
800
+ return null;
801
+ }
802
+
803
+ if (isCurrentRequest()) {
804
+ this.setStatus({ status: 'ready' });
805
+ }
739
806
  } catch (err) {
740
807
  // Ignore abort errors as they are expected.
741
808
  if (isAbort || (err as any).name === 'AbortError') {
742
809
  isAbort = true;
743
- this.setStatus({ status: 'ready' });
810
+ if (isCurrentRequest()) {
811
+ this.setStatus({ status: 'ready' });
812
+ }
813
+ return null;
814
+ }
815
+
816
+ if (!isCurrentRequest()) {
744
817
  return null;
745
818
  }
746
819
 
@@ -779,6 +852,8 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
779
852
  if (this.activeResponse === activeResponse) {
780
853
  this.activeResponse = undefined;
781
854
  }
855
+
856
+ clearActiveResumeRequest();
782
857
  }
783
858
 
784
859
  // automatically send the message if the sendAutomaticallyWhen function returns true
@@ -247,6 +247,7 @@ export abstract class HttpChatTransport<
247
247
  method: 'GET',
248
248
  headers,
249
249
  credentials,
250
+ signal: options.abortSignal,
250
251
  });
251
252
 
252
253
  // no active stream found, so we do not resume
@@ -13,11 +13,23 @@
13
13
  export async function consumeStream({
14
14
  stream,
15
15
  onError,
16
+ abortSignal,
16
17
  }: {
17
18
  stream: ReadableStream;
18
19
  onError?: (error: unknown) => void;
20
+ abortSignal?: AbortSignal;
19
21
  }): Promise<void> {
20
22
  const reader = stream.getReader();
23
+ const cancelOnAbort = () => {
24
+ reader.cancel().catch(() => {});
25
+ };
26
+
27
+ if (abortSignal?.aborted) {
28
+ cancelOnAbort();
29
+ } else {
30
+ abortSignal?.addEventListener('abort', cancelOnAbort, { once: true });
31
+ }
32
+
21
33
  try {
22
34
  while (true) {
23
35
  const { done } = await reader.read();
@@ -26,6 +38,7 @@ export async function consumeStream({
26
38
  } catch (error) {
27
39
  onError?.(error);
28
40
  } finally {
41
+ abortSignal?.removeEventListener('abort', cancelOnAbort);
29
42
  reader.releaseLock();
30
43
  }
31
44
  }