ai 7.0.103 → 7.0.105

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 (41) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/index.d.ts +37 -7
  3. package/dist/index.js +523 -298
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal/index.d.ts +1 -0
  6. package/dist/internal/index.js +4 -1
  7. package/dist/internal/index.js.map +1 -1
  8. package/docs/03-ai-sdk-core/18-code-mode.mdx +11 -0
  9. package/docs/03-ai-sdk-core/19-tool-search.mdx +80 -0
  10. package/docs/03-ai-sdk-core/32-evaluation.mdx +171 -7
  11. package/docs/03-ai-sdk-core/45-provider-management.mdx +10 -0
  12. package/docs/03-ai-sdk-core/index.mdx +6 -0
  13. package/docs/07-reference/01-ai-sdk-core/14-evaluate.mdx +22 -9
  14. package/docs/07-reference/01-ai-sdk-core/20-tool.mdx +7 -0
  15. package/docs/07-reference/01-ai-sdk-core/22-dynamic-tool.mdx +7 -0
  16. package/docs/07-reference/01-ai-sdk-core/23-tool-search.mdx +75 -0
  17. package/docs/07-reference/01-ai-sdk-core/40-provider-registry.mdx +18 -0
  18. package/docs/07-reference/01-ai-sdk-core/42-custom-provider.mdx +18 -0
  19. package/docs/07-reference/01-ai-sdk-core/index.mdx +6 -0
  20. package/docs/07-reference/05-ai-sdk-errors/ai-no-such-model-error.mdx +4 -0
  21. package/docs/07-reference/05-ai-sdk-errors/ai-no-such-provider-error.mdx +4 -0
  22. package/package.json +12 -12
  23. package/src/evaluate/evaluate.ts +4 -10
  24. package/src/evaluate/evaluation-provider.ts +6 -0
  25. package/src/evaluate/evaluation-result.ts +1 -1
  26. package/src/generate-text/generate-text.ts +9 -1
  27. package/src/generate-text/stream-text.ts +9 -1
  28. package/src/generate-text/tool-caller-configuration.ts +1 -1
  29. package/src/global.ts +1 -0
  30. package/src/index.ts +1 -0
  31. package/src/model/resolve-model.ts +53 -10
  32. package/src/prompt/prepare-tools.ts +1 -1
  33. package/src/realtime/browser-realtime-transport.ts +12 -1
  34. package/src/realtime/realtime-event-channel.ts +4 -0
  35. package/src/realtime/realtime-session.ts +9 -4
  36. package/src/registry/custom-provider.ts +37 -0
  37. package/src/registry/index.ts +2 -0
  38. package/src/registry/no-such-provider-error.ts +2 -1
  39. package/src/registry/provider-registry.ts +65 -6
  40. package/src/tool-search/prepare-tool-search.ts +143 -0
  41. package/src/tool-search/tool-search.ts +59 -0
@@ -63,6 +63,7 @@ import type { ActiveTools } from './active-tools';
63
63
  import { calculateTokensPerSecond } from './calculate-tokens-per-second';
64
64
  import { collectToolApprovals } from './collect-tool-approvals';
65
65
  import { convertLanguageModelContent } from './convert-language-model-content';
66
+ import { createToolSearchState } from '../tool-search/prepare-tool-search';
66
67
  import { executeToolCall } from './execute-tool-call';
67
68
  import {
68
69
  filterActiveTools,
@@ -575,6 +576,10 @@ export async function generateText<
575
576
  tools,
576
577
  toolCallers: experimental_toolCallers,
577
578
  });
579
+ const prepareToolSearch = createToolSearchState({
580
+ tools,
581
+ toolCallers: resolvedToolCallers,
582
+ });
578
583
  const stopConditions = asArray(stopWhen);
579
584
  const resolvedOnStart = onStart ?? experimental_onStart;
580
585
  const resolvedOnStepStart = onStepStart ?? experimental_onStepStart;
@@ -920,7 +925,10 @@ export async function generateText<
920
925
  modelTools: stepModelTools,
921
926
  toolCallerMessages,
922
927
  } = prepareToolsForToolCallers({
923
- tools: stepActiveTools,
928
+ tools: prepareToolSearch(stepActiveTools, {
929
+ toolsContext,
930
+ experimental_sandbox: stepSandbox,
931
+ }),
924
932
  toolCallers: resolvedToolCallers,
925
933
  });
926
934
  const stepToolOrder = prepareStepResult?.toolOrder ?? toolOrder;
@@ -89,6 +89,7 @@ import {
89
89
  executeToolsFromStream,
90
90
  type ExecuteToolsStreamPart,
91
91
  } from './execute-tools-from-stream';
92
+ import { createToolSearchState } from '../tool-search/prepare-tool-search';
92
93
  import { executeToolCall } from './execute-tool-call';
93
94
  import {
94
95
  filterActiveTools,
@@ -1382,6 +1383,10 @@ class DefaultStreamTextResult<
1382
1383
  tools,
1383
1384
  toolCallers: experimental_toolCallers,
1384
1385
  });
1386
+ const prepareToolSearch = createToolSearchState({
1387
+ tools,
1388
+ toolCallers: resolvedToolCallers,
1389
+ });
1385
1390
 
1386
1391
  const telemetryDispatcher = createRestrictedTelemetryDispatcher<
1387
1392
  TOOLS,
@@ -2333,7 +2338,10 @@ class DefaultStreamTextResult<
2333
2338
  modelTools: stepModelTools,
2334
2339
  toolCallerMessages,
2335
2340
  } = prepareToolsForToolCallers({
2336
- tools: stepActiveTools,
2341
+ tools: prepareToolSearch(stepActiveTools, {
2342
+ toolsContext,
2343
+ experimental_sandbox: stepSandbox,
2344
+ }),
2337
2345
  toolCallers: resolvedToolCallers,
2338
2346
  });
2339
2347
  const stepToolOrder = prepareStepResult?.toolOrder ?? toolOrder;
@@ -8,7 +8,7 @@ import {
8
8
  } from '@ai-sdk/provider-utils';
9
9
  import { InvalidArgumentError } from '../error/invalid-argument-error';
10
10
 
11
- const DIRECT_TOOL_CALL = 'AI_SDK_DIRECT_TOOL_CALL';
11
+ export const DIRECT_TOOL_CALL = 'AI_SDK_DIRECT_TOOL_CALL';
12
12
 
13
13
  type ToolCallerName<TOOLS extends ToolSet> = {
14
14
  [NAME in keyof TOOLS]: TOOLS[NAME] extends Experimental_ToolCallerTool
package/src/global.ts CHANGED
@@ -9,6 +9,7 @@ declare global {
9
9
  * String model ids are resolved to the default provider and model id.
10
10
  *
11
11
  * If not set, the default provider is the Vercel AI gateway provider.
12
+ * Experimental evaluation requires an explicitly configured evaluation-capable provider.
12
13
  *
13
14
  * @see https://ai-sdk.dev/docs/ai-sdk-core/provider-management#global-provider-configuration
14
15
  */
package/src/index.ts CHANGED
@@ -55,6 +55,7 @@ export * from './registry';
55
55
  export * from './rerank';
56
56
  export * from './telemetry';
57
57
  export * from './text-stream';
58
+ export { toolSearch } from './tool-search/tool-search';
58
59
  export * from './transcribe';
59
60
  export * from './translate';
60
61
  export * from './types';
@@ -1,15 +1,19 @@
1
1
  import { gateway } from '@ai-sdk/gateway';
2
- import type {
3
- EmbeddingModelV4,
4
- Experimental_SpeechTranslationModelV4,
5
- Experimental_VideoModelV4,
6
- ImageModelV4,
7
- LanguageModelV4,
8
- ProviderV4,
9
- RerankingModelV4,
10
- SpeechModelV4,
11
- TranscriptionModelV4,
2
+ import {
3
+ NoSuchModelError,
4
+ type Experimental_EvaluationModelV4 as EvaluationModelV4,
5
+ type EmbeddingModelV4,
6
+ type Experimental_SpeechTranslationModelV4,
7
+ type Experimental_VideoModelV4,
8
+ type ImageModelV4,
9
+ type LanguageModelV4,
10
+ type ProviderV4,
11
+ type RerankingModelV4,
12
+ type SpeechModelV4,
13
+ type TranscriptionModelV4,
12
14
  } from '@ai-sdk/provider';
15
+ import type { EvaluationModel } from '../evaluate/evaluation-result';
16
+ import type { EvaluationProvider } from '../evaluate/evaluation-provider';
13
17
  import { UnsupportedModelVersionError } from '../error';
14
18
  import type { EmbeddingModel } from '../types/embedding-model';
15
19
  import type { LanguageModel } from '../types/language-model';
@@ -213,6 +217,45 @@ export function resolveRerankingModel(model: RerankingModel): RerankingModelV4 {
213
217
  return asRerankingModelV4(model);
214
218
  }
215
219
 
220
+ export function resolveEvaluationModel(
221
+ model: EvaluationModel,
222
+ ): EvaluationModelV4 {
223
+ if (typeof model === 'string') {
224
+ // Use the original provider so experimental methods and their receiver survive.
225
+ const provider = (globalThis.AI_SDK_DEFAULT_PROVIDER ??
226
+ gateway) as EvaluationProvider;
227
+
228
+ if (typeof provider?.evaluationModel !== 'function') {
229
+ throw new NoSuchModelError({
230
+ modelId: model,
231
+ modelType: 'evaluationModel',
232
+ message:
233
+ 'The default provider does not support evaluation models. ' +
234
+ 'Pass an evaluation model instance or configure AI_SDK_DEFAULT_PROVIDER with an evaluationModel method.',
235
+ });
236
+ }
237
+
238
+ const resolvedModel = provider.evaluationModel(model);
239
+ if (resolvedModel == null) {
240
+ throw new NoSuchModelError({
241
+ modelId: model,
242
+ modelType: 'evaluationModel',
243
+ });
244
+ }
245
+ model = resolvedModel;
246
+ }
247
+
248
+ if (model.specificationVersion !== 'v4') {
249
+ throw new UnsupportedModelVersionError({
250
+ version: model.specificationVersion,
251
+ provider: model.provider,
252
+ modelId: model.modelId,
253
+ });
254
+ }
255
+
256
+ return model;
257
+ }
258
+
216
259
  function getGlobalProvider(): ProviderV4 {
217
260
  const provider = globalThis.AI_SDK_DEFAULT_PROVIDER ?? gateway;
218
261
  return asProviderV4(provider);
@@ -105,7 +105,7 @@ function orderToolEntries<TOOLS extends ToolSet>({
105
105
  return [...orderedTools, ...unorderedTools];
106
106
  }
107
107
 
108
- function resolveToolDescription<TOOLS extends ToolSet>({
108
+ export function resolveToolDescription<TOOLS extends ToolSet>({
109
109
  tool,
110
110
  toolName,
111
111
  toolsContext,
@@ -169,9 +169,20 @@ export class BrowserRealtimeTransport {
169
169
  if (this.ws === ws) {
170
170
  this.ws = null;
171
171
  const closeError = getCloseError(event) ?? connectionError;
172
+ const reportCloseErrorImmediately =
173
+ closeError != null && !codec.hasPendingIncoming;
172
174
  codec.stopWriting();
173
175
  this.notifyClosing();
174
176
  if (this.epoch !== epoch) return;
177
+ const drain = codec.finish();
178
+ if (reportCloseErrorImmediately) {
179
+ try {
180
+ this.onError(closeError);
181
+ } catch {
182
+ /* Application callbacks cannot interrupt teardown. */
183
+ }
184
+ if (this.epoch !== epoch) return;
185
+ }
175
186
  const complete = () => {
176
187
  if (this.epoch !== epoch) return;
177
188
  clearTimeout(this.drainTimer);
@@ -184,7 +195,7 @@ export class BrowserRealtimeTransport {
184
195
  }
185
196
  };
186
197
  this.drainTimer = setTimeout(complete, 1_000);
187
- void this.awaitDrain(codec.finish(), complete);
198
+ void this.awaitDrain(drain, complete);
188
199
  }
189
200
  };
190
201
  }
@@ -34,6 +34,10 @@ export class RealtimeEventChannel {
34
34
  options.model.parseServerEvent.bind(options.model);
35
35
  }
36
36
 
37
+ get hasPendingIncoming(): boolean {
38
+ return this.incomingCount > 0;
39
+ }
40
+
37
41
  dispose(): void {
38
42
  this.active = false;
39
43
  }
@@ -206,6 +206,7 @@ export abstract class AbstractRealtimeSession {
206
206
  if (current())
207
207
  this.fail(new Error('Realtime session startup timed out'));
208
208
  });
209
+ const reportedTransportErrors = new WeakSet<Error>();
209
210
  const callbacks = {
210
211
  model,
211
212
  onEvent: async (event: RealtimeServerEvent) => {
@@ -221,7 +222,10 @@ export abstract class AbstractRealtimeSession {
221
222
  }
222
223
  },
223
224
  onError: (error: Error) => {
224
- if (current()) void this.reportError(error, attempt);
225
+ if (current()) {
226
+ reportedTransportErrors.add(error);
227
+ void this.reportError(error, attempt);
228
+ }
225
229
  },
226
230
  onFatalError: (error: Error, drain?: Promise<void>) => {
227
231
  if (current()) this.fail(error, drain);
@@ -244,7 +248,8 @@ export abstract class AbstractRealtimeSession {
244
248
  const finalizationConfirmed =
245
249
  model.capabilities?.finalization === 'session-close' &&
246
250
  this.state.session?.finalization === 'confirmed';
247
- if (error != null && !finalizationConfirmed) this.fail(error);
251
+ if (error != null && !finalizationConfirmed)
252
+ this.fail(error, undefined, !reportedTransportErrors.has(error));
248
253
  else if (!attempt.ready && !finalizationConfirmed)
249
254
  this.fail(
250
255
  new Error('Realtime connection closed before becoming ready'),
@@ -348,7 +353,7 @@ export abstract class AbstractRealtimeSession {
348
353
  }
349
354
  }
350
355
 
351
- private fail(error: unknown, drain?: Promise<void>): void {
356
+ private fail(error: unknown, drain?: Promise<void>, report = true): void {
352
357
  const attempt = this.attempt;
353
358
  if (attempt == null || !attempt.active || attempt.cause != null) return;
354
359
  attempt.cause = error instanceof Error ? error : new Error(String(error));
@@ -359,7 +364,7 @@ export abstract class AbstractRealtimeSession {
359
364
  if (this.attempt !== attempt || !attempt.active) return;
360
365
  if (drain == null) this.disconnect();
361
366
  else void this.drainAttempt(attempt, drain);
362
- void this.reportError(attempt.cause, attempt);
367
+ if (report) void this.reportError(attempt.cause, attempt);
363
368
  }
364
369
 
365
370
  private async drainAttempt(
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type Experimental_EvaluationModelV4 as EvaluationModelV4,
2
3
  type EmbeddingModelV4,
3
4
  type Experimental_VideoModelV4,
4
5
  type FilesV4,
@@ -13,6 +14,8 @@ import {
13
14
  type SpeechModelV4,
14
15
  type TranscriptionModelV4,
15
16
  } from '@ai-sdk/provider';
17
+ import type { EvaluationModel } from '../evaluate/evaluation-result';
18
+ import type { EvaluationProvider } from '../evaluate/evaluation-provider';
16
19
  import { asProviderV4 } from '../model/as-provider-v4';
17
20
  import {
18
21
  resolveEmbeddingModel,
@@ -22,6 +25,7 @@ import {
22
25
  resolveSpeechModel,
23
26
  resolveTranscriptionModel,
24
27
  resolveVideoModel,
28
+ resolveEvaluationModel,
25
29
  } from '../model/resolve-model';
26
30
  import type { EmbeddingModel } from '../types/embedding-model';
27
31
  import type { ImageModel } from '../types/image-model';
@@ -42,6 +46,7 @@ import type { VideoModel } from '../types/video-model';
42
46
  * @param {Record<string, SpeechModel>} [options.speechModels] - A record of speech models, where keys are model IDs and values are speech model instances.
43
47
  * @param {Record<string, RerankingModel>} [options.rerankingModels] - A record of reranking models, where keys are model IDs and values are reranking model instances.
44
48
  * @param {Record<string, VideoModel>} [options.videoModels] - A record of video models, where keys are model IDs and values are video model instances.
49
+ * @param {Record<string, EvaluationModel>} [options.evaluationModels] - Experimental evaluation models or default-provider model IDs, keyed by alias.
45
50
  * @param {FilesV4} [options.files] - A files interface for uploading files.
46
51
  * @param {SkillsV4} [options.skills] - A skills interface for uploading skills.
47
52
  * @param {ProviderV2 | ProviderV3 | ProviderV4} [options.fallbackProvider] - An optional fallback provider to use when a requested model is not found in the custom provider.
@@ -60,6 +65,10 @@ export function customProvider<
60
65
  FILES extends FilesV4 | undefined = undefined,
61
66
  SKILLS extends SkillsV4 | undefined = undefined,
62
67
  FALLBACK extends ProviderV2 | ProviderV3 | ProviderV4 | undefined = undefined,
68
+ EVALUATION_MODELS extends Record<string, EvaluationModel> = Record<
69
+ string,
70
+ EvaluationModel
71
+ >,
63
72
  >({
64
73
  languageModels,
65
74
  embeddingModels,
@@ -68,6 +77,7 @@ export function customProvider<
68
77
  speechModels,
69
78
  rerankingModels,
70
79
  videoModels,
80
+ evaluationModels,
71
81
  files,
72
82
  skills,
73
83
  fallbackProvider: fallbackProviderArg,
@@ -79,6 +89,7 @@ export function customProvider<
79
89
  speechModels?: SPEECH_MODELS;
80
90
  rerankingModels?: RERANKING_MODELS;
81
91
  videoModels?: VIDEO_MODELS;
92
+ evaluationModels?: EVALUATION_MODELS;
82
93
  files?: FILES;
83
94
  skills?: SKILLS;
84
95
  fallbackProvider?: FALLBACK;
@@ -92,6 +103,9 @@ export function customProvider<
92
103
  rerankingModel(modelId: ExtractModelId<RERANKING_MODELS>): RerankingModelV4;
93
104
  speechModel(modelId: ExtractModelId<SPEECH_MODELS>): SpeechModelV4;
94
105
  videoModel(modelId: ExtractModelId<VIDEO_MODELS>): Experimental_VideoModelV4;
106
+ evaluationModel(
107
+ modelId: ExtractModelId<EVALUATION_MODELS>,
108
+ ): EvaluationModelV4;
95
109
  } & (FILES extends FilesV4
96
110
  ? { files(): FilesV4 }
97
111
  : [FALLBACK] extends [{ files: () => FilesV4 }]
@@ -117,6 +131,9 @@ export function customProvider<
117
131
  videoModel(
118
132
  modelId: ExtractModelId<VIDEO_MODELS>,
119
133
  ): Experimental_VideoModelV4;
134
+ evaluationModel(
135
+ modelId: ExtractModelId<EVALUATION_MODELS>,
136
+ ): EvaluationModelV4;
120
137
  } = {
121
138
  specificationVersion: 'v4',
122
139
  languageModel(modelId: ExtractModelId<LANGUAGE_MODELS>): LanguageModelV4 {
@@ -203,6 +220,26 @@ export function customProvider<
203
220
 
204
221
  throw new NoSuchModelError({ modelId, modelType: 'rerankingModel' });
205
222
  },
223
+ evaluationModel(
224
+ modelId: ExtractModelId<EVALUATION_MODELS>,
225
+ ): EvaluationModelV4 {
226
+ if (
227
+ evaluationModels != null &&
228
+ Object.hasOwn(evaluationModels, modelId)
229
+ ) {
230
+ return resolveEvaluationModel(evaluationModels[modelId]);
231
+ }
232
+
233
+ const provider = fallbackProviderArg as EvaluationProvider | undefined;
234
+ if (typeof provider?.evaluationModel === 'function') {
235
+ const model = provider.evaluationModel(modelId);
236
+ if (model != null) {
237
+ return resolveEvaluationModel(model);
238
+ }
239
+ }
240
+
241
+ throw new NoSuchModelError({ modelId, modelType: 'evaluationModel' });
242
+ },
206
243
  videoModel(
207
244
  modelId: ExtractModelId<VIDEO_MODELS>,
208
245
  ): Experimental_VideoModelV4 {
@@ -5,3 +5,5 @@ export {
5
5
  experimental_createProviderRegistry,
6
6
  } from './provider-registry';
7
7
  export type { ProviderRegistryProvider } from './provider-registry';
8
+
9
+ export type { EvaluationProviderRegistry as Experimental_EvaluationProviderRegistry } from './provider-registry';
@@ -25,7 +25,8 @@ export class NoSuchProviderError extends NoSuchModelError {
25
25
  | 'transcriptionModel'
26
26
  | 'speechModel'
27
27
  | 'rerankingModel'
28
- | 'videoModel';
28
+ | 'videoModel'
29
+ | 'evaluationModel';
29
30
  providerId: string;
30
31
  availableProviders: string[];
31
32
  message?: string;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type Experimental_EvaluationModelV4 as EvaluationModelV4,
2
3
  type EmbeddingModelV4,
3
4
  type Experimental_VideoModelV3,
4
5
  type Experimental_VideoModelV4,
@@ -13,6 +14,8 @@ import {
13
14
  type SpeechModelV4,
14
15
  type TranscriptionModelV4,
15
16
  } from '@ai-sdk/provider';
17
+ import type { EvaluationProvider } from '../evaluate/evaluation-provider';
18
+ import { resolveEvaluationModel } from '../model/resolve-model';
16
19
  import { wrapImageModel } from '../middleware/wrap-image-model';
17
20
  import { wrapLanguageModel } from '../middleware/wrap-language-model';
18
21
  import { asProviderV4 } from '../model/as-provider-v4';
@@ -34,7 +37,8 @@ type RegistryModelType =
34
37
  | 'transcriptionModel'
35
38
  | 'speechModel'
36
39
  | 'rerankingModel'
37
- | 'videoModel';
40
+ | 'videoModel'
41
+ | 'evaluationModel';
38
42
 
39
43
  type ProviderVideoModelIdentifier<PROVIDER> = PROVIDER extends {
40
44
  videoModel: (...args: infer ARGS) => unknown;
@@ -42,6 +46,30 @@ type ProviderVideoModelIdentifier<PROVIDER> = PROVIDER extends {
42
46
  ? ExtractLiteralUnion<ARGS[0]>
43
47
  : never;
44
48
 
49
+ type ProviderEvaluationModelIdentifier<PROVIDER> = PROVIDER extends {
50
+ evaluationModel: (...args: infer ARGS) => unknown;
51
+ }
52
+ ? ExtractLiteralUnion<ARGS[0]>
53
+ : never;
54
+
55
+ /** Registry with experimental evaluation access, separate from the stable interface. */
56
+ export type EvaluationProviderRegistry<
57
+ PROVIDERS extends Record<string, ProviderV4 | ProviderV3> = Record<
58
+ string,
59
+ ProviderV4 | ProviderV3
60
+ >,
61
+ SEPARATOR extends string = ':',
62
+ > = ProviderRegistryProvider<PROVIDERS, SEPARATOR> & {
63
+ evaluationModel<KEY extends keyof PROVIDERS>(
64
+ id: KEY extends string
65
+ ? `${KEY & string}${SEPARATOR}${ProviderEvaluationModelIdentifier<PROVIDERS[KEY]>}`
66
+ : never,
67
+ ): EvaluationModelV4;
68
+ evaluationModel<KEY extends keyof PROVIDERS>(
69
+ id: KEY extends string ? `${KEY & string}${SEPARATOR}${string}` : never,
70
+ ): EvaluationModelV4;
71
+ };
72
+
45
73
  export interface ProviderRegistryProvider<
46
74
  PROVIDERS extends Record<string, ProviderV4 | ProviderV3> = Record<
47
75
  string,
@@ -150,7 +178,7 @@ export function createProviderRegistry<
150
178
  | LanguageModelMiddleware[];
151
179
  imageModelMiddleware?: ImageModelMiddleware | ImageModelMiddleware[];
152
180
  } = {},
153
- ): ProviderRegistryProvider<PROVIDERS, SEPARATOR> {
181
+ ): EvaluationProviderRegistry<PROVIDERS, SEPARATOR> {
154
182
  const registry = new DefaultProviderRegistry<PROVIDERS, SEPARATOR>({
155
183
  separator,
156
184
  languageModelMiddleware,
@@ -175,9 +203,16 @@ export const experimental_createProviderRegistry = createProviderRegistry;
175
203
  class DefaultProviderRegistry<
176
204
  PROVIDERS extends Record<string, ProviderV4 | ProviderV3>,
177
205
  SEPARATOR extends string,
178
- > implements ProviderRegistryProvider<PROVIDERS, SEPARATOR> {
206
+ >
207
+ implements
208
+ ProviderRegistryProvider<PROVIDERS, SEPARATOR>,
209
+ EvaluationProviderRegistry<PROVIDERS, SEPARATOR>
210
+ {
179
211
  private providers: Partial<
180
- Record<keyof PROVIDERS, ProviderV4 & ProviderWithOptionalVideoModel>
212
+ Record<
213
+ keyof PROVIDERS,
214
+ ProviderV4 & ProviderWithOptionalVideoModel & EvaluationProvider
215
+ >
181
216
  > = {};
182
217
  private separator: SEPARATOR;
183
218
  private languageModelMiddleware?:
@@ -213,7 +248,11 @@ class DefaultProviderRegistry<
213
248
  provider as ProviderWithOptionalVideoModel
214
249
  ).videoModel?.bind(provider);
215
250
 
216
- this.providers[id] =
251
+ const evaluationModel = (
252
+ provider as EvaluationProvider
253
+ ).evaluationModel?.bind(provider);
254
+
255
+ const registeredProvider =
217
256
  videoModel == null
218
257
  ? providerV4
219
258
  : Object.assign(Object.create(Object.getPrototypeOf(providerV4)), {
@@ -221,12 +260,18 @@ class DefaultProviderRegistry<
221
260
  videoModel: (modelId: string) =>
222
261
  asVideoModelV4(videoModel(modelId)),
223
262
  });
263
+
264
+ // Keep v4 instances intact. Adapted providers need the original evaluation receiver.
265
+ if (registeredProvider !== provider && evaluationModel != null) {
266
+ Object.assign(registeredProvider, { evaluationModel });
267
+ }
268
+ this.providers[id] = registeredProvider;
224
269
  }
225
270
 
226
271
  private getProvider(
227
272
  id: string,
228
273
  modelType: RegistryModelType,
229
- ): ProviderV4 & ProviderWithOptionalVideoModel {
274
+ ): ProviderV4 & ProviderWithOptionalVideoModel & EvaluationProvider {
230
275
  const provider = this.providers[id as keyof PROVIDERS];
231
276
 
232
277
  if (provider == null) {
@@ -382,6 +427,20 @@ class DefaultProviderRegistry<
382
427
  return asVideoModelV4(model);
383
428
  }
384
429
 
430
+ evaluationModel<KEY extends keyof PROVIDERS>(
431
+ id: `${KEY & string}${SEPARATOR}${string}`,
432
+ ): EvaluationModelV4 {
433
+ const [providerId, modelId] = this.splitId(id, 'evaluationModel');
434
+ const provider = this.getProvider(providerId, 'evaluationModel');
435
+ const model = provider.evaluationModel?.(modelId);
436
+
437
+ if (model == null) {
438
+ throw new NoSuchModelError({ modelId: id, modelType: 'evaluationModel' });
439
+ }
440
+
441
+ return resolveEvaluationModel(model);
442
+ }
443
+
385
444
  files<KEY extends keyof PROVIDERS>(id: KEY & string): FilesV4 {
386
445
  const provider = this.getProvider(id, 'languageModel');
387
446
  const files = provider.files?.();
@@ -0,0 +1,143 @@
1
+ import {
2
+ experimental_getToolCaller,
3
+ type ToolSet,
4
+ type InferToolSetContext,
5
+ type Experimental_SandboxSession as SandboxSession,
6
+ } from '@ai-sdk/provider-utils';
7
+ import { InvalidArgumentError } from '../error/invalid-argument-error';
8
+ import {
9
+ DIRECT_TOOL_CALL,
10
+ type ResolvedToolCallers,
11
+ } from '../generate-text/tool-caller-configuration';
12
+ import { resolveToolDescription } from '../prompt/prepare-tools';
13
+ import { getOwn } from '../util/get-own';
14
+ import { isToolSearch } from './tool-search';
15
+
16
+ /** Create discovery state for one generation, never for a shared tool instance. */
17
+ export function createToolSearchState({
18
+ tools,
19
+ toolCallers,
20
+ }: {
21
+ tools: ToolSet | undefined;
22
+ toolCallers: ResolvedToolCallers | undefined;
23
+ }): (
24
+ activeTools: ToolSet | undefined,
25
+ options?: {
26
+ toolsContext?: InferToolSetContext<ToolSet>;
27
+ experimental_sandbox?: SandboxSession;
28
+ },
29
+ ) => ToolSet | undefined {
30
+ const searchTools = Object.entries(tools ?? {}).filter(
31
+ ([, tool]) => tool.deferLoading || isToolSearch(tool),
32
+ );
33
+ if (searchTools.length === 0) {
34
+ return activeTools => activeTools;
35
+ }
36
+
37
+ const discovered = new Set<string>();
38
+ // An omitted entry permits direct calls; an explicit empty list permits none.
39
+ const getCallers = (name: string) =>
40
+ getOwn(toolCallers, name) ?? [DIRECT_TOOL_CALL];
41
+
42
+ for (const [name, tool] of searchTools) {
43
+ const callers = getCallers(name);
44
+ if (
45
+ callers.some(name => {
46
+ if (name === DIRECT_TOOL_CALL) {
47
+ return false;
48
+ }
49
+ const caller = experimental_getToolCaller(tools?.[name]);
50
+ return caller?.type !== 'local' || caller.prepareModelMessage == null;
51
+ }) ||
52
+ (isToolSearch(tool) && tool.deferLoading)
53
+ ) {
54
+ throw new InvalidArgumentError({
55
+ parameter: 'tools',
56
+ value: name,
57
+ message: `tool "${name}" must be callable directly or through code mode with toolDiscovery: 'conversation'. The search tool itself must not defer loading.`,
58
+ });
59
+ }
60
+ }
61
+
62
+ return (activeTools, { toolsContext = {}, experimental_sandbox } = {}) => {
63
+ if (activeTools == null) {
64
+ return undefined;
65
+ }
66
+
67
+ // Snapshot eligibility before binding callers. Newly discovered tools are
68
+ // intentionally absent from this step's execution bindings and catalog.
69
+ const entries = Object.entries(activeTools);
70
+ return Object.fromEntries(
71
+ entries
72
+ .filter(([name, tool]) => !tool.deferLoading || discovered.has(name))
73
+ .map(([searchName, tool]) => {
74
+ if (!isToolSearch(tool)) {
75
+ return [searchName, tool];
76
+ }
77
+
78
+ const callers = getCallers(searchName).filter(
79
+ name =>
80
+ name === DIRECT_TOOL_CALL || Object.hasOwn(activeTools, name),
81
+ );
82
+ const candidates = entries.filter(
83
+ ([name, candidate]) =>
84
+ candidate.deferLoading &&
85
+ !isToolSearch(candidate) &&
86
+ callers.some(caller => getCallers(name).includes(caller)),
87
+ );
88
+
89
+ return [
90
+ searchName,
91
+ {
92
+ ...tool,
93
+ execute: ({ query }: { query: string }) => {
94
+ 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 };
113
+ })
114
+ .filter(match => match.score > 0)
115
+ .sort((a, b) => b.score - a.score)
116
+ .slice(0, 5);
117
+
118
+ for (const { name } of matches) {
119
+ discovered.add(name);
120
+ }
121
+
122
+ return {
123
+ tools: matches.map(({ name, description }) => ({
124
+ name,
125
+ ...(description == null ? {} : { description }),
126
+ })),
127
+ };
128
+ },
129
+ },
130
+ ];
131
+ }),
132
+ );
133
+ };
134
+ }
135
+
136
+ function tokenize(text: string): string[] {
137
+ return (
138
+ text
139
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
140
+ .toLowerCase()
141
+ .match(/[\p{L}\p{N}]+/gu) ?? []
142
+ );
143
+ }