Skip to content

Chapter 5 · Text Generation and Streaming Output ​

Chapter goals:

  • Master the适用 scenarios for the two core functions generateText and streamText
  • 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 / onEnd callbacks

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 model
  • streamText: 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):

ts
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:

tsx
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:

tsx
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所有 steps
  • result.text: text from the最后 step
  • result.files: files generated across所有 steps
  • result.sources: sources used as reference (only some models support)
  • result.toolCalls / result.toolResults: tool calls and their results across所有 steps
  • result.finishReason: reason the model stopped generating
  • result.rawFinishReason: raw stop reason from the provider
  • result.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-step performance
  • result.finalStep: details of the最后 step, including performance
  • result.output: structured output generated via the output spec

5.3 streamText: Streaming Generation ​

streamText streams the generation result as an异步 iterator. Each chunk contains incremental text, tool calls, and other events:

tsx
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 ​

ts
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 strings

5.5 Blocking UI vs. Streaming UI ​

AspectBlocking (generateText)Streaming (streamText)
UXUser waits for full responseIncremental display, feels instant
Error handlingThrows异常 immediatelyErrors进入 stream; use onError
Token costSameSame
Use caseBatch jobs, tool-based agentsChat interfaces, real-time feedback

5.6 Callbacks: onError, onChunk, onEnd ​

ts
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:

tsx
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:

ts
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:

tsx
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:

ts
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 ​

  • generateText is suitable for non-interactive one-shot generation; the result object provides text, usage, finishReason, steps, performance, and other完整 metadata;
  • streamText consumes text increments via textStream async 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 onError to log;
  • onChunk / onEnd callbacks cover each chunk and stream-end timing; experimental lifecycle callbacks (onStart/onStepStart/onStepEnd etc.) are suitable for logging and telemetry;
  • experimental_transform can smooth or自定义 transform the stream; stopStream() can terminate early (must simulate finish events);
  • Some models support sources to 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 ​

  1. Use generateText to have the model write a README intro for one of your真实 projects; print result.text and result.usage.totalTokens, and observe finalStep.performance.responseTimeMs.
  2. Implement a terminal typewriter effect with streamText: write each character via process.stdout.write(delta) while consuming textStream, and add an 8-second timeout with AbortSignal.timeout(8000).
  3. Write a custom transform that detects "sensitive words" in the text stream and calls stopStream() to terminate and simulate the finish event; then组合 test with smoothStream() to verify the order effect of two transforms.

Next: Chapter 6 · Generation Settings and Lifecycle Callbacks