-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat (ai/core): add experimental transform option to streamText (#4074)
- Loading branch information
Showing
16 changed files
with
391 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'ai': patch | ||
--- | ||
|
||
feat (ai/core): add smoothStream helper |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'ai': patch | ||
--- | ||
|
||
feat (ai/core): add experimental transform option to streamText |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
content/docs/07-reference/01-ai-sdk-core/80-smooth-stream.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
--- | ||
title: smoothStream | ||
description: Helper function for smoothing text streaming output | ||
--- | ||
|
||
# `smoothStream()` | ||
|
||
`smoothStream` is a utility function that creates a TransformStream | ||
for the `streamText` `transform` option | ||
to smooth out text streaming by buffering and releasing complete words with configurable delays. | ||
This creates a more natural reading experience when streaming text responses. | ||
|
||
```ts highlight={"6-8"} | ||
import { smoothStream, streamText } from 'ai'; | ||
|
||
const result = streamText({ | ||
model, | ||
prompt, | ||
experimental_transform: smoothStream({ | ||
delayInMs: 40, // optional: defaults to 40ms | ||
}), | ||
}); | ||
``` | ||
|
||
## Import | ||
|
||
<Snippet text={`import { smoothStream } from "ai"`} prompt={false} /> | ||
|
||
## API Signature | ||
|
||
### Parameters | ||
|
||
<PropertiesTable | ||
content={[ | ||
{ | ||
name: 'delayInMs', | ||
type: 'number', | ||
isOptional: true, | ||
description: | ||
'The delay in milliseconds between outputting each word. Defaults to 40ms. Set to 0 to disable delays.', | ||
}, | ||
]} | ||
/> | ||
|
||
### Returns | ||
|
||
Returns a `TransformStream` that: | ||
|
||
- Buffers incoming text chunks | ||
- Releases complete words when whitespace is encountered | ||
- Adds configurable delays between words for smooth output | ||
- Passes through non-text chunks (like step-finish events) immediately |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { anthropic } from '@ai-sdk/anthropic'; | ||
import { smoothStream, streamText } from 'ai'; | ||
import 'dotenv/config'; | ||
|
||
async function main() { | ||
const result = streamText({ | ||
model: anthropic('claude-3-5-sonnet-20240620'), | ||
prompt: 'Invent a new holiday and describe its traditions.', | ||
experimental_transform: smoothStream(), | ||
}); | ||
|
||
for await (const textPart of result.textStream) { | ||
process.stdout.write(textPart); | ||
} | ||
|
||
console.log(); | ||
console.log('Token usage:', await result.usage); | ||
console.log('Finish reason:', await result.finishReason); | ||
} | ||
|
||
main().catch(console.error); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { azure } from '@ai-sdk/azure'; | ||
import { smoothStream, streamText } from 'ai'; | ||
import 'dotenv/config'; | ||
|
||
async function main() { | ||
const result = streamText({ | ||
model: azure('gpt-4o'), // use your own deployment | ||
prompt: 'Invent a new holiday and describe its traditions.', | ||
experimental_transform: smoothStream(), | ||
}); | ||
|
||
for await (const textPart of result.textStream) { | ||
process.stdout.write(textPart); | ||
} | ||
|
||
console.log(); | ||
console.log('Token usage:', await result.usage); | ||
console.log('Finish reason:', await result.finishReason); | ||
} | ||
|
||
main().catch(console.error); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import { describe, expect, it } from 'vitest'; | ||
import { convertArrayToReadableStream } from '../../test'; | ||
import { smoothStream } from './smooth-stream'; | ||
|
||
describe('smoothStream', () => { | ||
it('should combine partial words', async () => { | ||
const events: any[] = []; | ||
|
||
const stream = convertArrayToReadableStream([ | ||
{ textDelta: 'Hello', type: 'text-delta' }, | ||
{ textDelta: ', ', type: 'text-delta' }, | ||
{ textDelta: 'world!', type: 'text-delta' }, | ||
{ type: 'step-finish' }, | ||
{ type: 'finish' }, | ||
]).pipeThrough( | ||
smoothStream({ | ||
delayInMs: 10, | ||
_internal: { | ||
delay: () => { | ||
events.push('delay'); | ||
return Promise.resolve(); | ||
}, | ||
}, | ||
}), | ||
); | ||
|
||
// Get a reader and read chunks | ||
const reader = stream.getReader(); | ||
while (true) { | ||
const { done, value } = await reader.read(); | ||
if (done) break; | ||
events.push(value); | ||
} | ||
|
||
expect(events).toEqual([ | ||
'delay', | ||
{ | ||
textDelta: 'Hello, ', | ||
type: 'text-delta', | ||
}, | ||
{ | ||
textDelta: 'world!', | ||
type: 'text-delta', | ||
}, | ||
{ | ||
type: 'step-finish', | ||
}, | ||
{ | ||
type: 'finish', | ||
}, | ||
]); | ||
}); | ||
|
||
it('should split larger text chunks', async () => { | ||
const events: any[] = []; | ||
|
||
const stream = convertArrayToReadableStream([ | ||
{ | ||
textDelta: 'Hello, World! This is an example text.', | ||
type: 'text-delta', | ||
}, | ||
{ type: 'step-finish' }, | ||
{ type: 'finish' }, | ||
]).pipeThrough( | ||
smoothStream({ | ||
delayInMs: 10, | ||
_internal: { | ||
delay: () => { | ||
events.push('delay'); | ||
return Promise.resolve(); | ||
}, | ||
}, | ||
}), | ||
); | ||
|
||
// Get a reader and read chunks | ||
const reader = stream.getReader(); | ||
while (true) { | ||
const { done, value } = await reader.read(); | ||
if (done) break; | ||
events.push(value); | ||
} | ||
|
||
expect(events).toEqual([ | ||
'delay', | ||
{ | ||
textDelta: 'Hello, ', | ||
type: 'text-delta', | ||
}, | ||
'delay', | ||
{ | ||
textDelta: 'World! ', | ||
type: 'text-delta', | ||
}, | ||
'delay', | ||
{ | ||
textDelta: 'This ', | ||
type: 'text-delta', | ||
}, | ||
'delay', | ||
{ | ||
textDelta: 'is ', | ||
type: 'text-delta', | ||
}, | ||
'delay', | ||
{ | ||
textDelta: 'an ', | ||
type: 'text-delta', | ||
}, | ||
'delay', | ||
{ | ||
textDelta: 'example ', | ||
type: 'text-delta', | ||
}, | ||
{ | ||
textDelta: 'text.', | ||
type: 'text-delta', | ||
}, | ||
{ | ||
type: 'step-finish', | ||
}, | ||
{ | ||
type: 'finish', | ||
}, | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { delay as originalDelay } from '../../util/delay'; | ||
import { CoreTool } from '../tool/tool'; | ||
import { TextStreamPart } from './stream-text-result'; | ||
|
||
export function smoothStream<TOOLS extends Record<string, CoreTool>>({ | ||
delayInMs = 40, | ||
_internal: { delay = originalDelay } = {}, | ||
}: { | ||
delayInMs?: number; | ||
|
||
/** | ||
* Internal. For test use only. May change without notice. | ||
*/ | ||
_internal?: { | ||
delay?: (delayInMs: number) => Promise<void>; | ||
}; | ||
} = {}): TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>> { | ||
let buffer = ''; | ||
|
||
return new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({ | ||
async transform(chunk, controller) { | ||
if (chunk.type === 'step-finish') { | ||
if (buffer.length > 0) { | ||
controller.enqueue({ type: 'text-delta', textDelta: buffer }); | ||
buffer = ''; | ||
} | ||
|
||
controller.enqueue(chunk); | ||
return; | ||
} | ||
|
||
if (chunk.type !== 'text-delta') { | ||
controller.enqueue(chunk); | ||
return; | ||
} | ||
|
||
buffer += chunk.textDelta; | ||
|
||
// Stream out complete words when whitespace is found | ||
while (buffer.match(/\s/)) { | ||
const whitespaceIndex = buffer.search(/\s/); | ||
const word = buffer.slice(0, whitespaceIndex + 1); | ||
controller.enqueue({ type: 'text-delta', textDelta: word }); | ||
buffer = buffer.slice(whitespaceIndex + 1); | ||
|
||
if (delayInMs > 0) { | ||
await delay(delayInMs); | ||
} | ||
} | ||
}, | ||
}); | ||
} |
Oops, something went wrong.