Chapter 5 · Text Generation and Streaming Output
Chapter goals:
- Master the适用 scenarios for the two core functions
generateTextandstreamText- Learn to read text, usage (usage), finish reason, and sources from generation results
- Understand the体验 difference and tradeoffs between Blocking UI and Streaming UI
- Master the usage of
onError/onChunk/onEndcallbacks
5.1 The Two Core Functions
Large language models (LLMs) can generate text based on a prompt, which can contain instructions and information to process. For example, asking the model to invent a recipe, draft an email, or summarize a document.
AI SDK Core provides two functions to generate and stream LLM text:
generateText: generates complete text for a given prompt and modelstreamText: streams text output for a given prompt and model
Advanced features like tool calling and structured data generation are built on top of text generation.
本章 examples uniformly use the Vercel AI Gateway to construct models (also applicable to custom providers, see Chapter 3):
import { createGateway } from 'ai';
export const gateway = createGateway({
apiKey: process.env.AI_GATEWAY_API_KEY ?? '',
});
// Equivalent custom OpenAI-compatible Provider写法:
// import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
// export const myProvider = createOpenAICompatible({
// name: 'my-provider',
// baseURL: process.env.OPENAI_COMPATIBLE_BASE_URL ?? '',
// apiKey: process.env.OPENAI_COMPATIBLE_API_KEY ?? '',
// });5.2 generateText: One-Shot Generation
Use the generateText function to generate text. This function is especially suitable for non-interactive scenarios — when you need to write a piece of text (such as drafting an email, summarizing a webpage) and for agent scenarios using tools:
import { generateText } from 'ai';
import { gateway } from './provider';
const { text } = await generateText({
model: gateway('openai/gpt-5'),
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
});You can also use a more advanced prompt to执行更 complex instructions:
import { generateText } from 'ai';
import { gateway } from './provider';
const { text } = await generateText({
model: gateway('openai/gpt-5'),
instructions:
'You are a professional writer. ' +
'You write simple, clear, and concise content.',
prompt: `Summarize the following article in 3-5 sentences: ${article}`,
});The result object from generateText contains the generated output and metadata:
result.content: content generated across所有 stepsresult.text: text from the最后 stepresult.files: files generated across所有 stepsresult.sources: sources used as reference (only some models support)result.toolCalls/result.toolResults: tool calls and their results across所有 stepsresult.finishReason: reason the model stopped generatingresult.rawFinishReason: raw stop reason from the providerresult.usage: total usage across所有 steps (for multi-step generation)result.warnings: warnings from the model provider (e.g. unsupported settings)result.steps: details of所有 steps, including per-stepperformanceresult.finalStep: details of the最后 step, includingperformanceresult.output: structured output generated via theoutputspec
5.3 streamText: Streaming Generation
streamText streams the generation result as an异步 iterator. Each chunk contains incremental text, tool calls, and other events:
import { streamText } from 'ai';
import { gateway } from './provider';
const result = streamText({
model: gateway('openai/gpt-5'),
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}⚠️ You must消费 the stream; otherwise the generation will not complete due to backpressure.
5.4 Result Object: Usage and Finish Reason
const result = await generateText({
model: gateway('openai/gpt-5'),
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
});
console.log(result.text); // generated text
console.log(result.usage); // { promptTokens: 20, completionTokens: 150, totalTokens: 170 }
console.log(result.finishReason); // 'stop' | 'length' | 'tool-calls' | 'error'
console.log(result.rawFinishReason); // provider-specific raw value
console.log(result.warnings); // array of warning strings5.5 Blocking UI vs. Streaming UI
| Aspect | Blocking (generateText) | Streaming (streamText) |
|---|---|---|
| UX | User waits for full response | Incremental display, feels instant |
| Error handling | Throws异常 immediately | Errors进入 stream; use onError |
| Token cost | Same | Same |
| Use case | Batch jobs, tool-based agents | Chat interfaces, real-time feedback |
5.6 Callbacks: onError, onChunk, onEnd
const result = streamText({
model: gateway('openai/gpt-5'),
prompt: 'Hello',
onError: (error) => {
console.error('Generation error:', error);
},
onChunk: ({ chunk }) => {
if (chunk.type === 'text-delta') {
process.stdout.write(chunk.textDelta);
}
},
onEnd: ({ finishReason, usage }) => {
console.log('Finished:', finishReason, 'Tokens:', usage.totalTokens);
},
});Experimental lifecycle callbacks (onStart / onStepStart / onStepEnd / onLanguageModelCallEnd / onToolExecutionEnd) are also available for logging and telemetry.
5.7 Experimental Transforms
AI SDK内置 smoothStream function to smooth text and reasoning streaming output:
import { smoothStream, streamText } from 'ai';
const result = streamText({
model,
prompt,
experimental_transform: smoothStream(),
});Custom Transform
You can also implement your own transform. The following example converts all text to uppercase:
import { streamText, type TextStreamPart, type ToolSet } from 'ai';
const upperCaseTransform =
<TOOLS extends ToolSet>() =>
(options: { tools: TOOLS; stopStream: () => void }) =>
new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({
transform(chunk, controller) {
controller.enqueue(
// For text-delta chunks, convert text to uppercase:
chunk.type === 'text-delta'
? { ...chunk, text: chunk.text.toUpperCase() }
: chunk,
);
},
});You can alsoterminate the stream early via stopStream() (e.g. when violating guardrails). When calling stopStream, simulate finish-step and finish events to ensure the stream format is complete and all callbacks are triggered. Multiple transforms are applied in the order provided:
const result = streamText({
model,
prompt,
experimental_transform: [firstTransform, secondTransform],
});5.8 Sources
Some providers (e.g. Perplexity, Google)附带 sources in the response. Currently sources are limited to webpages thatprovide basis for the answer, accessible via the result's sources property. Each url-type source contains id, url, title (optional), and providerMetadata:
const result = await generateText({
model: gateway('google/gemini-2.5-flash'),
prompt: 'List the top 5 San Francisco news from the past week.',
});
for (const source of result.sources) {
if (source.sourceType === 'url') {
console.log('ID:', source.id);
console.log('Title:', source.title);
console.log('URL:', source.url);
console.log();
}
}With streamText, sources can be read from the stream property as source events, or obtained via the result.sources promise.
5.9 Chapter Summary
generateTextis suitable for non-interactive one-shot generation; the result object provides text, usage, finishReason, steps, performance, and other完整 metadata;streamTextconsumes text increments viatextStreamasync iteration; the stream must be consumed, otherwise it will not complete due to backpressure;- Errors in streaming scenarios are not thrown but enter the stream;务必 configure
onErrorto log; onChunk/onEndcallbacks cover each chunk and stream-end timing; experimental lifecycle callbacks (onStart/onStepStart/onStepEnd etc.) are suitable for logging and telemetry;experimental_transformcan smooth or自定义 transform the stream;stopStream()can terminate early (must simulate finish events);- Some models support
sourcesto return citation references.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Regarding the choice between generateText and streamText, which statement is correct?
2. Why must the stream returned by streamText be consumed?
3. When an error occurs during streamText, what is the default behavior?
4. When calling stopStream() early in a custom stream transform, why manually enqueue finish-step and finish events?
🛠️ Hands-On Exercises
- Use
generateTextto have the model write a README intro for one of your真实 projects; printresult.textandresult.usage.totalTokens, and observefinalStep.performance.responseTimeMs. - Implement a terminal typewriter effect with
streamText: write each character viaprocess.stdout.write(delta)while consumingtextStream, and add an 8-second timeout withAbortSignal.timeout(8000). - Write a custom transform that detects "sensitive words" in the text stream and calls
stopStream()to terminate and simulate the finish event; then组合 test withsmoothStream()to verify the order effect of two transforms.
Next: Chapter 6 · Generation Settings and Lifecycle Callbacks