ai 6.0.288 → 6.0.290

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/index.d.mts +13 -0
  3. package/dist/index.d.ts +13 -0
  4. package/dist/index.js +377 -281
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +314 -213
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/internal/index.js +20 -5
  9. package/dist/internal/index.js.map +1 -1
  10. package/dist/internal/index.mjs +20 -5
  11. package/dist/internal/index.mjs.map +1 -1
  12. package/docs/03-ai-sdk-core/16-mcp-tools.mdx +57 -0
  13. package/docs/03-ai-sdk-core/40-middleware.mdx +92 -7
  14. package/docs/04-ai-sdk-ui/03-chatbot-tool-usage.mdx +7 -0
  15. package/package.json +5 -4
  16. package/src/embed/embed-many.ts +19 -3
  17. package/src/generate-speech/generated-audio-file.ts +0 -8
  18. package/src/generate-text/generated-file.ts +0 -4
  19. package/src/generate-text/parse-tool-call.ts +30 -23
  20. package/src/generate-text/run-tools-transformation.ts +37 -3
  21. package/src/generate-text/stream-text.ts +9 -28
  22. package/src/generate-text/to-response-messages.ts +7 -0
  23. package/src/generate-text/tool-call.ts +26 -0
  24. package/src/generate-text/validate-tool-approvals.ts +20 -2
  25. package/src/middleware/extract-reasoning-middleware.ts +1 -1
  26. package/src/middleware/wrap-embedding-model.ts +12 -2
  27. package/src/model/get-embedding-model-provider-options-transformer.ts +17 -0
  28. package/src/prompt/content-part.ts +2 -0
  29. package/src/prompt/convert-to-language-model-prompt.ts +7 -2
  30. package/src/prompt/data-content.ts +11 -2
  31. package/src/ui/chat.ts +108 -17
  32. package/src/ui/convert-to-model-messages.ts +8 -0
  33. package/src/ui/http-chat-transport.ts +1 -1
  34. package/src/ui/last-assistant-message-is-complete-with-approval-responses.ts +1 -1
  35. package/src/ui/last-assistant-message-is-complete-with-tool-calls.ts +2 -1
  36. package/src/ui/process-ui-message-stream.ts +6 -0
  37. package/src/ui/ui-messages.ts +10 -0
  38. package/src/ui/validate-ui-messages.ts +79 -114
  39. package/src/ui-message-stream/ui-message-chunks.ts +2 -0
  40. package/src/util/write-to-server-response.ts +0 -2
@@ -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();
@@ -487,6 +487,13 @@ the `approval-responded` state after `addToolApprovalResponse` is called. This
487
487
  lets clients render or persist server-computed approval metadata without using
488
488
  it to determine whether the tool was approved.
489
489
 
490
+ When a tool input schema transforms its input, approval requests also preserve
491
+ the original value as `part.approval.inputSchemaInput`. Keep this field when
492
+ persisting UI messages or model response messages so the SDK can reconstruct
493
+ and verify the approved input before execution. The original value can include
494
+ fields removed by the transform, so approval metadata should not be treated as
495
+ redacted input.
496
+
490
497
  ### Auto-Submit After Approval
491
498
 
492
499
  <Note>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "6.0.288",
3
+ "version": "6.0.290",
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.200",
49
+ "@ai-sdk/provider": "3.0.17",
50
+ "@ai-sdk/provider-utils": "4.0.54"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@edge-runtime/vm": "^5.0.0",
@@ -58,6 +58,7 @@
58
58
  "tsx": "^4.19.2",
59
59
  "typescript": "5.8.3",
60
60
  "zod": "3.25.76",
61
+ "@ai-sdk/google": "3.0.127",
61
62
  "@ai-sdk/test-server": "1.0.6",
62
63
  "@vercel/ai-tsconfig": "0.0.0"
63
64
  },
@@ -4,6 +4,7 @@ import {
4
4
  } from '@ai-sdk/provider-utils';
5
5
  import { logWarnings } from '../logger/log-warnings';
6
6
  import { getEmbeddingModelMaxInputBytesPerCall } from '../model/get-embedding-model-max-input-bytes-per-call';
7
+ import { getEmbeddingModelProviderOptionsTransformer } from '../model/get-embedding-model-provider-options-transformer';
7
8
  import { resolveEmbeddingModel } from '../model/resolve-model';
8
9
  import { assembleOperationName } from '../telemetry/assemble-operation-name';
9
10
  import { getBaseTelemetryAttributes } from '../telemetry/get-base-telemetry-attributes';
@@ -250,6 +251,8 @@ export async function embedMany({
250
251
  ? maxInputBytesPerCall
251
252
  : Infinity,
252
253
  });
254
+ const providerOptionsTransformer =
255
+ getEmbeddingModelProviderOptionsTransformer(model);
253
256
 
254
257
  // serially embed the chunks:
255
258
  const embeddings: Array<Embedding> = [];
@@ -269,9 +272,22 @@ export async function embedMany({
269
272
  supportsParallelCalls ? maxParallelCalls : 1,
270
273
  );
271
274
 
275
+ let nextChunkStartIndex = 0;
272
276
  for (const parallelChunk of parallelChunks) {
273
277
  const results = await Promise.all(
274
- parallelChunk.map(chunk => {
278
+ parallelChunk.map(async chunk => {
279
+ // Capture the range before awaiting transformations or retrying.
280
+ const startIndex = nextChunkStartIndex;
281
+ nextChunkStartIndex += chunk.length;
282
+ const chunkProviderOptions = providerOptionsTransformer
283
+ ? await providerOptionsTransformer({
284
+ providerOptions,
285
+ values,
286
+ startIndex,
287
+ endIndex: startIndex + chunk.length,
288
+ })
289
+ : providerOptions;
290
+
275
291
  return retry(() => {
276
292
  // nested spans to align with the embedMany telemetry data:
277
293
  return recordSpan({
@@ -296,7 +312,7 @@ export async function embedMany({
296
312
  values: chunk,
297
313
  abortSignal,
298
314
  headers: headersWithUserAgent,
299
- providerOptions,
315
+ providerOptions: chunkProviderOptions,
300
316
  });
301
317
 
302
318
  const embeddings = modelResponse.embeddings;
@@ -343,7 +359,7 @@ export async function embedMany({
343
359
  result.providerMetadata,
344
360
  )) {
345
361
  providerMetadata[providerName] = {
346
- ...(providerMetadata[providerName] ?? {}),
362
+ ...providerMetadata[providerName],
347
363
  ...metadata,
348
364
  };
349
365
  }
@@ -54,12 +54,4 @@ export class DefaultGeneratedAudioFile
54
54
 
55
55
  export class DefaultGeneratedAudioFileWithType extends DefaultGeneratedAudioFile {
56
56
  readonly type = 'audio';
57
-
58
- constructor(options: {
59
- data: string | Uint8Array;
60
- mediaType: string;
61
- format: string;
62
- }) {
63
- super(options);
64
- }
65
57
  }
@@ -63,8 +63,4 @@ export class DefaultGeneratedFile implements GeneratedFile {
63
63
 
64
64
  export class DefaultGeneratedFileWithType extends DefaultGeneratedFile {
65
65
  readonly type = 'file';
66
-
67
- constructor(options: { data: string | Uint8Array; mediaType: string }) {
68
- super(options);
69
- }
70
66
  }
@@ -9,7 +9,11 @@ import {
9
9
  import { InvalidToolInputError } from '../error/invalid-tool-input-error';
10
10
  import { NoSuchToolError } from '../error/no-such-tool-error';
11
11
  import { ToolCallRepairError } from '../error/tool-call-repair-error';
12
- import type { DynamicToolCall, TypedToolCall } from './tool-call';
12
+ import {
13
+ setToolCallInputSchemaInput,
14
+ type DynamicToolCall,
15
+ type TypedToolCall,
16
+ } from './tool-call';
13
17
  import type { ToolCallRepairFunction } from './tool-call-repair-function';
14
18
  import type { ToolSet } from './tool-set';
15
19
 
@@ -167,26 +171,29 @@ async function doParseToolCall<TOOLS extends ToolSet>({
167
171
  });
168
172
  }
169
173
 
170
- return tool.type === 'dynamic'
171
- ? {
172
- type: 'tool-call',
173
- toolCallId: toolCall.toolCallId,
174
- toolName: toolCall.toolName,
175
- input: parseResult.value,
176
- providerExecuted: toolCall.providerExecuted,
177
- providerMetadata: toolCall.providerMetadata,
178
- ...(tool.metadata != null ? { toolMetadata: tool.metadata } : {}),
179
- dynamic: true,
180
- title: tool.title,
181
- }
182
- : {
183
- type: 'tool-call',
184
- toolCallId: toolCall.toolCallId,
185
- toolName,
186
- input: parseResult.value,
187
- providerExecuted: toolCall.providerExecuted,
188
- providerMetadata: toolCall.providerMetadata,
189
- ...(tool.metadata != null ? { toolMetadata: tool.metadata } : {}),
190
- title: tool.title,
191
- };
174
+ return setToolCallInputSchemaInput(
175
+ tool.type === 'dynamic'
176
+ ? {
177
+ type: 'tool-call',
178
+ toolCallId: toolCall.toolCallId,
179
+ toolName: toolCall.toolName,
180
+ input: parseResult.value,
181
+ providerExecuted: toolCall.providerExecuted,
182
+ providerMetadata: toolCall.providerMetadata,
183
+ ...(tool.metadata != null ? { toolMetadata: tool.metadata } : {}),
184
+ dynamic: true,
185
+ title: tool.title,
186
+ }
187
+ : {
188
+ type: 'tool-call',
189
+ toolCallId: toolCall.toolCallId,
190
+ toolName,
191
+ input: parseResult.value,
192
+ providerExecuted: toolCall.providerExecuted,
193
+ providerMetadata: toolCall.providerMetadata,
194
+ ...(tool.metadata != null ? { toolMetadata: tool.metadata } : {}),
195
+ title: tool.title,
196
+ },
197
+ parseResult.rawValue,
198
+ );
192
199
  }
@@ -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',
@@ -79,6 +79,7 @@ import { consumeStream } from '../util/consume-stream';
79
79
  import { createIdMap } from '../util/create-id-map';
80
80
  import { createStitchableStream } from '../util/create-stitchable-stream';
81
81
  import type { DownloadFunction } from '../util/download/download-function';
82
+ import { isDeepEqualData } from '../util/is-deep-equal-data';
82
83
  import { mergeAbortSignals } from '../util/merge-abort-signals';
83
84
  import { mergeObjects } from '../util/merge-objects';
84
85
  import { now as originalNow } from '../util/now';
@@ -123,7 +124,7 @@ import type {
123
124
  UIMessageStreamOptions,
124
125
  } from './stream-text-result';
125
126
  import { toResponseMessages } from './to-response-messages';
126
- import type { TypedToolCall } from './tool-call';
127
+ import { getToolCallInputSchemaInput, type TypedToolCall } from './tool-call';
127
128
  import type { ToolCallRepairFunction } from './tool-call-repair-function';
128
129
  import type { ToolOutput } from './tool-output';
129
130
  import type { StaticToolOutputDenied } from './tool-output-denied';
@@ -1965,8 +1966,6 @@ class DefaultStreamTextResult<
1965
1966
  const stepToolOutputs: ToolOutput<TOOLS>[] = [];
1966
1967
  let warnings: SharedV3Warning[] | undefined;
1967
1968
 
1968
- const activeToolCallToolNames: Record<string, string> = {};
1969
-
1970
1969
  let stepFinishReason: FinishReason = 'other';
1971
1970
  let stepRawFinishReason: string | undefined = undefined;
1972
1971
 
@@ -2168,18 +2167,7 @@ class DefaultStreamTextResult<
2168
2167
  }
2169
2168
 
2170
2169
  case 'tool-input-start': {
2171
- activeToolCallToolNames[chunk.id] = chunk.toolName;
2172
-
2173
2170
  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
2171
  controller.enqueue({
2184
2172
  ...chunk,
2185
2173
  dynamic: chunk.dynamic ?? tool?.type === 'dynamic',
@@ -2189,25 +2177,11 @@ class DefaultStreamTextResult<
2189
2177
  }
2190
2178
 
2191
2179
  case 'tool-input-end': {
2192
- delete activeToolCallToolNames[chunk.id];
2193
2180
  controller.enqueue(chunk);
2194
2181
  break;
2195
2182
  }
2196
2183
 
2197
2184
  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
2185
  controller.enqueue(chunk);
2212
2186
  break;
2213
2187
  }
@@ -3011,10 +2985,17 @@ class DefaultStreamTextResult<
3011
2985
  }
3012
2986
 
3013
2987
  case 'tool-approval-request': {
2988
+ const inputSchemaInput = getToolCallInputSchemaInput(
2989
+ part.toolCall,
2990
+ );
3014
2991
  controller.enqueue({
3015
2992
  type: 'tool-approval-request',
3016
2993
  approvalId: part.approvalId,
3017
2994
  toolCallId: part.toolCall.toolCallId,
2995
+ ...(inputSchemaInput != null &&
2996
+ !isDeepEqualData(inputSchemaInput.value, part.toolCall.input)
2997
+ ? { inputSchemaInput: inputSchemaInput.value }
2998
+ : {}),
3018
2999
  ...(part.signature != null
3019
3000
  ? { signature: part.signature }
3020
3001
  : {}),
@@ -5,8 +5,10 @@ import type {
5
5
  ToolModelMessage,
6
6
  } from '../prompt';
7
7
  import { createToolModelOutput } from '../prompt/create-tool-model-output';
8
+ import { isDeepEqualData } from '../util/is-deep-equal-data';
8
9
  import type { ContentPart } from './content-part';
9
10
  import type { ToolSet } from './tool-set';
11
+ import { getToolCallInputSchemaInput } from './tool-call';
10
12
 
11
13
  /**
12
14
  * Converts the result of a `generateText` or `streamText` call to a list of response messages.
@@ -113,11 +115,16 @@ export async function toResponseMessages<TOOLS extends ToolSet>({
113
115
  break;
114
116
  }
115
117
  case 'tool-approval-request':
118
+ const inputSchemaInput = getToolCallInputSchemaInput(part.toolCall);
116
119
  content.push({
117
120
  type: 'tool-approval-request',
118
121
  approvalId: part.approvalId,
119
122
  toolCallId: part.toolCall.toolCallId,
120
123
  ...(part.signature != null ? { signature: part.signature } : {}),
124
+ ...(inputSchemaInput != null &&
125
+ !isDeepEqualData(inputSchemaInput.value, part.toolCall.input)
126
+ ? { inputSchemaInput: inputSchemaInput.value }
127
+ : {}),
121
128
  });
122
129
  break;
123
130
  }
@@ -4,6 +4,8 @@ import type { ProviderMetadata } from '../types';
4
4
  import type { ValueOf } from '../util/value-of';
5
5
  import type { ToolSet } from './tool-set';
6
6
 
7
+ const inputSchemaInputSymbol = Symbol('ai-sdk-tool-call-input-schema-input');
8
+
7
9
  type BaseToolCall = {
8
10
  type: 'tool-call';
9
11
  toolCallId: string;
@@ -47,3 +49,27 @@ export type DynamicToolCall = BaseToolCall & {
47
49
  export type TypedToolCall<TOOLS extends ToolSet> =
48
50
  | StaticToolCall<TOOLS>
49
51
  | DynamicToolCall;
52
+
53
+ export function setToolCallInputSchemaInput<TOOLS extends ToolSet>(
54
+ toolCall: TypedToolCall<TOOLS>,
55
+ inputSchemaInput: unknown,
56
+ ): TypedToolCall<TOOLS> {
57
+ Object.defineProperty(toolCall, inputSchemaInputSymbol, {
58
+ value: inputSchemaInput,
59
+ });
60
+ return toolCall;
61
+ }
62
+
63
+ export function getToolCallInputSchemaInput<TOOLS extends ToolSet>(
64
+ toolCall: TypedToolCall<TOOLS>,
65
+ ): { value: unknown } | undefined {
66
+ return inputSchemaInputSymbol in toolCall
67
+ ? {
68
+ value: (
69
+ toolCall as TypedToolCall<TOOLS> & {
70
+ [inputSchemaInputSymbol]: unknown;
71
+ }
72
+ )[inputSchemaInputSymbol],
73
+ }
74
+ : undefined;
75
+ }
@@ -5,6 +5,7 @@ import {
5
5
  } from '@ai-sdk/provider-utils';
6
6
  import { InvalidToolApprovalSignatureError } from '../error/invalid-tool-approval-signature-error';
7
7
  import { InvalidToolInputError } from '../error/invalid-tool-input-error';
8
+ import { isDeepEqualData } from '../util/is-deep-equal-data';
8
9
  import type { CollectedToolApprovals } from './collect-tool-approvals';
9
10
  import { isApprovalNeeded } from './is-approval-needed';
10
11
  import { verifyToolApprovalSignature } from './tool-approval-signature';
@@ -86,18 +87,35 @@ export async function validateApprovedToolApprovals<TOOLS extends ToolSet>({
86
87
  typeof tool.execute === 'function' &&
87
88
  tool.inputSchema != null
88
89
  ) {
90
+ const hasInputSchemaInput = Object.prototype.hasOwnProperty.call(
91
+ approvalRequest,
92
+ 'inputSchemaInput',
93
+ );
89
94
  const validation = await safeValidateTypes({
90
- value: toolCall.input,
95
+ value: hasInputSchemaInput
96
+ ? approvalRequest.inputSchemaInput
97
+ : toolCall.input,
91
98
  schema: asSchema(tool.inputSchema),
92
99
  });
93
100
 
101
+ let validationError: unknown;
94
102
  if (!validation.success) {
103
+ validationError = validation.error;
104
+ } else if (!isDeepEqualData(validation.value, toolCall.input)) {
105
+ // Revalidation must never change the operation that was approved,
106
+ // including when older or projected history omits the schema input.
107
+ validationError = new Error(
108
+ 'Approved tool input does not match the validated schema output.',
109
+ );
110
+ }
111
+
112
+ if (validationError != null) {
95
113
  invalid.push({
96
114
  ...approval,
97
115
  error: new InvalidToolInputError({
98
116
  toolName: toolCall.toolName,
99
117
  toolInput: JSON.stringify(toolCall.input),
100
- cause: validation.error,
118
+ cause: validationError,
101
119
  }),
102
120
  });
103
121
  continue;
@@ -24,7 +24,7 @@ export function extractReasoningMiddleware({
24
24
  startWithReasoning?: boolean;
25
25
  }): LanguageModelMiddleware {
26
26
  const openingTag = `<${tagName}>`;
27
- const closingTag = `<\/${tagName}>`;
27
+ const closingTag = `</${tagName}>`;
28
28
 
29
29
  return {
30
30
  specificationVersion: 'v3',
@@ -2,11 +2,18 @@ import type {
2
2
  EmbeddingModelV3,
3
3
  EmbeddingModelV3CallOptions,
4
4
  } from '@ai-sdk/provider';
5
- import { EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL } from '@ai-sdk/provider-utils';
5
+ import {
6
+ EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL,
7
+ EXPERIMENTAL_EMBEDDING_MODEL_PROVIDER_OPTIONS_TRANSFORMER,
8
+ } from '@ai-sdk/provider-utils';
6
9
  import {
7
10
  getEmbeddingModelMaxInputBytesPerCall,
8
11
  type EmbeddingModelWithMaxInputBytesPerCall,
9
12
  } from '../model/get-embedding-model-max-input-bytes-per-call';
13
+ import {
14
+ getEmbeddingModelProviderOptionsTransformer,
15
+ type EmbeddingModelWithProviderOptionsTransformer,
16
+ } from '../model/get-embedding-model-provider-options-transformer';
10
17
  import type { EmbeddingModelMiddleware } from '../types';
11
18
  import { asArray } from '../util/as-array';
12
19
 
@@ -57,7 +64,8 @@ const doWrap = ({
57
64
  middleware: EmbeddingModelMiddleware;
58
65
  modelId?: string;
59
66
  providerId?: string;
60
- }): EmbeddingModelWithMaxInputBytesPerCall => {
67
+ }): EmbeddingModelWithMaxInputBytesPerCall &
68
+ EmbeddingModelWithProviderOptionsTransformer => {
61
69
  async function doTransform({
62
70
  params,
63
71
  }: {
@@ -74,6 +82,8 @@ const doWrap = ({
74
82
  overrideMaxEmbeddingsPerCall?.({ model }) ?? model.maxEmbeddingsPerCall,
75
83
  [EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL]:
76
84
  getEmbeddingModelMaxInputBytesPerCall(model),
85
+ [EXPERIMENTAL_EMBEDDING_MODEL_PROVIDER_OPTIONS_TRANSFORMER]:
86
+ getEmbeddingModelProviderOptionsTransformer(model),
77
87
  supportsParallelCalls:
78
88
  overrideSupportsParallelCalls?.({ model }) ?? model.supportsParallelCalls,
79
89
  async doEmbed(
@@ -0,0 +1,17 @@
1
+ import type { EmbeddingModelV3 } from '@ai-sdk/provider';
2
+ import {
3
+ EXPERIMENTAL_EMBEDDING_MODEL_PROVIDER_OPTIONS_TRANSFORMER,
4
+ type EmbeddingModelProviderOptionsTransformer,
5
+ } from '@ai-sdk/provider-utils';
6
+
7
+ export type EmbeddingModelWithProviderOptionsTransformer = EmbeddingModelV3 & {
8
+ readonly [EXPERIMENTAL_EMBEDDING_MODEL_PROVIDER_OPTIONS_TRANSFORMER]?: EmbeddingModelProviderOptionsTransformer;
9
+ };
10
+
11
+ export function getEmbeddingModelProviderOptionsTransformer(
12
+ model: EmbeddingModelV3,
13
+ ) {
14
+ return (model as EmbeddingModelWithProviderOptionsTransformer)[
15
+ EXPERIMENTAL_EMBEDDING_MODEL_PROVIDER_OPTIONS_TRANSFORMER
16
+ ];
17
+ }