ai 6.0.312 → 6.0.314
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 +19 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +85 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +85 -18
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.mjs +1 -1
- package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +6 -0
- package/docs/03-ai-sdk-core/37-speech.mdx +1 -0
- package/docs/07-reference/05-ai-sdk-errors/ai-tool-choice-violation-error.mdx +11 -6
- package/package.json +2 -2
- package/src/error/tool-choice-violation-error.ts +6 -4
- package/src/generate-text/collect-tool-approvals.ts +10 -0
- package/src/generate-text/generate-text.ts +14 -10
- package/src/generate-text/prune-messages.ts +85 -32
- package/src/generate-text/run-tools-transformation.ts +53 -1
- package/src/generate-text/stream-text.ts +1 -0
- package/src/ui/process-ui-message-stream.ts +1 -0
package/dist/internal/index.js
CHANGED
|
@@ -164,7 +164,7 @@ function detectMediaType({
|
|
|
164
164
|
var import_provider_utils2 = require("@ai-sdk/provider-utils");
|
|
165
165
|
|
|
166
166
|
// src/version.ts
|
|
167
|
-
var VERSION = true ? "6.0.
|
|
167
|
+
var VERSION = true ? "6.0.314" : "0.0.0-test";
|
|
168
168
|
|
|
169
169
|
// src/util/download/download.ts
|
|
170
170
|
var download = async ({
|
package/dist/internal/index.mjs
CHANGED
|
@@ -603,6 +603,12 @@ It supports the following settings:
|
|
|
603
603
|
- `none`: the model must not call tools
|
|
604
604
|
- `{ type: 'tool', toolName: string (typed) }`: the model must call the specified tool
|
|
605
605
|
|
|
606
|
+
When `toolChoice` is `'none'`, the SDK prevents local tool execution even if the
|
|
607
|
+
model returns a tool call. Such responses produce a
|
|
608
|
+
[`ToolChoiceViolationError`](/docs/reference/ai-sdk-errors/ai-tool-choice-violation-error).
|
|
609
|
+
With `streamText`, tool parts are withheld from the stream, so client-side tools
|
|
610
|
+
are not invoked either. Tool call repair is skipped for rejected tool calls.
|
|
611
|
+
|
|
606
612
|
```ts highlight="18"
|
|
607
613
|
import { z } from 'zod';
|
|
608
614
|
import { generateText, tool } from 'ai';
|
|
@@ -142,6 +142,7 @@ try {
|
|
|
142
142
|
| [OpenAI](/providers/ai-sdk-providers/openai#speech-models) | `tts-1-hd` |
|
|
143
143
|
| [OpenAI](/providers/ai-sdk-providers/openai#speech-models) | `gpt-4o-mini-tts` |
|
|
144
144
|
| [Mistral](/providers/ai-sdk-providers/mistral#speech-models) | `voxtral-mini-tts-2603` |
|
|
145
|
+
| [Mistral](/providers/ai-sdk-providers/mistral#speech-models) | `voxtral-mini-tts-3-0` |
|
|
145
146
|
| [Azure Speech](/providers/ai-sdk-providers/azure#mai-voice) | `mai-voice-2.1-flash` |
|
|
146
147
|
| [Azure Speech](/providers/ai-sdk-providers/azure#mai-voice) | `mai-voice-2.1` |
|
|
147
148
|
| [Azure Speech](/providers/ai-sdk-providers/azure#mai-voice) | `mai-voice-2-flash` |
|
|
@@ -6,13 +6,15 @@ description: Learn how to handle AI_ToolChoiceViolationError
|
|
|
6
6
|
# AI_ToolChoiceViolationError
|
|
7
7
|
|
|
8
8
|
This error occurs when a model response does not satisfy an enforced tool
|
|
9
|
-
choice. It is thrown when `toolChoice` is set to `'
|
|
10
|
-
contains
|
|
11
|
-
called.
|
|
9
|
+
choice. It is thrown when `toolChoice` is set to `'none'` but the response
|
|
10
|
+
contains a tool call, when `'required'` produces no structured tool call, or
|
|
11
|
+
when a specifically selected tool was not called. With `streamText`, the error
|
|
12
|
+
is emitted in the stream when `'none'` is selected. Tool calls returned while
|
|
13
|
+
`'none'` is selected are not executed.
|
|
12
14
|
|
|
13
15
|
The error does not automatically interpret text or reasoning as an executable
|
|
14
|
-
tool call.
|
|
15
|
-
schema validation before executing any recovered call.
|
|
16
|
+
tool call. When tool calls are allowed, you can inspect `content` to implement
|
|
17
|
+
opt-in recovery, including schema validation before executing any recovered call.
|
|
16
18
|
|
|
17
19
|
## Properties
|
|
18
20
|
|
|
@@ -30,7 +32,10 @@ You can check if an error is an instance of `AI_ToolChoiceViolationError` using:
|
|
|
30
32
|
```typescript
|
|
31
33
|
import { ToolChoiceViolationError } from 'ai';
|
|
32
34
|
|
|
33
|
-
if (
|
|
35
|
+
if (
|
|
36
|
+
ToolChoiceViolationError.isInstance(error) &&
|
|
37
|
+
error.toolChoice.type !== 'none'
|
|
38
|
+
) {
|
|
34
39
|
const serializedCall = error.content.find(part => part.type === 'text')?.text;
|
|
35
40
|
|
|
36
41
|
// Parse and validate serializedCall before treating it as a tool call.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.314",
|
|
4
4
|
"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.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@opentelemetry/api": "^1.9.0",
|
|
48
|
-
"@ai-sdk/gateway": "3.0.
|
|
48
|
+
"@ai-sdk/gateway": "3.0.220",
|
|
49
49
|
"@ai-sdk/provider": "3.0.19",
|
|
50
50
|
"@ai-sdk/provider-utils": "4.0.62"
|
|
51
51
|
},
|
|
@@ -11,7 +11,7 @@ const symbol = Symbol.for(marker);
|
|
|
11
11
|
|
|
12
12
|
type EnforcedToolChoice = Extract<
|
|
13
13
|
LanguageModelV3ToolChoice,
|
|
14
|
-
{ type: 'required' } | { type: 'tool' }
|
|
14
|
+
{ type: 'none' } | { type: 'required' } | { type: 'tool' }
|
|
15
15
|
>;
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -54,9 +54,11 @@ export class ToolChoiceViolationError extends AISDKError {
|
|
|
54
54
|
provider,
|
|
55
55
|
modelId,
|
|
56
56
|
content,
|
|
57
|
-
message = toolChoice.type === '
|
|
58
|
-
? 'Model response
|
|
59
|
-
:
|
|
57
|
+
message = toolChoice.type === 'none'
|
|
58
|
+
? 'Model response contained a tool call even though tool choice was none.'
|
|
59
|
+
: toolChoice.type === 'required'
|
|
60
|
+
? 'Model response did not contain a tool call even though tool choice was required.'
|
|
61
|
+
: `Model response did not contain a call to the required tool '${toolChoice.toolName}'.`,
|
|
60
62
|
}: {
|
|
61
63
|
toolChoice: EnforcedToolChoice;
|
|
62
64
|
finishReason: FinishReason;
|
|
@@ -73,6 +73,7 @@ export function collectToolApprovals<TOOLS extends ToolSet>({
|
|
|
73
73
|
|
|
74
74
|
const approvedToolApprovals: Array<CollectedToolApprovals<TOOLS>> = [];
|
|
75
75
|
const deniedToolApprovals: Array<CollectedToolApprovals<TOOLS>> = [];
|
|
76
|
+
const collectedToolCallIds = new Set<string>();
|
|
76
77
|
|
|
77
78
|
const approvalResponses = lastMessage.content.filter(
|
|
78
79
|
part => part.type === 'tool-approval-response',
|
|
@@ -99,6 +100,15 @@ export function collectToolApprovals<TOOLS extends ToolSet>({
|
|
|
99
100
|
});
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
// Only the first response for a tool call is collected. Later responses
|
|
104
|
+
// (replays, conflicting decisions, or responses to another approval of the
|
|
105
|
+
// same call) would otherwise execute the tool twice or both execute and
|
|
106
|
+
// deny it.
|
|
107
|
+
if (collectedToolCallIds.has(toolCall.toolCallId)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
collectedToolCallIds.add(toolCall.toolCallId);
|
|
111
|
+
|
|
102
112
|
const approval: CollectedToolApprovals<TOOLS> = {
|
|
103
113
|
approvalRequest,
|
|
104
114
|
approvalResponse,
|
|
@@ -956,7 +956,12 @@ export async function generateText<
|
|
|
956
956
|
parseToolCall({
|
|
957
957
|
toolCall,
|
|
958
958
|
tools: stepToolSet,
|
|
959
|
-
|
|
959
|
+
// tool calls are rejected with tool choice none,
|
|
960
|
+
// so user callbacks must not process them:
|
|
961
|
+
repairToolCall:
|
|
962
|
+
stepToolChoice?.type === 'none'
|
|
963
|
+
? undefined
|
|
964
|
+
: repairToolCall,
|
|
960
965
|
system,
|
|
961
966
|
messages: stepInputMessages,
|
|
962
967
|
}),
|
|
@@ -964,18 +969,17 @@ export async function generateText<
|
|
|
964
969
|
);
|
|
965
970
|
|
|
966
971
|
const enforcedToolChoice =
|
|
967
|
-
stepToolChoice?.type === '
|
|
968
|
-
stepToolChoice?.type === 'tool'
|
|
969
|
-
? stepToolChoice
|
|
970
|
-
: undefined;
|
|
972
|
+
stepToolChoice?.type === 'auto' ? undefined : stepToolChoice;
|
|
971
973
|
|
|
972
974
|
if (
|
|
973
975
|
enforcedToolChoice != null &&
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
976
|
+
(enforcedToolChoice.type === 'none'
|
|
977
|
+
? stepToolCalls.length > 0
|
|
978
|
+
: !stepToolCalls.some(
|
|
979
|
+
toolCall =>
|
|
980
|
+
enforcedToolChoice.type === 'required' ||
|
|
981
|
+
toolCall.toolName === enforcedToolChoice.toolName,
|
|
982
|
+
))
|
|
979
983
|
) {
|
|
980
984
|
throw new ToolChoiceViolationError({
|
|
981
985
|
toolChoice: enforcedToolChoice,
|
|
@@ -161,42 +161,95 @@ export function pruneMessages({
|
|
|
161
161
|
return message;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
164
|
+
const keepParts = message.content.map(part => {
|
|
165
|
+
// keep non-tool parts:
|
|
166
|
+
if (
|
|
167
|
+
part.type !== 'tool-call' &&
|
|
168
|
+
part.type !== 'tool-result' &&
|
|
169
|
+
part.type !== 'tool-approval-request' &&
|
|
170
|
+
part.type !== 'tool-approval-response'
|
|
171
|
+
) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// keep parts that are associated with a tool call or approval that needs to be kept:
|
|
176
|
+
if (
|
|
177
|
+
((part.type === 'tool-call' || part.type === 'tool-result') &&
|
|
178
|
+
keptToolCallIds.has(part.toolCallId)) ||
|
|
179
|
+
((part.type === 'tool-approval-request' ||
|
|
180
|
+
part.type === 'tool-approval-response') &&
|
|
181
|
+
keptApprovalIds.has(part.approvalId))
|
|
182
|
+
) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// keep parts that are not associated with a tool that should be removed:
|
|
187
|
+
const partToolName =
|
|
188
|
+
part.type === 'tool-call' || part.type === 'tool-result'
|
|
189
|
+
? part.toolName
|
|
190
|
+
: approvalIdToToolName.get(part.approvalId);
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
toolCall.tools != null &&
|
|
194
|
+
partToolName != null &&
|
|
195
|
+
!toolCall.tools.includes(partToolName)
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Consecutive reasoning parts can belong to the same provider item.
|
|
200
|
+
// Remove the whole group when its following content was pruned.
|
|
201
|
+
if (message.role === 'assistant') {
|
|
202
|
+
for (
|
|
203
|
+
let reasoningIndex = 0;
|
|
204
|
+
reasoningIndex < message.content.length;
|
|
205
|
+
reasoningIndex++
|
|
206
|
+
) {
|
|
207
|
+
const part = message.content[reasoningIndex];
|
|
208
|
+
if (part.type !== 'reasoning') {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let reasoningEndIndex = reasoningIndex + 1;
|
|
213
|
+
while (reasoningEndIndex < message.content.length) {
|
|
214
|
+
const nextPart = message.content[reasoningEndIndex];
|
|
215
|
+
if (nextPart.type !== 'reasoning') {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
reasoningEndIndex++;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let followingPartIndex = reasoningEndIndex;
|
|
222
|
+
let hasFollowingPart = false;
|
|
223
|
+
let hasKeptFollowingPart = false;
|
|
224
|
+
|
|
225
|
+
while (followingPartIndex < message.content.length) {
|
|
226
|
+
const followingPart = message.content[followingPartIndex];
|
|
227
|
+
if (followingPart.type === 'reasoning') {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
hasFollowingPart = true;
|
|
232
|
+
hasKeptFollowingPart ||= keepParts[followingPartIndex];
|
|
233
|
+
followingPartIndex++;
|
|
175
234
|
}
|
|
176
235
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return true;
|
|
236
|
+
if (hasFollowingPart && !hasKeptFollowingPart) {
|
|
237
|
+
for (
|
|
238
|
+
let index = reasoningIndex;
|
|
239
|
+
index < reasoningEndIndex;
|
|
240
|
+
index++
|
|
241
|
+
) {
|
|
242
|
+
keepParts[index] = false;
|
|
243
|
+
}
|
|
186
244
|
}
|
|
187
245
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
toolCall.tools != null &&
|
|
196
|
-
partToolName != null &&
|
|
197
|
-
!toolCall.tools.includes(partToolName)
|
|
198
|
-
);
|
|
199
|
-
}),
|
|
246
|
+
reasoningIndex = followingPartIndex - 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
...message,
|
|
252
|
+
content: message.content.filter((_, index) => keepParts[index]),
|
|
200
253
|
} as AssistantModelMessage | ToolModelMessage;
|
|
201
254
|
});
|
|
202
255
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
LanguageModelV3Content,
|
|
2
3
|
LanguageModelV3StreamPart,
|
|
4
|
+
LanguageModelV3ToolChoice,
|
|
3
5
|
SharedV3Warning,
|
|
4
6
|
} from '@ai-sdk/provider';
|
|
5
7
|
import {
|
|
@@ -10,6 +12,7 @@ import {
|
|
|
10
12
|
} from '@ai-sdk/provider-utils';
|
|
11
13
|
import type { Tracer } from '@opentelemetry/api';
|
|
12
14
|
import { ToolCallNotFoundForApprovalError } from '../error/tool-call-not-found-for-approval-error';
|
|
15
|
+
import { ToolChoiceViolationError } from '../error/tool-choice-violation-error';
|
|
13
16
|
import type { TelemetrySettings } from '../telemetry/telemetry-settings';
|
|
14
17
|
import type {
|
|
15
18
|
FinishReason,
|
|
@@ -122,6 +125,7 @@ export type SingleRequestTextStreamPart<TOOLS extends ToolSet> =
|
|
|
122
125
|
|
|
123
126
|
export function runToolsTransformation<TOOLS extends ToolSet>({
|
|
124
127
|
tools,
|
|
128
|
+
toolChoice,
|
|
125
129
|
generatorStream,
|
|
126
130
|
tracer,
|
|
127
131
|
telemetry,
|
|
@@ -138,6 +142,7 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
|
|
|
138
142
|
onToolCallFinish,
|
|
139
143
|
}: {
|
|
140
144
|
tools: TOOLS | undefined;
|
|
145
|
+
toolChoice?: LanguageModelV3ToolChoice;
|
|
141
146
|
generatorStream: ReadableStream<LanguageModelV3StreamPart>;
|
|
142
147
|
tracer: Tracer;
|
|
143
148
|
telemetry: TelemetrySettings | undefined;
|
|
@@ -265,6 +270,13 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
|
|
|
265
270
|
// overtake pending start or delta callbacks in a downstream stream.
|
|
266
271
|
const activeToolCallToolNames = new Map<string, string>();
|
|
267
272
|
|
|
273
|
+
// Tool choice none rejects tool calls when the model call finishes.
|
|
274
|
+
// Withhold tool parts so that consumers (e.g. client-side tools) cannot
|
|
275
|
+
// act on them before the rejection, and skip user callbacks that would
|
|
276
|
+
// repair them or process their input.
|
|
277
|
+
const withholdToolParts = toolChoice?.type === 'none';
|
|
278
|
+
const withheldToolContent: Array<LanguageModelV3Content> = [];
|
|
279
|
+
|
|
268
280
|
// forward stream
|
|
269
281
|
const forwardStream = new TransformStream<
|
|
270
282
|
LanguageModelV3StreamPart,
|
|
@@ -278,6 +290,24 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
|
|
|
278
290
|
) {
|
|
279
291
|
const chunkType = chunk.type;
|
|
280
292
|
|
|
293
|
+
if (
|
|
294
|
+
withholdToolParts &&
|
|
295
|
+
(chunkType === 'tool-input-start' ||
|
|
296
|
+
chunkType === 'tool-input-delta' ||
|
|
297
|
+
chunkType === 'tool-input-end' ||
|
|
298
|
+
chunkType === 'tool-approval-request')
|
|
299
|
+
) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (
|
|
304
|
+
withholdToolParts &&
|
|
305
|
+
(chunkType === 'tool-call' || chunkType === 'tool-result')
|
|
306
|
+
) {
|
|
307
|
+
withheldToolContent.push(chunk);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
281
311
|
switch (chunkType) {
|
|
282
312
|
// forward:
|
|
283
313
|
case 'stream-start':
|
|
@@ -343,14 +373,36 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
|
|
|
343
373
|
}
|
|
344
374
|
|
|
345
375
|
case 'finish': {
|
|
376
|
+
const toolChoiceViolationError =
|
|
377
|
+
withholdToolParts &&
|
|
378
|
+
withheldToolContent.some(part => part.type === 'tool-call')
|
|
379
|
+
? new ToolChoiceViolationError({
|
|
380
|
+
toolChoice: { type: 'none' },
|
|
381
|
+
finishReason: chunk.finishReason.unified,
|
|
382
|
+
provider: model?.provider ?? 'unknown',
|
|
383
|
+
modelId: model?.modelId ?? 'unknown',
|
|
384
|
+
content: withheldToolContent,
|
|
385
|
+
})
|
|
386
|
+
: undefined;
|
|
387
|
+
|
|
346
388
|
finishChunk = {
|
|
347
389
|
type: 'finish',
|
|
348
|
-
finishReason:
|
|
390
|
+
finishReason:
|
|
391
|
+
toolChoiceViolationError == null
|
|
392
|
+
? chunk.finishReason.unified
|
|
393
|
+
: 'error',
|
|
349
394
|
rawFinishReason: chunk.finishReason.raw,
|
|
350
395
|
usage: asLanguageModelUsage(chunk.usage),
|
|
351
396
|
providerMetadata: chunk.providerMetadata,
|
|
352
397
|
};
|
|
353
398
|
|
|
399
|
+
if (toolChoiceViolationError != null) {
|
|
400
|
+
controller.enqueue({
|
|
401
|
+
type: 'error',
|
|
402
|
+
error: toolChoiceViolationError,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
354
406
|
if (isToolExecutionAllowedFinishReason(chunk.finishReason.unified)) {
|
|
355
407
|
for (const toolCall of toolCallsToExecute.splice(0)) {
|
|
356
408
|
executeToolCallAfterFinish(toolCall);
|
|
@@ -253,6 +253,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
253
253
|
|
|
254
254
|
if (part != null) {
|
|
255
255
|
part.state = options.state;
|
|
256
|
+
anyPart.type = `tool-${options.toolName}`;
|
|
256
257
|
anyPart.input = anyOptions.input;
|
|
257
258
|
anyPart.output = anyOptions.output;
|
|
258
259
|
anyPart.errorText = anyOptions.errorText;
|