ai 6.0.34 → 6.0.36

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.
@@ -153,7 +153,7 @@ var import_provider_utils2 = require("@ai-sdk/provider-utils");
153
153
  var import_provider_utils3 = require("@ai-sdk/provider-utils");
154
154
 
155
155
  // src/version.ts
156
- var VERSION = true ? "6.0.34" : "0.0.0-test";
156
+ var VERSION = true ? "6.0.36" : "0.0.0-test";
157
157
 
158
158
  // src/util/download/download.ts
159
159
  var download = async ({ url }) => {
@@ -128,7 +128,7 @@ import {
128
128
  } from "@ai-sdk/provider-utils";
129
129
 
130
130
  // src/version.ts
131
- var VERSION = true ? "6.0.34" : "0.0.0-test";
131
+ var VERSION = true ? "6.0.36" : "0.0.0-test";
132
132
 
133
133
  // src/util/download/download.ts
134
134
  var download = async ({ url }) => {
@@ -24,7 +24,7 @@ and [`streamText`](/docs/reference/ai-sdk-core/stream-text) by passing one or mo
24
24
  A tool consists of three properties:
25
25
 
26
26
  - **`description`**: An optional description of the tool that can influence when the tool is picked.
27
- - **`inputSchema`**: A [Zod schema](/docs/foundations/tools#schema-specification-and-validation-with-zod) or a [JSON schema](/docs/reference/ai-sdk-core/json-schema) that defines the input required for the tool to run. The schema is consumed by the LLM, and also used to validate the LLM tool calls.
27
+ - **`inputSchema`**: A [Zod schema](/docs/reference/ai-sdk-core/zod-schema) or a [JSON schema](/docs/reference/ai-sdk-core/json-schema) that defines the input required for the tool to run. The schema is consumed by the LLM, and also used to validate the LLM tool calls.
28
28
  - **`execute`**: An optional async function that is called with the arguments from the tool call.
29
29
 
30
30
  <Note>
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: AI_UIMessageStreamError
3
+ description: Learn how to fix AI_UIMessageStreamError
4
+ ---
5
+
6
+ # AI_UIMessageStreamError
7
+
8
+ This error occurs when a UI message stream contains invalid or out-of-sequence chunks.
9
+
10
+ Common causes:
11
+
12
+ - Receiving a `text-delta` chunk without a preceding `text-start` chunk
13
+ - Receiving a `text-end` chunk without a preceding `text-start` chunk
14
+ - Receiving a `reasoning-delta` chunk without a preceding `reasoning-start` chunk
15
+ - Receiving a `reasoning-end` chunk without a preceding `reasoning-start` chunk
16
+ - Receiving a `tool-input-delta` chunk without a preceding `tool-input-start` chunk
17
+ - Attempting to access a tool invocation that doesn't exist
18
+
19
+ This error often surfaces when an upstream request fails **before any tokens are streamed** and a custom transport tries to write an inline error message to the UI stream without the proper start chunk.
20
+
21
+ ## Properties
22
+
23
+ - `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)
25
+ - `message`: The error message with details about what went wrong
26
+
27
+ ## Checking for this Error
28
+
29
+ You can check if an error is an instance of `AI_UIMessageStreamError` using:
30
+
31
+ ```typescript
32
+ import { UIMessageStreamError } from 'ai';
33
+
34
+ if (UIMessageStreamError.isInstance(error)) {
35
+ console.log('Chunk type:', error.chunkType);
36
+ console.log('Chunk ID:', error.chunkId);
37
+ // Handle the error
38
+ }
39
+ ```
40
+
41
+ ## Common Solutions
42
+
43
+ 1. **Ensure proper chunk ordering**: Always send a `*-start` chunk before any `*-delta` or `*-end` chunks for the same ID:
44
+
45
+ ```typescript
46
+ // Correct order
47
+ writer.write({ type: 'text-start', id: 'my-text' });
48
+ writer.write({ type: 'text-delta', id: 'my-text', delta: 'Hello' });
49
+ writer.write({ type: 'text-end', id: 'my-text' });
50
+ ```
51
+
52
+ 2. **Verify IDs match**: Ensure the `id` used in `*-delta` and `*-end` chunks matches the `id` used in the corresponding `*-start` chunk.
53
+
54
+ 3. **Handle error paths correctly**: When writing error messages in custom transports, ensure you emit the full start/delta/end sequence:
55
+
56
+ ```typescript
57
+ // When handling errors in custom transports
58
+ writer.write({ type: 'text-start', id: errorId });
59
+ writer.write({
60
+ type: 'text-delta',
61
+ id: errorId,
62
+ delta: 'Request failed...',
63
+ });
64
+ writer.write({ type: 'text-end', id: errorId });
65
+ ```
66
+
67
+ 4. **Check stream producer logic**: Review your streaming implementation to ensure chunks are sent in the correct order, especially when dealing with concurrent operations or merged streams.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "6.0.34",
3
+ "version": "6.0.36",
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,9 +42,9 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@opentelemetry/api": "1.9.0",
45
- "@ai-sdk/gateway": "3.0.14",
45
+ "@ai-sdk/gateway": "3.0.15",
46
46
  "@ai-sdk/provider": "3.0.3",
47
- "@ai-sdk/provider-utils": "4.0.6"
47
+ "@ai-sdk/provider-utils": "4.0.7"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@edge-runtime/vm": "^5.0.0",
@@ -26,6 +26,7 @@ export { NoSpeechGeneratedError } from './no-speech-generated-error';
26
26
  export { NoSuchToolError } from './no-such-tool-error';
27
27
  export { ToolCallRepairError } from './tool-call-repair-error';
28
28
  export { UnsupportedModelVersionError } from './unsupported-model-version-error';
29
+ export { UIMessageStreamError } from './ui-message-stream-error';
29
30
 
30
31
  export { InvalidDataContentError } from '../prompt/invalid-data-content-error';
31
32
  export { InvalidMessageRoleError } from '../prompt/invalid-message-role-error';
@@ -0,0 +1,48 @@
1
+ import { AISDKError } from '@ai-sdk/provider';
2
+
3
+ const name = 'AI_UIMessageStreamError';
4
+ const marker = `vercel.ai.error.${name}`;
5
+ const symbol = Symbol.for(marker);
6
+
7
+ /**
8
+ * Error thrown when a UI message stream contains invalid or out-of-sequence chunks.
9
+ *
10
+ * This typically occurs when:
11
+ * - A delta chunk is received without a corresponding start chunk
12
+ * - An end chunk is received without a corresponding start chunk
13
+ * - A tool invocation is not found for the given toolCallId
14
+ *
15
+ * @see https://ai-sdk.dev/docs/reference/ai-sdk-errors/ai-ui-message-stream-error
16
+ */
17
+ export class UIMessageStreamError extends AISDKError {
18
+ private readonly [symbol] = true; // used in isInstance
19
+
20
+ /**
21
+ * The type of chunk that caused the error (e.g., 'text-delta', 'reasoning-end').
22
+ */
23
+ readonly chunkType: string;
24
+
25
+ /**
26
+ * The ID associated with the failing chunk (part ID or toolCallId).
27
+ */
28
+ readonly chunkId: string;
29
+
30
+ constructor({
31
+ chunkType,
32
+ chunkId,
33
+ message,
34
+ }: {
35
+ chunkType: string;
36
+ chunkId: string;
37
+ message: string;
38
+ }) {
39
+ super({ name, message });
40
+
41
+ this.chunkType = chunkType;
42
+ this.chunkId = chunkId;
43
+ }
44
+
45
+ static isInstance(error: unknown): error is UIMessageStreamError {
46
+ return AISDKError.hasMarker(error, marker);
47
+ }
48
+ }
@@ -8,6 +8,7 @@ import {
8
8
  } from './process-ui-message-stream';
9
9
  import { InferUIMessageData, UIMessage } from './ui-messages';
10
10
  import { beforeEach, describe, it, expect, vi } from 'vitest';
11
+ import { UIMessageStreamError } from '../error/ui-message-stream-error';
11
12
 
12
13
  function createUIMessageStream(parts: UIMessageChunk[]) {
13
14
  return convertArrayToReadableStream(parts);
@@ -224,6 +225,247 @@ describe('processUIMessageStream', () => {
224
225
  });
225
226
  });
226
227
 
228
+ describe('malformed stream errors', () => {
229
+ it('should throw descriptive error when text-delta is received without text-start', async () => {
230
+ const stream = createUIMessageStream([
231
+ { type: 'start', messageId: 'msg-123' },
232
+ { type: 'start-step' },
233
+ { type: 'text-delta', id: 'text-1', delta: 'Hello' },
234
+ ]);
235
+
236
+ state = createStreamingUIMessageState({
237
+ messageId: 'msg-123',
238
+ lastMessage: undefined,
239
+ });
240
+
241
+ await expect(
242
+ consumeStream({
243
+ stream: processUIMessageStream({
244
+ stream,
245
+ runUpdateMessageJob,
246
+ onError: error => {
247
+ throw error;
248
+ },
249
+ }),
250
+ onError: error => {
251
+ throw error;
252
+ },
253
+ }),
254
+ ).rejects.toThrow(
255
+ 'Received text-delta for missing text part with ID "text-1". ' +
256
+ 'Ensure a "text-start" chunk is sent before any "text-delta" chunks.',
257
+ );
258
+ });
259
+
260
+ it('should throw descriptive error when reasoning-delta is received without reasoning-start', async () => {
261
+ const stream = createUIMessageStream([
262
+ { type: 'start', messageId: 'msg-123' },
263
+ { type: 'start-step' },
264
+ { type: 'reasoning-delta', id: 'reasoning-1', delta: 'Thinking...' },
265
+ ]);
266
+
267
+ state = createStreamingUIMessageState({
268
+ messageId: 'msg-123',
269
+ lastMessage: undefined,
270
+ });
271
+
272
+ await expect(
273
+ consumeStream({
274
+ stream: processUIMessageStream({
275
+ stream,
276
+ runUpdateMessageJob,
277
+ onError: error => {
278
+ throw error;
279
+ },
280
+ }),
281
+ onError: error => {
282
+ throw error;
283
+ },
284
+ }),
285
+ ).rejects.toThrow(
286
+ 'Received reasoning-delta for missing reasoning part with ID "reasoning-1". ' +
287
+ 'Ensure a "reasoning-start" chunk is sent before any "reasoning-delta" chunks.',
288
+ );
289
+ });
290
+
291
+ it('should throw descriptive error when tool-input-delta is received without tool-input-start', async () => {
292
+ const stream = createUIMessageStream([
293
+ { type: 'start', messageId: 'msg-123' },
294
+ { type: 'start-step' },
295
+ {
296
+ type: 'tool-input-delta',
297
+ toolCallId: 'tool-1',
298
+ inputTextDelta: '{"key":',
299
+ },
300
+ ]);
301
+
302
+ state = createStreamingUIMessageState({
303
+ messageId: 'msg-123',
304
+ lastMessage: undefined,
305
+ });
306
+
307
+ await expect(
308
+ consumeStream({
309
+ stream: processUIMessageStream({
310
+ stream,
311
+ runUpdateMessageJob,
312
+ onError: error => {
313
+ throw error;
314
+ },
315
+ }),
316
+ onError: error => {
317
+ throw error;
318
+ },
319
+ }),
320
+ ).rejects.toThrow(
321
+ 'Received tool-input-delta for missing tool call with ID "tool-1". ' +
322
+ 'Ensure a "tool-input-start" chunk is sent before any "tool-input-delta" chunks.',
323
+ );
324
+ });
325
+
326
+ it('should throw descriptive error when text-end is received without text-start', async () => {
327
+ const stream = createUIMessageStream([
328
+ { type: 'start', messageId: 'msg-123' },
329
+ { type: 'start-step' },
330
+ { type: 'text-end', id: 'text-1' },
331
+ ]);
332
+
333
+ state = createStreamingUIMessageState({
334
+ messageId: 'msg-123',
335
+ lastMessage: undefined,
336
+ });
337
+
338
+ await expect(
339
+ consumeStream({
340
+ stream: processUIMessageStream({
341
+ stream,
342
+ runUpdateMessageJob,
343
+ onError: error => {
344
+ throw error;
345
+ },
346
+ }),
347
+ onError: error => {
348
+ throw error;
349
+ },
350
+ }),
351
+ ).rejects.toThrow(
352
+ 'Received text-end for missing text part with ID "text-1". ' +
353
+ 'Ensure a "text-start" chunk is sent before any "text-end" chunks.',
354
+ );
355
+ });
356
+
357
+ it('should throw descriptive error when reasoning-end is received without reasoning-start', async () => {
358
+ const stream = createUIMessageStream([
359
+ { type: 'start', messageId: 'msg-123' },
360
+ { type: 'start-step' },
361
+ { type: 'reasoning-end', id: 'reasoning-1' },
362
+ ]);
363
+
364
+ state = createStreamingUIMessageState({
365
+ messageId: 'msg-123',
366
+ lastMessage: undefined,
367
+ });
368
+
369
+ await expect(
370
+ consumeStream({
371
+ stream: processUIMessageStream({
372
+ stream,
373
+ runUpdateMessageJob,
374
+ onError: error => {
375
+ throw error;
376
+ },
377
+ }),
378
+ onError: error => {
379
+ throw error;
380
+ },
381
+ }),
382
+ ).rejects.toThrow(
383
+ 'Received reasoning-end for missing reasoning part with ID "reasoning-1". ' +
384
+ 'Ensure a "reasoning-start" chunk is sent before any "reasoning-end" chunks.',
385
+ );
386
+ });
387
+
388
+ it('should throw UIMessageStreamError with correct properties for text-delta without text-start', async () => {
389
+ const stream = createUIMessageStream([
390
+ { type: 'start', messageId: 'msg-123' },
391
+ { type: 'start-step' },
392
+ { type: 'text-delta', id: 'missing-id', delta: 'Hello' },
393
+ ]);
394
+
395
+ state = createStreamingUIMessageState({
396
+ messageId: 'msg-123',
397
+ lastMessage: undefined,
398
+ });
399
+
400
+ let caughtError: unknown;
401
+ try {
402
+ await consumeStream({
403
+ stream: processUIMessageStream({
404
+ stream,
405
+ runUpdateMessageJob,
406
+ onError: error => {
407
+ throw error;
408
+ },
409
+ }),
410
+ onError: error => {
411
+ throw error;
412
+ },
413
+ });
414
+ } catch (error) {
415
+ caughtError = error;
416
+ }
417
+
418
+ expect(UIMessageStreamError.isInstance(caughtError)).toBe(true);
419
+ expect((caughtError as UIMessageStreamError).chunkType).toBe(
420
+ 'text-delta',
421
+ );
422
+ expect((caughtError as UIMessageStreamError).chunkId).toBe('missing-id');
423
+ });
424
+
425
+ it('should throw UIMessageStreamError with correct properties for tool-input-delta without tool-input-start', async () => {
426
+ const stream = createUIMessageStream([
427
+ { type: 'start', messageId: 'msg-123' },
428
+ { type: 'start-step' },
429
+ {
430
+ type: 'tool-input-delta',
431
+ toolCallId: 'missing-tool-id',
432
+ inputTextDelta: '{"key":',
433
+ },
434
+ ]);
435
+
436
+ state = createStreamingUIMessageState({
437
+ messageId: 'msg-123',
438
+ lastMessage: undefined,
439
+ });
440
+
441
+ let caughtError: unknown;
442
+ try {
443
+ await consumeStream({
444
+ stream: processUIMessageStream({
445
+ stream,
446
+ runUpdateMessageJob,
447
+ onError: error => {
448
+ throw error;
449
+ },
450
+ }),
451
+ onError: error => {
452
+ throw error;
453
+ },
454
+ });
455
+ } catch (error) {
456
+ caughtError = error;
457
+ }
458
+
459
+ expect(UIMessageStreamError.isInstance(caughtError)).toBe(true);
460
+ expect((caughtError as UIMessageStreamError).chunkType).toBe(
461
+ 'tool-input-delta',
462
+ );
463
+ expect((caughtError as UIMessageStreamError).chunkId).toBe(
464
+ 'missing-tool-id',
465
+ );
466
+ });
467
+ });
468
+
227
469
  describe('server-side tool roundtrip', () => {
228
470
  beforeEach(async () => {
229
471
  const stream = createUIMessageStream([
@@ -1,4 +1,5 @@
1
1
  import { FlexibleSchema, validateTypes } from '@ai-sdk/provider-utils';
2
+ import { UIMessageStreamError } from '../error/ui-message-stream-error';
2
3
  import { ProviderMetadata } from '../types';
3
4
  import { FinishReason } from '../types/language-model';
4
5
  import {
@@ -108,9 +109,11 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
108
109
  );
109
110
 
110
111
  if (toolInvocation == null) {
111
- throw new Error(
112
- `no tool invocation found for tool call ${toolCallId}`,
113
- );
112
+ throw new UIMessageStreamError({
113
+ chunkType: 'tool-invocation',
114
+ chunkId: toolCallId,
115
+ message: `No tool invocation found for tool call ID "${toolCallId}".`,
116
+ });
114
117
  }
115
118
 
116
119
  return toolInvocation;
@@ -313,6 +316,15 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
313
316
 
314
317
  case 'text-delta': {
315
318
  const textPart = state.activeTextParts[chunk.id];
319
+ if (textPart == null) {
320
+ throw new UIMessageStreamError({
321
+ chunkType: 'text-delta',
322
+ chunkId: chunk.id,
323
+ message:
324
+ `Received text-delta for missing text part with ID "${chunk.id}". ` +
325
+ `Ensure a "text-start" chunk is sent before any "text-delta" chunks.`,
326
+ });
327
+ }
316
328
  textPart.text += chunk.delta;
317
329
  textPart.providerMetadata =
318
330
  chunk.providerMetadata ?? textPart.providerMetadata;
@@ -322,6 +334,15 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
322
334
 
323
335
  case 'text-end': {
324
336
  const textPart = state.activeTextParts[chunk.id];
337
+ if (textPart == null) {
338
+ throw new UIMessageStreamError({
339
+ chunkType: 'text-end',
340
+ chunkId: chunk.id,
341
+ message:
342
+ `Received text-end for missing text part with ID "${chunk.id}". ` +
343
+ `Ensure a "text-start" chunk is sent before any "text-end" chunks.`,
344
+ });
345
+ }
325
346
  textPart.state = 'done';
326
347
  textPart.providerMetadata =
327
348
  chunk.providerMetadata ?? textPart.providerMetadata;
@@ -345,6 +366,15 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
345
366
 
346
367
  case 'reasoning-delta': {
347
368
  const reasoningPart = state.activeReasoningParts[chunk.id];
369
+ if (reasoningPart == null) {
370
+ throw new UIMessageStreamError({
371
+ chunkType: 'reasoning-delta',
372
+ chunkId: chunk.id,
373
+ message:
374
+ `Received reasoning-delta for missing reasoning part with ID "${chunk.id}". ` +
375
+ `Ensure a "reasoning-start" chunk is sent before any "reasoning-delta" chunks.`,
376
+ });
377
+ }
348
378
  reasoningPart.text += chunk.delta;
349
379
  reasoningPart.providerMetadata =
350
380
  chunk.providerMetadata ?? reasoningPart.providerMetadata;
@@ -354,6 +384,15 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
354
384
 
355
385
  case 'reasoning-end': {
356
386
  const reasoningPart = state.activeReasoningParts[chunk.id];
387
+ if (reasoningPart == null) {
388
+ throw new UIMessageStreamError({
389
+ chunkType: 'reasoning-end',
390
+ chunkId: chunk.id,
391
+ message:
392
+ `Received reasoning-end for missing reasoning part with ID "${chunk.id}". ` +
393
+ `Ensure a "reasoning-start" chunk is sent before any "reasoning-end" chunks.`,
394
+ });
395
+ }
357
396
  reasoningPart.providerMetadata =
358
397
  chunk.providerMetadata ?? reasoningPart.providerMetadata;
359
398
  reasoningPart.state = 'done';
@@ -440,6 +479,15 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
440
479
 
441
480
  case 'tool-input-delta': {
442
481
  const partialToolCall = state.partialToolCalls[chunk.toolCallId];
482
+ if (partialToolCall == null) {
483
+ throw new UIMessageStreamError({
484
+ chunkType: 'tool-input-delta',
485
+ chunkId: chunk.toolCallId,
486
+ message:
487
+ `Received tool-input-delta for missing tool call with ID "${chunk.toolCallId}". ` +
488
+ `Ensure a "tool-input-start" chunk is sent before any "tool-input-delta" chunks.`,
489
+ });
490
+ }
443
491
 
444
492
  partialToolCall.text += chunk.inputTextDelta;
445
493