ai 7.0.99 → 7.0.100
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 +8 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +76 -21
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.js.map +1 -1
- package/docs/02-getting-started/07-expo.mdx +7 -0
- package/docs/03-ai-sdk-core/55-testing.mdx +75 -0
- package/docs/04-ai-sdk-ui/21-error-handling.mdx +28 -2
- package/docs/07-reference/05-ai-sdk-errors/ai-api-call-error.mdx +4 -0
- package/docs/07-reference/05-ai-sdk-errors/ai-ui-message-stream-error.mdx +6 -2
- package/package.json +2 -2
- package/src/error/ui-message-stream-error.ts +5 -2
- package/src/ui/call-completion-api.ts +22 -6
- package/src/ui/chat.ts +16 -5
- package/src/ui/convert-file-list-to-file-ui-parts.ts +5 -1
- package/src/ui/create-ui-api-call-error.ts +23 -0
- package/src/ui/http-chat-transport.ts +18 -8
|
@@ -103,6 +103,13 @@ Let's take a look at what is happening in this code:
|
|
|
103
103
|
|
|
104
104
|
This API route creates a POST request endpoint at `/api/chat`.
|
|
105
105
|
|
|
106
|
+
<Note>
|
|
107
|
+
If a deployed Expo API Route buffers the response despite the streaming
|
|
108
|
+
headers above, test the same handler without the deployment adapter to isolate
|
|
109
|
+
the cause. As a workaround, host the chat endpoint in a separate Vercel
|
|
110
|
+
Function and set `EXPO_PUBLIC_API_BASE_URL` to that function's base URL.
|
|
111
|
+
</Note>
|
|
112
|
+
|
|
106
113
|
## Choosing a Provider
|
|
107
114
|
|
|
108
115
|
The AI SDK supports dozens of model providers through [first-party](/providers/ai-sdk-providers), [OpenAI-compatible](/providers/openai-compatible-providers), and [ community ](/providers/community-providers) packages.
|
|
@@ -179,6 +179,81 @@ const result = streamText({
|
|
|
179
179
|
});
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
+
### ToolLoopAgent
|
|
183
|
+
|
|
184
|
+
You can provide a sequence of mock responses to test an agent that calls a tool
|
|
185
|
+
and continues to a final response:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { ToolLoopAgent, tool } from 'ai';
|
|
189
|
+
import { MockLanguageModelV4 } from 'ai/test';
|
|
190
|
+
import { expect, it } from 'vitest';
|
|
191
|
+
import { z } from 'zod';
|
|
192
|
+
|
|
193
|
+
it('executes a tool and continues the loop', async () => {
|
|
194
|
+
const weatherRequests: string[] = [];
|
|
195
|
+
const usage = {
|
|
196
|
+
inputTokens: {
|
|
197
|
+
total: 10,
|
|
198
|
+
noCache: 10,
|
|
199
|
+
cacheRead: undefined,
|
|
200
|
+
cacheWrite: undefined,
|
|
201
|
+
},
|
|
202
|
+
outputTokens: {
|
|
203
|
+
total: 5,
|
|
204
|
+
text: 5,
|
|
205
|
+
reasoning: undefined,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const model = new MockLanguageModelV4({
|
|
210
|
+
doGenerate: [
|
|
211
|
+
{
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: 'tool-call',
|
|
215
|
+
toolCallId: 'call-1',
|
|
216
|
+
toolName: 'weather',
|
|
217
|
+
input: '{"city":"San Francisco"}',
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
finishReason: { unified: 'tool-calls', raw: undefined },
|
|
221
|
+
usage,
|
|
222
|
+
warnings: [],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
content: [{ type: 'text', text: 'It is 72°F in San Francisco.' }],
|
|
226
|
+
finishReason: { unified: 'stop', raw: undefined },
|
|
227
|
+
usage,
|
|
228
|
+
warnings: [],
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const agent = new ToolLoopAgent({
|
|
234
|
+
model,
|
|
235
|
+
tools: {
|
|
236
|
+
weather: tool({
|
|
237
|
+
description: 'Get the weather for a city.',
|
|
238
|
+
inputSchema: z.object({ city: z.string() }),
|
|
239
|
+
execute: async ({ city }) => {
|
|
240
|
+
weatherRequests.push(city);
|
|
241
|
+
return { temperature: 72 };
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const result = await agent.generate({
|
|
248
|
+
prompt: 'What is the weather in San Francisco?',
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(weatherRequests).toEqual(['San Francisco']);
|
|
252
|
+
expect(result.text).toBe('It is 72°F in San Francisco.');
|
|
253
|
+
expect(model.doGenerateCalls).toHaveLength(2);
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
182
257
|
### Simulate UI Message Stream Responses
|
|
183
258
|
|
|
184
259
|
You can also simulate [UI Message Stream](/docs/ai-sdk-ui/stream-protocol#ui-message-stream-example) responses for testing,
|
|
@@ -169,8 +169,23 @@ export default function Chat() {
|
|
|
169
169
|
Errors can be processed by passing an [`onError`](/docs/reference/ai-sdk-ui/use-chat#on-error) callback function as an option to the [`useChat`](/docs/reference/ai-sdk-ui/use-chat) or [`useCompletion`](/docs/reference/ai-sdk-ui/use-completion) hooks.
|
|
170
170
|
The callback function receives an error object as an argument.
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
AI SDK-created client errors use exported error classes with marker-based
|
|
173
|
+
`.isInstance()` guards:
|
|
174
|
+
|
|
175
|
+
| Error class | AI SDK UI failure |
|
|
176
|
+
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
177
|
+
| [`APICallError`](/docs/reference/ai-sdk-errors/ai-api-call-error) | A chat transport or completion request returns a non-successful HTTP response. |
|
|
178
|
+
| [`EmptyResponseBodyError`](/docs/reference/ai-sdk-errors/ai-empty-response-body-error) | A successful chat transport or completion response has no body. |
|
|
179
|
+
| [`UIMessageStreamError`](/docs/reference/ai-sdk-errors/ai-ui-message-stream-error) | A completion data stream reports an error or a UI message stream contains invalid chunks. |
|
|
180
|
+
| [`InvalidArgumentError`](/docs/reference/ai-sdk-errors/ai-invalid-argument-error) | An invalid stream protocol or message ID is used. |
|
|
181
|
+
| [`UnsupportedFunctionalityError`](/docs/reference/ai-sdk-errors/ai-unsupported-functionality-error) | A `FileList` is used in an environment that does not support it. |
|
|
182
|
+
|
|
183
|
+
Errors thrown by custom fetch implementations, callbacks, and stream parsers
|
|
184
|
+
continue to propagate unchanged.
|
|
185
|
+
|
|
186
|
+
```tsx file="app/page.tsx" highlight="2,9-15"
|
|
173
187
|
import { useChat } from '@ai-sdk/react';
|
|
188
|
+
import { APICallError, EmptyResponseBodyError } from 'ai';
|
|
174
189
|
|
|
175
190
|
export default function Page() {
|
|
176
191
|
const {
|
|
@@ -178,12 +193,23 @@ export default function Page() {
|
|
|
178
193
|
} = useChat({
|
|
179
194
|
// handle error:
|
|
180
195
|
onError: error => {
|
|
181
|
-
|
|
196
|
+
if (APICallError.isInstance(error)) {
|
|
197
|
+
console.error('Request failed with status:', error.statusCode);
|
|
198
|
+
} else if (EmptyResponseBodyError.isInstance(error)) {
|
|
199
|
+
console.error('The server returned no response body.');
|
|
200
|
+
} else {
|
|
201
|
+
console.error(error);
|
|
202
|
+
}
|
|
182
203
|
},
|
|
183
204
|
});
|
|
184
205
|
}
|
|
185
206
|
```
|
|
186
207
|
|
|
208
|
+
For AI SDK UI requests, `APICallError.requestBodyValues` is `undefined` so
|
|
209
|
+
prompts and messages are not copied into client-facing error objects. The
|
|
210
|
+
response text remains available as `message` and `responseBody`; display a
|
|
211
|
+
generic message to users to avoid leaking server information.
|
|
212
|
+
|
|
187
213
|
### Injecting Errors for Testing
|
|
188
214
|
|
|
189
215
|
You might want to create errors for testing.
|
|
@@ -18,6 +18,10 @@ This error occurs when an API call fails.
|
|
|
18
18
|
- `data`: Any additional data associated with the error (optional)
|
|
19
19
|
- `cause`: The underlying error that caused the API call to fail (optional)
|
|
20
20
|
|
|
21
|
+
When this error is created for an AI SDK UI chat transport or completion
|
|
22
|
+
request, `requestBodyValues` is `undefined` so prompts and messages are not
|
|
23
|
+
copied into the client-facing error object.
|
|
24
|
+
|
|
21
25
|
## Checking for this Error
|
|
22
26
|
|
|
23
27
|
You can check if an error is an instance of `AI_APICallError` using:
|
|
@@ -5,10 +5,12 @@ description: Learn how to fix AI_UIMessageStreamError
|
|
|
5
5
|
|
|
6
6
|
# AI_UIMessageStreamError
|
|
7
7
|
|
|
8
|
-
This error occurs when a UI message stream
|
|
8
|
+
This error occurs when a UI message stream reports an error or contains invalid
|
|
9
|
+
or out-of-sequence chunks.
|
|
9
10
|
|
|
10
11
|
Common causes:
|
|
11
12
|
|
|
13
|
+
- Receiving an `error` chunk in a completion data stream
|
|
12
14
|
- Receiving a `text-delta` chunk without a preceding `text-start` chunk
|
|
13
15
|
- Receiving a `text-end` chunk without a preceding `text-start` chunk
|
|
14
16
|
- Receiving a `reasoning-delta` chunk without a preceding `reasoning-start` chunk
|
|
@@ -21,7 +23,9 @@ This error often surfaces when an upstream request fails **before any tokens are
|
|
|
21
23
|
## Properties
|
|
22
24
|
|
|
23
25
|
- `chunkType`: The type of chunk that caused the error (e.g., `text-delta`, `reasoning-end`, `tool-input-delta`)
|
|
24
|
-
- `chunkId`: The ID associated with the failing chunk (part ID or toolCallId)
|
|
26
|
+
- `chunkId`: The ID associated with the failing chunk (part ID or toolCallId).
|
|
27
|
+
This is an empty string for chunks, such as completion `error` chunks, that do
|
|
28
|
+
not have an ID.
|
|
25
29
|
- `message`: The error message with details about what went wrong
|
|
26
30
|
|
|
27
31
|
## Checking for this Error
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.100",
|
|
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,7 +42,7 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@ai-sdk/gateway": "4.0.
|
|
45
|
+
"@ai-sdk/gateway": "4.0.81",
|
|
46
46
|
"@ai-sdk/provider": "4.0.14",
|
|
47
47
|
"@ai-sdk/provider-utils": "5.0.40"
|
|
48
48
|
},
|
|
@@ -5,9 +5,11 @@ const marker = `vercel.ai.error.${name}`;
|
|
|
5
5
|
const symbol = Symbol.for(marker);
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Error thrown when a UI message stream
|
|
8
|
+
* Error thrown when a UI message stream reports an error or contains invalid
|
|
9
|
+
* or out-of-sequence chunks.
|
|
9
10
|
*
|
|
10
11
|
* This typically occurs when:
|
|
12
|
+
* - An error chunk is received
|
|
11
13
|
* - A delta chunk is received without a corresponding start chunk
|
|
12
14
|
* - An end chunk is received without a corresponding start chunk
|
|
13
15
|
* - A tool invocation is not found for the given toolCallId
|
|
@@ -23,7 +25,8 @@ export class UIMessageStreamError extends AISDKError {
|
|
|
23
25
|
readonly chunkType: string;
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
|
-
* The ID associated with the failing chunk (part ID or toolCallId)
|
|
28
|
+
* The ID associated with the failing chunk (part ID or toolCallId), or an
|
|
29
|
+
* empty string when the chunk does not have an ID.
|
|
27
30
|
*/
|
|
28
31
|
readonly chunkId: string;
|
|
29
32
|
|
|
@@ -4,11 +4,15 @@ import {
|
|
|
4
4
|
getRuntimeEnvironmentUserAgent,
|
|
5
5
|
type ParseResult,
|
|
6
6
|
} from '@ai-sdk/provider-utils';
|
|
7
|
+
import { EmptyResponseBodyError } from '@ai-sdk/provider';
|
|
8
|
+
import { InvalidArgumentError } from '../error/invalid-argument-error';
|
|
9
|
+
import { UIMessageStreamError } from '../error/ui-message-stream-error';
|
|
7
10
|
import {
|
|
8
11
|
uiMessageChunkSchema,
|
|
9
12
|
type UIMessageChunk,
|
|
10
13
|
} from '../ui-message-stream/ui-message-chunks';
|
|
11
14
|
import { consumeStream } from '../util/consume-stream';
|
|
15
|
+
import { createUIApiCallError } from './create-ui-api-call-error';
|
|
12
16
|
import { processTextStream } from './process-text-stream';
|
|
13
17
|
import { VERSION } from '../version';
|
|
14
18
|
|
|
@@ -75,13 +79,17 @@ export async function callCompletionApi({
|
|
|
75
79
|
});
|
|
76
80
|
|
|
77
81
|
if (!response.ok) {
|
|
78
|
-
throw
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
throw await createUIApiCallError({
|
|
83
|
+
response,
|
|
84
|
+
url: api,
|
|
85
|
+
fallbackMessage: 'Failed to fetch the chat response.',
|
|
86
|
+
});
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
if (!response.body) {
|
|
84
|
-
throw new
|
|
90
|
+
throw new EmptyResponseBodyError({
|
|
91
|
+
message: 'The response body is empty.',
|
|
92
|
+
});
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
let result = '';
|
|
@@ -114,7 +122,11 @@ export async function callCompletionApi({
|
|
|
114
122
|
result += streamPart.delta;
|
|
115
123
|
setCompletion(result);
|
|
116
124
|
} else if (streamPart.type === 'error') {
|
|
117
|
-
throw new
|
|
125
|
+
throw new UIMessageStreamError({
|
|
126
|
+
chunkType: 'error',
|
|
127
|
+
chunkId: '',
|
|
128
|
+
message: streamPart.errorText,
|
|
129
|
+
});
|
|
118
130
|
}
|
|
119
131
|
},
|
|
120
132
|
}),
|
|
@@ -127,7 +139,11 @@ export async function callCompletionApi({
|
|
|
127
139
|
}
|
|
128
140
|
default: {
|
|
129
141
|
const exhaustiveCheck: never = streamProtocol;
|
|
130
|
-
throw new
|
|
142
|
+
throw new InvalidArgumentError({
|
|
143
|
+
parameter: 'streamProtocol',
|
|
144
|
+
value: exhaustiveCheck,
|
|
145
|
+
message: `Unknown stream protocol: ${exhaustiveCheck}`,
|
|
146
|
+
});
|
|
131
147
|
}
|
|
132
148
|
}
|
|
133
149
|
|
package/src/ui/chat.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type IdGenerator,
|
|
5
5
|
type InferSchema,
|
|
6
6
|
} from '@ai-sdk/provider-utils';
|
|
7
|
+
import { InvalidArgumentError } from '../error/invalid-argument-error';
|
|
7
8
|
import type { FinishReason } from '../types/language-model';
|
|
8
9
|
import type { UIMessageChunk } from '../ui-message-stream/ui-message-chunks';
|
|
9
10
|
import { consumeStream } from '../util/consume-stream';
|
|
@@ -406,13 +407,19 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
406
407
|
);
|
|
407
408
|
|
|
408
409
|
if (messageIndex === -1) {
|
|
409
|
-
throw new
|
|
410
|
+
throw new InvalidArgumentError({
|
|
411
|
+
parameter: 'message.messageId',
|
|
412
|
+
value: message.messageId,
|
|
413
|
+
message: `message with id ${message.messageId} not found`,
|
|
414
|
+
});
|
|
410
415
|
}
|
|
411
416
|
|
|
412
417
|
if (this.state.messages[messageIndex].role !== 'user') {
|
|
413
|
-
throw new
|
|
414
|
-
|
|
415
|
-
|
|
418
|
+
throw new InvalidArgumentError({
|
|
419
|
+
parameter: 'message.messageId',
|
|
420
|
+
value: message.messageId,
|
|
421
|
+
message: `message with id ${message.messageId} is not a user message`,
|
|
422
|
+
});
|
|
416
423
|
}
|
|
417
424
|
|
|
418
425
|
// remove all messages after the message with the given id
|
|
@@ -457,7 +464,11 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
457
464
|
: this.state.messages.findIndex(message => message.id === messageId);
|
|
458
465
|
|
|
459
466
|
if (messageIndex === -1) {
|
|
460
|
-
throw new
|
|
467
|
+
throw new InvalidArgumentError({
|
|
468
|
+
parameter: 'messageId',
|
|
469
|
+
value: messageId,
|
|
470
|
+
message: `message ${messageId} not found`,
|
|
471
|
+
});
|
|
461
472
|
}
|
|
462
473
|
|
|
463
474
|
// set the messages to the message before the assistant message
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UnsupportedFunctionalityError } from '@ai-sdk/provider';
|
|
1
2
|
import type { FileUIPart } from './ui-messages';
|
|
2
3
|
|
|
3
4
|
export async function convertFileListToFileUIParts(
|
|
@@ -9,7 +10,10 @@ export async function convertFileListToFileUIParts(
|
|
|
9
10
|
|
|
10
11
|
// React-native doesn't have a FileList global:
|
|
11
12
|
if (!globalThis.FileList || !(files instanceof globalThis.FileList)) {
|
|
12
|
-
throw new
|
|
13
|
+
throw new UnsupportedFunctionalityError({
|
|
14
|
+
functionality: 'FileList',
|
|
15
|
+
message: 'FileList is not supported in the current environment',
|
|
16
|
+
});
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
return Promise.all(
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { APICallError } from '@ai-sdk/provider';
|
|
2
|
+
|
|
3
|
+
export async function createUIApiCallError({
|
|
4
|
+
response,
|
|
5
|
+
url,
|
|
6
|
+
fallbackMessage,
|
|
7
|
+
}: {
|
|
8
|
+
response: Response;
|
|
9
|
+
url: string;
|
|
10
|
+
fallbackMessage: string;
|
|
11
|
+
}) {
|
|
12
|
+
const responseBody = await response.text();
|
|
13
|
+
|
|
14
|
+
return new APICallError({
|
|
15
|
+
message: responseBody || fallbackMessage,
|
|
16
|
+
url,
|
|
17
|
+
// UI requests can contain prompts, messages, and other sensitive values.
|
|
18
|
+
// Keep them out of client-facing error objects.
|
|
19
|
+
requestBodyValues: undefined,
|
|
20
|
+
statusCode: response.status,
|
|
21
|
+
responseBody,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
type FetchFunction,
|
|
5
5
|
type Resolvable,
|
|
6
6
|
} from '@ai-sdk/provider-utils';
|
|
7
|
+
import { EmptyResponseBodyError } from '@ai-sdk/provider';
|
|
7
8
|
import type { UIMessageChunk } from '../ui-message-stream/ui-message-chunks';
|
|
8
9
|
import type { ChatTransport } from './chat-transport';
|
|
10
|
+
import { createUIApiCallError } from './create-ui-api-call-error';
|
|
9
11
|
import type { UIMessage } from './ui-messages';
|
|
10
12
|
|
|
11
13
|
export type PrepareSendMessagesRequest<UI_MESSAGE extends UIMessage> = (
|
|
@@ -200,13 +202,17 @@ export abstract class HttpChatTransport<
|
|
|
200
202
|
});
|
|
201
203
|
|
|
202
204
|
if (!response.ok) {
|
|
203
|
-
throw
|
|
204
|
-
|
|
205
|
-
|
|
205
|
+
throw await createUIApiCallError({
|
|
206
|
+
response,
|
|
207
|
+
url: api,
|
|
208
|
+
fallbackMessage: 'Failed to fetch the chat response.',
|
|
209
|
+
});
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
if (!response.body) {
|
|
209
|
-
throw new
|
|
213
|
+
throw new EmptyResponseBodyError({
|
|
214
|
+
message: 'The response body is empty.',
|
|
215
|
+
});
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
return this.processResponseStream(response.body);
|
|
@@ -256,13 +262,17 @@ export abstract class HttpChatTransport<
|
|
|
256
262
|
}
|
|
257
263
|
|
|
258
264
|
if (!response.ok) {
|
|
259
|
-
throw
|
|
260
|
-
|
|
261
|
-
|
|
265
|
+
throw await createUIApiCallError({
|
|
266
|
+
response,
|
|
267
|
+
url: api,
|
|
268
|
+
fallbackMessage: 'Failed to fetch the chat response.',
|
|
269
|
+
});
|
|
262
270
|
}
|
|
263
271
|
|
|
264
272
|
if (!response.body) {
|
|
265
|
-
throw new
|
|
273
|
+
throw new EmptyResponseBodyError({
|
|
274
|
+
message: 'The response body is empty.',
|
|
275
|
+
});
|
|
266
276
|
}
|
|
267
277
|
|
|
268
278
|
return this.processResponseStream(response.body);
|