Skip to main content

Go Integration

The Ferro Labs AI Gateway is written in Go. This guide covers everything a Go developer needs โ€” from pointing a standard OpenAI client at the gateway, to embedding the engine as a library, writing custom plugins, propagating trace context, and profiling with pprof.

1. Using the go-openai Clientโ€‹

The sashabaranov/go-openai client works out of the box. Point it at your gateway URL and set your API key.

package main

import (
"context"
"fmt"
"log"

openai "github.com/sashabaranov/go-openai"
)

func main() {
cfg := openai.DefaultConfig("your-ferro-api-key")
cfg.BaseURL = "http://localhost:8080/v1" // Ferro Labs AI Gateway

client := openai.NewClientWithConfig(cfg)

resp, err := client.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: openai.GPT4,
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleUser,
Content: "Explain the CAP theorem in two sentences.",
},
},
},
)
if err != nil {
log.Fatalf("completion error: %v", err)
}

fmt.Println(resp.Choices[0].Message.Content)
}

2. Embedding Ferro as a Libraryโ€‹

You can embed the gateway engine directly into your Go application instead of running it as a standalone process. This is useful for custom orchestration or testing.

The public aigateway package does not expose an HTTP server โ€” it gives you a routing engine. You create a Gateway, register providers, optionally load plugins, and then call Route / RouteStream / Embed / GenerateImage directly. (If you want the full HTTP server with the admin dashboard, run the ferrogw binary instead โ€” see Quickstart.)

package main

import (
"context"
"fmt"
"log"

gateway "github.com/ferro-labs/ai-gateway"
"github.com/ferro-labs/ai-gateway/providers"

// Built-in plugins are compiled in via blank imports of internal/plugins/*.
_ "github.com/ferro-labs/ai-gateway/internal/plugins/wordfilter"
)

func main() {
// LoadConfig returns *Config; New takes a Config value, so dereference it.
cfg, err := gateway.LoadConfig("ferro.yaml")
if err != nil {
log.Fatalf("failed to load config: %v", err)
}

gw, err := gateway.New(*cfg)
if err != nil {
log.Fatalf("failed to create gateway: %v", err)
}
defer gw.Close()

// Register every provider whose required environment variables are set
// (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY).
for _, entry := range providers.AllProviders() {
pcfg := providers.ProviderConfigFromEnv(entry)
if pcfg == nil {
continue // required env var unset โ€” skip
}
p, err := entry.Build(pcfg)
if err != nil {
log.Fatalf("provider %s init failed: %v", entry.ID, err)
}
gw.RegisterProvider(p)
}

// Initialize and register plugins declared in the config file.
if err := gw.LoadPlugins(); err != nil {
log.Fatalf("failed to load plugins: %v", err)
}

// Route a request through the engine.
resp, err := gw.Route(context.Background(), providers.Request{
Model: "gpt-4o-mini",
Messages: []providers.Message{
{Role: "user", Content: "Explain the CAP theorem in two sentences."},
},
})
if err != nil {
log.Fatalf("route error: %v", err)
}

fmt.Println(resp.Choices[0].Message.Content)
}

Streaming uses gw.RouteStream, which returns a <-chan providers.StreamChunk you range over. Call gw.Close() once at shutdown to stop the background hook workers and catalog refresher.

3. Writing a Custom Pluginโ€‹

Plugins implement the plugin.Plugin interface from github.com/ferro-labs/ai-gateway/plugin:

type Plugin interface {
Name() string
Type() PluginType
Init(config map[string]interface{}) error
Execute(ctx context.Context, pctx *Context) error
}

A plugin is registered at a lifecycle stage (before_request, after_request, or on_error) and is invoked via Execute at that stage. The *plugin.Context carries the request, the response (on later stages), and a Metadata map you can use to pass state between stages of the same request โ€” there is no SetMeta/GetMeta.

package myplugin

import (
"context"
"log"
"time"

"github.com/ferro-labs/ai-gateway/plugin"
)

// TimingPlugin logs the round-trip duration for every request.
type TimingPlugin struct{}

// Name returns the plugin identifier used in configuration.
func (p *TimingPlugin) Name() string { return "timing" }

// Type categorizes the plugin.
func (p *TimingPlugin) Type() plugin.PluginType { return plugin.TypeLogging }

// Init receives the plugin's config block from the gateway configuration.
func (p *TimingPlugin) Init(config map[string]interface{}) error { return nil }

// Execute runs at whatever stage the plugin is registered for. Stash the start
// time on pctx.Metadata in the before_request stage and read it back later.
func (p *TimingPlugin) Execute(ctx context.Context, pctx *plugin.Context) error {
if pctx.Response == nil {
// before_request stage โ€” record the start time.
pctx.Metadata["timing_start"] = time.Now()
log.Printf("[timing] request to model=%s started", pctx.Request.Model)
return nil
}

// after_request stage โ€” measure elapsed time.
start, ok := pctx.Metadata["timing_start"].(time.Time)
if !ok {
return nil
}
log.Printf("[timing] model=%s completed in %s (tokens: %d)",
pctx.Response.Model, time.Since(start), pctx.Response.Usage.TotalTokens)
return nil
}

Register the same instance at both stages programmatically:

p := &myplugin.TimingPlugin{}
_ = gw.RegisterPlugin(plugin.StageBeforeRequest, p)
_ = gw.RegisterPlugin(plugin.StageAfterRequest, p)

Alternatively, register a factory in an init() so the plugin can be enabled from config by name. Add a blank import of your package to cmd/ferrogw/main.go (this is how the built-in plugins are wired in), then list it under plugins: in your config and let gw.LoadPlugins() construct it:

func init() {
plugin.RegisterFactory("timing", func() plugin.Plugin {
return &TimingPlugin{}
})
}

4. Context Propagation and Trace ID Extractionโ€‹

The AI Gateway injects an X-Request-ID header into every response. Use this to correlate gateway logs with your application traces.

package main

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

openai "github.com/sashabaranov/go-openai"
)

func main() {
cfg := openai.DefaultConfig("your-ferro-api-key")
cfg.BaseURL = "http://localhost:8080/v1"

// Use a custom HTTP client to capture response headers.
var traceID string
cfg.HTTPClient = &http.Client{
Transport: &traceTransport{
base: http.DefaultTransport,
onResponse: func(resp *http.Response) {
traceID = resp.Header.Get("X-Request-ID")
},
},
}

client := openai.NewClientWithConfig(cfg)

resp, err := client.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: openai.GPT4,
Messages: []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleUser, Content: "Hello"},
},
},
)
if err != nil {
log.Fatalf("completion error: %v", err)
}

fmt.Printf("Response: %s\n", resp.Choices[0].Message.Content)
fmt.Printf("Trace ID: %s\n", traceID)
}

// traceTransport wraps an http.RoundTripper to expose response headers.
type traceTransport struct {
base http.RoundTripper
onResponse func(*http.Response)
}

func (t *traceTransport) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := t.base.RoundTrip(req)
if err != nil {
return resp, err
}
if t.onResponse != nil {
t.onResponse(resp)
}
return resp, nil
}

5. Benchmarking with pprofโ€‹

Enable the pprof endpoint to profile the gateway under load.

Enable the pprof endpointโ€‹

The ferrogw binary mounts the standard net/http/pprof handlers on its own server (port 8080 by default) under /debug/pprof/* when the ENABLE_PPROF environment variable is truthy:

ENABLE_PPROF=1 ferrogw serve

The profiling endpoints are then available at http://localhost:8080/debug/pprof/.

If you are embedding the engine as a library (section 2), there is no HTTP server, so expose pprof yourself on a separate port using the standard library:

import "net/http"
import _ "net/http/pprof"

go func() {
log.Println("pprof listening on :6060")
log.Println(http.ListenAndServe("localhost:6060", nil))
}()

Run a load test and capture a profileโ€‹

Use hey (or any HTTP load testing tool) alongside go tool pprof:

# Generate load โ€” 1000 requests, 50 concurrent
hey -n 1000 -c 50 \
-H "Authorization: Bearer your-ferro-api-key" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4","messages":[{"role":"user","content":"ping"}]}' \
http://localhost:8080/v1/chat/completions

# Capture a 30-second CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Capture a heap snapshot
go tool pprof http://localhost:6060/debug/pprof/heap

Analyze the profileโ€‹

Inside the pprof interactive shell:

(pprof) top 20
(pprof) web # opens a call-graph SVG in your browser
(pprof) list HandleRequest # source-level annotation

Write a Go benchmark testโ€‹

package gateway_test

import (
"context"
"testing"

openai "github.com/sashabaranov/go-openai"
)

func BenchmarkChatCompletion(b *testing.B) {
cfg := openai.DefaultConfig("your-ferro-api-key")
cfg.BaseURL = "http://localhost:8080/v1"
client := openai.NewClientWithConfig(cfg)

req := openai.ChatCompletionRequest{
Model: openai.GPT4,
Messages: []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleUser, Content: "ping"},
},
}

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := client.CreateChatCompletion(context.Background(), req)
if err != nil {
b.Fatalf("request failed: %v", err)
}
}
}

Run with:

go test -bench=BenchmarkChatCompletion -benchtime=30s -count=3 ./...