7. **`Result()` on streams is blocking** - it waits for the entire stream to complete. Call it after consuming `TextStream()`/`Stream()`, or on its own if you don't need incremental output.
Resources
15Install
npx skillscat add zendev-sh/goai Install via the SkillsCat registry.
GoAI SDK - AI Agent Skill
Instructions for AI coding agents helping developers use the GoAI SDK.
Load this file when a user's project importsgithub.com/zendev-sh/goai.
Overview
GoAI is a Go SDK for AI applications. One unified API across 22+ LLM providers. Inspired by the Vercel AI SDK, adapted to Go idioms (generics, interfaces, channels).
- Package:
github.com/zendev-sh/goai - Go version: 1.25+
- Dependencies: stdlib +
golang.org/x/oauth2for Vertex AI. Optionalobservability/otelsubmodule adds OTel SDK (separate go.mod, not pulled unless imported). - Docs: https://goai.sh
- GoDoc: https://pkg.go.dev/github.com/zendev-sh/goai
Quick Reference
go get github.com/zendev-sh/goai@latestimport (
"github.com/zendev-sh/goai"
"github.com/zendev-sh/goai/provider/openai" // or any provider
)Core API
7 Top-Level Functions
| Function | Purpose | Returns |
|---|---|---|
goai.GenerateText(ctx, model, opts...) |
Non-streaming text generation | (*TextResult, error) |
goai.StreamText(ctx, model, opts...) |
Streaming text via channels | (*TextStream, error) |
goai.GenerateObject[T](ctx, model, opts...) |
Typed structured output (JSON) | (*ObjectResult[T], error) |
goai.StreamObject[T](ctx, model, opts...) |
Streaming structured output | (*ObjectStream[T], error) |
goai.Embed(ctx, model, text, opts...) |
Single text embedding | (*EmbedResult, error) |
goai.EmbedMany(ctx, model, texts, opts...) |
Batch embeddings (auto-chunked) | (*EmbedManyResult, error) |
goai.GenerateImage(ctx, model, imgOpts...) |
Image generation | (*ImageResult, error) |
Model Constructors
Each provider has Chat(), and optionally Embedding() and Image():
// Language models
openai.Chat("gpt-4o")
anthropic.Chat("claude-sonnet-4-20250514")
google.Chat("gemini-2.5-flash")
bedrock.Chat("anthropic.claude-sonnet-4-20250514-v1:0")
azure.Chat("gpt-4o", azure.WithEndpoint("https://my-resource.openai.azure.com"))
vertex.Chat("gemini-2.5-flash", vertex.WithProject("my-project"), vertex.WithLocation("us-central1"))
groq.Chat("llama-3.3-70b-versatile")
ollama.Chat("llama3.2")
// Embedding models
openai.Embedding("text-embedding-3-small")
google.Embedding("text-embedding-004")
cohere.Embedding("embed-english-v3.0")
ollama.Embedding("nomic-embed-text")
// Image models
openai.Image("gpt-image-1")
google.Image("imagen-4.0-generate-001")Auth - Auto-Resolved from Environment
Providers auto-read API keys from env vars. No explicit config needed:
| Provider | Env Var |
|---|---|
| OpenAI | OPENAI_API_KEY |
| Anthropic | ANTHROPIC_API_KEY |
GOOGLE_GENERATIVE_AI_API_KEY or GEMINI_API_KEY |
|
| Bedrock | AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_REGION |
| Azure | AZURE_OPENAI_API_KEY |
| Vertex AI | Application Default Credentials (ADC) |
| xAI | XAI_API_KEY |
| Groq | GROQ_API_KEY |
| Cohere | COHERE_API_KEY |
| Mistral | MISTRAL_API_KEY |
| DeepSeek | DEEPSEEK_API_KEY |
Or set explicitly:
model := openai.Chat("gpt-4o", openai.WithAPIKey("sk-..."))Most providers support these options (Bedrock uses AWS credential options; Ollama requires no auth):
provider.WithAPIKey(key) // static API key
provider.WithTokenSource(ts) // dynamic auth (OAuth, service accounts)
provider.WithBaseURL(url) // override endpoint (Azure uses WithEndpoint)
provider.WithHeaders(h) // custom HTTP headers
provider.WithHTTPClient(c) // custom HTTP transportPatterns and Examples
1. Basic Text Generation
result, err := goai.GenerateText(ctx, openai.Chat("gpt-4o"),
goai.WithSystem("You are a helpful assistant."),
goai.WithPrompt("What is Go?"),
)
if err != nil {
return err
}
fmt.Println(result.Text)2. Streaming
stream, err := goai.StreamText(ctx, openai.Chat("gpt-4o"),
goai.WithPrompt("Write a poem about Go."),
)
if err != nil {
return err
}
// Option A: text-only channel
for text := range stream.TextStream() {
fmt.Print(text)
}
// Option B: raw chunks (mutually exclusive with TextStream)
// for chunk := range stream.Stream() { ... }
// Always available after streaming completes:
result := stream.Result()
fmt.Printf("\nTokens: %d in, %d out\n", result.TotalUsage.InputTokens, result.TotalUsage.OutputTokens)Important: Stream() and TextStream() are mutually exclusive. Only call one. Result() can always be called after either.
3. Structured Output (Generics)
type Recipe struct {
Name string `json:"name" jsonschema:"description=Recipe name"`
Ingredients []string `json:"ingredients"`
Steps []string `json:"steps"`
Difficulty string `json:"difficulty" jsonschema:"enum=easy|medium|hard"`
}
result, err := goai.GenerateObject[Recipe](ctx, openai.Chat("gpt-4o"),
goai.WithPrompt("Chocolate chip cookies recipe"),
)
if err != nil {
return err
}
fmt.Println(result.Object.Name) // typed access
fmt.Println(result.Object.Difficulty) // "easy", "medium", or "hard"Schema is auto-generated from struct tags. Supported tags:
json:"name"- property namejson:"-"- exclude fieldjsonschema:"description=..."- adds descriptionjsonschema:"enum=a|b|c"- restricts to enum values
Supported types: string, bool, int/uint (all sizes), float32/64, slices, maps (string keys), structs (embedded structs flattened), pointers (nullable).
4. Streaming Structured Output
stream, err := goai.StreamObject[Recipe](ctx, model,
goai.WithPrompt("Chocolate chip cookies recipe"),
)
if err != nil {
return err
}
for partial := range stream.PartialObjectStream() {
fmt.Printf("Name so far: %s\n", partial.Name)
}
result, err := stream.Result()
// result.Object is the final validated Recipe5. Tool Calling
weatherTool := goai.Tool{
Name: "get_weather",
Description: "Get weather for a city.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}`),
Execute: func(ctx context.Context, input json.RawMessage) (string, error) {
var params struct {
City string `json:"city"`
}
if err := json.Unmarshal(input, ¶ms); err != nil {
return "", err
}
return fmt.Sprintf("72F and sunny in %s", params.City), nil
},
}
result, err := goai.GenerateText(ctx, model,
goai.WithPrompt("What's the weather in Tokyo?"),
goai.WithTools(weatherTool),
goai.WithMaxSteps(3), // enable auto tool loop
)Key rules:
WithMaxSteps(1)(default) = no auto loop, tool calls returned inresult.ToolCallsWithMaxSteps(n)where n > 1 = auto loop: generate -> execute tools -> re-generate -> repeat- Tools without
Executeare sent as definitions only (for manual handling) - Tool errors are passed back to the model as
"error: ..."messages
6. Conversation History
result, err := goai.GenerateText(ctx, model,
goai.WithSystem("You are a helpful assistant."),
goai.WithMessages(
goai.UserMessage("Hi, my name is Alice."),
goai.AssistantMessage("Hello Alice! How can I help you?"),
goai.UserMessage("What's my name?"),
),
)Message builders:
goai.SystemMessage(text)- system messagegoai.UserMessage(text)- user messagegoai.AssistantMessage(text)- assistant messagegoai.ToolMessage(toolCallID, toolName, output)- tool result
7. Embeddings
// Single embedding
model := openai.Embedding("text-embedding-3-small")
result, err := goai.Embed(ctx, model, "Hello world")
fmt.Printf("Dimensions: %d\n", len(result.Embedding))
// Batch embeddings (auto-chunked, parallel)
results, err := goai.EmbedMany(ctx, model, []string{"text1", "text2", "text3"},
goai.WithMaxParallelCalls(4),
)8. Image Generation
result, err := goai.GenerateImage(ctx, openai.Image("gpt-image-1"),
goai.WithImagePrompt("A gopher writing Go code"),
goai.WithImageSize("1024x1024"),
goai.WithImageCount(1),
)
// result.Images[0].Data = raw bytes, result.Images[0].MediaType = "image/png"Note: GenerateImage uses ImageOption (not Option): WithImagePrompt, WithImageCount, WithImageSize, WithAspectRatio, WithImageMaxRetries, WithImageTimeout, WithImageProviderOptions.
9. Provider-Defined Tools (Server-Side)
Built-in tools executed by the provider, not your code:
// OpenAI web search
result, err := goai.GenerateText(ctx, openai.Chat("gpt-4o"),
goai.WithPrompt("Latest Go release?"),
goai.WithTools(providerTool(openai.Tools.WebSearch())),
)
// Google Search grounding
result, err := goai.GenerateText(ctx, google.Chat("gemini-2.5-flash"),
goai.WithPrompt("Latest news about Go"),
goai.WithTools(providerTool(google.Tools.GoogleSearch())),
)
// Anthropic code execution
result, err := goai.GenerateText(ctx, anthropic.Chat("claude-sonnet-4-20250514"),
goai.WithPrompt("Calculate fibonacci(10)"),
goai.WithTools(providerTool(anthropic.Tools.CodeExecution())),
)
// Helper to convert provider.ToolDefinition → goai.Tool:
func providerTool(td provider.ToolDefinition) goai.Tool {
return goai.Tool{
Name: td.Name, ProviderDefinedType: td.ProviderDefinedType,
ProviderDefinedOptions: td.ProviderDefinedOptions,
}
}Available provider tools:
- OpenAI:
WebSearch(),CodeInterpreter(),ImageGeneration(),FileSearch(opts...) - Anthropic:
WebSearch(),WebFetch(),Computer(opts),Bash(),TextEditor(),CodeExecution()(+ versioned variants) - Google:
GoogleSearch(),URLContext(),CodeExecution() - xAI:
WebSearch(),XSearch() - Groq:
BrowserSearch()
10. Observability Hooks
result, err := goai.GenerateText(ctx, model,
goai.WithPrompt("Hello"),
goai.WithOnRequest(func(info goai.RequestInfo) {
log.Printf("Calling %s with %d messages", info.Model, info.MessageCount)
}),
goai.WithOnResponse(func(info goai.ResponseInfo) {
log.Printf("Response in %v, tokens: %d", info.Latency, info.Usage.TotalTokens)
}),
goai.WithOnStepFinish(func(step goai.StepResult) {
log.Printf("Step %d: %s, tools: %d", step.Number, step.FinishReason, len(step.ToolCalls))
}),
goai.WithOnToolCall(func(info goai.ToolCallInfo) {
log.Printf("Tool %s (step %d) took %v, input: %s", info.ToolName, info.Step, info.Duration, info.Input)
}),
)Complete Options Reference
Core Options (type goai.Option)
goai.WithSystem(s string) // system prompt
goai.WithPrompt(s string) // single user message (shorthand)
goai.WithMessages(msgs ...provider.Message) // conversation history
goai.WithPromptCaching(bool) // enable prompt caching
goai.WithTools(tools ...goai.Tool) // available tools
goai.WithMaxSteps(n int) // auto tool loop iterations (default: 1)
goai.WithToolChoice(tc string) // "auto" | "none" | "required" | "<tool_name>"
goai.WithMaxOutputTokens(n int) // response length limit
goai.WithTemperature(t float64) // randomness (0.0 - 2.0)
goai.WithTopP(p float64) // nucleus sampling
goai.WithTopK(k int) // top-K sampling
goai.WithFrequencyPenalty(p float64) // frequency penalty
goai.WithPresencePenalty(p float64) // presence penalty
goai.WithSeed(s int) // deterministic generation
goai.WithStopSequences(seqs ...string) // stop sequences
goai.WithMaxRetries(n int) // retry count (default: 2)
goai.WithTimeout(d time.Duration) // request timeout
goai.WithHeaders(h map[string]string) // additional HTTP headers
goai.WithProviderOptions(opts map[string]any) // provider-specific params
goai.WithOnRequest(fn func(RequestInfo)) // before each API call
goai.WithOnResponse(fn func(ResponseInfo)) // after each API call
goai.WithOnStepFinish(fn func(StepResult)) // after each step
goai.WithOnToolCallStart(fn func(ToolCallStartInfo)) // before each tool execution
goai.WithOnToolCall(fn func(ToolCallInfo)) // after each tool execution
goai.WithExplicitSchema(schema json.RawMessage) // override auto-generated schema
goai.WithSchemaName(name string) // schema name (default: "response")
goai.WithMaxParallelCalls(n int) // EmbedMany parallelism (default: 4)
goai.WithEmbeddingProviderOptions(opts map[string]any) // embedding-specific paramsImage Options (type goai.ImageOption)
goai.WithImagePrompt(prompt string) // text prompt
goai.WithImageCount(n int) // number of images (default: 1)
goai.WithImageSize(size string) // e.g. "1024x1024"
goai.WithAspectRatio(ratio string) // e.g. "16:9"
goai.WithImageMaxRetries(n int) // retries on 429/5xx
goai.WithImageTimeout(d time.Duration) // overall timeout
goai.WithImageProviderOptions(opts map[string]any) // provider-specific paramsResult Types
TextResult (from GenerateText / StreamText)
type TextResult struct {
Text string // accumulated generated text
ToolCalls []provider.ToolCall // tool calls from final step
Steps []StepResult // per-step results
TotalUsage provider.Usage // aggregated token usage
FinishReason provider.FinishReason // "stop", "tool-calls", "length", etc.
Response provider.ResponseMetadata // provider metadata (ID, Model)
ProviderMetadata map[string]map[string]any // provider-specific response data
Sources []provider.Source // citations/references
}ObjectResult[T] (from GenerateObject / StreamObject)
type ObjectResult[T any] struct {
Object T // the parsed typed object
Usage provider.Usage // total token consumption across all steps
FinishReason provider.FinishReason
Response provider.ResponseMetadata
ProviderMetadata map[string]map[string]any // provider-specific response data
Steps []StepResult // results from each generation step (multi-step tool loops)
}EmbedResult / EmbedManyResult
type EmbedResult struct {
Embedding []float64
Usage provider.Usage
}
type EmbedManyResult struct {
Embeddings [][]float64
Usage provider.Usage
}ImageResult
type ImageResult struct {
Images []provider.ImageData // .Data = []byte, .MediaType = "image/png"
}provider.Usage
type Usage struct {
InputTokens int
OutputTokens int
TotalTokens int
ReasoningTokens int
CacheReadTokens int
CacheWriteTokens int
}Error Handling
import "errors"
result, err := goai.GenerateText(ctx, model, goai.WithPrompt("..."))
if err != nil {
var apiErr *goai.APIError
var overflowErr *goai.ContextOverflowError
if errors.As(err, &overflowErr) {
// Prompt too long for context window - truncate messages and retry
log.Printf("Context overflow: %s", overflowErr.Message)
} else if errors.As(err, &apiErr) {
log.Printf("API error %d: %s (retryable: %v)", apiErr.StatusCode, apiErr.Message, apiErr.IsRetryable)
} else {
log.Printf("Error: %v", err)
}
}Always use errors.As(), never type assertion.
Built-in retry: transient errors (429, 5xx) are retried automatically up to WithMaxRetries(2) times with exponential backoff.
TokenSource (Dynamic Auth)
For OAuth, service accounts, or rotating credentials:
ts := provider.CachedTokenSource(func(ctx context.Context) (*provider.Token, error) {
// Fetch token from your auth system
token, err := myAuthClient.GetToken(ctx)
if err != nil {
return nil, err
}
return &provider.Token{
Value: token.AccessToken,
ExpiresAt: token.ExpiresAt,
}, nil
})
model := openai.Chat("gpt-4o", openai.WithTokenSource(ts))provider.StaticToken(key)- wraps a static stringprovider.CachedTokenSource(fetchFn)- caches until expiry, thread-safe
Supported Providers
Tier 1 (dedicated implementations)
| Provider | Import | Chat | Embed | Image | Provider Tools |
|---|---|---|---|---|---|
| OpenAI | provider/openai |
Yes | Yes | Yes | 4 |
| Anthropic | provider/anthropic |
Yes | - | - | 10 |
| Google Gemini | provider/google |
Yes | Yes | Yes | 3 |
| AWS Bedrock | provider/bedrock |
Yes | Yes | - | - |
| Azure OpenAI | provider/azure |
Yes | - | Yes | - |
| Vertex AI | provider/vertex |
Yes | Yes | Yes | - |
Tier 2
| Provider | Import | Chat | Embed | Provider Tools |
|---|---|---|---|---|
| Cohere | provider/cohere |
Yes | Yes | - |
| Mistral | provider/mistral |
Yes | - | - |
| xAI (Grok) | provider/xai |
Yes | - | 2 |
| Groq | provider/groq |
Yes | - | 1 |
| DeepSeek | provider/deepseek |
Yes | - | - |
Tier 3 (OpenAI-compatible)
Fireworks, Together, DeepInfra, OpenRouter, Perplexity, Cerebras
Local
| Provider | Import | Chat | Embed | Default URL |
|---|---|---|---|---|
| Ollama | provider/ollama |
Yes | Yes | localhost:11434/v1 |
| vLLM | provider/vllm |
Yes | Yes | localhost:8000/v1 |
| RunPod | provider/runpod |
Yes | - | RUNPOD_ENDPOINT_ID |
| Custom | provider/compat |
Yes | Yes | user-defined |
Common Mistakes to Avoid
Don't use
WithPromptandWithMessageswith user message together carelessly -WithPrompt("x")is auto-wrapped as aUserMessageand prepended toMessages. Use one or the other for the initial user input.Don't call both
Stream()andTextStream()- they are mutually exclusive onTextStream. The second call returns a closed channel.Don't forget
WithMaxStepsfor tool loops - default is 1 (no loop). SetWithMaxSteps(n)where n > 1 to enable automatic tool execution.Don't type-assert errors - always use
errors.As(err, &target), nevererr.(*goai.APIError).Don't mix
OptionandImageOption-GenerateImageusesImageOption, all other functions useOption.Don't forget error handling on streaming - always check the error from
StreamText/StreamObjectbefore consuming the stream.Result()on streams is blocking - it waits for the entire stream to complete. Call it after consumingTextStream()/Stream(), or on its own if you don't need incremental output.