Skip to content

The layers

ai — Talk to a Model

One call in, one message out: providers, models, messages, tools, and streaming without an agent loop

Read this when
  • Making your first model call with pi-go
  • Deciding whether you need the agent loop at all
  • Building a mental model of the types every other layer reuses

You can make one model call with pkg/ai and keep full control over the prompt.

pkg/ai
fig. 01 — One call through pkg/ai

You build an ai.Prompt. A bound ai.LanguageModel sends it to its provider. One stream returns the live events and the final ai.Message.

model := ai.NewLanguageModel(modelInfo, provider)
stream := model.StreamText(ctx, ai.Prompt{
System: "Answer in one sentence.",
Messages: []ai.Message{ai.UserMessage("Why is Go good for agents?")},
}, ai.WithMaxTokens(200))
for event, err := range stream.Events() {
// text_delta, thinking_delta, tool_delta, ...
}
msg, err := stream.Wait()

This layer makes one request and returns one response. It runs no turns, keeps no history, and finds no credentials. Those jobs belong to the layers above.

A message is a list of content blocks

for _, block := range msg.Content {
if text, ok := ai.AsContent[ai.Text](block); ok {
fmt.Println(text.Text)
}
// ai.Thinking, ai.Image, ai.File, ai.ToolCall
}

UserMessage, UserImageMessage, AssistantMessage, and ToolResultMessage build the roles. Every layer above stores these same values.

You run the tools at this layer

weather := ai.DefineTool(
"weather",
"Reports the current temperature for a city.",
func(ctx context.Context, in Query) (Report, error) { /* ... */ },
)
prompt := ai.Prompt{
Messages: history,
Tools: []ai.ToolInfo{weather.Info()},
}

DefineTool derives the JSON Schema from the Go types. DefineParallelTool marks a tool that is safe to run beside other tools. DefineServerTool names a tool that the provider hosts and runs itself.

pkg/ai
fig. 02 — The tool round trip, by hand

The model returns tool calls. Your code runs them, appends a ai.ToolResultMessage for each one, and calls the model again. pkg/agent exists to own this loop for you.

Options ride with the request

model.StreamText(ctx, prompt,
ai.WithTemperature(0.2),
ai.WithMaxTokens(1024),
ai.WithThinking(ai.ThinkingHigh),
ai.WithToolChoice(ai.ToolChoiceRequired),
ai.WithCacheRetention(ai.CacheRetentionLong),
)

Each provider maps these values to its own API fields. Prompt caching is on by default where a provider supports it.

Usage arrives on the final message

msg.Usage.Input // prompt tokens
msg.Usage.CacheRead // tokens served from the cache
msg.Usage.Reasoning // thinking tokens
msg.Usage.Cost.Output // USD, when the adapter knows the price

Usage.Add sums two values category by category. The layers above use it to accumulate across turns and runs.

Go deeper