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

- [`ai.GenerateText()`](#generatetext) - Generates text for a given prompt and model.
- [`ai.StreamText()`](#streamtext) - Streams text from a given prompt and model.

Advanced LLM features such as [tool calling](https://goaisdk.com/docs/ai-sdk-core/tools-and-tool-calling.md) and [structured data generation](https://goaisdk.com/docs/ai-sdk-core/generating-structured-data.md) are built on top of text generation.

## GenerateText

You can generate text using the [`ai.GenerateText()`](https://goaisdk.com/docs/reference/ai/generate-text.md) 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.

```go
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](https://goaisdk.com/docs/foundations/prompts.md) to generate text with more complex instructions and content:

```go
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:

```go
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:

```go
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:

```go
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.

```go
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`.

```go
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()`](https://goaisdk.com/docs/reference/ai/stream-text.md) function which simplifies streaming text from LLMs:

```go
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:

```go
// 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.

```go
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:

```go
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:

```go
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

```go
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:

```go
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()`:

```go
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:

```go
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:

```go
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](https://goaisdk.com/docs/ai-sdk-core/tools-and-tool-calling.md) for more details.

### Step-by-Step Processing

Access intermediate steps in multi-step generations:

```go
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:

```go
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:

```go
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:

```go
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:

```go
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](https://goaisdk.com/docs/ai-sdk-core/settings.md) for all available options.

## Error Handling

Handle errors appropriately:

```go
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](https://goaisdk.com/docs/ai-sdk-core/error-handling.md) 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

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

### Streaming with Progress

```go
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

```go
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):

```go
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):

```go
// 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:

```go
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

- Learn about [Generating Structured Data](https://goaisdk.com/docs/ai-sdk-core/generating-structured-data.md)
- Explore [Tools and Tool Calling](https://goaisdk.com/docs/ai-sdk-core/tools-and-tool-calling.md)
- Build [Agents](https://goaisdk.com/docs/agents/overview.md)

## See Also

- [Prompts](https://goaisdk.com/docs/foundations/prompts.md)
- [Streaming](https://goaisdk.com/docs/foundations/streaming.md)
- [Settings](https://goaisdk.com/docs/ai-sdk-core/settings.md)
- [Error Handling](https://goaisdk.com/docs/ai-sdk-core/error-handling.md)
- [API Reference: GenerateText](https://goaisdk.com/docs/reference/ai/generate-text.md)
- [API Reference: StreamText](https://goaisdk.com/docs/reference/ai/stream-text.md)
