ai 7.0.98 → 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.
@@ -97,6 +97,7 @@ The open-source community has created the following providers:
97
97
  - [Crusoe Provider](/providers/community-providers/crusoe) (`crusoe-ai-provider`)
98
98
  - [Neon AI Gateway Provider](/providers/community-providers/neon-ai-gateway) (`@neon/ai-sdk-provider`)
99
99
  - [Interfaze Provider](/providers/community-providers/interfaze) (`@interfaze-ai/ai-sdk`)
100
+ - [Telnyx Provider](/providers/community-providers/telnyx) (`@telnyx/ai-sdk-provider`)
100
101
 
101
102
  ## Self-Hosted Models
102
103
 
@@ -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.
@@ -81,7 +81,7 @@ const { embeddings } = await embedMany({
81
81
  type: 'number',
82
82
  isOptional: true,
83
83
  description:
84
- 'Maximum number of concurrent requests to the provider. Default: Infinity.',
84
+ 'Maximum number of concurrent requests when a request is split into multiple model calls. Must be greater than 0 when chunking is active and the model supports parallel calls; invalid values throw AI_InvalidArgumentError. Default: Infinity.',
85
85
  },
86
86
  {
87
87
  name: 'runtimeContext',
@@ -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:
@@ -7,6 +7,11 @@ description: Learn how to fix AI_InvalidArgumentError
7
7
 
8
8
  This error occurs when an invalid argument was provided.
9
9
 
10
+ For example, `getTextFromDataUrl` throws this error when its `dataUrl` argument
11
+ is malformed or cannot be decoded. Utility validation that is used by higher
12
+ level APIs also uses this error, so you can handle invalid arguments with one
13
+ stable error guard.
14
+
10
15
  ## Properties
11
16
 
12
17
  - `parameter`: The name of the parameter that is invalid
@@ -21,6 +26,6 @@ You can check if an error is an instance of `AI_InvalidArgumentError` using:
21
26
  import { InvalidArgumentError } from 'ai';
22
27
 
23
28
  if (InvalidArgumentError.isInstance(error)) {
24
- // Handle the error
29
+ console.error(`Invalid ${error.parameter}:`, error.message);
25
30
  }
26
31
  ```
@@ -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.98",
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,14 +42,14 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.79",
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
  },
49
49
  "devDependencies": {
50
50
  "@ai-sdk/amazon-bedrock": "5.0.82",
51
- "@ai-sdk/deepseek": "3.0.43",
52
- "@ai-sdk/google": "4.0.68",
51
+ "@ai-sdk/deepseek": "3.0.44",
52
+ "@ai-sdk/google": "4.0.69",
53
53
  "@ai-sdk/groq": "4.0.41",
54
54
  "@ai-sdk/huggingface": "2.0.48",
55
55
  "@ai-sdk/moonshotai": "3.0.49",
@@ -40,7 +40,9 @@ const originalGenerateCallId = createIdGenerator({
40
40
  * @param abortSignal - An optional abort signal that can be used to cancel the call.
41
41
  * @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
42
42
  *
43
- * @param maxParallelCalls - Maximum number of concurrent requests. Default: Infinity.
43
+ * @param maxParallelCalls - Maximum number of concurrent requests when a request is split into
44
+ * multiple model calls. Must be greater than 0 when the model supports parallel calls.
45
+ * Default: Infinity.
44
46
  *
45
47
  * @param telemetry - Optional telemetry configuration.
46
48
  * @param runtimeContext - User-defined runtime context passed to callbacks and, when explicitly included, telemetry.
@@ -121,7 +123,8 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
121
123
  providerOptions?: ProviderOptions;
122
124
 
123
125
  /**
124
- * Maximum number of concurrent requests.
126
+ * Maximum number of concurrent requests when a request is split into multiple model calls.
127
+ * Must be greater than 0 when the model supports parallel calls.
125
128
  *
126
129
  * @default Infinity
127
130
  */
@@ -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);
@@ -1,3 +1,5 @@
1
+ import { InvalidArgumentError } from '../error/invalid-argument-error';
2
+
1
3
  // atob needs to be invoked as a function call, not as a method call.
2
4
  // Otherwise Cloudflare will throw a
3
5
  // "TypeError: Illegal invocation: function called with incorrect this reference"
@@ -5,18 +7,28 @@ const { atob } = globalThis;
5
7
 
6
8
  /**
7
9
  * Converts a data URL of type text/* to a text string.
10
+ *
11
+ * @throws {InvalidArgumentError} If the data URL is malformed or cannot be decoded.
8
12
  */
9
13
  export function getTextFromDataUrl(dataUrl: string): string {
10
14
  const [header, base64Content] = dataUrl.split(',');
11
15
  const mediaType = header.split(';')[0].split(':')[1];
12
16
 
13
17
  if (mediaType == null || base64Content == null) {
14
- throw new Error('Invalid data URL format');
18
+ throw new InvalidArgumentError({
19
+ parameter: 'dataUrl',
20
+ value: dataUrl,
21
+ message: 'Invalid data URL format',
22
+ });
15
23
  }
16
24
 
17
25
  try {
18
26
  return atob(base64Content);
19
27
  } catch {
20
- throw new Error(`Error decoding data URL`);
28
+ throw new InvalidArgumentError({
29
+ parameter: 'dataUrl',
30
+ value: dataUrl,
31
+ message: 'Error decoding data URL',
32
+ });
21
33
  }
22
34
  }
@@ -1,3 +1,5 @@
1
+ import { InvalidArgumentError } from '../error/invalid-argument-error';
2
+
1
3
  /**
2
4
  * Splits an array into chunks of a specified size.
3
5
  *
@@ -5,10 +7,15 @@
5
7
  * @param {T[]} array - The array to split.
6
8
  * @param {number} chunkSize - The size of each chunk.
7
9
  * @returns {T[][]} - A new array containing the chunks.
10
+ * @throws {InvalidArgumentError} If the chunk size is not greater than 0.
8
11
  */
9
12
  export function splitArray<T>(array: T[], chunkSize: number): T[][] {
10
13
  if (chunkSize <= 0) {
11
- throw new Error('chunkSize must be greater than 0');
14
+ throw new InvalidArgumentError({
15
+ parameter: 'chunkSize',
16
+ value: chunkSize,
17
+ message: 'chunkSize must be greater than 0',
18
+ });
12
19
  }
13
20
 
14
21
  const result = [];