ai 7.0.102 → 7.0.103

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 (35) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/index.d.ts +57 -3
  3. package/dist/index.js +427 -50
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal/index.js +2 -1
  6. package/dist/internal/index.js.map +1 -1
  7. package/dist/test/index.d.ts +16 -2
  8. package/dist/test/index.js +17 -0
  9. package/dist/test/index.js.map +1 -1
  10. package/docs/03-ai-sdk-core/18-code-mode.mdx +39 -0
  11. package/docs/03-ai-sdk-core/32-evaluation.mdx +110 -0
  12. package/docs/03-ai-sdk-core/42-batch.mdx +1 -2
  13. package/docs/04-ai-sdk-ui/03-chatbot-message-persistence.mdx +35 -0
  14. package/docs/06-advanced/11-secure-url-fetching.mdx +8 -2
  15. package/docs/07-reference/01-ai-sdk-core/14-evaluate.mdx +54 -0
  16. package/docs/07-reference/01-ai-sdk-core/32-validate-ui-messages.mdx +12 -0
  17. package/docs/07-reference/01-ai-sdk-core/33-safe-validate-ui-messages.mdx +12 -0
  18. package/docs/07-reference/02-ai-sdk-ui/31-convert-to-model-messages.mdx +12 -0
  19. package/docs/07-reference/05-ai-sdk-errors/ai-evaluation-unsupported-question-type-error.mdx +31 -0
  20. package/package.json +12 -12
  21. package/src/error/index.ts +1 -0
  22. package/src/evaluate/evaluate.ts +112 -0
  23. package/src/evaluate/evaluation-result.ts +39 -0
  24. package/src/evaluate/index.ts +7 -0
  25. package/src/evaluate/validate-evaluation.ts +298 -0
  26. package/src/generate-text/generate-text.ts +16 -12
  27. package/src/generate-text/stream-text.ts +6 -1
  28. package/src/generate-text/tool-caller-configuration.ts +58 -4
  29. package/src/index.ts +1 -0
  30. package/src/test/evaluation-mock-model-v4.ts +27 -0
  31. package/src/ui/convert-to-model-messages.ts +3 -0
  32. package/src/ui/process-ui-message-stream.ts +4 -0
  33. package/src/ui/ui-messages.ts +5 -1
  34. package/src/ui/validate-ui-messages.ts +3 -0
  35. package/src/ui/warn-if-ui-message-has-deprecated-raw-input.ts +36 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.102",
3
+ "version": "7.0.103",
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.82",
46
- "@ai-sdk/provider": "4.0.15",
47
- "@ai-sdk/provider-utils": "5.0.41"
45
+ "@ai-sdk/gateway": "4.0.83",
46
+ "@ai-sdk/provider": "4.0.16",
47
+ "@ai-sdk/provider-utils": "5.0.42"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.83",
51
- "@ai-sdk/deepseek": "3.0.45",
52
- "@ai-sdk/google": "4.0.71",
53
- "@ai-sdk/groq": "4.0.42",
54
- "@ai-sdk/huggingface": "2.0.49",
55
- "@ai-sdk/moonshotai": "3.0.50",
56
- "@ai-sdk/openai": "4.0.67",
50
+ "@ai-sdk/amazon-bedrock": "5.0.85",
51
+ "@ai-sdk/deepseek": "3.0.46",
52
+ "@ai-sdk/google": "4.0.73",
53
+ "@ai-sdk/groq": "4.0.43",
54
+ "@ai-sdk/huggingface": "2.0.50",
55
+ "@ai-sdk/moonshotai": "3.0.51",
56
+ "@ai-sdk/openai": "4.0.68",
57
57
  "@ai-sdk/test-server": "2.0.1",
58
- "@ai-sdk/xai": "4.0.59",
58
+ "@ai-sdk/xai": "5.0.1",
59
59
  "@edge-runtime/vm": "^5.0.0",
60
60
  "@smithy/eventstream-codec": "^4.3.3",
61
61
  "@smithy/util-utf8": "^4.3.3",
@@ -2,6 +2,7 @@ export {
2
2
  AISDKError,
3
3
  APICallError,
4
4
  EmptyResponseBodyError,
5
+ Experimental_EvaluationUnsupportedQuestionTypeError,
5
6
  InvalidPromptError,
6
7
  InvalidResponseDataError,
7
8
  JSONParseError,
@@ -0,0 +1,112 @@
1
+ import {
2
+ Experimental_EvaluationUnsupportedQuestionTypeError as EvaluationUnsupportedQuestionTypeError,
3
+ type Experimental_EvaluationModelV4CallOptions as EvaluationModelV4CallOptions,
4
+ } from '@ai-sdk/provider';
5
+ import {
6
+ withUserAgentSuffix,
7
+ type ProviderOptions,
8
+ } from '@ai-sdk/provider-utils';
9
+ import { UnsupportedModelVersionError } from '../error/unsupported-model-version-error';
10
+ import { logWarnings } from '../logger/log-warnings';
11
+ import { prepareRetries } from '../util/prepare-retries';
12
+ import { VERSION } from '../version';
13
+ import type {
14
+ EvaluationModel,
15
+ EvaluationQuestion,
16
+ EvaluationResult,
17
+ } from './evaluation-result';
18
+ import {
19
+ validateEvaluationInput,
20
+ validateEvaluationAnswers,
21
+ } from './validate-evaluation';
22
+
23
+ /** Evaluate typed questions against one shared state. Experimental. */
24
+ export async function evaluate<
25
+ const QUESTIONS extends Record<string, EvaluationQuestion>,
26
+ >({
27
+ model,
28
+ state,
29
+ questions,
30
+ maxRetries,
31
+ abortSignal,
32
+ headers,
33
+ providerOptions = {},
34
+ }: {
35
+ /** An evaluation model instance. String model resolution is not yet supported. */
36
+ model: EvaluationModel;
37
+ state: EvaluationModelV4CallOptions['state'];
38
+ questions: QUESTIONS;
39
+ /** Maximum retries for transient provider failures. Defaults to 2. */
40
+ maxRetries?: number;
41
+ abortSignal?: AbortSignal;
42
+ headers?: Record<string, string>;
43
+ providerOptions?: ProviderOptions;
44
+ }): Promise<EvaluationResult<QUESTIONS>> {
45
+ if (model.specificationVersion !== 'v4') {
46
+ throw new UnsupportedModelVersionError({
47
+ version: model.specificationVersion,
48
+ provider: model.provider,
49
+ modelId: model.modelId,
50
+ });
51
+ }
52
+
53
+ validateEvaluationInput({ state, questions });
54
+
55
+ for (const [questionId, question] of Object.entries(questions)) {
56
+ if (!model.supportedQuestionTypes.includes(question.type)) {
57
+ throw new EvaluationUnsupportedQuestionTypeError({
58
+ questionId,
59
+ questionType: question.type,
60
+ provider: model.provider,
61
+ modelId: model.modelId,
62
+ });
63
+ }
64
+ }
65
+
66
+ const { retry } = prepareRetries({ maxRetries, abortSignal });
67
+ const result = await retry(() => {
68
+ abortSignal?.throwIfAborted();
69
+ return model.doEvaluate({
70
+ state,
71
+ questions,
72
+ abortSignal,
73
+ headers: withUserAgentSuffix(headers ?? {}, `ai/${VERSION}`),
74
+ providerOptions,
75
+ });
76
+ });
77
+
78
+ abortSignal?.throwIfAborted();
79
+ validateEvaluationAnswers({
80
+ questions,
81
+ answers: result.answers,
82
+ rounding: result.rounding,
83
+ });
84
+ logWarnings({
85
+ warnings: result.warnings,
86
+ provider: model.provider,
87
+ model: model.modelId,
88
+ });
89
+
90
+ const inputTokens = result.usage?.inputTokens;
91
+ const outputTokens = result.usage?.outputTokens;
92
+
93
+ return {
94
+ answers: result.answers as EvaluationResult<QUESTIONS>['answers'],
95
+ usage: {
96
+ inputTokens,
97
+ outputTokens,
98
+ totalTokens:
99
+ inputTokens != null && outputTokens != null
100
+ ? inputTokens + outputTokens
101
+ : undefined,
102
+ },
103
+ warnings: result.warnings,
104
+ rounding: result.rounding,
105
+ providerMetadata: result.providerMetadata,
106
+ response: {
107
+ ...result.response,
108
+ timestamp: result.response?.timestamp ?? new Date(),
109
+ modelId: result.response?.modelId ?? model.modelId,
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,39 @@
1
+ import type {
2
+ Experimental_EvaluationModelV4 as EvaluationModelV4,
3
+ Experimental_EvaluationModelV4Question as EvaluationModelV4Question,
4
+ Experimental_EvaluationModelV4Result as EvaluationModelV4Result,
5
+ } from '@ai-sdk/provider';
6
+
7
+ export type EvaluationModel = EvaluationModelV4;
8
+ export type EvaluationQuestion = EvaluationModelV4Question;
9
+
10
+ export type EvaluationAnswer<QUESTION extends EvaluationQuestion> =
11
+ QUESTION extends { type: 'choice'; criteria: infer CRITERIA }
12
+ ? {
13
+ type: 'choice';
14
+ choice: Extract<keyof CRITERIA, string>;
15
+ probabilities?: Record<Extract<keyof CRITERIA, string>, number>;
16
+ }
17
+ : QUESTION extends { type: 'score' }
18
+ ? { type: 'score'; score: number; probabilities?: Record<string, number> }
19
+ : { type: 'boolean'; probability: number };
20
+
21
+ export type EvaluationResult<
22
+ QUESTIONS extends Record<string, EvaluationQuestion>,
23
+ > = {
24
+ readonly answers: {
25
+ [ID in keyof QUESTIONS]: EvaluationAnswer<QUESTIONS[ID]>;
26
+ };
27
+ readonly usage: {
28
+ inputTokens: number | undefined;
29
+ outputTokens: number | undefined;
30
+ totalTokens: number | undefined;
31
+ };
32
+ readonly warnings: EvaluationModelV4Result['warnings'];
33
+ readonly rounding: EvaluationModelV4Result['rounding'];
34
+ readonly providerMetadata: EvaluationModelV4Result['providerMetadata'];
35
+ readonly response: NonNullable<EvaluationModelV4Result['response']> & {
36
+ timestamp: Date;
37
+ modelId: string;
38
+ };
39
+ };
@@ -0,0 +1,7 @@
1
+ export { evaluate as experimental_evaluate } from './evaluate';
2
+ export type {
3
+ EvaluationModel as Experimental_EvaluationModel,
4
+ EvaluationQuestion as Experimental_EvaluationQuestion,
5
+ EvaluationAnswer as Experimental_EvaluationAnswer,
6
+ EvaluationResult as Experimental_EvaluationResult,
7
+ } from './evaluation-result';
@@ -0,0 +1,298 @@
1
+ import {
2
+ InvalidResponseDataError,
3
+ type Experimental_EvaluationModelV4CallOptions as EvaluationModelV4CallOptions,
4
+ type Experimental_EvaluationModelV4Result as EvaluationModelV4Result,
5
+ } from '@ai-sdk/provider';
6
+ import { InvalidArgumentError } from '../error/invalid-argument-error';
7
+
8
+ // Absolute tolerance for sums and means. Never renormalize provider output.
9
+ const tolerance = 1e-6;
10
+
11
+ function isRecord(value: unknown): value is Record<string, unknown> {
12
+ if (value == null || typeof value !== 'object' || Array.isArray(value))
13
+ return false;
14
+ const prototype = Object.getPrototypeOf(value);
15
+ return prototype === Object.prototype || prototype === null;
16
+ }
17
+
18
+ function isJSON(value: unknown, ancestors = new Set<object>()): boolean {
19
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
20
+ return true;
21
+ if (typeof value === 'number') return Number.isFinite(value);
22
+ if (typeof value !== 'object' || (!Array.isArray(value) && !isRecord(value)))
23
+ return false;
24
+ if (ancestors.has(value)) return false;
25
+ ancestors.add(value);
26
+ const valid =
27
+ Object.getOwnPropertySymbols(value).length === 0 &&
28
+ (Array.isArray(value)
29
+ ? Array.from(value).every(item => isJSON(item, ancestors))
30
+ : Object.values(value).every(item => isJSON(item, ancestors)));
31
+ ancestors.delete(value);
32
+ return valid;
33
+ }
34
+
35
+ function isInput(value: unknown): boolean {
36
+ return (
37
+ (typeof value === 'string' || Array.isArray(value) || isRecord(value)) &&
38
+ isJSON(value)
39
+ );
40
+ }
41
+
42
+ function invalidInput(
43
+ parameter: string,
44
+ value: unknown,
45
+ message: string,
46
+ ): never {
47
+ throw new InvalidArgumentError({ parameter, value, message });
48
+ }
49
+
50
+ export function validateEvaluationInput({
51
+ state,
52
+ questions,
53
+ }: EvaluationModelV4CallOptions) {
54
+ if (!isInput(state)) {
55
+ invalidInput(
56
+ 'state',
57
+ state,
58
+ 'must be a JSON-compatible string, object, or array',
59
+ );
60
+ }
61
+ if (!isRecord(questions) || Object.keys(questions).length === 0) {
62
+ invalidInput('questions', questions, 'must be a nonempty question map');
63
+ }
64
+
65
+ for (const [id, question] of Object.entries(questions)) {
66
+ const parameter = `questions.${id}`;
67
+ if (!isRecord(question) || !isInput(question.instructions)) {
68
+ invalidInput(
69
+ parameter,
70
+ question,
71
+ 'instructions must be a JSON-compatible string, object, or array',
72
+ );
73
+ }
74
+ const criteria = question.criteria;
75
+ switch (question.type) {
76
+ case 'choice':
77
+ if (!isRecord(criteria) || Object.keys(criteria).length === 0) {
78
+ invalidInput(
79
+ parameter,
80
+ question,
81
+ 'choice criteria must be a nonempty option map',
82
+ );
83
+ }
84
+ break;
85
+ case 'score':
86
+ if (!Array.isArray(criteria) || criteria.length < 2) {
87
+ invalidInput(
88
+ parameter,
89
+ question,
90
+ 'score criteria must contain at least two ordered levels',
91
+ );
92
+ }
93
+ break;
94
+ case 'boolean':
95
+ if (criteria === undefined) continue;
96
+ if (
97
+ !isRecord(criteria) ||
98
+ Object.keys(criteria).some(key => key !== 'true' && key !== 'false')
99
+ ) {
100
+ invalidInput(
101
+ parameter,
102
+ question,
103
+ 'boolean criteria may only describe true and false',
104
+ );
105
+ }
106
+ break;
107
+ default:
108
+ invalidInput(
109
+ parameter,
110
+ question,
111
+ 'question type must be choice, score, or boolean',
112
+ );
113
+ }
114
+ if (
115
+ !isJSON(criteria) ||
116
+ Object.values(criteria).some(value => value !== null && !isInput(value))
117
+ ) {
118
+ invalidInput(
119
+ parameter,
120
+ question,
121
+ 'criteria descriptions must be JSON-compatible strings, objects, arrays, or null',
122
+ );
123
+ }
124
+ }
125
+ }
126
+
127
+ function invalidAnswer(answers: unknown, message: string): never {
128
+ throw new InvalidResponseDataError({ data: answers, message });
129
+ }
130
+
131
+ function isProbability(value: unknown): value is number {
132
+ return (
133
+ typeof value === 'number' &&
134
+ Number.isFinite(value) &&
135
+ value >= 0 &&
136
+ value <= 1
137
+ );
138
+ }
139
+
140
+ function hasExactKeys(value: Record<string, unknown>, keys: string[]): boolean {
141
+ return (
142
+ Object.keys(value).length === keys.length &&
143
+ keys.every(key => Object.hasOwn(value, key))
144
+ );
145
+ }
146
+
147
+ function validateDistribution(
148
+ value: unknown,
149
+ keys: string[],
150
+ answers: unknown,
151
+ id: string,
152
+ roundingError: number,
153
+ ): asserts value is Record<string, number> {
154
+ if (
155
+ !isRecord(value) ||
156
+ !hasExactKeys(value, keys) ||
157
+ !Object.values(value).every(isProbability)
158
+ ) {
159
+ invalidAnswer(
160
+ answers,
161
+ `Question "${id}" must have a complete distribution of finite probabilities in [0, 1].`,
162
+ );
163
+ }
164
+ const sum = Object.values(value).reduce<number>(
165
+ (total, probability) => total + (probability as number),
166
+ 0,
167
+ );
168
+ if (Math.abs(sum - 1) > tolerance + keys.length * roundingError) {
169
+ invalidAnswer(
170
+ answers,
171
+ `Question "${id}" probabilities must sum to 1 within the declared rounding precision.`,
172
+ );
173
+ }
174
+ }
175
+
176
+ export function validateEvaluationAnswers({
177
+ questions,
178
+ answers,
179
+ rounding,
180
+ }: {
181
+ questions: EvaluationModelV4CallOptions['questions'];
182
+ answers: unknown;
183
+ rounding?: EvaluationModelV4Result['rounding'];
184
+ }) {
185
+ function roundingError(decimals: number | undefined): number {
186
+ if (decimals === undefined) return 0;
187
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 15) {
188
+ invalidAnswer(
189
+ answers,
190
+ 'Evaluation rounding decimals must be integers between 0 and 15.',
191
+ );
192
+ }
193
+ return 0.5 * 10 ** -decimals;
194
+ }
195
+ const probabilityError = roundingError(rounding?.probabilityDecimals);
196
+ const scoreError = roundingError(rounding?.scoreDecimals);
197
+ if (!isRecord(answers) || !hasExactKeys(answers, Object.keys(questions))) {
198
+ invalidAnswer(
199
+ answers,
200
+ 'Evaluation must return exactly one answer for every question.',
201
+ );
202
+ }
203
+
204
+ for (const [id, question] of Object.entries(questions)) {
205
+ const answer = answers[id];
206
+ if (!isRecord(answer) || answer.type !== question.type) {
207
+ invalidAnswer(
208
+ answers,
209
+ `Question "${id}" returned an answer with the wrong type.`,
210
+ );
211
+ }
212
+
213
+ switch (question.type) {
214
+ case 'choice': {
215
+ if (
216
+ typeof answer.choice !== 'string' ||
217
+ !Object.hasOwn(question.criteria, answer.choice)
218
+ ) {
219
+ invalidAnswer(
220
+ answers,
221
+ `Question "${id}" selected an unknown option.`,
222
+ );
223
+ }
224
+ if (answer.probabilities !== undefined) {
225
+ validateDistribution(
226
+ answer.probabilities,
227
+ Object.keys(question.criteria),
228
+ answers,
229
+ id,
230
+ probabilityError,
231
+ );
232
+ const selected = answer.probabilities[answer.choice];
233
+ if (
234
+ Object.values(answer.probabilities).some(
235
+ probability => probability > selected + tolerance,
236
+ )
237
+ ) {
238
+ invalidAnswer(
239
+ answers,
240
+ `Question "${id}" did not select a highest-probability option.`,
241
+ );
242
+ }
243
+ }
244
+ break;
245
+ }
246
+ case 'score': {
247
+ if (
248
+ typeof answer.score !== 'number' ||
249
+ !Number.isFinite(answer.score) ||
250
+ answer.score < 0 ||
251
+ answer.score > question.criteria.length - 1
252
+ ) {
253
+ invalidAnswer(
254
+ answers,
255
+ `Question "${id}" score must be in [0, ${question.criteria.length - 1}].`,
256
+ );
257
+ }
258
+ if (answer.probabilities !== undefined) {
259
+ const keys = question.criteria.map((_, index) => String(index));
260
+ validateDistribution(
261
+ answer.probabilities,
262
+ keys,
263
+ answers,
264
+ id,
265
+ probabilityError,
266
+ );
267
+ const mean = Object.entries(answer.probabilities).reduce(
268
+ (total, [index, probability]) =>
269
+ total + Number(index) * probability,
270
+ 0,
271
+ );
272
+ const meanRoundingError = keys.reduce(
273
+ (total, index) => total + Number(index) * probabilityError,
274
+ 0,
275
+ );
276
+ if (
277
+ Math.abs(mean - answer.score) >
278
+ tolerance + meanRoundingError + scoreError
279
+ ) {
280
+ invalidAnswer(
281
+ answers,
282
+ `Question "${id}" score must equal the probability-weighted mean within the declared rounding precision.`,
283
+ );
284
+ }
285
+ }
286
+ break;
287
+ }
288
+ case 'boolean':
289
+ if (!isProbability(answer.probability)) {
290
+ invalidAnswer(
291
+ answers,
292
+ `Question "${id}" must return P(true) as a finite probability in [0, 1].`,
293
+ );
294
+ }
295
+ break;
296
+ }
297
+ }
298
+ }
@@ -106,6 +106,7 @@ import type { ToolApprovalConfiguration } from './tool-approval-configuration';
106
106
  import type { ToolApprovalRequestOutput } from './tool-approval-request-output';
107
107
  import type { ToolApprovalResponseOutput } from './tool-approval-response-output';
108
108
  import {
109
+ appendToolCallerMessages,
109
110
  prepareToolsForToolCallers,
110
111
  resolveToolCallerConfiguration,
111
112
  type Experimental_ToolCallers,
@@ -906,16 +907,6 @@ export async function generateText<
906
907
  prepareStepResult?.system ??
907
908
  instructionsForNextStep;
908
909
 
909
- const promptMessages = await convertToLanguageModelPrompt({
910
- prompt: {
911
- instructions: stepInstructions,
912
- messages: prepareStepResult?.messages ?? stepInputMessages,
913
- },
914
- supportedUrls: await stepModel.supportedUrls,
915
- download,
916
- provider: stepModel.provider.split('.')[0],
917
- });
918
-
919
910
  runtimeContext =
920
911
  prepareStepResult?.runtimeContext ?? runtimeContext;
921
912
  toolsContext = prepareStepResult?.toolsContext ?? toolsContext;
@@ -927,6 +918,7 @@ export async function generateText<
927
918
  const {
928
919
  executionTools: stepExecutionTools,
929
920
  modelTools: stepModelTools,
921
+ toolCallerMessages,
930
922
  } = prepareToolsForToolCallers({
931
923
  tools: stepActiveTools,
932
924
  toolCallers: resolvedToolCallers,
@@ -952,8 +944,20 @@ export async function generateText<
952
944
  toolChoice: prepareStepResult?.toolChoice ?? toolChoice,
953
945
  });
954
946
 
955
- const stepMessages =
956
- prepareStepResult?.messages ?? stepInputMessages;
947
+ const stepMessages = appendToolCallerMessages({
948
+ messages: prepareStepResult?.messages ?? stepInputMessages,
949
+ toolCallerMessages,
950
+ });
951
+
952
+ const promptMessages = await convertToLanguageModelPrompt({
953
+ prompt: {
954
+ instructions: stepInstructions,
955
+ messages: stepMessages,
956
+ },
957
+ supportedUrls: await stepModel.supportedUrls,
958
+ download,
959
+ provider: stepModel.provider.split('.')[0],
960
+ });
957
961
 
958
962
  const stepProviderOptions = mergeObjects(
959
963
  providerOptions,
@@ -146,6 +146,7 @@ import type {
146
146
  import { toResponseMessages } from './to-response-messages';
147
147
  import type { ToolApprovalConfiguration } from './tool-approval-configuration';
148
148
  import {
149
+ appendToolCallerMessages,
149
150
  prepareToolsForToolCallers,
150
151
  resolveToolCallerConfiguration,
151
152
  type Experimental_ToolCallers,
@@ -2330,6 +2331,7 @@ class DefaultStreamTextResult<
2330
2331
  const {
2331
2332
  executionTools: stepExecutionTools,
2332
2333
  modelTools: stepModelTools,
2334
+ toolCallerMessages,
2333
2335
  } = prepareToolsForToolCallers({
2334
2336
  tools: stepActiveTools,
2335
2337
  toolCallers: resolvedToolCallers,
@@ -2355,7 +2357,10 @@ class DefaultStreamTextResult<
2355
2357
  toolChoice: prepareStepResult?.toolChoice ?? toolChoice,
2356
2358
  });
2357
2359
 
2358
- const stepMessages = prepareStepResult?.messages ?? stepInputMessages;
2360
+ const stepMessages = appendToolCallerMessages({
2361
+ messages: prepareStepResult?.messages ?? stepInputMessages,
2362
+ toolCallerMessages,
2363
+ });
2359
2364
  currentStepMessages = stepMessages;
2360
2365
  const stepInstructions =
2361
2366
  prepareStepResult?.instructions ??
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  experimental_getToolCaller,
3
3
  type Experimental_ToolCallerTool,
4
+ type ModelMessage,
4
5
  type Tool,
5
6
  type ToolSet,
7
+ type UserModelMessage,
6
8
  } from '@ai-sdk/provider-utils';
7
9
  import { InvalidArgumentError } from '../error/invalid-argument-error';
8
10
 
@@ -86,15 +88,28 @@ export function prepareToolsForToolCallers({
86
88
  }): {
87
89
  executionTools: ToolSet | undefined;
88
90
  modelTools: ToolSet | undefined;
91
+ toolCallerMessages: UserModelMessage[];
89
92
  } {
90
93
  if (tools == null || toolCallers == null) {
91
- return { executionTools: tools, modelTools: tools };
94
+ return {
95
+ executionTools: tools,
96
+ modelTools: tools,
97
+ toolCallerMessages: [],
98
+ };
92
99
  }
93
100
 
101
+ // Tools available to the runtime for execution.
94
102
  const executionTools: ToolSet = { ...tools };
103
+
104
+ // Tools exposed directly to the model.
95
105
  const modelTools: ToolSet = { ...tools };
106
+
107
+ // Tools routed through each local caller.
96
108
  const localToolsByCaller = new Map<string, ToolSet>();
97
109
 
110
+ // User messages describing caller-accessible tools.
111
+ const toolCallerMessages: UserModelMessage[] = [];
112
+
98
113
  for (const [toolName, callerNames] of Object.entries(toolCallers)) {
99
114
  const tool = executionTools[toolName];
100
115
  if (tool == null) {
@@ -146,13 +161,52 @@ export function prepareToolsForToolCallers({
146
161
  continue;
147
162
  }
148
163
 
149
- const boundCaller = caller.bind(localToolsByCaller.get(callerName) ?? {});
164
+ const callerTools = localToolsByCaller.get(callerName) ?? {};
165
+ const boundCaller = caller.bind(callerTools);
150
166
  executionTools[callerName] = boundCaller;
151
167
 
152
168
  if (Object.prototype.hasOwnProperty.call(modelTools, callerName)) {
153
- modelTools[callerName] = boundCaller;
169
+ if (caller.prepareModelMessage == null) {
170
+ modelTools[callerName] = boundCaller;
171
+ } else {
172
+ const content = caller.prepareModelMessage(callerTools);
173
+ if (content != null) {
174
+ toolCallerMessages.push({ role: 'user', content });
175
+ }
176
+ }
154
177
  }
155
178
  }
156
179
 
157
- return { executionTools, modelTools };
180
+ return { executionTools, modelTools, toolCallerMessages };
181
+ }
182
+
183
+ export function appendToolCallerMessages({
184
+ messages,
185
+ toolCallerMessages,
186
+ }: {
187
+ messages: ModelMessage[];
188
+ toolCallerMessages: UserModelMessage[];
189
+ }): ModelMessage[] {
190
+ if (toolCallerMessages.length === 0) {
191
+ return messages;
192
+ }
193
+
194
+ const latestUserText = messages.findLast(
195
+ message => message.role === 'user' && typeof message.content === 'string',
196
+ )?.content;
197
+ const existingUserText = new Set(
198
+ latestUserText == null ? [] : [latestUserText],
199
+ );
200
+ const additions = toolCallerMessages.filter(message => {
201
+ if (
202
+ typeof message.content !== 'string' ||
203
+ existingUserText.has(message.content)
204
+ ) {
205
+ return false;
206
+ }
207
+ existingUserText.add(message.content);
208
+ return true;
209
+ });
210
+
211
+ return additions.length === 0 ? messages : [...messages, ...additions];
158
212
  }
package/src/index.ts CHANGED
@@ -41,6 +41,7 @@ export * from './agent';
41
41
  export * from './batch';
42
42
  export * from './embed';
43
43
  export * from './error';
44
+ export * from './evaluate';
44
45
  export * from './generate-image';
45
46
  export * from './generate-object';
46
47
  export * from './generate-speech';