Skip to main content

Development Tools & Debugging

The Go-AI SDK provides several tools and techniques for debugging, observability, and testing your AI applications. This guide covers logging, request inspection, performance profiling, and testing utilities.

Debug Mode​

Enabling Debug Logging​

There is no built-in DebugMode flag. Every provider's Config accepts an HTTPClient override, so you can log requests and responses by supplying an http.Client with a logging http.RoundTripper (see Intercepting HTTP Requests below for the LoggingTransport implementation):

package main

import (
"context"
"log"
"net/http"
"os"

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

// simpleLoggingTransport logs the method and URL of every request.
// See LoggingTransport under "Intercepting HTTP Requests" below for a
// version that also logs headers and bodies.
type simpleLoggingTransport struct {
Transport http.RoundTripper
}

func (t *simpleLoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
log.Printf("Request: %s %s", req.Method, req.URL)
resp, err := t.Transport.RoundTrip(req)
if err == nil {
log.Printf("Response Status: %s", resp.Status)
}
return resp, err
}

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

// Log all HTTP requests and responses via a custom HTTPClient
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
HTTPClient: &http.Client{
Transport: &simpleLoggingTransport{Transport: http.DefaultTransport},
},
})

model, _ := provider.LanguageModel("gpt-4")

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

if err != nil {
log.Fatal(err)
}

log.Printf("Result: %s", result.Text)
}

Provider-Specific Debug Options​

The same HTTPClient override works for every provider:

// Anthropic
anthropicProvider := anthropic.New(anthropic.Config{
APIKey: os.Getenv("ANTHROPIC_API_KEY"),
HTTPClient: httpClient,
})

// Google
googleProvider := google.New(google.Config{
APIKey: os.Getenv("GOOGLE_GENERATIVE_AI_API_KEY"),
HTTPClient: httpClient,
})

// Bedrock does not expose an HTTPClient override; use Headers for
// provider-visible debug markers, or inspect traffic at the network layer.
bedrockProvider := bedrock.New(bedrock.Config{
Region: "us-east-1",
})

Request & Response Inspection​

Logging Request Details​

Inspect full request configuration before sending:

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-4")

temperature := 0.7
maxTokens := 500

options := ai.GenerateTextOptions{
Model: model,
Prompt: "Explain quantum computing",
Temperature: &temperature,
MaxTokens: &maxTokens,
System: "You are a physics professor.",
}

// Log request configuration
fmt.Println("=== Request Configuration ===")
fmt.Printf("Model: %T\n", options.Model)
fmt.Printf("Temperature: %.2f\n", *options.Temperature)
fmt.Printf("MaxTokens: %d\n", *options.MaxTokens)
fmt.Printf("System: %s\n", options.System)
fmt.Printf("Prompt: %s\n", options.Prompt)
fmt.Println()

result, err := ai.GenerateText(ctx, options)
if err != nil {
log.Fatal(err)
}

// Log response details
fmt.Println("=== Response Details ===")
fmt.Printf("Text: %s\n", result.Text)
fmt.Printf("Finish Reason: %s\n", result.FinishReason)
fmt.Printf("Model: %s\n", result.Response.ModelID)
fmt.Println()

// Log usage statistics
fmt.Println("=== Usage Statistics ===")
fmt.Printf("Prompt Tokens: %d\n", result.Usage.GetInputTokens())
fmt.Printf("Completion Tokens: %d\n", result.Usage.GetOutputTokens())
fmt.Printf("Total Tokens: %d\n", result.Usage.GetTotalTokens())
}

Intercepting HTTP Requests​

Use a custom HTTP client to intercept and log requests:

package main

import (
"bytes"
"context"
"io"
"log"
"net/http"
"os"

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

// LoggingTransport wraps http.RoundTripper to log requests
type LoggingTransport struct {
Transport http.RoundTripper
}

func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Log request
log.Printf("Request: %s %s", req.Method, req.URL)
log.Printf("Headers: %v", req.Header)

// Read and log body
if req.Body != nil {
body, _ := io.ReadAll(req.Body)
log.Printf("Body: %s", string(body))
req.Body = io.NopCloser(bytes.NewBuffer(body))
}

// Execute request
resp, err := t.Transport.RoundTrip(req)
if err != nil {
return nil, err
}

// Log response
log.Printf("Response Status: %s", resp.Status)
log.Printf("Response Headers: %v", resp.Header)

return resp, nil
}

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

// Create custom HTTP client with logging
httpClient := &http.Client{
Transport: &LoggingTransport{
Transport: http.DefaultTransport,
},
}

provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
HTTPClient: httpClient,
})

model, _ := provider.LanguageModel("gpt-4")

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

if err != nil {
log.Fatal(err)
}

log.Printf("Result: %s", result.Text)
}

Token Usage Monitoring​

Tracking Token Consumption​

Monitor token usage to optimize costs:

package main

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

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

type TokenTracker struct {
TotalPromptTokens int64
TotalCompletionTokens int64
TotalCost float64
}

func (t *TokenTracker) Track(result *ai.GenerateTextResult) {
t.TotalPromptTokens += result.Usage.GetInputTokens()
t.TotalCompletionTokens += result.Usage.GetOutputTokens()

// GPT-4 pricing (example)
promptCost := float64(result.Usage.GetInputTokens()) * 0.03 / 1000
completionCost := float64(result.Usage.GetOutputTokens()) * 0.06 / 1000
t.TotalCost += promptCost + completionCost
}

func (t *TokenTracker) Report() {
fmt.Println("=== Token Usage Report ===")
fmt.Printf("Total Prompt Tokens: %d\n", t.TotalPromptTokens)
fmt.Printf("Total Completion Tokens: %d\n", t.TotalCompletionTokens)
fmt.Printf("Total Tokens: %d\n", t.TotalPromptTokens+t.TotalCompletionTokens)
fmt.Printf("Estimated Cost: $%.4f\n", t.TotalCost)
}

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

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

tracker := &TokenTracker{}

// Make multiple requests
prompts := []string{
"What is Go?",
"Explain concurrency in Go",
"What are goroutines?",
}

for _, prompt := range prompts {
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: prompt,
})

if err != nil {
log.Fatal(err)
}

tracker.Track(result)
fmt.Printf("Prompt: %s\n", prompt)
fmt.Printf("Response: %s\n", result.Text)
fmt.Printf("Tokens: %d\n\n", result.Usage.GetTotalTokens())
}

tracker.Report()
}

Setting Token Budgets​

Enforce token limits to prevent runaway costs:

package main

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

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

type BudgetedAI struct {
Provider *openai.Provider
TokenBudget int64
TokensUsed int64
}

func (b *BudgetedAI) GenerateText(ctx context.Context, opts ai.GenerateTextOptions) (*ai.GenerateTextResult, error) {
if b.TokensUsed >= b.TokenBudget {
return nil, errors.New("token budget exceeded")
}

result, err := ai.GenerateText(ctx, opts)
if err != nil {
return nil, err
}

b.TokensUsed += result.Usage.GetTotalTokens()

fmt.Printf("Tokens used: %d/%d (%.1f%%)\n",
b.TokensUsed, b.TokenBudget,
float64(b.TokensUsed)/float64(b.TokenBudget)*100)

return result, nil
}

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

provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})

budgetedAI := &BudgetedAI{
Provider: provider,
TokenBudget: 1000, // Maximum 1000 tokens
}

model, _ := provider.LanguageModel("gpt-4")

result, err := budgetedAI.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Write a short story",
})

if err != nil {
log.Fatal(err)
}

fmt.Println(result.Text)
}

Performance Profiling​

Using Go's pprof​

Profile CPU and memory usage:

package main

import (
"context"
"log"
"os"
"runtime/pprof"

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

func main() {
// Start CPU profiling
cpuFile, err := os.Create("cpu.prof")
if err != nil {
log.Fatal(err)
}
defer cpuFile.Close()

pprof.StartCPUProfile(cpuFile)
defer pprof.StopCPUProfile()

// Your AI operations
ctx := context.Background()

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

for i := 0; i < 10; i++ {
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Hello!",
})

if err != nil {
log.Fatal(err)
}

log.Printf("Iteration %d: %s", i, result.Text)
}

// Write heap profile
heapFile, err := os.Create("heap.prof")
if err != nil {
log.Fatal(err)
}
defer heapFile.Close()

pprof.WriteHeapProfile(heapFile)
}

Analyze profiles:

# View CPU profile
go tool pprof cpu.prof

# View memory profile
go tool pprof heap.prof

# Generate visualization
go tool pprof -http=:8080 cpu.prof

Measuring Request Latency​

Track operation duration:

package main

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

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

type LatencyTracker struct {
Latencies []time.Duration
}

func (t *LatencyTracker) Track(duration time.Duration) {
t.Latencies = append(t.Latencies, duration)
}

func (t *LatencyTracker) Report() {
if len(t.Latencies) == 0 {
return
}

var total time.Duration
min := t.Latencies[0]
max := t.Latencies[0]

for _, d := range t.Latencies {
total += d
if d < min {
min = d
}
if d > max {
max = d
}
}

avg := total / time.Duration(len(t.Latencies))

fmt.Println("=== Latency Report ===")
fmt.Printf("Requests: %d\n", len(t.Latencies))
fmt.Printf("Min: %v\n", min)
fmt.Printf("Max: %v\n", max)
fmt.Printf("Avg: %v\n", avg)
}

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

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

tracker := &LatencyTracker{}

for i := 0; i < 5; i++ {
start := time.Now()

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

duration := time.Since(start)
tracker.Track(duration)

if err != nil {
log.Fatal(err)
}

fmt.Printf("Request %d: %v - %s\n", i+1, duration, result.Text)
}

tracker.Report()
}

Testing Utilities​

Mocking AI Responses​

Create mock providers for testing:

package main

import (
"context"
"testing"

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

// MockLanguageModel implements provider.LanguageModel
type MockLanguageModel struct {
Response string
Error error
}

func int64Ptr(v int64) *int64 {
return &v
}

func (m *MockLanguageModel) SpecificationVersion() string { return "v3" }
func (m *MockLanguageModel) Provider() string { return "mock" }
func (m *MockLanguageModel) ModelID() string { return "mock-model" }
func (m *MockLanguageModel) SupportsTools() bool { return false }
func (m *MockLanguageModel) SupportsStructuredOutput() bool { return false }
func (m *MockLanguageModel) SupportsImageInput() bool { return false }

func (m *MockLanguageModel) DoGenerate(ctx context.Context, opts *provider.GenerateOptions) (*types.GenerateResult, error) {
if m.Error != nil {
return nil, m.Error
}

return &types.GenerateResult{
Text: m.Response,
FinishReason: types.FinishReasonStop,
Usage: types.Usage{
InputTokens: int64Ptr(10),
OutputTokens: int64Ptr(20),
TotalTokens: int64Ptr(30),
},
}, nil
}

func (m *MockLanguageModel) DoStream(ctx context.Context, opts *provider.GenerateOptions) (provider.TextStream, error) {
return nil, nil // Implement if needed
}

// Example test
func TestGenerateText(t *testing.T) {
ctx := context.Background()

mock := &MockLanguageModel{
Response: "This is a mocked response",
}

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: mock,
Prompt: "Test prompt",
})

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

if result.Text != "This is a mocked response" {
t.Errorf("Expected 'This is a mocked response', got '%s'", result.Text)
}

if result.Usage.GetTotalTokens() != 30 {
t.Errorf("Expected 30 total tokens, got %d", result.Usage.GetTotalTokens())
}
}

Integration Testing​

Test with real providers in controlled environments:

package main

import (
"context"
"os"
"testing"

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

func TestOpenAIIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}

apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
t.Skip("OPENAI_API_KEY not set")
}

ctx := context.Background()

provider := openai.New(openai.Config{
APIKey: apiKey,
})
model, err := provider.LanguageModel("gpt-3.5-turbo")
if err != nil {
t.Fatal(err)
}

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Say 'test'",
})

if err != nil {
t.Fatalf("GenerateText failed: %v", err)
}

if result.Text == "" {
t.Error("Expected non-empty response")
}

if result.Usage.GetTotalTokens() == 0 {
t.Error("Expected non-zero token usage")
}
}

Run integration tests:

# Run all tests including integration tests
go test ./...

# Skip integration tests (faster)
go test -short ./...

# Run with verbose output
go test -v ./...

Snapshot Testing​

Test output consistency:

package main

import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"

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

// MockLanguageModel implements provider.LanguageModel (same definition as
// shown earlier under "Mocking AI Responses").
type MockLanguageModel struct {
Response string
Error error
}

func (m *MockLanguageModel) SpecificationVersion() string { return "v3" }
func (m *MockLanguageModel) Provider() string { return "mock" }
func (m *MockLanguageModel) ModelID() string { return "mock-model" }
func (m *MockLanguageModel) SupportsTools() bool { return false }
func (m *MockLanguageModel) SupportsStructuredOutput() bool { return false }
func (m *MockLanguageModel) SupportsImageInput() bool { return false }

func (m *MockLanguageModel) DoGenerate(ctx context.Context, opts *provider.GenerateOptions) (*types.GenerateResult, error) {
if m.Error != nil {
return nil, m.Error
}
return &types.GenerateResult{
Text: m.Response,
FinishReason: types.FinishReasonStop,
}, nil
}

func (m *MockLanguageModel) DoStream(ctx context.Context, opts *provider.GenerateOptions) (provider.TextStream, error) {
return nil, nil
}

func TestGenerateTextSnapshot(t *testing.T) {
ctx := context.Background()

mock := &MockLanguageModel{
Response: "Consistent response for snapshot",
}

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: mock,
Prompt: "Test prompt",
})

if err != nil {
t.Fatal(err)
}

// Create snapshot
snapshotPath := filepath.Join("testdata", "snapshots", "generate_text.json")

data, _ := json.MarshalIndent(result, "", " ")

// Check if snapshot exists
if _, err := os.Stat(snapshotPath); os.IsNotExist(err) {
// Create snapshot
os.MkdirAll(filepath.Dir(snapshotPath), 0755)
os.WriteFile(snapshotPath, data, 0644)
t.Log("Created new snapshot")
return
}

// Compare with snapshot
expected, _ := os.ReadFile(snapshotPath)

if string(data) != string(expected) {
t.Errorf("Snapshot mismatch.\nExpected:\n%s\n\nGot:\n%s", expected, data)
}
}

Observability​

Structured Logging​

Use structured logging for better observability:

package main

import (
"context"
"log/slog"
"os"

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

func main() {
// Set up structured logger
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))

ctx := context.Background()

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

logger.Info("Starting AI request",
"model", "gpt-4",
"prompt", "Hello!",
)

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

if err != nil {
logger.Error("AI request failed",
"error", err,
)
return
}

logger.Info("AI request completed",
"model", result.Response.ModelID,
"finish_reason", result.FinishReason,
"prompt_tokens", result.Usage.GetInputTokens(),
"completion_tokens", result.Usage.GetOutputTokens(),
"total_tokens", result.Usage.GetTotalTokens(),
)
}

Tracing with OpenTelemetry​

Add distributed tracing:

package main

import (
"context"
"log"
"os"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"

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

func generateWithTracing(ctx context.Context, model provider.LanguageModel, prompt string) (*ai.GenerateTextResult, error) {
tracer := otel.Tracer("go-ai-app")
ctx, span := tracer.Start(ctx, "ai.GenerateText")
defer span.End()

span.SetAttributes(
attribute.String("ai.model", "gpt-4"),
attribute.String("ai.prompt", prompt),
)

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: prompt,
})

if err != nil {
span.RecordError(err)
return nil, err
}

span.SetAttributes(
attribute.Int64("ai.tokens.prompt", result.Usage.GetInputTokens()),
attribute.Int64("ai.tokens.completion", result.Usage.GetOutputTokens()),
attribute.Int64("ai.tokens.total", result.Usage.GetTotalTokens()),
attribute.String("ai.finish_reason", string(result.FinishReason)),
)

return result, nil
}

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

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

result, err := generateWithTracing(ctx, model, "Hello!")
if err != nil {
log.Fatal(err)
}

log.Printf("Result: %s", result.Text)
}

Debugging Common Issues​

Rate Limiting​

Debug rate limit errors:

if err != nil {
if strings.Contains(err.Error(), "rate_limit") {
log.Printf("Rate limit hit: %v", err)
log.Printf("Retry after delay...")
time.Sleep(60 * time.Second)
// Retry logic
}
}

Context Cancellation​

Debug timeout issues:

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: prompt,
})

if err != nil {
if ctx.Err() == context.DeadlineExceeded {
log.Println("Request timed out after 30 seconds")
} else {
log.Printf("Request failed: %v", err)
}
}

Tool Execution Errors​

Debug tool calling issues:

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Tools: tools,
Prompt: "Use the weather tool",
OnStepFinish: func(ctx context.Context, step types.StepResult, userContext interface{}) {
log.Printf("Step %d:", step.StepNumber)
for _, call := range step.ToolCalls {
log.Printf(" Tool: %s", call.ToolName)
log.Printf(" Args: %+v", call.Arguments)
}
for _, res := range step.ToolResults {
if res.Error != nil {
log.Printf(" Error: %v", res.Error)
} else {
log.Printf(" Result: %+v", res.Result)
}
}
},
})

Best Practices​

1. Use Debug Mode in Development​

// ✅ Good: Debug mode in development
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
DebugMode: os.Getenv("ENV") == "development",
})

2. Monitor Token Usage​

// ✅ Good: Track and log token usage
if result.Usage.GetTotalTokens() > 10000 {
log.Printf("WARNING: High token usage: %d", result.Usage.GetTotalTokens())
}

3. Use Structured Logging​

// ✅ Good: Structured logging
logger.Info("AI request",
"model", "gpt-4",
"tokens", result.Usage.GetTotalTokens(),
"duration", duration,
)

4. Test with Mocks​

// ✅ Good: Use mocks for unit tests
mock := &MockLanguageModel{Response: "test"}
result, _ := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: mock,
})

5. Profile Production Issues​

// ✅ Good: Enable profiling in production
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()

Tools Summary​

ToolPurposeUse Case
Debug ModeLog HTTP requests/responsesDevelopment debugging
Token TrackingMonitor API costsCost optimization
pprofCPU/memory profilingPerformance tuning
Latency TrackingMeasure request durationPerformance monitoring
Mock ProvidersUnit testingTest automation
Structured LoggingObservabilityProduction monitoring
OpenTelemetryDistributed tracingMicroservices debugging

See Also​