ai 7.0.101 → 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.
- package/CHANGELOG.md +49 -0
- package/dist/index.d.ts +138 -22
- package/dist/index.js +2373 -316
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +2 -1
- package/dist/internal/index.js.map +1 -1
- package/dist/test/index.d.ts +16 -2
- package/dist/test/index.js +17 -0
- package/dist/test/index.js.map +1 -1
- package/docs/03-agents/07-workflow-agent.mdx +1 -1
- package/docs/03-ai-sdk-core/18-code-mode.mdx +39 -0
- package/docs/03-ai-sdk-core/32-evaluation.mdx +110 -0
- package/docs/03-ai-sdk-core/36-realtime.mdx +41 -18
- package/docs/03-ai-sdk-core/42-batch.mdx +1 -2
- package/docs/04-ai-sdk-ui/03-chatbot-message-persistence.mdx +35 -0
- package/docs/04-ai-sdk-ui/21-transport.mdx +1 -1
- package/docs/06-advanced/11-secure-url-fetching.mdx +8 -2
- package/docs/07-reference/01-ai-sdk-core/14-evaluate.mdx +54 -0
- package/docs/07-reference/01-ai-sdk-core/32-validate-ui-messages.mdx +12 -0
- package/docs/07-reference/01-ai-sdk-core/33-safe-validate-ui-messages.mdx +12 -0
- package/docs/07-reference/02-ai-sdk-ui/05-use-realtime.mdx +364 -48
- package/docs/07-reference/02-ai-sdk-ui/31-convert-to-model-messages.mdx +12 -0
- package/docs/07-reference/04-ai-sdk-workflow/02-workflow-chat-transport.mdx +4 -4
- package/docs/07-reference/05-ai-sdk-errors/ai-evaluation-unsupported-question-type-error.mdx +31 -0
- package/docs/07-reference/05-ai-sdk-errors/index.mdx +131 -36
- package/package.json +12 -12
- package/src/error/index.ts +1 -0
- package/src/evaluate/evaluate.ts +112 -0
- package/src/evaluate/evaluation-result.ts +39 -0
- package/src/evaluate/index.ts +7 -0
- package/src/evaluate/validate-evaluation.ts +298 -0
- package/src/generate-text/execute-tools-from-stream.ts +7 -0
- package/src/generate-text/generate-text.ts +16 -12
- package/src/generate-text/stream-text.ts +9 -2
- package/src/generate-text/tool-caller-configuration.ts +58 -4
- package/src/index.ts +1 -0
- package/src/realtime/__fixtures__/fake-live-websocket.ts +71 -0
- package/src/realtime/__fixtures__/fake-realtime.ts +36 -0
- package/src/realtime/__fixtures__/fake-webrtc.ts +133 -0
- package/src/realtime/browser-realtime-audio.ts +107 -10
- package/src/realtime/browser-realtime-live-websocket.ts +247 -0
- package/src/realtime/browser-realtime-transport.ts +235 -69
- package/src/realtime/browser-realtime-webrtc.ts +582 -0
- package/src/realtime/encode-realtime-frame.ts +33 -0
- package/src/realtime/index.ts +1 -0
- package/src/realtime/realtime-attempt.ts +45 -0
- package/src/realtime/realtime-command-tracker.ts +81 -0
- package/src/realtime/realtime-event-channel.ts +170 -0
- package/src/realtime/realtime-event-reducer.ts +3 -0
- package/src/realtime/realtime-session-state.ts +65 -0
- package/src/realtime/realtime-session.ts +768 -218
- package/src/realtime/realtime-types.ts +1 -1
- package/src/realtime/validate-realtime-setup.ts +37 -0
- package/src/test/evaluation-mock-model-v4.ts +27 -0
- package/src/ui/convert-to-model-messages.ts +3 -0
- package/src/ui/process-ui-message-stream.ts +4 -0
- package/src/ui/ui-messages.ts +5 -1
- package/src/ui/validate-ui-messages.ts +3 -0
- package/src/ui/warn-if-ui-message-has-deprecated-raw-input.ts +36 -0
|
@@ -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
|
+
}
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
OnToolExecutionEndCallback,
|
|
26
26
|
OnToolExecutionStartCallback,
|
|
27
27
|
} from './tool-execution-events';
|
|
28
|
+
import type { StaticToolOutputDenied } from './tool-output-denied';
|
|
28
29
|
|
|
29
30
|
export type ToolExecutionEndStreamPart = {
|
|
30
31
|
type: 'tool-execution-end';
|
|
@@ -34,6 +35,7 @@ export type ToolExecutionEndStreamPart = {
|
|
|
34
35
|
|
|
35
36
|
export type ExecuteToolsStreamPart<TOOLS extends ToolSet = ToolSet> =
|
|
36
37
|
| LanguageModelStreamPart<TOOLS>
|
|
38
|
+
| StaticToolOutputDenied<TOOLS>
|
|
37
39
|
| ToolExecutionEndStreamPart
|
|
38
40
|
| StreamRetryAttemptBoundaryPart;
|
|
39
41
|
|
|
@@ -180,6 +182,11 @@ export function executeToolsFromStream<
|
|
|
180
182
|
reason: toolApprovalStatus.reason,
|
|
181
183
|
providerExecuted: chunk.providerExecuted,
|
|
182
184
|
});
|
|
185
|
+
controller.enqueue({
|
|
186
|
+
type: 'tool-output-denied',
|
|
187
|
+
toolCallId: chunk.toolCallId,
|
|
188
|
+
toolName: chunk.toolName,
|
|
189
|
+
} as StaticToolOutputDenied<TOOLS>);
|
|
183
190
|
|
|
184
191
|
return; // don't execute tool
|
|
185
192
|
}
|
|
@@ -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,
|
|
@@ -196,6 +197,7 @@ const isOutputChunkType = {
|
|
|
196
197
|
'tool-call': true,
|
|
197
198
|
'tool-result': false,
|
|
198
199
|
'tool-error': false,
|
|
200
|
+
'tool-output-denied': false,
|
|
199
201
|
'tool-execution-end': false,
|
|
200
202
|
'model-call-start': false,
|
|
201
203
|
'model-call-response-metadata': false,
|
|
@@ -2329,6 +2331,7 @@ class DefaultStreamTextResult<
|
|
|
2329
2331
|
const {
|
|
2330
2332
|
executionTools: stepExecutionTools,
|
|
2331
2333
|
modelTools: stepModelTools,
|
|
2334
|
+
toolCallerMessages,
|
|
2332
2335
|
} = prepareToolsForToolCallers({
|
|
2333
2336
|
tools: stepActiveTools,
|
|
2334
2337
|
toolCallers: resolvedToolCallers,
|
|
@@ -2354,7 +2357,10 @@ class DefaultStreamTextResult<
|
|
|
2354
2357
|
toolChoice: prepareStepResult?.toolChoice ?? toolChoice,
|
|
2355
2358
|
});
|
|
2356
2359
|
|
|
2357
|
-
const stepMessages =
|
|
2360
|
+
const stepMessages = appendToolCallerMessages({
|
|
2361
|
+
messages: prepareStepResult?.messages ?? stepInputMessages,
|
|
2362
|
+
toolCallerMessages,
|
|
2363
|
+
});
|
|
2358
2364
|
currentStepMessages = stepMessages;
|
|
2359
2365
|
const stepInstructions =
|
|
2360
2366
|
prepareStepResult?.instructions ??
|
|
@@ -2833,7 +2839,8 @@ class DefaultStreamTextResult<
|
|
|
2833
2839
|
case 'tool-input-start':
|
|
2834
2840
|
case 'tool-input-end':
|
|
2835
2841
|
case 'tool-input-delta':
|
|
2836
|
-
case 'tool-approval-request':
|
|
2842
|
+
case 'tool-approval-request':
|
|
2843
|
+
case 'tool-output-denied': {
|
|
2837
2844
|
enqueueStepPart(controller, chunk);
|
|
2838
2845
|
break;
|
|
2839
2846
|
}
|
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { vi, type Mock } from 'vitest';
|
|
2
|
+
import type { RealtimeServerEvent } from '../../types/realtime-model';
|
|
3
|
+
import { fakeStream } from './fake-realtime';
|
|
4
|
+
|
|
5
|
+
export class FakeWebSocket {
|
|
6
|
+
static OPEN = 1;
|
|
7
|
+
static instances: FakeWebSocket[] = [];
|
|
8
|
+
readyState = 0;
|
|
9
|
+
bufferedAmount = 0;
|
|
10
|
+
onopen: (() => void) | null = null;
|
|
11
|
+
onclose: ((event: CloseEvent) => void) | null = null;
|
|
12
|
+
onerror: (() => void) | null = null;
|
|
13
|
+
onmessage: ((event: { data: unknown }) => void) | null = null;
|
|
14
|
+
sent: Array<{ type: string; [key: string]: unknown }> = [];
|
|
15
|
+
constructor(public url: string, public protocols?: string[]) { FakeWebSocket.instances.push(this); }
|
|
16
|
+
send = vi.fn((data: string) => { this.sent.push(JSON.parse(data)); });
|
|
17
|
+
close = vi.fn(() => { this.closeFromServer(); });
|
|
18
|
+
open() { this.readyState = 1; this.onopen?.(); }
|
|
19
|
+
emit(event: RealtimeServerEvent) { this.onmessage?.({ data: JSON.stringify(event) }); }
|
|
20
|
+
closeFromServer({ code = 1000, reason = '', wasClean = true }: Partial<Pick<CloseEvent, 'code' | 'reason' | 'wasClean'>> = {}) {
|
|
21
|
+
this.readyState = 3;
|
|
22
|
+
this.onclose?.({ code, reason, wasClean } as CloseEvent);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class FakeAudioNode {
|
|
27
|
+
connect: Mock = vi.fn();
|
|
28
|
+
disconnect: Mock = vi.fn();
|
|
29
|
+
onaudioprocess: ((event: { inputBuffer: { getChannelData: (channel: number) => Float32Array } }) => void) | null = null;
|
|
30
|
+
onended: (() => void) | null = null;
|
|
31
|
+
buffer: unknown;
|
|
32
|
+
start: Mock = vi.fn();
|
|
33
|
+
stop: Mock = vi.fn();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class FakeAudioContext {
|
|
37
|
+
static instances: FakeAudioContext[] = [];
|
|
38
|
+
state = 'suspended';
|
|
39
|
+
currentTime = 0;
|
|
40
|
+
sampleRate: number;
|
|
41
|
+
destination = {};
|
|
42
|
+
processors: FakeAudioNode[] = [];
|
|
43
|
+
sources: FakeAudioNode[] = [];
|
|
44
|
+
mediaSources: FakeAudioNode[] = [];
|
|
45
|
+
constructor(options: { sampleRate: number }) { this.sampleRate = options.sampleRate; FakeAudioContext.instances.push(this); }
|
|
46
|
+
resume = vi.fn(async () => { this.state = 'running'; });
|
|
47
|
+
close = vi.fn(async () => { this.state = 'closed'; });
|
|
48
|
+
createScriptProcessor = vi.fn(() => {
|
|
49
|
+
const node = new FakeAudioNode(); this.processors.push(node); return node;
|
|
50
|
+
});
|
|
51
|
+
createMediaStreamSource = vi.fn(() => {
|
|
52
|
+
const node = new FakeAudioNode(); this.mediaSources.push(node); return node;
|
|
53
|
+
});
|
|
54
|
+
createBufferSource = vi.fn(() => {
|
|
55
|
+
const node = new FakeAudioNode(); this.sources.push(node); return node;
|
|
56
|
+
});
|
|
57
|
+
createBuffer = vi.fn((_channels: number, size: number, rate: number) => ({ duration: size / rate, getChannelData: () => new Float32Array(size) }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function installLiveWebSocket() {
|
|
61
|
+
const media = fakeStream();
|
|
62
|
+
const getUserMedia = vi.fn(async () => media.stream);
|
|
63
|
+
const fetch = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>();
|
|
64
|
+
vi.stubGlobal('navigator', { mediaDevices: { getUserMedia } });
|
|
65
|
+
vi.stubGlobal('fetch', fetch);
|
|
66
|
+
FakeWebSocket.instances = [];
|
|
67
|
+
FakeAudioContext.instances = [];
|
|
68
|
+
vi.stubGlobal('WebSocket', FakeWebSocket);
|
|
69
|
+
vi.stubGlobal('AudioContext', FakeAudioContext);
|
|
70
|
+
return { ...media, getUserMedia, fetch };
|
|
71
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import type { RealtimeModel, RealtimeServerEvent } from '../../types/realtime-model';
|
|
3
|
+
|
|
4
|
+
export class FakeTrack extends EventTarget {
|
|
5
|
+
enabled = true;
|
|
6
|
+
muted = false;
|
|
7
|
+
readyState = 'live';
|
|
8
|
+
stop = vi.fn(() => { this.readyState = 'ended'; });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function fakeStream() {
|
|
12
|
+
const track = new FakeTrack();
|
|
13
|
+
return { track, stream: { getTracks: () => [track], getAudioTracks: () => [track] } as unknown as MediaStream };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function liveModel(): RealtimeModel {
|
|
17
|
+
return {
|
|
18
|
+
specificationVersion: 'v4', provider: 'test', modelId: 'live',
|
|
19
|
+
capabilities: { conversation: 'continuous', transports: ['websocket'], connections: ['server-websocket'], startup: 'session-start', finalization: 'session-close' },
|
|
20
|
+
getWebSocketConfig: vi.fn(),
|
|
21
|
+
doCreateClientSecret: vi.fn(),
|
|
22
|
+
buildSessionConfig: vi.fn(),
|
|
23
|
+
parseServerEvent: event => event as RealtimeServerEvent,
|
|
24
|
+
serializeClientEvent: async event => event,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function flushEvents() {
|
|
29
|
+
for (let i = 0; i < 100; i++) await Promise.resolve();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function deferred<T>() {
|
|
33
|
+
let resolve: (value: T) => void = () => {};
|
|
34
|
+
const promise = new Promise<T>(r => { resolve = r; });
|
|
35
|
+
return { promise, resolve };
|
|
36
|
+
}
|