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.
- package/CHANGELOG.md +17 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +71 -28
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +1 -1
- package/docs/02-getting-started/09-coding-agents.mdx +1 -1
- package/docs/03-ai-sdk-core/36-transcription.mdx +6 -0
- package/docs/03-ai-sdk-core/65-devtools.mdx +6 -2
- package/docs/07-reference/01-ai-sdk-core/11-stream-transcribe.mdx +1 -1
- package/package.json +3 -3
- package/src/transcribe/stream-transcribe-result.ts +5 -0
- package/src/transcribe/stream-transcribe.ts +46 -7
- package/src/ui/chat.ts +24 -17
- package/src/ui/validate-ui-messages.ts +21 -9
package/dist/internal/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
45
|
+
"@ai-sdk/gateway": "4.0.20",
|
|
46
46
|
"@ai-sdk/provider": "4.0.3",
|
|
47
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
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
|
|
664
|
+
activeResponse = response;
|
|
665
|
+
|
|
666
|
+
response.abortController.signal.addEventListener('abort', () => {
|
|
664
667
|
isAbort = true;
|
|
665
668
|
});
|
|
666
669
|
|
|
667
|
-
this.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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
709
|
+
response.state.message,
|
|
707
710
|
);
|
|
708
711
|
} else {
|
|
709
|
-
this.state.pushMessage(
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
),
|