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.
@@ -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
- ```tsx file="app/page.tsx" highlight="6-9"
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
- console.error(error);
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 contains invalid or out-of-sequence chunks.
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.99",
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.80",
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 contains invalid or out-of-sequence chunks.
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 new Error(
79
- (await response.text()) || 'Failed to fetch the chat response.',
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 Error('The response body is empty.');
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 Error(streamPart.errorText);
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 Error(`Unknown stream protocol: ${exhaustiveCheck}`);
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 Error(`message with id ${message.messageId} not found`);
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 Error(
414
- `message with id ${message.messageId} is not a user message`,
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 Error(`message ${messageId} not found`);
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 Error('FileList is not supported in the current environment');
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 new Error(
204
- (await response.text()) || 'Failed to fetch the chat response.',
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 Error('The response body is empty.');
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 new Error(
260
- (await response.text()) || 'Failed to fetch the chat response.',
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 Error('The response body is empty.');
273
+ throw new EmptyResponseBodyError({
274
+ message: 'The response body is empty.',
275
+ });
266
276
  }
267
277
 
268
278
  return this.processResponseStream(response.body);