ai 7.0.111 → 7.0.112

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.
@@ -310,6 +310,9 @@ You can implement any of the following three function to modify the behavior of
310
310
  3. `wrapStream`: Wraps the `doStream` method of the [language model](https://github.com/vercel/ai/blob/main/packages/provider/src/language-model/v4/language-model-v4.ts).
311
311
  You can modify the parameters, call the language model, and modify the result.
312
312
 
313
+ Every `LanguageModelV4Middleware` object must set
314
+ `specificationVersion: 'v4'`.
315
+
313
316
  Here are some examples of how to implement language model middleware:
314
317
 
315
318
  ## Examples
@@ -330,6 +333,8 @@ import type {
330
333
  } from '@ai-sdk/provider';
331
334
 
332
335
  export const yourLogMiddleware: LanguageModelV4Middleware = {
336
+ specificationVersion: 'v4',
337
+
333
338
  wrapGenerate: async ({ doGenerate, params }) => {
334
339
  console.log('doGenerate called');
335
340
  console.log(`params: ${JSON.stringify(params, null, 2)}`);
@@ -407,6 +412,8 @@ import type { LanguageModelV4Middleware } from '@ai-sdk/provider';
407
412
  const cache = new Map<string, any>();
408
413
 
409
414
  export const yourCacheMiddleware: LanguageModelV4Middleware = {
415
+ specificationVersion: 'v4',
416
+
410
417
  wrapGenerate: async ({ doGenerate, params }) => {
411
418
  const cacheKey = JSON.stringify(params);
412
419
 
@@ -439,6 +446,8 @@ This example shows how to use RAG as middleware.
439
446
  import type { LanguageModelV4Middleware } from '@ai-sdk/provider';
440
447
 
441
448
  export const yourRagMiddleware: LanguageModelV4Middleware = {
449
+ specificationVersion: 'v4',
450
+
442
451
  transformParams: async ({ params }) => {
443
452
  const lastUserMessageText = getLastUserMessageText({
444
453
  prompt: params.prompt,
@@ -465,28 +474,102 @@ Guard rails are a way to ensure that the generated text of a language model call
465
474
  is safe and appropriate. This example shows how to use guardrails as middleware.
466
475
 
467
476
  ```ts
468
- import type { LanguageModelV4Middleware } from '@ai-sdk/provider';
477
+ import type {
478
+ LanguageModelV4Middleware,
479
+ LanguageModelV4StreamPart,
480
+ } from '@ai-sdk/provider';
481
+
482
+ const redactText = (text: string) => text.replace(/badword/g, '<REDACTED>');
469
483
 
470
484
  export const yourGuardrailMiddleware: LanguageModelV4Middleware = {
485
+ specificationVersion: 'v4',
486
+
471
487
  wrapGenerate: async ({ doGenerate }) => {
472
488
  const result = await doGenerate();
473
489
 
474
490
  // filtering approach, e.g. for PII or other sensitive information:
475
491
  const content = result.content.map(part =>
476
- part.type === 'text'
477
- ? { ...part, text: part.text.replace(/badword/g, '<REDACTED>') }
478
- : part,
492
+ part.type === 'text' ? { ...part, text: redactText(part.text) } : part,
479
493
  );
480
494
 
481
495
  return { ...result, content };
482
496
  },
483
497
 
484
- // here you would implement the guardrail logic for streaming
485
- // Note: streaming guardrails are difficult to implement, because
486
- // you do not know the full content of the stream until it's finished.
498
+ wrapStream: async ({ doStream }) => {
499
+ const { stream, ...rest } = await doStream();
500
+
501
+ // Keep a separate buffer for each text block in the stream.
502
+ const buffers = new Map<string, string>();
503
+
504
+ const transformStream = new TransformStream<
505
+ LanguageModelV4StreamPart,
506
+ LanguageModelV4StreamPart
507
+ >({
508
+ transform(chunk, controller) {
509
+ if (chunk.type === 'text-start') {
510
+ buffers.set(chunk.id, '');
511
+ controller.enqueue(chunk);
512
+ return;
513
+ }
514
+
515
+ if (chunk.type === 'text-delta') {
516
+ buffers.set(chunk.id, (buffers.get(chunk.id) ?? '') + chunk.delta);
517
+ return;
518
+ }
519
+
520
+ if (chunk.type === 'text-end') {
521
+ const bufferedText = buffers.get(chunk.id);
522
+
523
+ if (bufferedText != null) {
524
+ const redactedText = redactText(bufferedText);
525
+
526
+ if (redactedText) {
527
+ controller.enqueue({
528
+ type: 'text-delta',
529
+ id: chunk.id,
530
+ delta: redactedText,
531
+ });
532
+ }
533
+
534
+ buffers.delete(chunk.id);
535
+ }
536
+ }
537
+
538
+ controller.enqueue(chunk);
539
+ },
540
+
541
+ flush(controller) {
542
+ for (const [id, bufferedText] of buffers) {
543
+ const redactedText = redactText(bufferedText);
544
+
545
+ if (redactedText) {
546
+ controller.enqueue({
547
+ type: 'text-delta',
548
+ id,
549
+ delta: redactedText,
550
+ });
551
+ }
552
+ }
553
+ },
554
+ });
555
+
556
+ return {
557
+ stream: stream.pipeThrough(transformStream),
558
+ ...rest,
559
+ };
560
+ },
487
561
  };
488
562
  ```
489
563
 
564
+ <Note>
565
+ The streaming example buffers each text block until `text-end` so matches
566
+ split across `text-delta` chunks cannot leak through. This delays output and
567
+ uses memory proportional to the text block size. Do not redact each delta
568
+ independently. An incremental implementation must retain every possible
569
+ incomplete match, and a fixed-size buffer alone is not safe for unbounded
570
+ variable-length patterns.
571
+ </Note>
572
+
490
573
  ## Configuring Per Request Custom Metadata
491
574
 
492
575
  To send and access custom metadata in Middleware, you can use `providerOptions`. This is useful when building logging middleware where you want to pass additional context like user IDs, timestamps, or other contextual data that can help with tracking and debugging.
@@ -497,6 +580,8 @@ __PROVIDER_IMPORT__;
497
580
  import type { LanguageModelV4Middleware } from '@ai-sdk/provider';
498
581
 
499
582
  export const yourLogMiddleware: LanguageModelV4Middleware = {
583
+ specificationVersion: 'v4',
584
+
500
585
  wrapGenerate: async ({ doGenerate, params }) => {
501
586
  console.log('METADATA', params?.providerMetadata?.yourLogMiddleware);
502
587
  const result = await doGenerate();
@@ -205,6 +205,14 @@ export async function POST(req: Request) {
205
205
 
206
206
  The `consumeStream` function is necessary for proper abort handling in UI message streams. It ensures that the stream is properly consumed even when aborted, preventing potential memory leaks or hanging connections.
207
207
 
208
+ The `onEnd` callback distinguishes consumer cancellation from an observed
209
+ abort. When the consumer cancels the UI message stream before an outcome is
210
+ declared, such as during a client disconnect, `isCancelled` is `true`,
211
+ `outcome.status` remains `'unknown'`, and `isAborted` remains `false`. When the
212
+ stream observes an `abort` part first, `outcome.status` is `'aborted'`,
213
+ `isAborted` is `true`, and `isCancelled` is absent. Check both flags when the
214
+ same cleanup should run for either case.
215
+
208
216
  ## AI SDK RSC
209
217
 
210
218
  <Note type="warning">
@@ -4082,13 +4082,13 @@ To see `streamText` in action, check out [these examples](#examples).
4082
4082
  },
4083
4083
  {
4084
4084
  name: 'onEnd',
4085
- type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; outcome: UIMessageStreamOutcome; finishReason?: FinishReason; }) => PromiseLike<void> | void',
4085
+ type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; isCancelled?: true; outcome: UIMessageStreamOutcome; finishReason?: FinishReason; }) => PromiseLike<void> | void',
4086
4086
  isOptional: true,
4087
- description: 'Callback function called when the stream ends. Provides the updated messages, continuation and abort state, model finish reason, and operation-level outcome.',
4087
+ description: 'Callback function called when the stream ends. Provides the updated messages, continuation, abort and consumer-cancellation state, model finish reason, and operation-level outcome.',
4088
4088
  },
4089
4089
  {
4090
4090
  name: 'onFinish',
4091
- type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; outcome: UIMessageStreamOutcome; finishReason?: FinishReason; }) => PromiseLike<void> | void',
4091
+ type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; isCancelled?: true; outcome: UIMessageStreamOutcome; finishReason?: FinishReason; }) => PromiseLike<void> | void',
4092
4092
  isOptional: true,
4093
4093
  description: 'Deprecated alias for `onEnd`.',
4094
4094
  },
@@ -134,7 +134,7 @@ outcomes and call `setOutcome` once.
134
134
  },
135
135
  {
136
136
  name: 'onEnd',
137
- type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; outcome: UIMessageStreamOutcome; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
137
+ type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; isCancelled?: true; outcome: UIMessageStreamOutcome; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
138
138
  description: 'A callback function that is called when the stream ends.',
139
139
  properties: [
140
140
  {
@@ -156,11 +156,17 @@ outcomes and call `setOutcome` once.
156
156
  type: 'boolean',
157
157
  description: 'Indicates whether the stream was aborted.',
158
158
  },
159
+ {
160
+ name: 'isCancelled',
161
+ type: 'true | undefined',
162
+ description:
163
+ 'Present and true when the consumer cancelled the stream before an outcome was declared, for example because the client disconnected.',
164
+ },
159
165
  {
160
166
  name: 'outcome',
161
167
  type: "UIMessageStreamOutcome = { status: 'completed' } | { status: 'failed'; error?: unknown } | { status: 'aborted' } | { status: 'unknown' }",
162
168
  description:
163
- 'The operation-level outcome of the stream. It reflects the stream owner declaration unless a fatal stream-processing failure occurs, and is separate from model finish reasons and individual error chunks.',
169
+ "The operation-level outcome of the stream. It reflects the stream owner declaration unless a fatal stream-processing failure occurs, and is separate from model finish reasons and individual error chunks. It remains 'unknown' when the consumer cancels before an outcome is declared; check isCancelled to distinguish that case from normal closure without a declared outcome.",
164
170
  },
165
171
  {
166
172
  name: 'responseMessage',
@@ -180,7 +186,7 @@ outcomes and call `setOutcome` once.
180
186
  },
181
187
  {
182
188
  name: 'onFinish',
183
- type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; outcome: UIMessageStreamOutcome; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
189
+ type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; isCancelled?: true; outcome: UIMessageStreamOutcome; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
184
190
  description: 'Deprecated alias for `onEnd`.',
185
191
  },
186
192
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.111",
3
+ "version": "7.0.112",
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,20 +42,20 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.89",
46
- "@ai-sdk/provider": "4.0.17",
47
- "@ai-sdk/provider-utils": "5.0.45"
45
+ "@ai-sdk/gateway": "4.0.90",
46
+ "@ai-sdk/provider": "4.0.18",
47
+ "@ai-sdk/provider-utils": "5.0.46"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.91",
51
- "@ai-sdk/deepseek": "3.0.50",
52
- "@ai-sdk/google": "4.0.77",
53
- "@ai-sdk/groq": "4.0.46",
54
- "@ai-sdk/huggingface": "2.0.53",
55
- "@ai-sdk/moonshotai": "3.0.54",
56
- "@ai-sdk/openai": "4.0.72",
50
+ "@ai-sdk/amazon-bedrock": "5.0.92",
51
+ "@ai-sdk/deepseek": "3.0.51",
52
+ "@ai-sdk/google": "4.0.78",
53
+ "@ai-sdk/groq": "4.0.47",
54
+ "@ai-sdk/huggingface": "2.0.54",
55
+ "@ai-sdk/moonshotai": "3.0.55",
56
+ "@ai-sdk/openai": "4.0.73",
57
57
  "@ai-sdk/test-server": "2.0.1",
58
- "@ai-sdk/xai": "5.0.5",
58
+ "@ai-sdk/xai": "5.0.6",
59
59
  "@edge-runtime/vm": "^5.0.0",
60
60
  "@smithy/eventstream-codec": "^4.3.3",
61
61
  "@smithy/util-utf8": "^4.3.3",
@@ -47,7 +47,9 @@ export function pruneMessages({
47
47
 
48
48
  return {
49
49
  ...message,
50
- content: message.content.filter(part => part.type !== 'reasoning'),
50
+ content: message.content.filter(
51
+ part => part.type !== 'reasoning' && part.type !== 'reasoning-file',
52
+ ),
51
53
  };
52
54
  });
53
55
  }
@@ -661,6 +661,7 @@ export function mapToolResultOutput({
661
661
  }
662
662
  case 'file-url': {
663
663
  const mediaType = item.mediaType ?? getMediaTypeFromUrl(item.url);
664
+ const url = new URL(item.url);
664
665
  let message = `The "file-url" type for tool result content is deprecated. Use the "file" type with mediaType and { type: 'url', url } instead.`;
665
666
  if (!item.mediaType) {
666
667
  const inferenceSuffix =
@@ -676,7 +677,11 @@ export function mapToolResultOutput({
676
677
  });
677
678
  return {
678
679
  type: 'file' as const,
679
- data: { type: 'url' as const, url: new URL(item.url) },
680
+ data: {
681
+ type: 'url' as const,
682
+ url,
683
+ ...(url.toString() !== item.url ? { originalUrl: item.url } : {}),
684
+ },
680
685
  mediaType,
681
686
  providerOptions: item.providerOptions,
682
687
  };
@@ -732,6 +737,7 @@ export function mapToolResultOutput({
732
737
  };
733
738
  }
734
739
  case 'image-url': {
740
+ const url = new URL(item.url);
735
741
  warnings.push({
736
742
  type: 'deprecated',
737
743
  setting: '"tool-result" content of type "image-url"',
@@ -739,7 +745,11 @@ export function mapToolResultOutput({
739
745
  });
740
746
  return {
741
747
  type: 'file' as const,
742
- data: { type: 'url' as const, url: new URL(item.url) },
748
+ data: {
749
+ type: 'url' as const,
750
+ url,
751
+ ...(url.toString() !== item.url ? { originalUrl: item.url } : {}),
752
+ },
743
753
  mediaType: 'image',
744
754
  providerOptions: item.providerOptions,
745
755
  };
@@ -41,6 +41,16 @@ function convertUrlToFilePartData(url: URL): ConvertResult {
41
41
  return { data: { type: 'url', url }, mediaType: undefined };
42
42
  }
43
43
 
44
+ function convertUrlStringToFilePartData(content: string): ConvertResult {
45
+ const result = convertUrlToFilePartData(new URL(content));
46
+
47
+ if (result.data.type === 'url' && result.data.url.toString() !== content) {
48
+ result.data.originalUrl = content;
49
+ }
50
+
51
+ return result;
52
+ }
53
+
44
54
  function convertInlineDataToFilePartData(content: DataContent): ConvertResult {
45
55
  if (content instanceof Uint8Array) {
46
56
  return { data: { type: 'data', data: content }, mediaType: undefined };
@@ -108,7 +118,7 @@ export function convertToLanguageModelV4FilePart(
108
118
 
109
119
  if (typeof content === 'string') {
110
120
  try {
111
- return convertUrlToFilePartData(new URL(content));
121
+ return convertUrlStringToFilePartData(content);
112
122
  } catch {
113
123
  return convertInlineDataToFilePartData(content);
114
124
  }
@@ -489,7 +489,7 @@ async function safeValidateUIMessagesInternal<UI_MESSAGE extends UIMessage>(
489
489
 
490
490
  if (metadataSchema) {
491
491
  for (const [msgIdx, message] of validatedMessages.entries()) {
492
- await validateTypes({
492
+ message.metadata = await validateTypes({
493
493
  value: message.metadata,
494
494
  schema: metadataSchema,
495
495
  context: {
@@ -527,7 +527,7 @@ async function safeValidateUIMessagesInternal<UI_MESSAGE extends UIMessage>(
527
527
  };
528
528
  }
529
529
 
530
- await validateTypes({
530
+ dataPart.data = await validateTypes({
531
531
  value: dataPart.data,
532
532
  schema: dataSchema,
533
533
  context: {
@@ -147,7 +147,7 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
147
147
 
148
148
  let finishCalled = false;
149
149
 
150
- const callOnEnd = async () => {
150
+ const callOnEnd = async ({ isCancelled }: { isCancelled: boolean }) => {
151
151
  if (finishCalled || !resolvedOnEnd) {
152
152
  return;
153
153
  }
@@ -160,9 +160,11 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
160
160
  : declaredOutcome.status === 'unknown' && isAborted
161
161
  ? { status: 'aborted' }
162
162
  : declaredOutcome;
163
+ const isConsumerCancellation = isCancelled && outcome.status === 'unknown';
163
164
 
164
165
  await resolvedOnEnd({
165
166
  isAborted: isAborted || outcome.status === 'aborted',
167
+ ...(isConsumerCancellation ? { isCancelled: true as const } : {}),
166
168
  isContinuation,
167
169
  outcome,
168
170
  responseMessage: state.message as UI_MESSAGE,
@@ -215,11 +217,11 @@ export function handleUIMessageStreamFinish<UI_MESSAGE extends UIMessage>({
215
217
  },
216
218
  // @ts-expect-error cancel is still new and missing from types https://developer.mozilla.org/en-US/docs/Web/API/TransformStream#browser_compatibility
217
219
  async cancel() {
218
- await callOnEnd();
220
+ await callOnEnd({ isCancelled: true });
219
221
  },
220
222
 
221
223
  async flush() {
222
- await callOnEnd();
224
+ await callOnEnd({ isCancelled: false });
223
225
  },
224
226
  }),
225
227
  );
@@ -20,6 +20,14 @@ export type UIMessageStreamOnEndCallback<UI_MESSAGE extends UIMessage> =
20
20
  */
21
21
  isAborted: boolean;
22
22
 
23
+ /**
24
+ * Indicates that the consumer cancelled the stream before an outcome was
25
+ * declared, for example because the client disconnected.
26
+ *
27
+ * This property is only present when it is `true`.
28
+ */
29
+ isCancelled?: true;
30
+
23
31
  /**
24
32
  * The operation-level outcome of the stream. Fatal stream-processing
25
33
  * failures override outcomes declared by the stream owner.
@@ -4,6 +4,10 @@
4
4
  * This is separate from model finish reasons and individual stream chunks.
5
5
  * Fatal stream-processing failures override outcomes declared by the stream
6
6
  * owner.
7
+ *
8
+ * Consumer cancellation before an outcome is declared keeps the `unknown`
9
+ * status and is reported separately through the end callback's `isCancelled`
10
+ * property.
7
11
  */
8
12
  export type UIMessageStreamOutcome =
9
13
  | { status: 'completed' }