ai 7.0.106 → 7.0.108

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 (37) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +390 -95
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal/index.d.ts +16 -2
  6. package/dist/internal/index.js +323 -53
  7. package/dist/internal/index.js.map +1 -1
  8. package/docs/00-introduction/index.mdx +1 -1
  9. package/docs/02-getting-started/03-nextjs-pages-router.mdx +1 -1
  10. package/docs/02-getting-started/05-nuxt.mdx +1 -1
  11. package/docs/03-ai-sdk-core/16-mcp-tools.mdx +31 -0
  12. package/docs/03-ai-sdk-core/19-tool-search.mdx +4 -2
  13. package/docs/04-ai-sdk-ui/02-chatbot.mdx +26 -11
  14. package/docs/04-ai-sdk-ui/05-completion.mdx +1 -1
  15. package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +7 -0
  16. package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +7 -0
  17. package/docs/07-reference/01-ai-sdk-core/23-tool-search.mdx +3 -2
  18. package/docs/07-reference/02-ai-sdk-ui/01-use-chat.mdx +1 -1
  19. package/package.json +11 -11
  20. package/src/generate-speech/generate-speech.ts +44 -4
  21. package/src/generate-text/generate-text.ts +29 -16
  22. package/src/generate-text/invoke-tool-callbacks-from-stream.ts +68 -16
  23. package/src/generate-text/parse-tool-call.ts +64 -12
  24. package/src/generate-text/stream-language-model-call.ts +32 -21
  25. package/src/generate-text/stream-text.ts +1 -1
  26. package/src/generate-text/tool-call-repair-function.ts +2 -0
  27. package/src/generate-video/generate-video.ts +61 -10
  28. package/src/middleware/wrap-provider.ts +2 -0
  29. package/src/model/as-language-model-v4.ts +166 -6
  30. package/src/test/mock-server-response.ts +19 -3
  31. package/src/text-stream/pipe-text-stream-to-response.ts +3 -5
  32. package/src/ui/http-chat-transport.ts +12 -2
  33. package/src/ui/last-assistant-message-is-complete-with-approval-responses.ts +1 -1
  34. package/src/ui/last-assistant-message-is-complete-with-tool-calls.ts +2 -1
  35. package/src/ui-message-stream/pipe-ui-message-stream-to-response.ts +1 -3
  36. package/src/util/data-url.ts +13 -1
  37. package/src/util/write-to-server-response.ts +7 -3
@@ -26,7 +26,7 @@ For example, here’s how you can generate text with various models using the AI
26
26
  The AI SDK has these primary surfaces:
27
27
 
28
28
  - **[AI SDK Core](/docs/ai-sdk-core):** A unified API for generating text, structured objects, tool calls, and building agents with LLMs.
29
- - **[AI SDK UI](/docs/ai-sdk-ui):** A set of framework-agnostic hooks for quickly building chat and generative user interface.
29
+ - **[AI SDK UI](/docs/ai-sdk-ui):** A set of framework-agnostic hooks for quickly building chat and generative user interfaces.
30
30
  - **[AI SDK Harnesses](/docs/ai-sdk-harnesses):** A uniform API for running established agent harnesses with `HarnessAgent`.
31
31
 
32
32
  ## Model Providers
@@ -153,7 +153,7 @@ Pick the approach that best matches how you want to manage providers across your
153
153
 
154
154
  ## Wire up the UI
155
155
 
156
- Now that you have an API route that can query an LLM, it's time to setup your frontend. The AI SDK's [ UI ](/docs/ai-sdk-ui) package abstract the complexity of a chat interface into one hook, [`useChat`](/docs/reference/ai-sdk-ui/use-chat).
156
+ Now that you have an API route that can query an LLM, it's time to set up your frontend. The AI SDK's [ UI ](/docs/ai-sdk-ui) package abstracts the complexity of a chat interface into one hook, [`useChat`](/docs/reference/ai-sdk-ui/use-chat).
157
157
 
158
158
  Update your root page (`pages/index.tsx`) with the following code to show a list of chat messages and provide a user message input:
159
159
 
@@ -154,7 +154,7 @@ model: openai('gpt-5.1');
154
154
 
155
155
  ## Wire up the UI
156
156
 
157
- Now that you have an API route that can query an LLM, it's time to setup your frontend. The AI SDK's [ UI ](/docs/ai-sdk-ui/overview) package abstract the complexity of a chat interface into one hook, [`useChat`](/docs/reference/ai-sdk-ui/use-chat).
157
+ Now that you have an API route that can query an LLM, it's time to set up your frontend. The AI SDK's [ UI ](/docs/ai-sdk-ui/overview) package abstracts the complexity of a chat interface into one hook, [`useChat`](/docs/reference/ai-sdk-ui/use-chat).
158
158
 
159
159
  Update your root page (`pages/index.vue`) with the following code to show a list of chat messages and provide a user message input:
160
160
 
@@ -236,6 +236,37 @@ client infers `native` for loopback, localhost, and custom-scheme redirects,
236
236
  and `web` for remote HTTP(S) redirects. You can override this by setting
237
237
  `application_type` in the provider's `clientMetadata`.
238
238
 
239
+ ### Dynamic Client Credential Recovery
240
+
241
+ To let the SDK replace invalid credentials created through dynamic client
242
+ registration, implement `isClientInformationDynamicallyRegistered` on the
243
+ OAuth client provider. Return `true` only when the current client information
244
+ came from a dynamic registration response. Persist this provenance alongside
245
+ the credentials when the provider is recreated between requests.
246
+
247
+ ```typescript
248
+ const myOAuthClientProvider = {
249
+ // ...other OAuthClientProvider methods
250
+
251
+ async saveClientInformation(clientInformation) {
252
+ await saveClientRecord({
253
+ clientInformation,
254
+ dynamicallyRegistered: true,
255
+ });
256
+ },
257
+
258
+ async isClientInformationDynamicallyRegistered() {
259
+ return (await loadClientRecord())?.dynamicallyRegistered === true;
260
+ },
261
+ };
262
+ ```
263
+
264
+ When this method is omitted or returns `false`, client information is treated
265
+ as pre-registered and is preserved after `invalid_client` and
266
+ `unauthorized_client` errors. The SDK does not infer provenance from fields such
267
+ as `redirect_uris`, because pre-registered clients can contain the same
268
+ metadata as dynamically registered clients.
269
+
239
270
  ### Retrying Transient Tool Failures
240
271
 
241
272
  MCP tool calls can fail for transient transport reasons, such as rate limits,
@@ -10,8 +10,10 @@ definition into its initial context. Register tools with `deferLoading: true`;
10
10
  search matches their names and descriptions and makes them available on the
11
11
  **next model step**.
12
12
 
13
- Use it with `generateText`, `streamText`, or `ToolLoopAgent`. The factory takes no
14
- arguments; the model supplies a search query.
13
+ Use it with `generateText`, `streamText`, `ToolLoopAgent`, or `WorkflowAgent` from
14
+ `@ai-sdk/workflow`. The factory takes no arguments; the model supplies a search
15
+ query. `WorkflowAgent` supports direct tool calling; the other APIs also support
16
+ cache-preserving code mode.
15
17
 
16
18
  ## Direct Tool Calling
17
19
 
@@ -323,17 +323,32 @@ When the user clicks the "Regenerate" button, the AI provider will regenerate th
323
323
 
324
324
  ### Throttling UI Updates
325
325
 
326
- <Note>This feature is currently only available for React.</Note>
327
-
328
326
  By default, the `useChat` hook will trigger a render every time a new chunk is received.
329
- You can throttle the UI updates with the `throttle` option.
330
-
331
- ```tsx filename="page.tsx" highlight="2-3"
332
- const { messages, ... } = useChat({
333
- // Throttle the messages and data updates to 50ms:
334
- throttle: 50
335
- })
336
- ```
327
+ React and Vue applications can throttle reactive message updates with the `throttle` option.
328
+ Stream processing and event callbacks remain immediate, and the latest messages are published before the chat enters a terminal `ready` or `error` status.
329
+
330
+ <Tabs items={['React', 'Vue']}>
331
+ <Tab>
332
+ ```tsx filename="page.tsx" highlight="2-3"
333
+ const { messages, ... } = useChat({
334
+ // Throttle reactive message updates to 50ms:
335
+ throttle: 50,
336
+ });
337
+ ```
338
+ </Tab>
339
+ <Tab>
340
+ ```vue filename="pages/index.vue" highlight="4-6"
341
+ <script setup lang="ts">
342
+ import { useChat } from '@ai-sdk/vue';
343
+
344
+ const { messages } = useChat({
345
+ throttle: 50,
346
+ });
347
+ </script>
348
+ ```
349
+
350
+ </Tab>
351
+ </Tabs>
337
352
 
338
353
  ## Event Callbacks
339
354
 
@@ -369,7 +384,7 @@ It's worth noting that you can abort the processing by throwing an error in the
369
384
 
370
385
  ### Custom headers, body, and credentials
371
386
 
372
- By default, the `useChat` hook sends a HTTP POST request to the `/api/chat` endpoint with the message list as the request body. You can customize the request in two ways:
387
+ By default, the `useChat` hook sends an HTTP POST request to the `/api/chat` endpoint with the message list as the request body. You can customize the request in two ways:
373
388
 
374
389
  #### Hook-Level Configuration (Applied to all requests)
375
390
 
@@ -169,7 +169,7 @@ const { ... } = useCompletion({
169
169
 
170
170
  ## Configure Request Options
171
171
 
172
- By default, the `useCompletion` hook sends a HTTP POST request to the `/api/completion` endpoint with the prompt as part of the request body. You can customize the request by passing additional options to the `useCompletion` hook:
172
+ By default, the `useCompletion` hook sends an HTTP POST request to the `/api/completion` endpoint with the prompt as part of the request body. You can customize the request by passing additional options to the `useCompletion` hook:
173
173
 
174
174
  ```tsx
175
175
  const { messages, input, handleInputChange, handleSubmit } = useCompletion({
@@ -929,6 +929,13 @@ To see `generateText` in action, check out [these examples](#examples).
929
929
  description:
930
930
  'The error that occurred while parsing the tool call.',
931
931
  },
932
+ {
933
+ name: 'abortSignal',
934
+ type: 'AbortSignal',
935
+ isOptional: true,
936
+ description:
937
+ 'An optional signal for cancelling the tool call repair.',
938
+ },
932
939
  ],
933
940
  },
934
941
  ],
@@ -980,6 +980,13 @@ To see `streamText` in action, check out [these examples](#examples).
980
980
  description:
981
981
  'The error that occurred while parsing the tool call.',
982
982
  },
983
+ {
984
+ name: 'abortSignal',
985
+ type: 'AbortSignal',
986
+ isOptional: true,
987
+ description:
988
+ 'An optional signal for cancelling the tool call repair.',
989
+ },
983
990
  ],
984
991
  },
985
992
  ],
@@ -7,8 +7,9 @@ description: Search deferred tools and load their definitions on demand for dire
7
7
 
8
8
  Creates a tool that searches the surrounding generation's deferred tools by name
9
9
  and description. The factory takes no arguments. Use it with `generateText`,
10
- `streamText`, or `ToolLoopAgent`, either with direct tool calling or with code mode
11
- configured with `toolDiscovery: 'conversation'`.
10
+ `streamText`, `ToolLoopAgent`, or `WorkflowAgent` from `@ai-sdk/workflow`.
11
+ `WorkflowAgent` supports direct tool calling. The other APIs also support code
12
+ mode configured with `toolDiscovery: 'conversation'`.
12
13
 
13
14
  ```ts
14
15
  import { toolSearch } from 'ai';
@@ -338,7 +338,7 @@ Allows you to easily create a conversational user interface for your chatbot app
338
338
  type: 'number',
339
339
  isOptional: true,
340
340
  description:
341
- 'Custom throttle wait in ms for the chat messages and data updates. Default is undefined, which disables throttling.',
341
+ 'React and Vue only. Custom throttle wait time in milliseconds for reactive chat message updates. Positive values reduce UI update frequency without delaying stream processing or callbacks, and the latest messages are published before a ready or error status. Default is undefined, which disables throttling.',
342
342
  },
343
343
  {
344
344
  name: 'resume',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.106",
3
+ "version": "7.0.108",
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.86",
45
+ "@ai-sdk/gateway": "4.0.88",
46
46
  "@ai-sdk/provider": "4.0.17",
47
- "@ai-sdk/provider-utils": "5.0.44"
47
+ "@ai-sdk/provider-utils": "5.0.45"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.87",
51
- "@ai-sdk/deepseek": "3.0.48",
52
- "@ai-sdk/google": "4.0.75",
53
- "@ai-sdk/groq": "4.0.45",
54
- "@ai-sdk/huggingface": "2.0.52",
55
- "@ai-sdk/moonshotai": "3.0.53",
56
- "@ai-sdk/openai": "4.0.70",
50
+ "@ai-sdk/amazon-bedrock": "5.0.89",
51
+ "@ai-sdk/deepseek": "3.0.50",
52
+ "@ai-sdk/google": "4.0.76",
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",
57
57
  "@ai-sdk/test-server": "2.0.1",
58
- "@ai-sdk/xai": "5.0.3",
58
+ "@ai-sdk/xai": "5.0.5",
59
59
  "@edge-runtime/vm": "^5.0.0",
60
60
  "@smithy/eventstream-codec": "^4.3.3",
61
61
  "@smithy/util-utf8": "^4.3.3",
@@ -155,14 +155,19 @@ export async function generateSpeech({
155
155
  model: resolvedModel.modelId,
156
156
  });
157
157
 
158
+ const detectedMediaType = detectMediaType({
159
+ data: result.audio,
160
+ topLevelType: 'audio',
161
+ });
162
+
158
163
  return new DefaultSpeechResult({
159
164
  audio: new DefaultGeneratedAudioFile({
160
165
  data: result.audio,
161
166
  mediaType:
162
- detectMediaType({
163
- data: result.audio,
164
- topLevelType: 'audio',
165
- }) ?? 'audio/mp3',
167
+ detectedMediaType ??
168
+ getResponseAudioMediaType(result.response.headers) ??
169
+ getOutputFormatMediaType(outputFormat) ??
170
+ 'audio/mp3',
166
171
  }),
167
172
  warnings: result.warnings,
168
173
  responses: [result.response],
@@ -170,6 +175,41 @@ export async function generateSpeech({
170
175
  });
171
176
  }
172
177
 
178
+ function getResponseAudioMediaType(
179
+ headers: Record<string, string> | undefined,
180
+ ): string | undefined {
181
+ const mediaType = Object.entries(headers ?? {}).find(
182
+ ([name]) => name.toLowerCase() === 'content-type',
183
+ )?.[1];
184
+
185
+ if (mediaType == null) {
186
+ return undefined;
187
+ }
188
+
189
+ const normalizedMediaType = mediaType.split(';', 1)[0].trim().toLowerCase();
190
+
191
+ if (normalizedMediaType.length === 0) {
192
+ return undefined;
193
+ }
194
+
195
+ return normalizedMediaType.startsWith('audio/')
196
+ ? normalizedMediaType
197
+ : undefined;
198
+ }
199
+
200
+ function getOutputFormatMediaType(outputFormat: string | undefined) {
201
+ if (outputFormat == null) {
202
+ return undefined;
203
+ }
204
+
205
+ const normalizedOutputFormat = outputFormat.trim().toLowerCase();
206
+
207
+ return normalizedOutputFormat === 'pcm' ||
208
+ normalizedOutputFormat === 'audio/pcm'
209
+ ? 'audio/pcm'
210
+ : undefined;
211
+ }
212
+
173
213
  class DefaultSpeechResult implements SpeechResult {
174
214
  readonly audio: GeneratedAudioFile;
175
215
  readonly warnings: Array<Warning>;
@@ -118,6 +118,7 @@ import type {
118
118
  OnToolExecutionEndCallback,
119
119
  OnToolExecutionStartCallback,
120
120
  } from './tool-execution-events';
121
+ import { validateToolContext } from './validate-tool-context';
121
122
  import type { ToolInputRefinement } from './tool-input-refinement';
122
123
  import type { ToolOrder } from './tool-order';
123
124
  import type { ToolOutput } from './tool-output';
@@ -1078,11 +1079,12 @@ export async function generateText<
1078
1079
  .map(toolCall =>
1079
1080
  parseToolCall({
1080
1081
  toolCall,
1081
- tools: stepExecutionTools as TOOLS,
1082
+ tools: stepModelTools as TOOLS,
1082
1083
  repairToolCall,
1083
1084
  refineToolInput,
1084
1085
  instructions: stepInstructions,
1085
1086
  messages: stepMessages,
1087
+ abortSignal: mergedAbortSignal,
1086
1088
  }),
1087
1089
  ),
1088
1090
  );
@@ -1183,23 +1185,34 @@ export async function generateText<
1183
1185
  continue;
1184
1186
  }
1185
1187
 
1186
- if (tool.onInputStart != null) {
1187
- await tool.onInputStart({
1188
- toolCallId: toolCall.toolCallId,
1189
- messages: stepMessages,
1190
- abortSignal: mergedAbortSignal,
1191
- context: runtimeContext,
1188
+ if (
1189
+ tool.onInputStart != null ||
1190
+ tool.onInputAvailable != null
1191
+ ) {
1192
+ const context = await validateToolContext({
1193
+ toolName: toolCall.toolName,
1194
+ context: getOwn(toolsContext, toolCall.toolName),
1195
+ contextSchema: tool.contextSchema,
1192
1196
  });
1193
- }
1194
1197
 
1195
- if (tool?.onInputAvailable != null) {
1196
- await tool.onInputAvailable({
1197
- input: toolCall.input,
1198
- toolCallId: toolCall.toolCallId,
1199
- messages: stepMessages,
1200
- abortSignal: mergedAbortSignal,
1201
- context: runtimeContext,
1202
- });
1198
+ if (tool.onInputStart != null) {
1199
+ await tool.onInputStart({
1200
+ toolCallId: toolCall.toolCallId,
1201
+ messages: stepMessages,
1202
+ abortSignal: mergedAbortSignal,
1203
+ context,
1204
+ });
1205
+ }
1206
+
1207
+ if (tool.onInputAvailable != null) {
1208
+ await tool.onInputAvailable({
1209
+ input: toolCall.input,
1210
+ toolCallId: toolCall.toolCallId,
1211
+ messages: stepMessages,
1212
+ abortSignal: mergedAbortSignal,
1213
+ context,
1214
+ });
1215
+ }
1203
1216
  }
1204
1217
 
1205
1218
  const toolApprovalStatus = await resolveToolApproval({
@@ -1,4 +1,8 @@
1
- import type { Context, ModelMessage, ToolSet } from '@ai-sdk/provider-utils';
1
+ import type {
2
+ InferToolSetContext,
3
+ ModelMessage,
4
+ ToolSet,
5
+ } from '@ai-sdk/provider-utils';
2
6
  import { createIdMap } from '../util/create-id-map';
3
7
  import { getOwn } from '../util/get-own';
4
8
  import type { LanguageModelStreamPart } from './stream-language-model-call';
@@ -6,30 +10,61 @@ import {
6
10
  isStreamRetryAttemptBoundaryPart,
7
11
  type StreamRetryAttemptBoundaryPart,
8
12
  } from './stream-retry-attempt-boundary';
13
+ import { validateToolContext } from './validate-tool-context';
9
14
 
10
15
  type ToolCallbackStreamPart<TOOLS extends ToolSet> =
11
16
  | LanguageModelStreamPart<TOOLS>
12
17
  | StreamRetryAttemptBoundaryPart;
13
18
 
14
- export function invokeToolCallbacksFromStream<
15
- TOOLS extends ToolSet,
16
- RUNTIME_CONTEXT extends Context,
17
- >({
19
+ export function invokeToolCallbacksFromStream<TOOLS extends ToolSet>({
18
20
  stream,
19
21
  tools,
20
22
  stepInputMessages,
21
23
  abortSignal,
22
- runtimeContext,
24
+ toolsContext,
23
25
  }: {
24
26
  stream: ReadableStream<ToolCallbackStreamPart<TOOLS>>;
25
27
  tools: TOOLS | undefined;
26
28
  stepInputMessages: Array<ModelMessage>;
27
29
  abortSignal: AbortSignal | undefined;
28
- runtimeContext: RUNTIME_CONTEXT;
30
+ toolsContext: InferToolSetContext<TOOLS>;
29
31
  }): ReadableStream<ToolCallbackStreamPart<TOOLS>> {
30
32
  if (tools == null) return stream;
31
33
 
32
- const ongoingToolCallToolNames: Record<string, string> = createIdMap();
34
+ let ongoingToolCalls: Record<
35
+ string,
36
+ {
37
+ toolName: string;
38
+ validatedContext: Promise<unknown> | undefined;
39
+ }
40
+ > = createIdMap();
41
+
42
+ const getValidatedContext = ({
43
+ toolCallId,
44
+ toolName,
45
+ }: {
46
+ toolCallId: string;
47
+ toolName: string;
48
+ }): Promise<unknown> => {
49
+ const ongoingToolCall = ongoingToolCalls[toolCallId];
50
+
51
+ if (ongoingToolCall?.validatedContext != null) {
52
+ return ongoingToolCall.validatedContext;
53
+ }
54
+
55
+ const tool = getOwn(tools, toolName);
56
+ const validatedContext = validateToolContext({
57
+ toolName,
58
+ context: getOwn(toolsContext, toolName),
59
+ contextSchema: tool?.contextSchema,
60
+ });
61
+
62
+ if (ongoingToolCall != null) {
63
+ ongoingToolCall.validatedContext = validatedContext;
64
+ }
65
+
66
+ return validatedContext;
67
+ };
33
68
 
34
69
  return stream.pipeThrough(
35
70
  new TransformStream({
@@ -37,12 +72,16 @@ export function invokeToolCallbacksFromStream<
37
72
  controller.enqueue(chunk);
38
73
 
39
74
  if (isStreamRetryAttemptBoundaryPart(chunk)) {
75
+ ongoingToolCalls = createIdMap();
40
76
  return;
41
77
  }
42
78
 
43
79
  switch (chunk.type) {
44
80
  case 'tool-input-start': {
45
- ongoingToolCallToolNames[chunk.id] = chunk.toolName;
81
+ ongoingToolCalls[chunk.id] = {
82
+ toolName: chunk.toolName,
83
+ validatedContext: undefined,
84
+ };
46
85
 
47
86
  const tool = getOwn(tools, chunk.toolName);
48
87
  if (tool?.onInputStart != null) {
@@ -50,7 +89,10 @@ export function invokeToolCallbacksFromStream<
50
89
  toolCallId: chunk.id,
51
90
  messages: stepInputMessages,
52
91
  abortSignal,
53
- context: runtimeContext,
92
+ context: await getValidatedContext({
93
+ toolCallId: chunk.id,
94
+ toolName: chunk.toolName,
95
+ }),
54
96
  });
55
97
  }
56
98
 
@@ -58,7 +100,7 @@ export function invokeToolCallbacksFromStream<
58
100
  }
59
101
 
60
102
  case 'tool-input-delta': {
61
- const toolName = ongoingToolCallToolNames[chunk.id];
103
+ const toolName = ongoingToolCalls[chunk.id]?.toolName;
62
104
  const tool = getOwn(tools, toolName);
63
105
 
64
106
  if (tool?.onInputDelta != null) {
@@ -67,7 +109,10 @@ export function invokeToolCallbacksFromStream<
67
109
  toolCallId: chunk.id,
68
110
  messages: stepInputMessages,
69
111
  abortSignal,
70
- context: runtimeContext,
112
+ context: await getValidatedContext({
113
+ toolCallId: chunk.id,
114
+ toolName,
115
+ }),
71
116
  });
72
117
  }
73
118
 
@@ -75,19 +120,26 @@ export function invokeToolCallbacksFromStream<
75
120
  }
76
121
 
77
122
  case 'tool-call': {
78
- const toolName = ongoingToolCallToolNames[chunk.toolCallId];
123
+ const toolName = ongoingToolCalls[chunk.toolCallId]?.toolName;
79
124
  const tool = getOwn(tools, toolName);
80
125
 
81
- delete ongoingToolCallToolNames[chunk.toolCallId];
82
-
83
126
  if (!chunk.invalid && tool?.onInputAvailable != null) {
127
+ const validatedContext = getValidatedContext({
128
+ toolCallId: chunk.toolCallId,
129
+ toolName,
130
+ });
131
+
132
+ delete ongoingToolCalls[chunk.toolCallId];
133
+
84
134
  await tool.onInputAvailable({
85
135
  input: chunk.input,
86
136
  toolCallId: chunk.toolCallId,
87
137
  messages: stepInputMessages,
88
138
  abortSignal,
89
- context: runtimeContext,
139
+ context: await validatedContext,
90
140
  });
141
+ } else {
142
+ delete ongoingToolCalls[chunk.toolCallId];
91
143
  }
92
144
  }
93
145
  }
@@ -23,6 +23,7 @@ export async function parseToolCall<TOOLS extends ToolSet>({
23
23
  refineToolInput,
24
24
  messages,
25
25
  instructions,
26
+ abortSignal,
26
27
  }: {
27
28
  toolCall: LanguageModelV4ToolCall;
28
29
  tools: TOOLS | undefined;
@@ -30,6 +31,7 @@ export async function parseToolCall<TOOLS extends ToolSet>({
30
31
  refineToolInput?: ToolInputRefinement<TOOLS> | undefined;
31
32
  instructions: Instructions | undefined;
32
33
  messages: ModelMessage[];
34
+ abortSignal?: AbortSignal;
33
35
  }): Promise<TypedToolCall<TOOLS>> {
34
36
  try {
35
37
  if (tools == null) {
@@ -63,19 +65,25 @@ export async function parseToolCall<TOOLS extends ToolSet>({
63
65
  let repairedToolCall: LanguageModelV4ToolCall | null = null;
64
66
 
65
67
  try {
66
- repairedToolCall = await repairToolCall({
67
- toolCall,
68
- tools,
69
- inputSchema: async ({ toolName }) => {
70
- const inputSchema = getOwn(tools, toolName)?.inputSchema;
71
- return await asSchema(inputSchema).jsonSchema;
72
- },
73
- instructions,
74
- system: instructions,
75
- messages,
76
- error,
68
+ abortSignal?.throwIfAborted();
69
+ repairedToolCall = await waitForPromiseWithAbortSignal({
70
+ promise: repairToolCall({
71
+ toolCall,
72
+ tools,
73
+ inputSchema: async ({ toolName }) => {
74
+ const inputSchema = getOwn(tools, toolName)?.inputSchema;
75
+ return await asSchema(inputSchema).jsonSchema;
76
+ },
77
+ instructions,
78
+ system: instructions,
79
+ messages,
80
+ error,
81
+ abortSignal,
82
+ }),
83
+ abortSignal,
77
84
  });
78
85
  } catch (repairError) {
86
+ abortSignal?.throwIfAborted();
79
87
  throw new ToolCallRepairError({
80
88
  cause: repairError,
81
89
  originalError: error,
@@ -87,12 +95,18 @@ export async function parseToolCall<TOOLS extends ToolSet>({
87
95
  throw error;
88
96
  }
89
97
 
90
- return await refineParsedToolCallInput({
98
+ const parsedRepairedToolCall = await refineParsedToolCallInput({
91
99
  toolCall: await doParseToolCall({ toolCall: repairedToolCall, tools }),
92
100
  refineToolInput,
93
101
  });
102
+
103
+ abortSignal?.throwIfAborted();
104
+
105
+ return parsedRepairedToolCall;
94
106
  }
95
107
  } catch (error) {
108
+ abortSignal?.throwIfAborted();
109
+
96
110
  // use parsed input when possible
97
111
  const parsedInput = await safeParseJSON({ text: toolCall.input });
98
112
  const input = parsedInput.success ? parsedInput.value : toolCall.input;
@@ -115,6 +129,44 @@ export async function parseToolCall<TOOLS extends ToolSet>({
115
129
  }
116
130
  }
117
131
 
132
+ async function waitForPromiseWithAbortSignal<T>({
133
+ promise,
134
+ abortSignal,
135
+ }: {
136
+ promise: PromiseLike<T>;
137
+ abortSignal: AbortSignal | undefined;
138
+ }): Promise<T> {
139
+ if (abortSignal == null) {
140
+ return await promise;
141
+ }
142
+
143
+ return await new Promise<T>((resolve, reject) => {
144
+ const cleanup = () => {
145
+ abortSignal.removeEventListener('abort', onAbort);
146
+ };
147
+ const onAbort = () => {
148
+ cleanup();
149
+ reject(abortSignal.reason);
150
+ };
151
+
152
+ Promise.resolve(promise)
153
+ .then(value => {
154
+ cleanup();
155
+ resolve(value);
156
+ })
157
+ .catch(error => {
158
+ cleanup();
159
+ reject(error);
160
+ });
161
+
162
+ abortSignal.addEventListener('abort', onAbort, { once: true });
163
+
164
+ if (abortSignal.aborted) {
165
+ onAbort();
166
+ }
167
+ });
168
+ }
169
+
118
170
  async function refineParsedToolCallInput<TOOLS extends ToolSet>({
119
171
  toolCall,
120
172
  refineToolInput,