ai 7.0.55 → 7.0.57

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.
@@ -1399,6 +1399,8 @@ type LanguageModelCallEndEvent<TOOLS extends ToolSet = ToolSet> = ModelInfo & {
1399
1399
  readonly content: ReadonlyArray<ContentPart<TOOLS>>;
1400
1400
  /** The provider-returned response id for this model call. */
1401
1401
  readonly responseId: string;
1402
+ /** Optional provider-specific metadata for this model call. */
1403
+ readonly providerMetadata?: ProviderMetadata;
1402
1404
  /** Performance metrics for the model call. */
1403
1405
  readonly performance: {
1404
1406
  /** Time spent waiting for the language model response in milliseconds. */
@@ -92,7 +92,7 @@ import {
92
92
  } from "@ai-sdk/provider-utils";
93
93
 
94
94
  // src/version.ts
95
- var VERSION = true ? "7.0.55" : "0.0.0-test";
95
+ var VERSION = true ? "7.0.57" : "0.0.0-test";
96
96
 
97
97
  // src/util/download/download.ts
98
98
  var download = async ({
@@ -187,7 +187,7 @@ The available lifecycle callbacks are:
187
187
  - **`onStart`**: Called once when the `generateText` operation begins, before any LLM calls. Receives model info, messages, settings, and `runtimeContext`.
188
188
  - **`onStepStart`**: Called before each step (LLM call). Receives the step number, model, messages being sent, tools, and prior steps.
189
189
  - **`onLanguageModelCallStart`**: Called immediately before the provider model call begins. Useful when you want to observe the model invocation separately from later tool execution.
190
- - **`onLanguageModelCallEnd`**: Called after the model response has been normalized and parsed, but before any client-side tool execution begins. Receives the model-call content parts, usage, and finish reason.
190
+ - **`onLanguageModelCallEnd`**: Called after the model response has been normalized and parsed, but before any client-side tool execution begins. Receives the model-call content parts, usage, finish reason, and provider metadata.
191
191
  - **`onToolExecutionStart`**: Called right before a tool's `execute` function runs. Receives the tool call object, messages, and `toolContext`.
192
192
  - **`onToolExecutionEnd`**: Called right after a tool's `execute` function completes or errors. Receives the tool call object, `toolExecutionMs`, and a `toolOutput` discriminated union (`type: 'tool-result'` with `output`, or `type: 'tool-error'` with `error`).
193
193
  - **`onStepEnd`**: Called after each step finishes. Includes `stepNumber` (zero-based index of the completed step).
@@ -417,7 +417,7 @@ The available lifecycle callbacks are:
417
417
  - **`onStart`**: Called once when the `streamText` operation begins, before any LLM calls. Receives model info, messages, settings, and `runtimeContext`.
418
418
  - **`onStepStart`**: Called before each step (LLM call). Receives the step number, model, messages being sent, tools, and prior steps.
419
419
  - **`onLanguageModelCallStart`**: Called immediately before the provider model call begins. Useful when you want to observe the model invocation separately from later tool execution.
420
- - **`onLanguageModelCallEnd`**: Called after the model response has been normalized and parsed, but before any client-side tool execution begins. Receives the model-call content parts, usage, and finish reason.
420
+ - **`onLanguageModelCallEnd`**: Called after the model response has been normalized and parsed, but before any client-side tool execution begins. Receives the model-call content parts, usage, finish reason, and provider metadata.
421
421
  - **`onToolExecutionStart`**: Called right before a tool's `execute` function runs. Receives the tool call object, messages, and `toolContext`.
422
422
  - **`onToolExecutionEnd`**: Called right after a tool's `execute` function completes or errors. Receives the tool call object, `toolExecutionMs`, and a `toolOutput` discriminated union (`type: 'tool-result'` with `output`, or `type: 'tool-error'` with `error`).
423
423
  - **`onStepEnd`**: Called after each step finishes. Receives the finish reason, usage, and other step details.
@@ -356,7 +356,7 @@ export function myIntegration(): Telemetry {
356
356
  name: 'onLanguageModelCallEnd',
357
357
  type: '(event: LanguageModelCallEndEvent) => void | PromiseLike<void>',
358
358
  description:
359
- 'Called after the model response has been normalized and parsed, but before any client-side tool execution begins.',
359
+ 'Called after the model response has been normalized and parsed, but before any client-side tool execution begins. Includes provider-specific metadata when available.',
360
360
  },
361
361
  {
362
362
  name: 'onToolExecutionStart',
@@ -628,7 +628,7 @@ registerTelemetry(
628
628
  The available options are:
629
629
 
630
630
  - `usage`: detailed usage attributes that are not covered by GenAI usage attributes, such as uncached input tokens and output text/reasoning token details.
631
- - `providerMetadata`: `ai.response.providerMetadata`.
631
+ - `providerMetadata`: `ai.response.providerMetadata` on operation, step, and model-call spans.
632
632
  - `embedding`: embedding inputs and outputs.
633
633
  - `reranking`: rerank input documents and ranking output.
634
634
  - `runtimeContext`: `ai.settings.context.*`.
@@ -97,7 +97,13 @@ const result = streamText({
97
97
  model: __MODEL__,
98
98
  prompt: 'Explain partial prerendering in two paragraphs.',
99
99
 
100
- onLanguageModelCallEnd({ callId, modelId, usage, performance }) {
100
+ onLanguageModelCallEnd({
101
+ callId,
102
+ modelId,
103
+ usage,
104
+ performance,
105
+ providerMetadata,
106
+ }) {
101
107
  metrics.histogram('ai.model.response_time_ms', performance.responseTimeMs, {
102
108
  callId,
103
109
  modelId,
@@ -108,6 +114,11 @@ const result = streamText({
108
114
  total: performance.effectiveTotalTokensPerSecond,
109
115
  tokens: usage.totalTokens,
110
116
  });
117
+
118
+ logger.info('ai.model.provider_metadata', {
119
+ callId,
120
+ providerMetadata,
121
+ });
111
122
  },
112
123
  });
113
124
 
@@ -675,6 +686,12 @@ Called after the provider response has been normalized and parsed, before local
675
686
  type: 'string',
676
687
  description: 'Provider-returned response ID for this model call.',
677
688
  },
689
+ {
690
+ name: 'providerMetadata',
691
+ type: 'ProviderMetadata | undefined',
692
+ description:
693
+ 'Provider-specific metadata for this model call, when returned by the provider.',
694
+ },
678
695
  {
679
696
  name: 'performance',
680
697
  type: 'LanguageModelCallPerformance',
@@ -1438,6 +1438,12 @@ To see `generateText` in action, check out [these examples](#examples).
1438
1438
  description:
1439
1439
  'The provider-returned response ID for this model call.',
1440
1440
  },
1441
+ {
1442
+ name: 'providerMetadata',
1443
+ type: 'ProviderMetadata | undefined',
1444
+ description:
1445
+ 'Provider-specific metadata for this model call, when returned by the provider.',
1446
+ },
1441
1447
  {
1442
1448
  name: 'performance',
1443
1449
  type: '{ responseTimeMs: number; effectiveOutputTokensPerSecond: number; outputTokensPerSecond: number | undefined; inputTokensPerSecond: number | undefined; effectiveTotalTokensPerSecond: number; timeToFirstOutputMs: number | undefined; timeBetweenOutputChunksMs?: OutputChunkTimingStats }',
@@ -2422,6 +2422,12 @@ To see `streamText` in action, check out [these examples](#examples).
2422
2422
  description:
2423
2423
  'The provider-returned response ID for this model call.',
2424
2424
  },
2425
+ {
2426
+ name: 'providerMetadata',
2427
+ type: 'ProviderMetadata | undefined',
2428
+ description:
2429
+ 'Provider-specific metadata for this model call, when returned by the provider.',
2430
+ },
2425
2431
  {
2426
2432
  name: 'performance',
2427
2433
  type: '{ responseTimeMs: number; effectiveOutputTokensPerSecond: number; outputTokensPerSecond: number | undefined; inputTokensPerSecond: number | undefined; effectiveTotalTokensPerSecond: number; timeToFirstOutputMs: number | undefined; timeBetweenOutputChunksMs?: OutputChunkTimingStats }',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.55",
3
+ "version": "7.0.57",
4
4
  "type": "module",
5
5
  "description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
6
6
  "license": "Apache-2.0",
@@ -42,9 +42,9 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.43",
45
+ "@ai-sdk/gateway": "4.0.45",
46
46
  "@ai-sdk/provider": "4.0.6",
47
- "@ai-sdk/provider-utils": "5.0.23"
47
+ "@ai-sdk/provider-utils": "5.0.24"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@edge-runtime/vm": "^5.0.0",
@@ -1085,6 +1085,11 @@ export async function generateText<
1085
1085
  usage: stepUsage,
1086
1086
  content: modelCallContent,
1087
1087
  responseId: currentModelResponse.response.id,
1088
+ ...(currentModelResponse.providerMetadata != null
1089
+ ? {
1090
+ providerMetadata: currentModelResponse.providerMetadata,
1091
+ }
1092
+ : {}),
1088
1093
  performance: {
1089
1094
  responseTimeMs,
1090
1095
  effectiveOutputTokensPerSecond: calculateTokensPerSecond({
@@ -1,6 +1,7 @@
1
1
  import type { ToolSet } from '@ai-sdk/provider-utils';
2
2
  import type { Callback } from '../util/callback';
3
3
  import type { FinishReason } from '../types/language-model';
4
+ import type { ProviderMetadata } from '../types/provider-metadata';
4
5
  import type { LanguageModelUsage } from '../types/usage';
5
6
  import type { ContentPart } from './content-part';
6
7
  import type { StandardizedPrompt } from '../prompt/standardize-prompt';
@@ -55,6 +56,9 @@ export type LanguageModelCallEndEvent<TOOLS extends ToolSet = ToolSet> =
55
56
  /** The provider-returned response id for this model call. */
56
57
  readonly responseId: string;
57
58
 
59
+ /** Optional provider-specific metadata for this model call. */
60
+ readonly providerMetadata?: ProviderMetadata;
61
+
58
62
  /** Performance metrics for the model call. */
59
63
  readonly performance: {
60
64
  /** Time spent waiting for the language model response in milliseconds. */
@@ -596,6 +596,9 @@ function createLanguageModelV4StreamPartToLanguageModelStreamPartTransform<
596
596
  usage,
597
597
  content: modelCallContent,
598
598
  responseId,
599
+ ...(chunk.providerMetadata != null
600
+ ? { providerMetadata: chunk.providerMetadata }
601
+ : {}),
599
602
  performance,
600
603
  },
601
604
  callbacks: onLanguageModelCallEnd,
@@ -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 =
@@ -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
  }