ai 6.0.41 → 6.0.43

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 (31) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.d.mts +23 -13
  3. package/dist/index.d.ts +23 -13
  4. package/dist/index.js +450 -355
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +381 -287
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/internal/index.js +138 -45
  9. package/dist/internal/index.js.map +1 -1
  10. package/dist/internal/index.mjs +127 -34
  11. package/dist/internal/index.mjs.map +1 -1
  12. package/docs/02-getting-started/02-nextjs-app-router.mdx +3 -3
  13. package/docs/02-getting-started/03-nextjs-pages-router.mdx +2 -2
  14. package/docs/02-getting-started/04-svelte.mdx +2 -2
  15. package/docs/02-getting-started/05-nuxt.mdx +2 -2
  16. package/docs/02-getting-started/06-nodejs.mdx +2 -2
  17. package/docs/02-getting-started/07-expo.mdx +1 -1
  18. package/docs/02-getting-started/08-tanstack-start.mdx +2 -2
  19. package/docs/05-ai-sdk-rsc/03-generative-ui-state.mdx +1 -1
  20. package/docs/05-ai-sdk-rsc/05-streaming-values.mdx +1 -1
  21. package/docs/06-advanced/02-stopping-streams.mdx +2 -2
  22. package/docs/07-reference/02-ai-sdk-ui/02-use-completion.mdx +1 -1
  23. package/docs/08-migration-guides/26-migration-guide-5-0.mdx +3 -3
  24. package/docs/09-troubleshooting/15-abort-breaks-resumable-streams.mdx +1 -1
  25. package/docs/09-troubleshooting/21-missing-tool-results-error.mdx +82 -0
  26. package/package.json +2 -2
  27. package/src/error/index.ts +1 -1
  28. package/src/error/missing-tool-result-error.ts +28 -0
  29. package/src/prompt/__snapshots__/convert-to-language-model-prompt.validation.test.ts.snap +76 -0
  30. package/src/prompt/convert-to-language-model-prompt.ts +85 -1
  31. package/src/prompt/convert-to-language-model-prompt.validation.test.ts +138 -0
@@ -50,7 +50,7 @@ Allows you to create text completion based capabilities for your application. It
50
50
  name: 'id',
51
51
  type: 'string',
52
52
  description:
53
- 'An unique identifier for the completion. If not provided, a random one will be generated. When provided, the `useCompletion` hook with the same `id` will have shared states across components. This is useful when you have multiple components showing the same chat stream',
53
+ 'A unique identifier for the completion. If not provided, a random one will be generated. When provided, the `useCompletion` hook with the same `id` will have shared states across components. This is useful when you have multiple components showing the same chat stream',
54
54
  },
55
55
  {
56
56
  name: 'initialInput',
@@ -1386,7 +1386,7 @@ const { messages } = useChat({
1386
1386
  });
1387
1387
  ```
1388
1388
 
1389
- For a complete example of sharing chat state across components, see the [Share Chat State Across Components](/docs/cookbook/use-shared-chat-context) recipe.
1389
+ For a complete example of sharing chat state across components, see the [Share Chat State Across Components](/cookbook/next/use-shared-chat-context) recipe.
1390
1390
 
1391
1391
  #### Chat Transport Architecture
1392
1392
 
@@ -3104,7 +3104,7 @@ const result = await generateObject({
3104
3104
  }),
3105
3105
  providerOptions: {
3106
3106
  openai: {
3107
- strictSchemas: true, // default behaviour in AI SDK 4
3107
+ strictSchemas: true, // default behavior in AI SDK 4
3108
3108
  } satisfies OpenAIResponsesProviderOptions,
3109
3109
  },
3110
3110
  });
@@ -3122,7 +3122,7 @@ const result = await generateObject({
3122
3122
  }),
3123
3123
  providerOptions: {
3124
3124
  openai: {
3125
- strictJsonSchema: true, // defaults to false, opt back in to the AI SDK 4 strict behaviour
3125
+ strictJsonSchema: true, // defaults to false, opt back in to the AI SDK 4 strict behavior
3126
3126
  } satisfies OpenAIResponsesProviderOptions,
3127
3127
  },
3128
3128
  });
@@ -45,7 +45,7 @@ If you need to allow users to stop streams manually:
45
45
  ```tsx
46
46
  const { messages, sendMessage, stop } = useChat({
47
47
  id: chatId,
48
- resume: false, // Disable stream resumption (default behaviour)
48
+ resume: false, // Disable stream resumption (default behavior)
49
49
  });
50
50
  ```
51
51
 
@@ -0,0 +1,82 @@
1
+ ---
2
+ title: Missing Tool Results Error
3
+ description: How to fix the "Tool results are missing for tool calls" error when using the AI SDK.
4
+ ---
5
+
6
+ # Missing Tool Results Error
7
+
8
+ ## Issue
9
+
10
+ You encounter the error `AI_MissingToolResultsError` with a message like:
11
+
12
+ > Tool results are missing for tool calls: ...
13
+
14
+ ## Cause
15
+
16
+ This error occurs when you attempt to send a new message to the Large Language Model (LLM) while there are pending tool calls from a previous turn that have not yet been resolved.
17
+
18
+ The AI SDK core logic validates that all `tool-call` parts in the conversation history are resolved before proceeding. "Resolved" typically means:
19
+
20
+ 1. The tool has been executed and a `tool-result` has been added to the history.
21
+ 2. Or, the tool call has triggered a `tool-approval-response` (if using tool approvals).
22
+
23
+ If a tool call is found without a corresponding result or approval response, this error is thrown to prevent sending an invalid conversation history to the model.
24
+
25
+ ## Solution
26
+
27
+ Ensure that every tool call in your conversation history is properly handled.
28
+
29
+ ### 1. Provide Tool Results
30
+
31
+ For standard tool calls, ensure that you provide the output of the tool execution.
32
+
33
+ ```typescript
34
+ const messages = [
35
+ { role: 'user', content: 'What is the weather in NY?' },
36
+ {
37
+ role: 'assistant',
38
+ content: [
39
+ {
40
+ type: 'tool-call',
41
+ toolCallId: 'call_123',
42
+ toolName: 'getWeather',
43
+ args: { location: 'New York' },
44
+ },
45
+ ],
46
+ },
47
+ // You MUST include this tool message with the result:
48
+ {
49
+ role: 'tool',
50
+ content: [
51
+ {
52
+ type: 'tool-result',
53
+ toolCallId: 'call_123',
54
+ toolName: 'getWeather',
55
+ result: 'Sunny, 25°C',
56
+ },
57
+ ],
58
+ },
59
+ // Now you can add a new user message
60
+ { role: 'user', content: 'And in London?' },
61
+ ];
62
+ ```
63
+
64
+ ### 2. Handle Tool Approvals
65
+
66
+ If you are using the tool approval workflow, ensure that you include the `tool-approval-response`.
67
+
68
+ ```typescript
69
+ const messages = [
70
+ // ... assistant requests tool execution (needs approval)
71
+ {
72
+ role: 'tool',
73
+ content: [
74
+ {
75
+ type: 'tool-approval-response',
76
+ approvalId: 'approval_123',
77
+ approved: true, // or false
78
+ },
79
+ ],
80
+ },
81
+ ];
82
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "6.0.41",
3
+ "version": "6.0.43",
4
4
  "description": "AI SDK by Vercel - The AI Toolkit for TypeScript and JavaScript",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@opentelemetry/api": "1.9.0",
45
- "@ai-sdk/gateway": "3.0.16",
45
+ "@ai-sdk/gateway": "3.0.17",
46
46
  "@ai-sdk/provider": "3.0.4",
47
47
  "@ai-sdk/provider-utils": "4.0.8"
48
48
  },
@@ -19,6 +19,7 @@ export { InvalidStreamPartError } from './invalid-stream-part-error';
19
19
  export { InvalidToolApprovalError } from './invalid-tool-approval-error';
20
20
  export { InvalidToolInputError } from './invalid-tool-input-error';
21
21
  export { ToolCallNotFoundForApprovalError } from './tool-call-not-found-for-approval-error';
22
+ export { MissingToolResultsError } from './missing-tool-result-error';
22
23
  export { NoImageGeneratedError } from './no-image-generated-error';
23
24
  export { NoObjectGeneratedError } from './no-object-generated-error';
24
25
  export { NoOutputGeneratedError } from './no-output-generated-error';
@@ -27,7 +28,6 @@ export { NoSuchToolError } from './no-such-tool-error';
27
28
  export { ToolCallRepairError } from './tool-call-repair-error';
28
29
  export { UnsupportedModelVersionError } from './unsupported-model-version-error';
29
30
  export { UIMessageStreamError } from './ui-message-stream-error';
30
-
31
31
  export { InvalidDataContentError } from '../prompt/invalid-data-content-error';
32
32
  export { InvalidMessageRoleError } from '../prompt/invalid-message-role-error';
33
33
  export { MessageConversionError } from '../prompt/message-conversion-error';
@@ -0,0 +1,28 @@
1
+ import { AISDKError } from '@ai-sdk/provider';
2
+
3
+ const name = 'AI_MissingToolResultsError';
4
+ const marker = `vercel.ai.error.${name}`;
5
+ const symbol = Symbol.for(marker);
6
+
7
+ export class MissingToolResultsError extends AISDKError {
8
+ private readonly [symbol] = true;
9
+
10
+ readonly toolCallIds: string[];
11
+
12
+ constructor({ toolCallIds }: { toolCallIds: string[] }) {
13
+ super({
14
+ name,
15
+ message: `Tool result${
16
+ toolCallIds.length > 1 ? 's are' : ' is'
17
+ } missing for tool call${toolCallIds.length > 1 ? 's' : ''} ${toolCallIds.join(
18
+ ', ',
19
+ )}.`,
20
+ });
21
+
22
+ this.toolCallIds = toolCallIds;
23
+ }
24
+
25
+ static isInstance(error: unknown): error is MissingToolResultsError {
26
+ return AISDKError.hasMarker(error, marker);
27
+ }
28
+ }
@@ -0,0 +1,76 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`tool validation > should pass validation for provider-executed tools (deferred results) 1`] = `
4
+ [
5
+ {
6
+ "content": [
7
+ {
8
+ "input": {
9
+ "code": "print("hello")",
10
+ },
11
+ "providerExecuted": true,
12
+ "providerOptions": undefined,
13
+ "toolCallId": "call_1",
14
+ "toolName": "code_interpreter",
15
+ "type": "tool-call",
16
+ },
17
+ ],
18
+ "providerOptions": undefined,
19
+ "role": "assistant",
20
+ },
21
+ ]
22
+ `;
23
+
24
+ exports[`tool validation > should pass validation for tool-approval-response 1`] = `
25
+ [
26
+ {
27
+ "content": [
28
+ {
29
+ "input": {
30
+ "action": "delete_db",
31
+ },
32
+ "providerExecuted": undefined,
33
+ "providerOptions": undefined,
34
+ "toolCallId": "call_to_approve",
35
+ "toolName": "dangerous_action",
36
+ "type": "tool-call",
37
+ },
38
+ ],
39
+ "providerOptions": undefined,
40
+ "role": "assistant",
41
+ },
42
+ ]
43
+ `;
44
+
45
+ exports[`tool validation > should preserve provider-executed tool-approval-response 1`] = `
46
+ [
47
+ {
48
+ "content": [
49
+ {
50
+ "input": {
51
+ "action": "execute",
52
+ },
53
+ "providerExecuted": true,
54
+ "providerOptions": undefined,
55
+ "toolCallId": "call_provider_executed",
56
+ "toolName": "mcp_tool",
57
+ "type": "tool-call",
58
+ },
59
+ ],
60
+ "providerOptions": undefined,
61
+ "role": "assistant",
62
+ },
63
+ {
64
+ "content": [
65
+ {
66
+ "approvalId": "approval_provider",
67
+ "approved": true,
68
+ "reason": undefined,
69
+ "type": "tool-approval-response",
70
+ },
71
+ ],
72
+ "providerOptions": undefined,
73
+ "role": "tool",
74
+ },
75
+ ]
76
+ `;
@@ -29,6 +29,7 @@ import { convertToLanguageModelV3DataContent } from './data-content';
29
29
  import { InvalidMessageRoleError } from './invalid-message-role-error';
30
30
  import { StandardizedPrompt } from './standardize-prompt';
31
31
  import { asArray } from '../util/as-array';
32
+ import { MissingToolResultsError } from '../error/missing-tool-result-error';
32
33
 
33
34
  export async function convertToLanguageModelPrompt({
34
35
  prompt,
@@ -45,6 +46,38 @@ export async function convertToLanguageModelPrompt({
45
46
  supportedUrls,
46
47
  );
47
48
 
49
+ const approvalIdToToolCallId = new Map<string, string>();
50
+ for (const message of prompt.messages) {
51
+ if (message.role === 'assistant' && Array.isArray(message.content)) {
52
+ for (const part of message.content) {
53
+ if (
54
+ part.type === 'tool-approval-request' &&
55
+ 'approvalId' in part &&
56
+ 'toolCallId' in part
57
+ ) {
58
+ approvalIdToToolCallId.set(
59
+ part.approvalId as string,
60
+ part.toolCallId as string,
61
+ );
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ const approvedToolCallIds = new Set<string>();
68
+ for (const message of prompt.messages) {
69
+ if (message.role === 'tool') {
70
+ for (const part of message.content) {
71
+ if (part.type === 'tool-approval-response') {
72
+ const toolCallId = approvalIdToToolCallId.get(part.approvalId);
73
+ if (toolCallId) {
74
+ approvedToolCallIds.add(toolCallId);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+
48
81
  const messages = [
49
82
  ...(prompt.system != null
50
83
  ? typeof prompt.system === 'string'
@@ -76,7 +109,58 @@ export async function convertToLanguageModelPrompt({
76
109
  }
77
110
  }
78
111
 
79
- return combinedMessages;
112
+ const toolCallIds = new Set<string>();
113
+
114
+ for (const message of combinedMessages) {
115
+ switch (message.role) {
116
+ case 'assistant': {
117
+ for (const content of message.content) {
118
+ if (content.type === 'tool-call' && !content.providerExecuted) {
119
+ toolCallIds.add(content.toolCallId);
120
+ }
121
+ }
122
+ break;
123
+ }
124
+ case 'tool': {
125
+ for (const content of message.content) {
126
+ if (content.type === 'tool-result') {
127
+ toolCallIds.delete(content.toolCallId);
128
+ }
129
+ }
130
+ break;
131
+ }
132
+ case 'user':
133
+ case 'system':
134
+ // remove approved tool calls from the set before checking:
135
+ for (const id of approvedToolCallIds) {
136
+ toolCallIds.delete(id);
137
+ }
138
+
139
+ if (toolCallIds.size > 0) {
140
+ throw new MissingToolResultsError({
141
+ toolCallIds: Array.from(toolCallIds),
142
+ });
143
+ }
144
+ break;
145
+ }
146
+ }
147
+
148
+ // remove approved tool calls from the set before checking:
149
+ for (const id of approvedToolCallIds) {
150
+ toolCallIds.delete(id);
151
+ }
152
+
153
+ if (toolCallIds.size > 0) {
154
+ throw new MissingToolResultsError({ toolCallIds: Array.from(toolCallIds) });
155
+ }
156
+
157
+ return combinedMessages.filter(
158
+ // Filter out empty tool messages (e.g. if they only contained
159
+ // tool-approval-response parts that were removed).
160
+ // This prevents sending invalid empty messages to the provider.
161
+ // Note: provider-executed tool-approval-response parts are preserved.
162
+ message => message.role !== 'tool' || message.content.length > 0,
163
+ );
80
164
  }
81
165
 
82
166
  /**
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { convertToLanguageModelPrompt } from './convert-to-language-model-prompt';
3
+ import { MissingToolResultsError } from '../error/missing-tool-result-error';
4
+
5
+ describe('tool validation', () => {
6
+ it('should pass validation for provider-executed tools (deferred results)', async () => {
7
+ const result = await convertToLanguageModelPrompt({
8
+ prompt: {
9
+ messages: [
10
+ {
11
+ role: 'assistant',
12
+ content: [
13
+ {
14
+ type: 'tool-call',
15
+ toolCallId: 'call_1',
16
+ toolName: 'code_interpreter',
17
+ input: { code: 'print("hello")' },
18
+ providerExecuted: true,
19
+ },
20
+ ],
21
+ },
22
+ ],
23
+ },
24
+ supportedUrls: {},
25
+ download: undefined,
26
+ });
27
+
28
+ expect(result).toMatchSnapshot();
29
+ });
30
+
31
+ it('should pass validation for tool-approval-response', async () => {
32
+ const result = await convertToLanguageModelPrompt({
33
+ prompt: {
34
+ messages: [
35
+ {
36
+ role: 'assistant',
37
+ content: [
38
+ {
39
+ type: 'tool-call',
40
+ toolCallId: 'call_to_approve',
41
+ toolName: 'dangerous_action',
42
+ input: { action: 'delete_db' },
43
+ },
44
+ {
45
+ type: 'tool-approval-request',
46
+ toolCallId: 'call_to_approve',
47
+ approvalId: 'approval_123',
48
+ toolName: 'dangerous_action',
49
+ input: { action: 'delete_db' },
50
+ } as any,
51
+ ],
52
+ },
53
+ {
54
+ role: 'tool',
55
+ content: [
56
+ {
57
+ type: 'tool-approval-response',
58
+ approvalId: 'approval_123',
59
+ approved: true,
60
+ } as any,
61
+ ],
62
+ },
63
+ ],
64
+ },
65
+ supportedUrls: {},
66
+ download: undefined,
67
+ });
68
+
69
+ expect(result).toMatchSnapshot();
70
+ });
71
+
72
+ it('should preserve provider-executed tool-approval-response', async () => {
73
+ const result = await convertToLanguageModelPrompt({
74
+ prompt: {
75
+ messages: [
76
+ {
77
+ role: 'assistant',
78
+ content: [
79
+ {
80
+ type: 'tool-call',
81
+ toolCallId: 'call_provider_executed',
82
+ toolName: 'mcp_tool',
83
+ input: { action: 'execute' },
84
+ providerExecuted: true,
85
+ },
86
+ {
87
+ type: 'tool-approval-request',
88
+ toolCallId: 'call_provider_executed',
89
+ approvalId: 'approval_provider',
90
+ toolName: 'mcp_tool',
91
+ input: { action: 'execute' },
92
+ } as any,
93
+ ],
94
+ },
95
+ {
96
+ role: 'tool',
97
+ content: [
98
+ {
99
+ type: 'tool-approval-response',
100
+ approvalId: 'approval_provider',
101
+ approved: true,
102
+ providerExecuted: true,
103
+ },
104
+ ],
105
+ },
106
+ ],
107
+ },
108
+ supportedUrls: {},
109
+ download: undefined,
110
+ });
111
+
112
+ expect(result).toMatchSnapshot();
113
+ });
114
+
115
+ it('should throw error for actual missing results', async () => {
116
+ await expect(async () => {
117
+ await convertToLanguageModelPrompt({
118
+ prompt: {
119
+ messages: [
120
+ {
121
+ role: 'assistant',
122
+ content: [
123
+ {
124
+ type: 'tool-call',
125
+ toolCallId: 'call_missing_result',
126
+ toolName: 'regular_tool',
127
+ input: {},
128
+ },
129
+ ],
130
+ },
131
+ ],
132
+ },
133
+ supportedUrls: {},
134
+ download: undefined,
135
+ });
136
+ }).rejects.toThrow(MissingToolResultsError);
137
+ });
138
+ });