ai 7.0.126 → 7.0.127

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.
@@ -11,8 +11,9 @@ search matches their names and descriptions and makes them available on the
11
11
  **next model step**.
12
12
 
13
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
14
+ `@ai-sdk/workflow`. The model supplies a search query; the factory optionally
15
+ accepts an application-defined `search` callback and a `maxResults` limit.
16
+ `WorkflowAgent` supports direct tool calling; the other APIs also support
16
17
  cache-preserving code mode.
17
18
 
18
19
  ## Direct Tool Calling
@@ -73,10 +74,45 @@ The model first runs `tools.search({ query: 'weather forecast' })`. On the next
73
74
  step, it receives the updated capability catalog and can call
74
75
  `tools.weather({ city: 'Bangalore' })`.
75
76
 
76
- In both modes, search loads up to five matches, respects `activeTools` and caller
77
- routing, and keeps discovered tools available for the rest of the generation.
77
+ In both modes, search loads up to five matches by default, respects `activeTools`
78
+ and caller routing, and keeps discovered tools available for the rest of the
79
+ generation.
78
80
  MCP client tools work too: add `deferLoading: true` to the tools returned by
79
81
  `client.tools()`.
80
82
 
83
+ Set `maxResults` to change the number of matching tools loaded per search:
84
+
85
+ ```ts
86
+ const search = toolSearch({ maxResults: 10 });
87
+ ```
88
+
89
+ `maxResults` must be a positive safe integer. A larger limit can load related
90
+ tools in one search, but also adds more tool definitions to the model context.
91
+ It does not change keyword matching or ranking.
92
+
93
+ ## Custom Search
94
+
95
+ Provide a `search` callback when your application needs different matching or
96
+ ranking. It receives the query and all eligible deferred tool names and resolved
97
+ descriptions, and returns ranked names. It can be synchronous or asynchronous.
98
+
99
+ ```ts
100
+ const search = toolSearch({
101
+ search: ({ query, tools }) => {
102
+ // Discover related Gmail actions together, in the application's preferred order.
103
+ const preferred = /\bgmail\b/i.test(query)
104
+ ? ['gmail_list_messages', 'gmail_get_message']
105
+ : [];
106
+ return preferred.filter(name => tools.some(tool => tool.name === name));
107
+ },
108
+ });
109
+ ```
110
+
111
+ The SDK filters unknown names and duplicates before keeping the first `maxResults`
112
+ valid results (five by default). Tools excluded by `activeTools` or caller routing
113
+ remain unavailable.
114
+ Discovery still takes effect on the next model step. Without a callback, search
115
+ keeps its default keyword matching and ranking.
116
+
81
117
  See the [`toolSearch()` reference](/docs/reference/ai-sdk-core/tool-search) for
82
118
  input, output, and matching details.
@@ -400,6 +400,11 @@ for (const file of result.files) {
400
400
  | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `black-forest-labs/FLUX.1.1-pro` | 512x512, 768x768, 1024x1024 |
401
401
  | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `black-forest-labs/FLUX.1-pro` | 512x512, 768x768, 1024x1024 |
402
402
  | [Together.ai](/providers/ai-sdk-providers/togetherai#image-models) | `black-forest-labs/FLUX.1-schnell-Free` | 512x512, 768x768, 1024x1024 |
403
+ | [Black Forest Labs](/providers/ai-sdk-providers/black-forest-labs#image-models) | `flux-2-pro` | From 3:7 (portrait) to 7:3 (landscape) |
404
+ | [Black Forest Labs](/providers/ai-sdk-providers/black-forest-labs#image-models) | `flux-2-max` | From 3:7 (portrait) to 7:3 (landscape) |
405
+ | [Black Forest Labs](/providers/ai-sdk-providers/black-forest-labs#image-models) | `flux-2-flex` | From 3:7 (portrait) to 7:3 (landscape) |
406
+ | [Black Forest Labs](/providers/ai-sdk-providers/black-forest-labs#image-models) | `flux-2-klein-9b` | From 3:7 (portrait) to 7:3 (landscape) |
407
+ | [Black Forest Labs](/providers/ai-sdk-providers/black-forest-labs#image-models) | `flux-2-klein-4b` | From 3:7 (portrait) to 7:3 (landscape) |
403
408
  | [Black Forest Labs](/providers/ai-sdk-providers/black-forest-labs#image-models) | `flux-kontext-pro` | From 3:7 (portrait) to 7:3 (landscape) |
404
409
  | [Black Forest Labs](/providers/ai-sdk-providers/black-forest-labs#image-models) | `flux-kontext-max` | From 3:7 (portrait) to 7:3 (landscape) |
405
410
  | [Black Forest Labs](/providers/ai-sdk-providers/black-forest-labs#image-models) | `flux-pro-1.1-ultra` | From 3:7 (portrait) to 7:3 (landscape) |
@@ -138,7 +138,9 @@ The `audio` stream must contain raw audio chunks. `Uint8Array` chunks are raw by
138
138
  OpenAI streaming transcription uses `openai.transcription('gpt-realtime-whisper')`.
139
139
  Cartesia uses `cartesia.transcription('ink-2')` for streaming-only Ink 2
140
140
  transcription. ElevenLabs uses
141
- `elevenLabs.transcription('scribe_v2_realtime')` for Scribe v2 Realtime. xAI
141
+ `elevenLabs.transcription('scribe_v2_realtime')` for Scribe v2 Realtime. Azure
142
+ uses `azure.transcription('mai-transcribe-2-streaming')` for streaming-only
143
+ MAI-Transcribe-2-Streaming. xAI
142
144
  uses the same `xai.transcription()` model for request/response and streaming
143
145
  transcription; `experimental_streamTranscribe` selects the provider's
144
146
  WebSocket STT transport.
@@ -329,43 +331,44 @@ try {
329
331
 
330
332
  ## Transcription Models
331
333
 
332
- | Provider | Model |
333
- | ----------------------------------------------------------------------------------- | --------------------------- |
334
- | [OpenAI](/providers/ai-sdk-providers/openai#transcription-models) | `whisper-1` |
335
- | [OpenAI](/providers/ai-sdk-providers/openai#transcription-models) | `gpt-4o-transcribe` |
336
- | [OpenAI](/providers/ai-sdk-providers/openai#transcription-models) | `gpt-4o-mini-transcribe` |
337
- | [OpenAI](/providers/ai-sdk-providers/openai#transcription-models) | `gpt-4o-transcribe-diarize` |
338
- | [ElevenLabs](/providers/ai-sdk-providers/elevenlabs#transcription-models) | `scribe_v1` |
339
- | [ElevenLabs](/providers/ai-sdk-providers/elevenlabs#transcription-models) | `scribe_v1_experimental` |
340
- | [ElevenLabs](/providers/ai-sdk-providers/elevenlabs#transcription-models) | `scribe_v2` |
341
- | [ElevenLabs](/providers/ai-sdk-providers/elevenlabs#streaming-transcription-models) | `scribe_v2_realtime` |
342
- | [Groq](/providers/ai-sdk-providers/groq#transcription-models) | `whisper-large-v3-turbo` |
343
- | [Groq](/providers/ai-sdk-providers/groq#transcription-models) | `whisper-large-v3` |
344
- | [Mistral](/providers/ai-sdk-providers/mistral#transcription-models) | `voxtral-mini-latest` |
345
- | [Azure OpenAI](/providers/ai-sdk-providers/azure#transcription-models) | `whisper-1` |
346
- | [Azure Speech](/providers/ai-sdk-providers/azure#mai-transcribe) | `mai-transcribe-2` |
347
- | [Azure Speech](/providers/ai-sdk-providers/azure#mai-transcribe) | `mai-transcribe-1.5` |
348
- | [Azure OpenAI](/providers/ai-sdk-providers/azure#transcription-models) | `gpt-4o-transcribe` |
349
- | [Azure OpenAI](/providers/ai-sdk-providers/azure#transcription-models) | `gpt-4o-mini-transcribe` |
350
- | [Rev.ai](/providers/ai-sdk-providers/revai#transcription-models) | `machine` |
351
- | [Rev.ai](/providers/ai-sdk-providers/revai#transcription-models) | `low_cost` |
352
- | [Rev.ai](/providers/ai-sdk-providers/revai#transcription-models) | `fusion` |
353
- | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `base` (+ variants) |
354
- | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `enhanced` (+ variants) |
355
- | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `nova` (+ variants) |
356
- | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `nova-2` (+ variants) |
357
- | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `nova-3` (+ variants) |
358
- | [Gladia](/providers/ai-sdk-providers/gladia#transcription-models) | `default` |
359
- | [AssemblyAI](/providers/ai-sdk-providers/assemblyai#transcription-models) | `universal-3-5-pro` |
360
- | [AssemblyAI](/providers/ai-sdk-providers/assemblyai#transcription-models) | `universal-3-pro` |
361
- | [Fal](/providers/ai-sdk-providers/fal#transcription-models) | `whisper` |
362
- | [Fal](/providers/ai-sdk-providers/fal#transcription-models) | `wizper` |
363
- | [Google Vertex](/providers/ai-sdk-providers/google-vertex#transcription-models) | `chirp_2` |
364
- | [Google Vertex](/providers/ai-sdk-providers/google-vertex#transcription-models) | `chirp_3` |
365
- | [Google Vertex](/providers/ai-sdk-providers/google-vertex#transcription-models) | `telephony` |
366
- | [xAI](/providers/ai-sdk-providers/xai#transcription-models) | `default` |
367
- | [Cartesia](/providers/ai-sdk-providers/cartesia#transcription-models) | `ink-whisper` |
368
- | [Cartesia](/providers/ai-sdk-providers/cartesia#streaming-transcription-models) | `ink-2` |
369
- | [Fish Audio](/providers/ai-sdk-providers/fish-audio#transcription-models) | `transcribe-1` |
334
+ | Provider | Model |
335
+ | ----------------------------------------------------------------------------------- | ---------------------------- |
336
+ | [OpenAI](/providers/ai-sdk-providers/openai#transcription-models) | `whisper-1` |
337
+ | [OpenAI](/providers/ai-sdk-providers/openai#transcription-models) | `gpt-4o-transcribe` |
338
+ | [OpenAI](/providers/ai-sdk-providers/openai#transcription-models) | `gpt-4o-mini-transcribe` |
339
+ | [OpenAI](/providers/ai-sdk-providers/openai#transcription-models) | `gpt-4o-transcribe-diarize` |
340
+ | [ElevenLabs](/providers/ai-sdk-providers/elevenlabs#transcription-models) | `scribe_v1` |
341
+ | [ElevenLabs](/providers/ai-sdk-providers/elevenlabs#transcription-models) | `scribe_v1_experimental` |
342
+ | [ElevenLabs](/providers/ai-sdk-providers/elevenlabs#transcription-models) | `scribe_v2` |
343
+ | [ElevenLabs](/providers/ai-sdk-providers/elevenlabs#streaming-transcription-models) | `scribe_v2_realtime` |
344
+ | [Groq](/providers/ai-sdk-providers/groq#transcription-models) | `whisper-large-v3-turbo` |
345
+ | [Groq](/providers/ai-sdk-providers/groq#transcription-models) | `whisper-large-v3` |
346
+ | [Mistral](/providers/ai-sdk-providers/mistral#transcription-models) | `voxtral-mini-latest` |
347
+ | [Azure OpenAI](/providers/ai-sdk-providers/azure#transcription-models) | `whisper-1` |
348
+ | [Azure Speech](/providers/ai-sdk-providers/azure#mai-transcribe) | `mai-transcribe-2` |
349
+ | [Azure Speech](/providers/ai-sdk-providers/azure#mai-transcribe) | `mai-transcribe-1.5` |
350
+ | [Azure MAI](/providers/ai-sdk-providers/azure#mai-transcribe-2-streaming) | `mai-transcribe-2-streaming` |
351
+ | [Azure OpenAI](/providers/ai-sdk-providers/azure#transcription-models) | `gpt-4o-transcribe` |
352
+ | [Azure OpenAI](/providers/ai-sdk-providers/azure#transcription-models) | `gpt-4o-mini-transcribe` |
353
+ | [Rev.ai](/providers/ai-sdk-providers/revai#transcription-models) | `machine` |
354
+ | [Rev.ai](/providers/ai-sdk-providers/revai#transcription-models) | `low_cost` |
355
+ | [Rev.ai](/providers/ai-sdk-providers/revai#transcription-models) | `fusion` |
356
+ | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `base` (+ variants) |
357
+ | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `enhanced` (+ variants) |
358
+ | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `nova` (+ variants) |
359
+ | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `nova-2` (+ variants) |
360
+ | [Deepgram](/providers/ai-sdk-providers/deepgram#transcription-models) | `nova-3` (+ variants) |
361
+ | [Gladia](/providers/ai-sdk-providers/gladia#transcription-models) | `default` |
362
+ | [AssemblyAI](/providers/ai-sdk-providers/assemblyai#transcription-models) | `universal-3-5-pro` |
363
+ | [AssemblyAI](/providers/ai-sdk-providers/assemblyai#transcription-models) | `universal-3-pro` |
364
+ | [Fal](/providers/ai-sdk-providers/fal#transcription-models) | `whisper` |
365
+ | [Fal](/providers/ai-sdk-providers/fal#transcription-models) | `wizper` |
366
+ | [Google Vertex](/providers/ai-sdk-providers/google-vertex#transcription-models) | `chirp_2` |
367
+ | [Google Vertex](/providers/ai-sdk-providers/google-vertex#transcription-models) | `chirp_3` |
368
+ | [Google Vertex](/providers/ai-sdk-providers/google-vertex#transcription-models) | `telephony` |
369
+ | [xAI](/providers/ai-sdk-providers/xai#transcription-models) | `default` |
370
+ | [Cartesia](/providers/ai-sdk-providers/cartesia#transcription-models) | `ink-whisper` |
371
+ | [Cartesia](/providers/ai-sdk-providers/cartesia#streaming-transcription-models) | `ink-2` |
372
+ | [Fish Audio](/providers/ai-sdk-providers/fish-audio#transcription-models) | `transcribe-1` |
370
373
 
371
374
  Above are a small subset of the transcription models supported by the AI SDK providers. For more, see the respective provider documentation.
@@ -158,6 +158,8 @@ try {
158
158
  | [OpenAI](/providers/ai-sdk-providers/openai#speech-models) | `tts-1-hd` |
159
159
  | [OpenAI](/providers/ai-sdk-providers/openai#speech-models) | `gpt-4o-mini-tts` |
160
160
  | [Mistral](/providers/ai-sdk-providers/mistral#speech-models) | `voxtral-mini-tts-2603` |
161
+ | [Azure Speech](/providers/ai-sdk-providers/azure#mai-voice) | `mai-voice-2.1-flash` |
162
+ | [Azure Speech](/providers/ai-sdk-providers/azure#mai-voice) | `mai-voice-2.1` |
161
163
  | [Azure Speech](/providers/ai-sdk-providers/azure#mai-voice) | `mai-voice-2-flash` |
162
164
  | [Azure Speech](/providers/ai-sdk-providers/azure#mai-voice) | `mai-voice-2` |
163
165
  | [ElevenLabs](/providers/ai-sdk-providers/elevenlabs#speech-models) | `eleven_v3` |
@@ -323,7 +323,8 @@ When the user clicks the "Regenerate" button, the AI provider will regenerate th
323
323
 
324
324
  ### Throttling UI Updates
325
325
 
326
- By default, the `useChat` hook will trigger a render every time a new chunk is received.
326
+ By default, `useChat` publishes message updates as each chunk is processed.
327
+ React may batch multiple updates into a single render.
327
328
  React and Vue applications can throttle reactive message updates with the `throttle` option.
328
329
  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
 
@@ -5,8 +5,8 @@ description: Search deferred tools and load their definitions on demand for dire
5
5
 
6
6
  # `toolSearch()`
7
7
 
8
- Creates a tool that searches the surrounding generation's deferred tools by name
9
- and description. The factory takes no arguments. Use it with `generateText`,
8
+ Creates a tool that searches the surrounding generation's deferred tools, using
9
+ keyword matching by default or a custom search callback. Use it with `generateText`,
10
10
  `streamText`, `ToolLoopAgent`, or `WorkflowAgent` from `@ai-sdk/workflow`.
11
11
  `WorkflowAgent` supports direct tool calling. The other APIs also support code
12
12
  mode configured with `toolDiscovery: 'conversation'`.
@@ -20,6 +20,60 @@ const search = toolSearch();
20
20
  See the [Tool Search guide](/docs/ai-sdk-core/tool-search) for direct-calling and
21
21
  code mode examples, including how code mode preserves the tool-definition cache.
22
22
 
23
+ ## Options
24
+
25
+ Pass an optional configuration object to control the result limit:
26
+
27
+ ```ts
28
+ const search = toolSearch({ maxResults: 10 });
29
+ ```
30
+
31
+ `maxResults` is the maximum number of matching tools returned and discovered per
32
+ search. It defaults to `5` and must be a positive safe integer. Invalid values
33
+ throw `InvalidArgumentError` when creating the search tool. The model-facing
34
+ description reflects the configured limit.
35
+
36
+ The limit applies to both default keyword search and custom search callbacks,
37
+ without changing their matching or ranking. Discovered tools accumulate across
38
+ searches, so this is not a limit on the total number of tools loaded during a
39
+ generation.
40
+
41
+ Pass a `search` callback to select and rank tools using your application's search
42
+ logic. The callback can return synchronously or asynchronously:
43
+
44
+ ```ts
45
+ const search = toolSearch({
46
+ search: ({ query, tools }) => {
47
+ const preferred = /\bgmail\b/i.test(query)
48
+ ? ['gmail_list_messages', 'gmail_get_message']
49
+ : [];
50
+ return preferred.filter(name => tools.some(tool => tool.name === name));
51
+ },
52
+ });
53
+ ```
54
+
55
+ The callback has this signature:
56
+
57
+ ```ts
58
+ search?: (options: {
59
+ query: string;
60
+ tools: Array<{ name: string; description?: string }>;
61
+ }) => string[] | PromiseLike<string[]>;
62
+ ```
63
+
64
+ `tools` contains all eligible deferred tools, before keyword matching or ranking.
65
+ Eligibility respects `activeTools` and caller routing. Descriptions are resolved
66
+ using the current tool context and sandbox; schemas are not included.
67
+
68
+ Return tool names in ranked order. The SDK ignores unknown or ineligible names,
69
+ removes duplicates while preserving order, and keeps the first `maxResults` valid
70
+ names (five by default). Returned descriptions come from the SDK's candidate
71
+ catalog. An empty array
72
+ discovers no tools. Callback errors follow normal tool-execution error handling;
73
+ the SDK does not fall back to keyword search.
74
+
75
+ Without a callback, `toolSearch()` uses the default keyword search described below.
76
+
23
77
  ## Model Input
24
78
 
25
79
  The model supplies the following input to the search tool:
@@ -28,11 +82,12 @@ The model supplies the following input to the search tool:
28
82
  { "query": "weather forecast" }
29
83
  ```
30
84
 
31
- `query` is a required, nonempty string of search keywords. Search is local and
85
+ `query` is a required, nonempty string. By default, search is local and
32
86
  case-insensitive, matching words in tool names and descriptions. Camel-case names
33
87
  are split into words. Name matches rank above description matches; equal scores
34
88
  preserve registration order. Function descriptions are resolved with the current
35
- tool context and sandbox. No embedding service or additional model call is used.
89
+ tool context and sandbox. The default search uses no embedding service or
90
+ additional model call. A custom callback can use either.
36
91
 
37
92
  ## Output
38
93
 
@@ -44,8 +99,9 @@ tool context and sandbox. No embedding service or additional model call is used.
44
99
  }
45
100
  ```
46
101
 
47
- Results contain at most five matching tools, with their names and optional
48
- descriptions. They do not include schemas. No matches returns `{ tools: [] }`.
102
+ Results contain at most `maxResults` matching tools (five by default), with their
103
+ names and optional descriptions. They do not include schemas. No matches returns
104
+ `{ tools: [] }`.
49
105
  Every returned match is queued for discovery. Newly discovered tools can only be
50
106
  called on the next model step, after their definitions have been provided. A
51
107
  parallel call in the same response as the search cannot use a newly discovered
@@ -486,6 +486,19 @@ Allows you to easily create a conversational user interface for your chatbot app
486
486
  ]}
487
487
  />
488
488
 
489
+ ## React rendering and shared Chat instances
490
+
491
+ In React, `messages`, `status`, and `error` describe the snapshot for the current render.
492
+ The underlying `Chat` updates as stream chunks are processed. React may batch updates,
493
+ and `throttle` can delay message rendering without delaying stream processing or callbacks.
494
+ Status and error updates include the latest messages, so a completed or failed response
495
+ is rendered with its final message snapshot.
496
+
497
+ Each `useChat({ chat })` call subscribes independently. Consumers sharing a `Chat`
498
+ can display different message snapshots when they use different throttle intervals.
499
+ A functional `setMessages` updater receives the Chat's latest messages, which may be
500
+ newer than the messages in the current render.
501
+
489
502
  ## Learn more
490
503
 
491
504
  - [Chatbot](/docs/ai-sdk-ui/chatbot)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.126",
3
+ "version": "7.0.127",
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,12 +42,12 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.102",
45
+ "@ai-sdk/gateway": "4.0.103",
46
46
  "@ai-sdk/provider": "4.0.21",
47
47
  "@ai-sdk/provider-utils": "5.0.53"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.104",
50
+ "@ai-sdk/amazon-bedrock": "5.0.105",
51
51
  "@ai-sdk/deepseek": "3.0.58",
52
52
  "@ai-sdk/google": "4.0.87",
53
53
  "@ai-sdk/groq": "4.0.54",
@@ -117,9 +117,13 @@ export async function validateApprovedToolApprovals<
117
117
  refineToolInput,
118
118
  });
119
119
 
120
+ // Clone both inputs into the current realm before comparing them.
121
+ const revalidatedInput = structuredClone(revalidatedToolCall.input);
122
+ const approvedInput = structuredClone(toolCall.input);
123
+
120
124
  // Revalidation must never change the operation that was approved,
121
125
  // including when older or projected history omits the schema input.
122
- if (!isDeepEqualData(revalidatedToolCall.input, toolCall.input)) {
126
+ if (!isDeepEqualData(revalidatedInput, approvedInput)) {
123
127
  validationError = new Error(
124
128
  'Approved tool input does not match the validated schema output.',
125
129
  );
@@ -11,7 +11,11 @@ import {
11
11
  } from '../generate-text/tool-caller-configuration';
12
12
  import { resolveToolDescription } from '../prompt/prepare-tools';
13
13
  import { getOwn } from '../util/get-own';
14
- import { isToolSearch } from './tool-search';
14
+ import {
15
+ getToolSearchFunction,
16
+ getToolSearchMaxResults,
17
+ isToolSearch,
18
+ } from './tool-search';
15
19
 
16
20
  /** Create discovery state for one generation, never for a shared tool instance. */
17
21
  export function createToolSearchState({
@@ -90,41 +94,57 @@ export function createToolSearchState({
90
94
  searchName,
91
95
  {
92
96
  ...tool,
93
- execute: ({ query }: { query: string }) => {
97
+ execute: async ({ query }: { query: string }) => {
98
+ const availableTools = candidates.map(([name, candidate]) => {
99
+ const description = resolveToolDescription({
100
+ tool: candidate,
101
+ toolName: name,
102
+ toolsContext,
103
+ experimental_sandbox,
104
+ });
105
+ return {
106
+ name,
107
+ ...(description == null ? {} : { description }),
108
+ };
109
+ });
110
+ const toolsByName = new Map(
111
+ availableTools.map(tool => [tool.name, tool]),
112
+ );
113
+ const customSearch = getToolSearchFunction(tool);
94
114
  const terms = [...new Set(tokenize(query))];
95
- const matches = candidates
96
- .map(([name, candidate]) => {
97
- const description = resolveToolDescription({
98
- tool: candidate,
99
- toolName: name,
100
- toolsContext,
101
- experimental_sandbox,
102
- });
103
- const nameTerms = tokenize(name);
104
- const descriptionTerms = tokenize(description ?? '');
105
- const score = terms.reduce(
106
- (score, term) =>
107
- score +
108
- (nameTerms.includes(term) ? 2 : 0) +
109
- (descriptionTerms.includes(term) ? 1 : 0),
110
- 0,
111
- );
112
- return { name, description, score };
115
+ const rankedNames = customSearch
116
+ ? await customSearch({
117
+ query,
118
+ tools: availableTools.map(tool => ({ ...tool })),
119
+ })
120
+ : availableTools
121
+ .map(({ name, description }) => {
122
+ const nameTerms = tokenize(name);
123
+ const descriptionTerms = tokenize(description ?? '');
124
+ const score = terms.reduce(
125
+ (score, term) =>
126
+ score +
127
+ (nameTerms.includes(term) ? 2 : 0) +
128
+ (descriptionTerms.includes(term) ? 1 : 0),
129
+ 0,
130
+ );
131
+ return { name, score };
132
+ })
133
+ .filter(match => match.score > 0)
134
+ .sort((a, b) => b.score - a.score)
135
+ .map(match => match.name);
136
+ const matches = [...new Set(rankedNames)]
137
+ .flatMap(name => {
138
+ const match = toolsByName.get(name);
139
+ return match == null ? [] : [match];
113
140
  })
114
- .filter(match => match.score > 0)
115
- .sort((a, b) => b.score - a.score)
116
- .slice(0, 5);
141
+ .slice(0, getToolSearchMaxResults(tool));
117
142
 
118
143
  for (const { name } of matches) {
119
144
  discovered.add(name);
120
145
  }
121
146
 
122
- return {
123
- tools: matches.map(({ name, description }) => ({
124
- name,
125
- ...(description == null ? {} : { description }),
126
- })),
127
- };
147
+ return { tools: matches };
128
148
  },
129
149
  },
130
150
  ];
@@ -1,6 +1,16 @@
1
1
  import { jsonSchema, tool, type Tool } from '@ai-sdk/provider-utils';
2
+ import { InvalidArgumentError } from '../error/invalid-argument-error';
2
3
 
3
4
  const toolSearchSymbol = Symbol.for('vercel.ai.toolSearch');
5
+ const toolSearchFunctionSymbol = Symbol.for('vercel.ai.toolSearch.search');
6
+ const toolSearchMaxResultsSymbol = Symbol.for(
7
+ 'vercel.ai.toolSearch.maxResults',
8
+ );
9
+
10
+ type ToolSearchFunction = (options: {
11
+ query: string;
12
+ tools: Array<{ name: string; description?: string }>;
13
+ }) => string[] | PromiseLike<string[]>;
4
14
 
5
15
  type ToolSearchInput = { query: string };
6
16
  type ToolSearchOutput = {
@@ -12,13 +22,36 @@ type ToolSearchOutput = {
12
22
  * binds the search registry and makes matches available on the next step.
13
23
  * Use directly or through code mode with `toolDiscovery: 'conversation'`.
14
24
  */
15
- export function toolSearch(): Tool<ToolSearchInput, ToolSearchOutput> & {
25
+ export function toolSearch({
26
+ search,
27
+ maxResults = 5,
28
+ }: {
29
+ /**
30
+ * Search eligible deferred tools and return their names in ranked order.
31
+ * Supports synchronous and asynchronous callbacks. Unknown names and
32
+ * duplicates are ignored before applying the result limit.
33
+ */
34
+ search?: ToolSearchFunction;
35
+ /**
36
+ * Maximum number of matching tools returned per search.
37
+ *
38
+ * @default 5
39
+ */
40
+ maxResults?: number;
41
+ } = {}): Tool<ToolSearchInput, ToolSearchOutput> & {
16
42
  type: 'function';
17
43
  } {
44
+ if (!Number.isSafeInteger(maxResults) || maxResults < 1) {
45
+ throw new InvalidArgumentError({
46
+ parameter: 'maxResults',
47
+ value: maxResults,
48
+ message: 'maxResults must be a positive safe integer.',
49
+ });
50
+ }
51
+
18
52
  return Object.assign(
19
53
  tool({
20
- description:
21
- 'Search for tools by keywords in their names and descriptions. Returns up to five matching tools. Matches become available on the next model step, after this execution finishes. Wait for their tool definitions before calling the discovered tools. If no tools match, try different keywords.',
54
+ description: `${search == null ? 'Search for tools by keywords in their names and descriptions.' : 'Search for tools matching a query.'} Returns up to ${maxResults === 5 ? 'five' : maxResults} matching tools. Matches become available on the next model step, after this execution finishes. Wait for their tool definitions before calling the discovered tools. If no tools match, try different keywords.`,
22
55
  inputSchema: jsonSchema<ToolSearchInput>({
23
56
  type: 'object',
24
57
  properties: { query: { type: 'string', minLength: 1 } },
@@ -48,10 +81,31 @@ export function toolSearch(): Tool<ToolSearchInput, ToolSearchOutput> & {
48
81
  throw new Error('toolSearch must be bound by an AI SDK generation.');
49
82
  },
50
83
  }),
51
- { type: 'function' as const, [toolSearchSymbol]: true },
84
+ {
85
+ type: 'function' as const,
86
+ [toolSearchSymbol]: true,
87
+ [toolSearchFunctionSymbol]: search,
88
+ [toolSearchMaxResultsSymbol]: maxResults,
89
+ },
52
90
  );
53
91
  }
54
92
 
93
+ export function getToolSearchMaxResults(tool: Tool): number {
94
+ return (
95
+ (tool as Tool & { [toolSearchMaxResultsSymbol]?: number })[
96
+ toolSearchMaxResultsSymbol
97
+ ] ?? 5
98
+ );
99
+ }
100
+
101
+ export function getToolSearchFunction(
102
+ tool: Tool,
103
+ ): ToolSearchFunction | undefined {
104
+ return (tool as Tool & { [toolSearchFunctionSymbol]?: ToolSearchFunction })[
105
+ toolSearchFunctionSymbol
106
+ ];
107
+ }
108
+
55
109
  export function isToolSearch(tool: Tool): boolean {
56
110
  return (
57
111
  (tool as Tool & { [toolSearchSymbol]?: boolean })[toolSearchSymbol] === true
@@ -73,12 +73,25 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
73
73
  >;
74
74
 
75
75
  const ongoingStreamPromises: Promise<void>[] = [];
76
+ const activeReaders = new Set<
77
+ ReadableStreamDefaultReader<InferUIMessageChunk<UI_MESSAGE>>
78
+ >();
79
+ let isCancelled = false;
80
+ let cancelReason: unknown;
76
81
  let outcome: UIMessageStreamOutcome = { status: 'unknown' };
77
82
 
78
83
  const stream = new ReadableStream({
79
84
  start(controllerArg) {
80
85
  controller = controllerArg;
81
86
  },
87
+ async cancel(reason) {
88
+ isCancelled = true;
89
+ cancelReason = reason;
90
+
91
+ await Promise.all(
92
+ Array.from(activeReaders, reader => reader.cancel(reason)),
93
+ );
94
+ },
82
95
  });
83
96
 
84
97
  function safeEnqueue(data: InferUIMessageChunk<UI_MESSAGE>) {
@@ -132,13 +145,25 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
132
145
  safeEnqueue(part);
133
146
  },
134
147
  merge(streamArg) {
148
+ if (isCancelled) {
149
+ void streamArg.cancel(cancelReason).catch(() => {});
150
+ return;
151
+ }
152
+
135
153
  ongoingStreamPromises.push(
136
154
  (async () => {
137
155
  const reader = streamArg.getReader();
138
- while (true) {
139
- const { done, value } = await reader.read();
140
- if (done) break;
141
- safeEnqueue(value);
156
+ activeReaders.add(reader);
157
+
158
+ try {
159
+ while (true) {
160
+ const { done, value } = await reader.read();
161
+ if (done) break;
162
+ safeEnqueue(value);
163
+ }
164
+ } finally {
165
+ activeReaders.delete(reader);
166
+ reader.releaseLock();
142
167
  }
143
168
  })().catch(error => {
144
169
  handleError(error);