Skip to main content

Generating and Streaming Text

Large language models (LLMs) can generate text in response to a prompt, which can contain instructions and information to process. For example, you can ask a model to come up with a recipe, draft an email, or summarize a document.

The Go AI SDK Core provides two functions to generate text from LLMs:

Advanced LLM features such as tool calling and structured data generation are built on top of text generation.

GenerateText​

You can generate text using the ai.GenerateText() function. This function is ideal for non-interactive use cases where you need to write text (e.g. drafting email or summarizing web pages) and for agents that use tools.

package main

import (
"context"
"fmt"
"log"
"os"

"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)

func main() {
ctx := context.Background()

provider := openai.New(openai.Config{APIKey: os.Getenv("OPENAI_API_KEY")})
model, _ := provider.LanguageModel("gpt-5.2")

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Write a vegetarian lasagna recipe for 4 people.",
})
if err != nil {
log.Fatal(err)
}

fmt.Println(result.Text)
}

You can use more advanced prompts to generate text with more complex instructions and content:

article := "..." // your article text

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
System: "You are a professional writer. " +
"You write simple, clear, and concise content.",
Prompt: fmt.Sprintf("Summarize the following article in 3-5 sentences: %s", article),
})
if err != nil {
log.Fatal(err)
}

fmt.Println(result.Text)

Result Object​

The result object of GenerateText contains several fields that provide information about the generation:

type GenerateTextResult struct {
// The generated text
Text string

// Ordered output content from all completed steps. Includes text,
// reasoning, tool calls, tool results, files, sources, and provider
// metadata-bearing parts in the same order emitted by the model/tool loop.
Content []types.ContentPart

// Output contains the parsed output when a WithOutput option was provided.
// Type-assert to the concrete type, e.g.: recipe := result.Output.(Recipe)
// Nil when no Output option was set.
Output any

// Tool calls made during generation
ToolCalls []types.ToolCall

// Tool results from executed tools
ToolResults []types.ToolResult

// Steps taken during generation (for multi-step tool calling)
Steps []types.StepResult

// Reason the model finished generating
FinishReason types.FinishReason

// Token usage information
Usage types.Usage

// Warnings from the model provider
Warnings []types.Warning

// Provider-specific metadata
ProviderMetadata map[string]interface{}

// Raw request/response (for debugging)
RawRequest interface{}
RawResponse interface{}
}

Accessing Response Headers & Body​

Sometimes you need access to the full response from the model provider, e.g. to access some provider-specific headers or body content.

You can access the raw request and response using the RawRequest and RawResponse fields:

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Hello",
})
if err != nil {
log.Fatal(err)
}

fmt.Printf("Raw request: %+v\n", result.RawRequest)
fmt.Printf("Raw response: %+v\n", result.RawResponse)

OnFinish Callback​

When using GenerateText, you can provide an OnFinish callback that is triggered after the last step is finished. It contains the text, usage information, finish reason, and more:

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Invent a new holiday and describe its traditions.",
OnFinish: func(ctx context.Context, result *ai.GenerateTextResult, userContext interface{}) {
// Your own logic, e.g. for saving the chat history or recording usage
fmt.Printf("Text: %s\n", result.Text)
fmt.Printf("Finish reason: %s\n", result.FinishReason)
fmt.Printf("Usage: %+v\n", result.Usage)
fmt.Printf("Steps: %d\n", len(result.Steps))
},
})

ToolOrder​

ToolOrder controls the order tools are sent to providers after active-tool filtering, middleware transforms, and PrepareStep updates. Listed names are sent first in the provided order; unlisted tools follow alphabetically.

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Inspect the user account.",
Tools: tools,
ToolOrder: []string{"readProfile", "listOrders"},
})

OnStepEnd​

OnStepEnd is the canonical callback name for step completion. OnStepFinish remains as a deprecated compatibility alias; when both are set, OnStepEnd wins. The same migration applies to typed event callbacks: use OnStepEndEvent instead of OnStepFinishEvent.

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Use a tool, then summarize.",
Tools: tools,
StopWhen: []ai.StopCondition{ai.IsStepCount(5)},
OnStepEnd: func(ctx context.Context, step types.StepResult, userContext interface{}) {
fmt.Println(step.StepNumber, step.FinishReason, step.Usage)
},
})

StreamText​

Depending on your model and prompt, it can take a large language model (LLM) up to a minute to finish generating its response. This delay can be unacceptable for interactive use cases such as chatbots or real-time applications, where users expect immediate responses.

The Go AI SDK Core provides the ai.StreamText() function which simplifies streaming text from LLMs:

result, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Invent a new holiday and describe its traditions.",
})
if err != nil {
log.Fatal(err)
}
defer result.Close()

// Stream text chunks as they arrive
for chunk := range result.Chunks() {
fmt.Print(chunk.Text)
}

Note: StreamText immediately starts streaming and errors become part of the stream. Use the OnError callback to log errors.

Note: StreamText uses backpressure and only generates tokens as they are requested. You need to consume the channel for the stream to finish.

Stream Result Object​

The stream result object provides access to the streamed data:

// StreamTextResult provides methods to access streamed data:

// Streaming methods (available immediately)
result.Chunks() <-chan provider.StreamChunk // Channel for receiving chunks
result.Close() error // Close the stream
result.ReadAll() (string, error) // Read all text (blocks until complete)

// Accessor methods (available after stream completes)
result.Text() string // Complete generated text
result.Content() []types.ContentPart // Ordered content across all steps
result.FinishReason() types.FinishReason // Reason the model finished
result.Usage() types.Usage // Token usage information
result.ToolCalls() []types.ToolCall // Tool calls made during streaming
result.ToolResults() []types.ToolResult // Tool results from executed tools
result.ContextManagement() interface{} // Context management stats (Anthropic)
result.Err() error // Error if stream failed

Standalone Stream Helpers​

For new code, use result.Stream() with the package-level helpers. The result-bound helpers are retained only as deprecated compatibility wrappers.

textStream, errStream := ai.ToTextStream(ctx, result.Stream())
for text := range textStream {
fmt.Print(text)
}
if err := <-errStream; err != nil {
log.Fatal(err)
}

Use ai.ToUIMessageStream(ctx, result.Stream(), ...) to convert provider stream chunks into UI message chunks. result.FullStream() remains as a deprecated alias for result.Stream().

Channel-Based Streaming​

The Go AI SDK uses Go channels for streaming, providing a natural and idiomatic way to handle streaming data:

result, _ := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Write a story",
})
defer result.Close()

// Channel closes automatically when done
for chunk := range result.Chunks() {
if chunk.Type == provider.ChunkTypeError {
fmt.Printf("Error: %s\n", chunk.AbortReason)
break
}
fmt.Print(chunk.Text)
}

OnError Callback​

StreamText immediately starts streaming to enable sending data without waiting for the model. Errors become part of the stream and are not returned to prevent servers from crashing.

To log errors, you can provide an OnError callback that is triggered when an error occurs:

result, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Generate text",
OnError: func(ctx context.Context, err error) {
log.Printf("Stream error: %v", err) // Your error logging logic here
},
})

OnChunk Callback​

When using StreamText, you can provide an OnChunk callback that is triggered for each chunk of the stream.

It receives the following chunk types:

  • text - Text content
  • tool-call - Tool call
  • tool-result - Tool result
  • finish - Stream finish
result, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Generate text",
OnChunk: func(chunk provider.StreamChunk) {
// Implement your own logic here
if chunk.Type == provider.ChunkTypeText {
fmt.Print(chunk.Text)
} else if chunk.Type == provider.ChunkTypeToolCall {
fmt.Printf("\n[Tool call: %s]\n", chunk.ToolCall.ToolName)
}
},
})

OnFinish Callback​

When using StreamText, you can provide an OnFinish callback that is triggered when the stream is finished. It contains the text, usage information, finish reason, and more:

result, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Generate text",
OnFinish: func(result *ai.StreamTextResult) {
// Your own logic, e.g. for saving the chat history or recording usage
fmt.Printf("\n\nText: %s\n", result.Text())
fmt.Printf("Finish reason: %s\n", result.FinishReason())
fmt.Printf("Usage: %+v\n", result.Usage())
},
})

Accumulating Streamed Text​

If you need the complete text after streaming, you can use ReadAll():

result, _ := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Write a story",
})
defer result.Close()

// Option 1: Use ReadAll (blocks until complete)
fullText, err := result.ReadAll()
if err != nil {
log.Fatal(err)
}
fmt.Println(fullText)

// Option 2: Manually accumulate
var builder strings.Builder
for chunk := range result.Chunks() {
fmt.Print(chunk.Text) // Display to user
builder.WriteString(chunk.Text) // Accumulate
}
completeText := builder.String()

Context Cancellation​

Streaming respects context cancellation, allowing you to stop generation early:

ctx, cancel := context.WithCancel(context.Background())

go func() {
// Cancel after 5 seconds
time.Sleep(5 * time.Second)
cancel()
}()

result, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Write a very long story...",
})
if err != nil {
log.Fatal(err)
}
defer result.Close()

for chunk := range result.Chunks() {
select {
case <-ctx.Done():
fmt.Println("\n\nCancelled!")
return
default:
fmt.Print(chunk.Text)
}
}

Advanced Features​

Multi-Step Generation with Tools​

Both GenerateText and StreamText support multi-step generation with automatic tool execution:

weatherTool := types.Tool{
Name: "get_weather",
Description: "Get weather for a location",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"location": map[string]interface{}{"type": "string"},
},
"required": []string{"location"},
},
Execute: func(ctx context.Context, input map[string]interface{}, opts types.ToolExecutionOptions) (interface{}, error) {
location := input["location"].(string)
return map[string]interface{}{
"temperature": 72,
"condition": "sunny",
"location": location,
}, nil
},
}

maxSteps := 5 // Allow up to 5 steps of tool calling
result, _ := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "What's the weather in London?",
Tools: []types.Tool{weatherTool},
MaxSteps: &maxSteps,
})

fmt.Println(result.Text)
// Output: The weather in London is sunny with a temperature of 72°F.

See Tools and Tool Calling for more details.

Step-by-Step Processing​

Access intermediate steps in multi-step generations:

maxSteps := 10
result, _ := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "What's the weather in Tokyo and Paris?",
Tools: []types.Tool{weatherTool},
MaxSteps: &maxSteps,
OnStepFinish: func(ctx context.Context, step types.StepResult, userContext interface{}) {
fmt.Printf("Step %d finished\n", step.StepNumber)
for _, toolCall := range step.ToolCalls {
fmt.Printf(" Tool: %s\n", toolCall.ToolName)
}
for _, toolResult := range step.ToolResults {
fmt.Printf(" Result: %v\n", toolResult.Result)
}
},
})

// Access all steps after completion
for i, step := range result.Steps {
fmt.Printf("Step %d: %d tool calls, %d results\n",
i+1, len(step.ToolCalls), len(step.ToolResults))
}

Sources​

Some providers such as Perplexity and Google Generative AI include sources in the response. These are web pages that ground the response.

You can access them using the Sources field of the result:

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "List the top 5 San Francisco news from the past week.",
})
if err != nil {
log.Fatal(err)
}

for _, source := range result.Sources {
if source.SourceType == "url" {
fmt.Printf("ID: %s\n", source.ID)
fmt.Printf("Title: %s\n", source.Title)
fmt.Printf("URL: %s\n", source.URL)
fmt.Println()
}
}

When streaming, sources are available both as ChunkTypeSource chunks and via result.Sources() after the stream completes:

result, _ := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Latest news",
})
defer result.Close()

for chunk := range result.Chunks() {
if chunk.Type == provider.ChunkTypeSource && chunk.SourceContent != nil {
if chunk.SourceContent.SourceType == "url" {
fmt.Printf("Source: %s - %s\n", chunk.SourceContent.Title, chunk.SourceContent.URL)
}
}
}

// Or access all sources after streaming completes
for _, source := range result.Sources() {
fmt.Printf("Source: %s - %s\n", source.Title, source.URL)
}

Message-Based Generation​

For chat applications, use message-based generation:

import "github.com/digitallysavvy/go-ai/pkg/provider/types"

messages := []types.Message{
{Role: types.RoleUser, Content: []types.ContentPart{types.TextContent{Text: "Hi!"}}},
{Role: types.RoleAssistant, Content: []types.ContentPart{types.TextContent{Text: "Hello! How can I help?"}}},
{Role: types.RoleUser, Content: []types.ContentPart{types.TextContent{Text: "Tell me a joke."}}},
}

result, _ := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Messages: messages,
})

fmt.Println(result.Text)

Settings​

Control generation behavior with various settings:

temperature := 0.8
maxTokens := 500
topP := 0.9
topK := 40

result, _ := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Generate creative text",
Temperature: &temperature, // Higher = more creative
MaxTokens: &maxTokens, // Limit response length
TopP: &topP, // Nucleus sampling
TopK: &topK, // Top-k sampling
StopSequences: []string{"\n\n"}, // Stop at double newline
})

See Settings for all available options.

Error Handling​

Handle errors appropriately:

import providererrors "github.com/digitallysavvy/go-ai/pkg/provider/errors"

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Generate text",
})
if err != nil {
// Check for specific error types
var rateLimitErr *providererrors.RateLimitError
if errors.As(err, &rateLimitErr) {
fmt.Println("Rate limit exceeded, retry later")
return
}
if providererrors.IsValidationError(err) {
fmt.Println("Invalid request parameters")
return
}
log.Fatal(err)
}

See Error Handling for comprehensive error handling strategies.

Best Practices​

  1. Use Context: Always pass context for cancellation and timeouts
  2. Close Streams: Use defer result.Close() for streaming
  3. Handle Errors: Check errors from both function calls and stream chunks
  4. Choose Wisely: Use GenerateText for short responses, StreamText for long ones
  5. Set Limits: Use MaxTokens to control costs and response length
  6. Monitor Usage: Track token usage for cost management
  7. Cache Results: Consider caching for repeated queries

Examples​

Basic Text Generation​

result, _ := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Explain quantum entanglement in simple terms.",
})
fmt.Println(result.Text)

Streaming with Progress​

result, _ := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Write a long article about AI",
})
defer result.Close()

tokenCount := 0
for chunk := range result.Chunks() {
fmt.Print(chunk.Text)
tokenCount += len(strings.Fields(chunk.Text))
fmt.Printf("\r[Tokens: ~%d]", tokenCount)
}
fmt.Println("\nComplete!")

Concurrent Generation​

prompts := []string{
"Explain photosynthesis",
"Explain gravity",
"Explain evolution",
}

var wg sync.WaitGroup
results := make([]string, len(prompts))

for i, prompt := range prompts {
wg.Add(1)
go func(idx int, p string) {
defer wg.Done()
result, _ := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: p,
})
results[idx] = result.Text
}(i, prompt)
}

wg.Wait()

for i, result := range results {
fmt.Printf("%d: %s\n\n", i+1, result)
}

Custom Content Types​

Some providers return ordered content that carries provider-specific metadata. The Go AI SDK preserves that metadata on output content as ProviderMetadata; when the content is replayed to a provider as response messages, it is converted to provider-facing ProviderOptions, matching the TypeScript SDK.

This applies to standard content such as text, reasoning, tool-call, tool-result, and tool-error parts, and to provider-specific content that does not map to standard text, reasoning, or tool call types. The Go AI SDK represents provider-specific parts as CustomContent and ReasoningFileContent.

CustomContent​

CustomContent wraps provider-specific response parts that have no standard mapping (e.g., XAI citations, OpenAI compaction events):

import "github.com/digitallysavvy/go-ai/pkg/provider/types"

// CustomContent is emitted by providers for unknown/provider-specific parts
type CustomContent struct {
Kind string // e.g. "xai.citation", "openai.compaction"
ProviderOptions map[string]interface{} // input: provider-specific options to forward
ProviderMetadata json.RawMessage // output: raw JSON from provider
}
  • Kind identifies the content type using the format "{provider}.{type}" (e.g., "xai.citation")
  • ProviderMetadata carries the raw JSON returned by the provider (output direction)
  • ProviderOptions carries provider-specific options keyed by provider name (input direction)

ReasoningFileContent​

ReasoningFileContent carries reasoning output as a binary file (e.g., PDFs from Google models):

// ReasoningFileContent carries reasoning output as a binary file
type ReasoningFileContent struct {
MediaType string // e.g. "application/pdf", "image/png"
Data []byte // auto base64-encodes in JSON marshaling
ProviderOptions map[string]interface{} // input: provider-specific options to forward
ProviderMetadata json.RawMessage // output: raw JSON from provider
}
  • MediaType is the IANA media type of the file
  • Data holds raw bytes; Go's encoding/json automatically handles base64 encoding/decoding
  • ProviderOptions and ProviderMetadata serve the same input/output roles as on CustomContent

Stream Chunk Handling​

Both content types flow through stream chunks via OnChunk. Use ChunkTypeCustom and ChunkTypeReasoningFile to detect them:

import "github.com/digitallysavvy/go-ai/pkg/provider"

result, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Analyze this data with reasoning.",
OnChunk: func(chunk provider.StreamChunk) {
switch chunk.Type {
case provider.ChunkTypeText:
fmt.Print(chunk.Text)

case provider.ChunkTypeCustom:
// Provider-specific content
fmt.Printf("Custom content [%s]: %s\n",
chunk.CustomContent.Kind,
string(chunk.CustomContent.ProviderMetadata))

case provider.ChunkTypeReasoningFile:
// Reasoning output as file
fmt.Printf("Reasoning file: %s (%d bytes)\n",
chunk.ReasoningFileContent.MediaType,
len(chunk.ReasoningFileContent.Data))
}
},
})
if err != nil {
log.Fatal(err)
}
defer result.Close()

for range result.Chunks() {
}

Multi-Turn Behavior​

In multi-turn conversations, CustomContent and ReasoningFileContent appear in assistant messages within the conversation history:

  • ProviderOptions is forwarded back to the provider in subsequent requests. This allows provider-specific state (e.g., cached references) to round-trip correctly.
  • ProviderMetadata is output-only and is not re-sent to the provider. It is preserved in conversation history for logging and debugging.

Next Steps​

See Also​