zendev-sh

GoAI SDK - AI Agent Skill

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.

zendev-sh 119 13 Updated 1mo ago

Resources

15
GitHub

Install

npx skillscat add zendev-sh/goai

Install via the SkillsCat registry.

SKILL.md

GoAI SDK - AI Agent Skill

Instructions for AI coding agents helping developers use the GoAI SDK.
Load this file when a user's project imports github.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).

Quick Reference

go get github.com/zendev-sh/goai@latest
import (
    "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 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 transport

Patterns 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 name
  • json:"-" - exclude field
  • jsonschema:"description=..." - adds description
  • jsonschema:"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 Recipe

5. 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, &params); 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 in result.ToolCalls
  • WithMaxSteps(n) where n > 1 = auto loop: generate -> execute tools -> re-generate -> repeat
  • Tools without Execute are 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 message
  • goai.UserMessage(text) - user message
  • goai.AssistantMessage(text) - assistant message
  • goai.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 params

Image 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 params

Result 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 string
  • provider.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

  1. Don't use WithPrompt and WithMessages with user message together carelessly - WithPrompt("x") is auto-wrapped as a UserMessage and prepended to Messages. Use one or the other for the initial user input.

  2. Don't call both Stream() and TextStream() - they are mutually exclusive on TextStream. The second call returns a closed channel.

  3. Don't forget WithMaxSteps for tool loops - default is 1 (no loop). Set WithMaxSteps(n) where n > 1 to enable automatic tool execution.

  4. Don't type-assert errors - always use errors.As(err, &target), never err.(*goai.APIError).

  5. Don't mix Option and ImageOption - GenerateImage uses ImageOption, all other functions use Option.

  6. Don't forget error handling on streaming - always check the error from StreamText/StreamObject before consuming the stream.

  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.