ai 7.0.26 → 7.0.28

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.
@@ -85,7 +85,7 @@ import {
85
85
  } from "@ai-sdk/provider-utils";
86
86
 
87
87
  // src/version.ts
88
- var VERSION = true ? "7.0.26" : "0.0.0-test";
88
+ var VERSION = true ? "7.0.28" : "0.0.0-test";
89
89
 
90
90
  // src/util/download/download.ts
91
91
  var download = async ({
@@ -84,7 +84,7 @@ const result = await generateText({
84
84
  Start the DevTools viewer in a separate terminal:
85
85
 
86
86
  ```bash
87
- npx @ai-sdk/devtools
87
+ npx @ai-sdk/devtools@latest
88
88
  ```
89
89
 
90
90
  Open [http://localhost:4983](http://localhost:4983) to inspect your AI SDK interactions in real time.
@@ -71,6 +71,12 @@ for await (const part of result.fullStream) {
71
71
  console.log(await result.text);
72
72
  ```
73
73
 
74
+ `fullStream` is a single-consumer live stream and can only be accessed once.
75
+ When you need both stream parts and final results, access `fullStream` first and
76
+ await the result promises while or after consuming it. Accessing a result
77
+ promise first consumes the stream internally, so `fullStream` is no longer
78
+ available. This avoids retaining an unbounded replay buffer for live audio.
79
+
74
80
  To access the final transcript metadata:
75
81
 
76
82
  ```ts
@@ -74,7 +74,7 @@ const result = streamText({
74
74
  Start the DevTools viewer:
75
75
 
76
76
  ```bash
77
- npx @ai-sdk/devtools
77
+ npx @ai-sdk/devtools@latest
78
78
  ```
79
79
 
80
80
  Open [http://localhost:4983](http://localhost:4983) to view your AI SDK interactions.
@@ -87,9 +87,13 @@ For example, if your API is in `apps/api`, run:
87
87
 
88
88
  ```bash
89
89
  cd apps/api
90
- npx @ai-sdk/devtools
90
+ npx @ai-sdk/devtools@latest
91
91
  ```
92
92
 
93
+ The explicit `@latest` tag ensures that `npx` installs an executable copy
94
+ instead of selecting a transitive dependency whose binary is not linked into
95
+ the workspace.
96
+
93
97
  ## Captured data
94
98
 
95
99
  DevTools captures the following information from your AI SDK calls:
@@ -99,7 +99,7 @@ console.log(await result.text);
99
99
  name: 'fullStream',
100
100
  type: 'AsyncIterableStream<TranscriptionStreamPart>',
101
101
  description:
102
- 'A stream of transcription parts: `transcript-delta`, `transcript-partial`, `transcript-final`, `raw`, and `error`.',
102
+ 'A single-consumer live stream of transcription parts: `transcript-delta`, `transcript-partial`, `transcript-final`, `raw`, and `error`. Access it once, before any result promise, when both stream parts and final results are needed. Accessing a result promise first consumes the stream internally and makes `fullStream` unavailable.',
103
103
  },
104
104
  {
105
105
  name: 'text',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.26",
3
+ "version": "7.0.28",
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,9 +42,9 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.19",
45
+ "@ai-sdk/gateway": "4.0.20",
46
46
  "@ai-sdk/provider": "4.0.3",
47
- "@ai-sdk/provider-utils": "5.0.9"
47
+ "@ai-sdk/provider-utils": "5.0.10"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@edge-runtime/vm": "^5.0.0",
@@ -81,6 +81,11 @@ export interface StreamTranscriptionResult {
81
81
 
82
82
  /**
83
83
  * Full stream of transcription parts.
84
+ *
85
+ * This is a single-consumer live stream and can only be accessed once.
86
+ * Access it before any result promise when both stream parts and final
87
+ * results are needed; accessing a result promise first consumes the stream
88
+ * internally and makes `fullStream` unavailable.
84
89
  */
85
90
  readonly fullStream: AsyncIterableStream<TranscriptionStreamPart>;
86
91
  }
@@ -285,34 +285,73 @@ export function streamTranscribe({
285
285
  });
286
286
  });
287
287
 
288
+ // Transcription streams can be unbounded (live microphone + raw chunks), so
289
+ // unlike streamText we cannot retain an unread tee branch for replay. The
290
+ // output stream has one owner: either fullStream, or the first result promise
291
+ // getter which claims and drains it internally.
292
+ let streamOwner: 'unclaimed' | 'full-stream' | 'result-promises' =
293
+ 'unclaimed';
294
+
295
+ function consumeStream() {
296
+ if (streamOwner === 'full-stream' || streamOwner === 'result-promises') {
297
+ return;
298
+ }
299
+ streamOwner = 'result-promises';
300
+ const reader = transform.readable.getReader();
301
+ void (async () => {
302
+ while (!(await reader.read()).done) {
303
+ // drain; results surface via the promises
304
+ }
305
+ })().catch(() => {
306
+ // stream errors reject the promises via the pipe handler above
307
+ });
308
+ }
309
+
310
+ function getFullStream() {
311
+ if (streamOwner !== 'unclaimed') {
312
+ throw new Error(
313
+ streamOwner === 'full-stream'
314
+ ? 'fullStream can only be accessed once.'
315
+ : 'fullStream cannot be accessed after a result promise.',
316
+ );
317
+ }
318
+ streamOwner = 'full-stream';
319
+ // Direct ownership preserves backpressure and cancellation: cancelling
320
+ // this stream reaches Transformer.cancel and aborts the model pipe.
321
+ return asAsyncIterableStream(transform.readable);
322
+ }
323
+
288
324
  return {
289
325
  get text() {
326
+ consumeStream();
290
327
  return textPromise.promise;
291
328
  },
292
329
  get segments() {
330
+ consumeStream();
293
331
  return segmentsPromise.promise;
294
332
  },
295
333
  get language() {
334
+ consumeStream();
296
335
  return languagePromise.promise;
297
336
  },
298
337
  get durationInSeconds() {
338
+ consumeStream();
299
339
  return durationInSecondsPromise.promise;
300
340
  },
301
341
  get warnings() {
342
+ consumeStream();
302
343
  return warningsPromise.promise;
303
344
  },
304
345
  get responses() {
346
+ consumeStream();
305
347
  return responsesPromise.promise;
306
348
  },
307
349
  get providerMetadata() {
350
+ consumeStream();
308
351
  return providerMetadataPromise.promise;
309
352
  },
310
- // `transform.readable` is fresh and exclusively owned here, so attach the
311
- // async iterator in place rather than piping through another transform.
312
- // The extra transform (as `createAsyncIterableStream` would add) chains two
313
- // transforms fed by the active model pipe below and surfaces a spurious
314
- // unhandled `undefined` rejection when the consumer cancels early on
315
- // Node.js 26.
316
- fullStream: asAsyncIterableStream(transform.readable),
353
+ get fullStream() {
354
+ return getFullStream();
355
+ },
317
356
  };
318
357
  }
package/src/ui/chat.ts CHANGED
@@ -650,9 +650,10 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
650
650
  let isAbort = false;
651
651
  let isDisconnect = false;
652
652
  let isError = false;
653
+ let activeResponse: ActiveResponse<UI_MESSAGE> | undefined;
653
654
 
654
655
  try {
655
- const activeResponse = {
656
+ const response = {
656
657
  state: createStreamingUIMessageState({
657
658
  lastMessage: this.state.snapshot(lastMessage),
658
659
  messageId: this.generateId(),
@@ -660,11 +661,13 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
660
661
  abortController: new AbortController(),
661
662
  } as ActiveResponse<UI_MESSAGE>;
662
663
 
663
- activeResponse.abortController.signal.addEventListener('abort', () => {
664
+ activeResponse = response;
665
+
666
+ response.abortController.signal.addEventListener('abort', () => {
664
667
  isAbort = true;
665
668
  });
666
669
 
667
- this.activeResponse = activeResponse;
670
+ this.activeResponse = response;
668
671
 
669
672
  let stream: ReadableStream<UIMessageChunk>;
670
673
 
@@ -674,7 +677,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
674
677
  stream = await this.transport.sendMessages({
675
678
  chatId: this.id,
676
679
  messages: this.state.messages,
677
- abortSignal: activeResponse.abortController.signal,
680
+ abortSignal: response.abortController.signal,
678
681
  metadata,
679
682
  headers,
680
683
  body,
@@ -692,21 +695,21 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
692
695
  // serialize the job execution to avoid race conditions:
693
696
  this.jobExecutor.run(() =>
694
697
  job({
695
- state: activeResponse.state,
698
+ state: response.state,
696
699
  write: () => {
697
700
  // streaming is set on first write (before it should be "submitted")
698
701
  this.setStatus({ status: 'streaming' });
699
702
 
700
703
  const replaceLastMessage =
701
- activeResponse.state.message.id === this.lastMessage?.id;
704
+ response.state.message.id === this.lastMessage?.id;
702
705
 
703
706
  if (replaceLastMessage) {
704
707
  this.state.replaceMessage(
705
708
  this.state.messages.length - 1,
706
- activeResponse.state.message,
709
+ response.state.message,
707
710
  );
708
711
  } else {
709
- this.state.pushMessage(activeResponse.state.message);
712
+ this.state.pushMessage(response.state.message);
710
713
  }
711
714
  },
712
715
  }),
@@ -756,19 +759,23 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
756
759
  this.setStatus({ status: 'error', error: err as Error });
757
760
  } finally {
758
761
  try {
759
- this.onFinish?.({
760
- message: this.activeResponse!.state.message,
761
- messages: this.state.messages,
762
- isAbort,
763
- isDisconnect,
764
- isError,
765
- finishReason: this.activeResponse?.state.finishReason,
766
- });
762
+ if (activeResponse) {
763
+ this.onFinish?.({
764
+ message: activeResponse.state.message,
765
+ messages: this.state.messages,
766
+ isAbort,
767
+ isDisconnect,
768
+ isError,
769
+ finishReason: activeResponse.state.finishReason,
770
+ });
771
+ }
767
772
  } catch (err) {
768
773
  console.error(err);
769
774
  }
770
775
 
771
- this.activeResponse = undefined;
776
+ if (this.activeResponse === activeResponse) {
777
+ this.activeResponse = undefined;
778
+ }
772
779
  }
773
780
 
774
781
  // automatically send the message if the sendAutomaticallyWhen function returns true
@@ -30,12 +30,12 @@ const uiMessagesSchema = lazySchema(() =>
30
30
  zodSchema(
31
31
  z
32
32
  .array(
33
- z.object({
34
- id: z.string(),
35
- role: z.enum(['system', 'user', 'assistant']),
36
- metadata: z.unknown().optional(),
37
- parts: z
38
- .array(
33
+ z
34
+ .object({
35
+ id: z.string(),
36
+ role: z.enum(['system', 'user', 'assistant']),
37
+ metadata: z.unknown().optional(),
38
+ parts: z.array(
39
39
  z.union([
40
40
  z.object({
41
41
  type: z.literal('text'),
@@ -343,9 +343,21 @@ const uiMessagesSchema = lazySchema(() =>
343
343
  }),
344
344
  }),
345
345
  ]),
346
- )
347
- .nonempty('Message must contain at least one part'),
348
- }),
346
+ ),
347
+ })
348
+ .superRefine((message, context) => {
349
+ if (message.role !== 'assistant' && message.parts.length === 0) {
350
+ context.addIssue({
351
+ origin: 'array',
352
+ code: 'too_small',
353
+ minimum: 1,
354
+ inclusive: true,
355
+ input: message.parts,
356
+ path: ['parts'],
357
+ message: 'Message must contain at least one part',
358
+ });
359
+ }
360
+ }),
349
361
  )
350
362
  .nonempty('Messages array must not be empty'),
351
363
  ),