ai 6.0.288 → 6.0.289

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.
@@ -282,6 +282,9 @@ You can implement any of the following three function to modify the behavior of
282
282
  3. `wrapStream`: Wraps the `doStream` method of the [language model](https://github.com/vercel/ai/blob/release-v6.0/packages/provider/src/language-model/v3/language-model-v3.ts).
283
283
  You can modify the parameters, call the language model, and modify the result.
284
284
 
285
+ Every `LanguageModelV3Middleware` object must set
286
+ `specificationVersion: 'v3'`.
287
+
285
288
  Here are some examples of how to implement language model middleware:
286
289
 
287
290
  ## Examples
@@ -302,6 +305,8 @@ import type {
302
305
  } from '@ai-sdk/provider';
303
306
 
304
307
  export const yourLogMiddleware: LanguageModelV3Middleware = {
308
+ specificationVersion: 'v3',
309
+
305
310
  wrapGenerate: async ({ doGenerate, params }) => {
306
311
  console.log('doGenerate called');
307
312
  console.log(`params: ${JSON.stringify(params, null, 2)}`);
@@ -379,6 +384,8 @@ import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
379
384
  const cache = new Map<string, any>();
380
385
 
381
386
  export const yourCacheMiddleware: LanguageModelV3Middleware = {
387
+ specificationVersion: 'v3',
388
+
382
389
  wrapGenerate: async ({ doGenerate, params }) => {
383
390
  const cacheKey = JSON.stringify(params);
384
391
 
@@ -411,6 +418,8 @@ This example shows how to use RAG as middleware.
411
418
  import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
412
419
 
413
420
  export const yourRagMiddleware: LanguageModelV3Middleware = {
421
+ specificationVersion: 'v3',
422
+
414
423
  transformParams: async ({ params }) => {
415
424
  const lastUserMessageText = getLastUserMessageText({
416
425
  prompt: params.prompt,
@@ -437,28 +446,102 @@ Guard rails are a way to ensure that the generated text of a language model call
437
446
  is safe and appropriate. This example shows how to use guardrails as middleware.
438
447
 
439
448
  ```ts
440
- import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
449
+ import type {
450
+ LanguageModelV3Middleware,
451
+ LanguageModelV3StreamPart,
452
+ } from '@ai-sdk/provider';
453
+
454
+ const redactText = (text: string) => text.replace(/badword/g, '<REDACTED>');
441
455
 
442
456
  export const yourGuardrailMiddleware: LanguageModelV3Middleware = {
457
+ specificationVersion: 'v3',
458
+
443
459
  wrapGenerate: async ({ doGenerate }) => {
444
460
  const result = await doGenerate();
445
461
 
446
462
  // filtering approach, e.g. for PII or other sensitive information:
447
463
  const content = result.content.map(part =>
448
- part.type === 'text'
449
- ? { ...part, text: part.text.replace(/badword/g, '<REDACTED>') }
450
- : part,
464
+ part.type === 'text' ? { ...part, text: redactText(part.text) } : part,
451
465
  );
452
466
 
453
467
  return { ...result, content };
454
468
  },
455
469
 
456
- // here you would implement the guardrail logic for streaming
457
- // Note: streaming guardrails are difficult to implement, because
458
- // you do not know the full content of the stream until it's finished.
470
+ wrapStream: async ({ doStream }) => {
471
+ const { stream, ...rest } = await doStream();
472
+
473
+ // Keep a separate buffer for each text block in the stream.
474
+ const buffers = new Map<string, string>();
475
+
476
+ const transformStream = new TransformStream<
477
+ LanguageModelV3StreamPart,
478
+ LanguageModelV3StreamPart
479
+ >({
480
+ transform(chunk, controller) {
481
+ if (chunk.type === 'text-start') {
482
+ buffers.set(chunk.id, '');
483
+ controller.enqueue(chunk);
484
+ return;
485
+ }
486
+
487
+ if (chunk.type === 'text-delta') {
488
+ buffers.set(chunk.id, (buffers.get(chunk.id) ?? '') + chunk.delta);
489
+ return;
490
+ }
491
+
492
+ if (chunk.type === 'text-end') {
493
+ const bufferedText = buffers.get(chunk.id);
494
+
495
+ if (bufferedText != null) {
496
+ const redactedText = redactText(bufferedText);
497
+
498
+ if (redactedText) {
499
+ controller.enqueue({
500
+ type: 'text-delta',
501
+ id: chunk.id,
502
+ delta: redactedText,
503
+ });
504
+ }
505
+
506
+ buffers.delete(chunk.id);
507
+ }
508
+ }
509
+
510
+ controller.enqueue(chunk);
511
+ },
512
+
513
+ flush(controller) {
514
+ for (const [id, bufferedText] of buffers) {
515
+ const redactedText = redactText(bufferedText);
516
+
517
+ if (redactedText) {
518
+ controller.enqueue({
519
+ type: 'text-delta',
520
+ id,
521
+ delta: redactedText,
522
+ });
523
+ }
524
+ }
525
+ },
526
+ });
527
+
528
+ return {
529
+ stream: stream.pipeThrough(transformStream),
530
+ ...rest,
531
+ };
532
+ },
459
533
  };
460
534
  ```
461
535
 
536
+ <Note>
537
+ The streaming example buffers each text block until `text-end` so matches
538
+ split across `text-delta` chunks cannot leak through. This delays output and
539
+ uses memory proportional to the text block size. Do not redact each delta
540
+ independently. An incremental implementation must retain every possible
541
+ incomplete match, and a fixed-size buffer alone is not safe for unbounded
542
+ variable-length patterns.
543
+ </Note>
544
+
462
545
  ## Configuring Per Request Custom Metadata
463
546
 
464
547
  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.
@@ -469,6 +552,8 @@ __PROVIDER_IMPORT__;
469
552
  import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
470
553
 
471
554
  export const yourLogMiddleware: LanguageModelV3Middleware = {
555
+ specificationVersion: 'v3',
556
+
472
557
  wrapGenerate: async ({ doGenerate, params }) => {
473
558
  console.log('METADATA', params?.providerMetadata?.yourLogMiddleware);
474
559
  const result = await doGenerate();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "6.0.288",
3
+ "version": "6.0.289",
4
4
  "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.",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -45,9 +45,9 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@opentelemetry/api": "^1.9.0",
48
- "@ai-sdk/gateway": "3.0.198",
49
- "@ai-sdk/provider": "3.0.16",
50
- "@ai-sdk/provider-utils": "4.0.52"
48
+ "@ai-sdk/gateway": "3.0.199",
49
+ "@ai-sdk/provider": "3.0.17",
50
+ "@ai-sdk/provider-utils": "4.0.53"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@edge-runtime/vm": "^5.0.0",
@@ -261,6 +261,10 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
261
261
  });
262
262
  }
263
263
 
264
+ // Keep input callbacks in the same transform so input availability cannot
265
+ // overtake pending start or delta callbacks in a downstream stream.
266
+ const activeToolCallToolNames = new Map<string, string>();
267
+
264
268
  // forward stream
265
269
  const forwardStream = new TransformStream<
266
270
  LanguageModelV3StreamPart,
@@ -283,9 +287,6 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
283
287
  case 'reasoning-start':
284
288
  case 'reasoning-delta':
285
289
  case 'reasoning-end':
286
- case 'tool-input-start':
287
- case 'tool-input-delta':
288
- case 'tool-input-end':
289
290
  case 'source':
290
291
  case 'response-metadata':
291
292
  case 'error':
@@ -294,6 +295,39 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
294
295
  break;
295
296
  }
296
297
 
298
+ case 'tool-input-start': {
299
+ activeToolCallToolNames.set(chunk.id, chunk.toolName);
300
+ await tools?.[chunk.toolName]?.onInputStart?.({
301
+ toolCallId: chunk.id,
302
+ messages,
303
+ abortSignal,
304
+ experimental_context,
305
+ });
306
+ controller.enqueue(chunk);
307
+ break;
308
+ }
309
+
310
+ case 'tool-input-delta': {
311
+ const toolName = activeToolCallToolNames.get(chunk.id);
312
+ if (toolName != null) {
313
+ await tools?.[toolName]?.onInputDelta?.({
314
+ inputTextDelta: chunk.delta,
315
+ toolCallId: chunk.id,
316
+ messages,
317
+ abortSignal,
318
+ experimental_context,
319
+ });
320
+ }
321
+ controller.enqueue(chunk);
322
+ break;
323
+ }
324
+
325
+ case 'tool-input-end': {
326
+ activeToolCallToolNames.delete(chunk.id);
327
+ controller.enqueue(chunk);
328
+ break;
329
+ }
330
+
297
331
  case 'file': {
298
332
  controller.enqueue({
299
333
  type: 'file',
@@ -1965,8 +1965,6 @@ class DefaultStreamTextResult<
1965
1965
  const stepToolOutputs: ToolOutput<TOOLS>[] = [];
1966
1966
  let warnings: SharedV3Warning[] | undefined;
1967
1967
 
1968
- const activeToolCallToolNames: Record<string, string> = {};
1969
-
1970
1968
  let stepFinishReason: FinishReason = 'other';
1971
1969
  let stepRawFinishReason: string | undefined = undefined;
1972
1970
 
@@ -2168,18 +2166,7 @@ class DefaultStreamTextResult<
2168
2166
  }
2169
2167
 
2170
2168
  case 'tool-input-start': {
2171
- activeToolCallToolNames[chunk.id] = chunk.toolName;
2172
-
2173
2169
  const tool = stepToolSet?.[chunk.toolName];
2174
- if (tool?.onInputStart != null) {
2175
- await tool.onInputStart({
2176
- toolCallId: chunk.id,
2177
- messages: stepInputMessages,
2178
- abortSignal,
2179
- experimental_context,
2180
- });
2181
- }
2182
-
2183
2170
  controller.enqueue({
2184
2171
  ...chunk,
2185
2172
  dynamic: chunk.dynamic ?? tool?.type === 'dynamic',
@@ -2189,25 +2176,11 @@ class DefaultStreamTextResult<
2189
2176
  }
2190
2177
 
2191
2178
  case 'tool-input-end': {
2192
- delete activeToolCallToolNames[chunk.id];
2193
2179
  controller.enqueue(chunk);
2194
2180
  break;
2195
2181
  }
2196
2182
 
2197
2183
  case 'tool-input-delta': {
2198
- const toolName = activeToolCallToolNames[chunk.id];
2199
- const tool = stepToolSet?.[toolName];
2200
-
2201
- if (tool?.onInputDelta != null) {
2202
- await tool.onInputDelta({
2203
- inputTextDelta: chunk.delta,
2204
- toolCallId: chunk.id,
2205
- messages: stepInputMessages,
2206
- abortSignal,
2207
- experimental_context,
2208
- });
2209
- }
2210
-
2211
2184
  controller.enqueue(chunk);
2212
2185
  break;
2213
2186
  }
@@ -492,8 +492,11 @@ function convertPartToLanguageModelPart(
492
492
  throw new Error(`Unsupported part type: ${type}`);
493
493
  }
494
494
 
495
- const { data: convertedData, mediaType: convertedMediaType } =
496
- convertToLanguageModelV3DataContent(originalData);
495
+ const {
496
+ data: convertedData,
497
+ mediaType: convertedMediaType,
498
+ originalUrl,
499
+ } = convertToLanguageModelV3DataContent(originalData);
497
500
 
498
501
  let mediaType: string | undefined = convertedMediaType ?? part.mediaType;
499
502
  let data: Uint8Array | string | URL = convertedData; // binary | base64 | url
@@ -525,6 +528,7 @@ function convertPartToLanguageModelPart(
525
528
  mediaType: mediaType ?? 'image/*', // any image
526
529
  filename: undefined,
527
530
  data,
531
+ ...(data instanceof URL && originalUrl != null ? { originalUrl } : {}),
528
532
  providerOptions: part.providerOptions,
529
533
  };
530
534
  }
@@ -540,6 +544,7 @@ function convertPartToLanguageModelPart(
540
544
  mediaType,
541
545
  filename: part.filename,
542
546
  data,
547
+ ...(data instanceof URL && originalUrl != null ? { originalUrl } : {}),
543
548
  providerOptions: part.providerOptions,
544
549
  };
545
550
  }
@@ -28,7 +28,10 @@ export function convertToLanguageModelV3DataContent(
28
28
  ): {
29
29
  data: LanguageModelV3DataContent;
30
30
  mediaType: string | undefined;
31
+ originalUrl?: string;
31
32
  } {
33
+ let originalUrl: string | undefined;
34
+
32
35
  // Buffer & Uint8Array:
33
36
  if (content instanceof Uint8Array) {
34
37
  return { data: content, mediaType: undefined };
@@ -43,7 +46,9 @@ export function convertToLanguageModelV3DataContent(
43
46
  // is not a URL and likely some other sort of data.
44
47
  if (typeof content === 'string') {
45
48
  try {
46
- content = new URL(content);
49
+ const url = new URL(content);
50
+ originalUrl = url.toString() !== content ? content : undefined;
51
+ content = url;
47
52
  } catch (error) {
48
53
  // ignored
49
54
  }
@@ -65,7 +70,11 @@ export function convertToLanguageModelV3DataContent(
65
70
  return { data: base64Content, mediaType: dataUrlMediaType };
66
71
  }
67
72
 
68
- return { data: content, mediaType: undefined };
73
+ return {
74
+ data: content,
75
+ mediaType: undefined,
76
+ ...(originalUrl != null ? { originalUrl } : {}),
77
+ };
69
78
  }
70
79
 
71
80
  /**
@@ -191,7 +191,7 @@ export abstract class HttpChatTransport<
191
191
  const response = await fetch(api, {
192
192
  method: 'POST',
193
193
  headers: {
194
- 'Content-Type': 'application/json',
194
+ 'content-type': 'application/json',
195
195
  ...headers,
196
196
  },
197
197
  body: JSON.stringify(body),
@@ -35,7 +35,7 @@ export function lastAssistantMessageIsCompleteWithApprovalResponses({
35
35
  // all tool approvals must have a response
36
36
  lastStepToolInvocations.every(
37
37
  part =>
38
- part.state === 'output-available' ||
38
+ (part.state === 'output-available' && part.preliminary !== true) ||
39
39
  part.state === 'output-error' ||
40
40
  part.state === 'output-denied' ||
41
41
  part.state === 'approval-responded',
@@ -33,7 +33,8 @@ export function lastAssistantMessageIsCompleteWithToolCalls({
33
33
  lastStepToolInvocations.length > 0 &&
34
34
  lastStepToolInvocations.every(
35
35
  part =>
36
- part.state === 'output-available' || part.state === 'output-error',
36
+ (part.state === 'output-available' && part.preliminary !== true) ||
37
+ part.state === 'output-error',
37
38
  )
38
39
  );
39
40
  }