Go SDK
The official Go SDK for ModelRelay. Provides idiomatic Go patterns with strong typing, context support, and streaming capabilities.
Installation
go get github.com/modelrelay/sdk-go
Quick Start
package main
import (
"context"
"fmt"
"os"
sdk "github.com/modelrelay/sdk-go"
)
func main() {
client, err := sdk.NewClientFromAPIKey(os.Getenv("MODELRELAY_API_KEY"))
if err != nil {
panic(err)
}
answer, err := client.Responses.Text(
context.Background(),
sdk.NewModelID("claude-sonnet-4-5"),
"You are a helpful assistant.",
"What is the capital of France?",
)
if err != nil {
panic(err)
}
fmt.Println(answer)
// "The capital of France is Paris."
}
Convenience API
The simplest way to get started. Three methods cover the most common use cases:
Ask — Get a Quick Answer
client, _ := sdk.NewClientFromAPIKey(os.Getenv("MODELRELAY_API_KEY"))
answer, _ := client.Ask(ctx, "claude-sonnet-4-5", "What is 2 + 2?", nil)
fmt.Println(answer) // "4"
Chat — Full Response with Metadata
response, _ := client.Chat(ctx, "claude-sonnet-4-5", "Explain quantum computing", &sdk.ChatOptions{
System: "You are a physics professor",
})
fmt.Println(response.AssistantText())
fmt.Println("Tokens:", response.Usage.TotalTokens)
Agent — Agentic Tool Loops
Run an agent that automatically executes tools until completion:
type ReadFileArgs struct {
Path string `json:"path" description:"File path to read"`
}
tools := sdk.NewToolBuilder()
sdk.AddFunc(tools, "read_file", "Read a file", func(args ReadFileArgs) (any, error) {
content, err := os.ReadFile(args.Path)
return string(content), err
})
result, _ := client.Agent(ctx, "claude-sonnet-4-5", sdk.AgentOptions{
Tools: tools,
Prompt: "Read config.json and summarize it",
System: "You are a helpful file assistant",
})
fmt.Println(result.Output)
fmt.Println("Tool calls:", result.Usage.ToolCalls)
Configuration
From API Key
import sdk "github.com/modelrelay/sdk-go"
// From secret key (backend use)
client, err := sdk.NewClientFromSecretKey("mr_sk_...")
// Auto-detect key type
client, err := sdk.NewClientFromAPIKey(os.Getenv("MODELRELAY_API_KEY"))
// Parse and validate first
secret, err := sdk.ParseSecretKey("mr_sk_...")
client, err := sdk.NewClientWithKey(secret)
From Bearer Token
For customer-scoped access using a minted token:
client, err := sdk.NewClientWithToken("eyJ...")
Configuration Options
Use functional options to configure the client:
client, err := sdk.NewClientFromSecretKey("mr_sk_...",
sdk.WithBaseURL("https://custom.api.com"),
sdk.WithConnectTimeout(5*time.Second),
sdk.WithRequestTimeout(30*time.Second),
sdk.WithRetryConfig(sdk.RetryConfig{MaxAttempts: 3}),
)
| Option | Default | Description |
|---|---|---|
WithBaseURL(url) |
https://api.modelrelay.ai/api/v1 |
API base URL |
WithConnectTimeout(d) |
5s |
Connection timeout |
WithRequestTimeout(d) |
60s |
Request timeout |
WithRetryConfig(cfg) |
No retry | Retry/backoff policy |
WithHTTPClient(c) |
Default | Custom HTTP client |
WithUserAgent(ua) |
SDK version | Custom User-Agent |
Making Requests
Simple Text Response
For the common “system prompt + user message → text” pattern:
ctx := context.Background()
answer, err := client.Responses.Text(
ctx,
sdk.NewModelID("claude-sonnet-4-5"),
"You are a helpful assistant.",
"What is 2 + 2?",
)
if err != nil {
return err
}
fmt.Println(answer)
Request Builder
For more control over request parameters:
ctx := context.Background()
req, callOpts, err := client.Responses.New().
Model(sdk.NewModelID("claude-sonnet-4-5")).
System("You are a helpful assistant.").
User("What is 2 + 2?").
MaxOutputTokens(256).
Temperature(0.7).
Build()
if err != nil {
return err
}
resp, err := client.Responses.Create(ctx, req, callOpts...)
if err != nil {
return err
}
fmt.Println(resp.Text()) // Extract text from response
fmt.Println(resp.Usage) // {InputTokens, OutputTokens, TotalTokens}
Customer-Attributed Requests
For metered billing, attribute requests to customers:
ctx := context.Background()
// Option 1: Use CustomerID in the builder (tier determines model)
req, callOpts, err := client.Responses.New().
CustomerID("customer-123").
System("You are a helpful assistant.").
User("Hello!").
Build()
if err != nil {
return err
}
resp, err := client.Responses.Create(ctx, req, callOpts...)
// Option 2: Use the convenience method
answer, err := client.Responses.TextForCustomer(
ctx,
"customer-123",
"You are a helpful assistant.",
"Hello!",
)
Streaming
Stream Text Deltas
For real-time response streaming with just the text:
ctx := context.Background()
stream, err := client.Responses.StreamTextDeltas(
ctx,
sdk.NewModelID("claude-sonnet-4-5"),
"You are a helpful assistant.",
"Write a haiku about programming.",
)
if err != nil {
return err
}
defer stream.Close()
for {
delta, ok, err := stream.Next()
if err != nil {
return err
}
if !ok {
break
}
fmt.Print(delta)
}
Full Event Stream
For access to all streaming events:
ctx := context.Background()
req, callOpts, err := client.Responses.New().
Model(sdk.NewModelID("claude-sonnet-4-5")).
System("You are helpful.").
User("Hello!").
Build()
if err != nil {
return err
}
stream, err := client.Responses.Stream(ctx, req, callOpts...)
if err != nil {
return err
}
defer stream.Close()
for {
event, ok, err := stream.Next()
if err != nil {
return err
}
if !ok {
break
}
fmt.Print(event.TextDelta)
}
Structured Output
Parse to Typed Struct
Use generics to parse responses into typed Go structs:
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
ctx := context.Background()
req, callOpts, err := client.Responses.New().
Model(sdk.NewModelID("claude-sonnet-4-5")).
User("Extract: John Doe is 30 years old").
Build()
if err != nil {
return err
}
result, err := sdk.Structured[Person](
ctx,
client.Responses,
req,
sdk.StructuredOptions{MaxRetries: 2},
callOpts...,
)
if err != nil {
return err
}
fmt.Printf("Name: %s, Age: %d\n", result.Value.Name, result.Value.Age)
Streaming Structured Output
Build progressive UIs that render fields as they complete:
type Article struct {
Title string `json:"title"`
Summary string `json:"summary"`
Body string `json:"body"`
}
ctx := context.Background()
req, callOpts, err := client.Responses.New().
Model(sdk.NewModelID("claude-sonnet-4-5")).
User("Write an article about Go").
Build()
if err != nil {
return err
}
stream, err := sdk.StreamStructured[Article](ctx, client.Responses, req, "", callOpts...)
if err != nil {
return err
}
defer stream.Close()
for {
event, ok, err := stream.Next()
if err != nil {
return err
}
if !ok {
break
}
if event.Payload == nil {
continue
}
// Render fields as soon as they're complete
if event.CompleteFields["title"] {
renderTitle(event.Payload.Title) // Safe to display
}
if event.CompleteFields["summary"] {
renderSummary(event.Payload.Summary)
}
// Show streaming preview of incomplete fields
if !event.CompleteFields["body"] && event.Payload.Body != "" {
renderBodyPreview(event.Payload.Body + "▋")
}
}
Error Handling
Error Types
The SDK provides typed errors for different failure modes:
import (
sdk "github.com/modelrelay/sdk-go"
"errors"
)
answer, err := client.Responses.Text(ctx, model, system, user)
if err != nil {
var apiErr sdk.APIError
if errors.As(err, &apiErr) {
// Server returned an error response
fmt.Printf("API error %d: %s\n", apiErr.Status, apiErr.Message)
fmt.Printf("Code: %s\n", apiErr.Code)
fmt.Printf("Request ID: %s\n", apiErr.RequestID)
// Check specific error types
if apiErr.IsRateLimit() {
fmt.Println("Rate limited, retry later")
}
if apiErr.IsUnauthorized() {
fmt.Println("Invalid API key")
}
if apiErr.IsValidation() {
for _, field := range apiErr.Fields {
fmt.Printf("Field %s: %s\n", field.Field, field.Message)
}
}
return
}
var transportErr sdk.TransportError
if errors.As(err, &transportErr) {
// Network or connection error
fmt.Printf("Transport error: %s\n", transportErr.Message)
return
}
var configErr sdk.ConfigError
if errors.As(err, &configErr) {
// Invalid configuration
fmt.Printf("Config error: %s\n", configErr.Reason)
return
}
// Unknown error
return err
}
Error Codes
Use typed error codes for programmatic handling:
var apiErr sdk.APIError
if errors.As(err, &apiErr) {
switch apiErr.Code {
case sdk.ErrCodeRateLimit:
// Back off and retry
case sdk.ErrCodeUnauthorized:
// Re-authenticate
case sdk.ErrCodeNotFound:
// Resource doesn't exist
case sdk.ErrCodeValidation, sdk.ErrCodeInvalidInput:
// Check request parameters
case sdk.ErrCodePaymentRequired:
// Customer quota exceeded
}
}
Stream Timeout Errors
Handle streaming-specific timeouts:
var timeoutErr sdk.StreamTimeoutError
if errors.As(err, &timeoutErr) {
switch timeoutErr.Kind {
case sdk.StreamTimeoutTTFT:
fmt.Println("No response within time-to-first-token limit")
case sdk.StreamTimeoutIdle:
fmt.Println("Stream went idle")
case sdk.StreamTimeoutTotal:
fmt.Println("Total stream time exceeded")
}
}
Context Usage
All SDK methods accept context.Context for cancellation and timeouts:
// With timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
answer, err := client.Responses.Text(ctx, model, system, user)
// With cancellation
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(5 * time.Second)
cancel() // Cancel the request
}()
answer, err := client.Responses.Text(ctx, model, system, user)
if errors.Is(err, context.Canceled) {
fmt.Println("Request was cancelled")
}
Customer Management
Manage customers for billing and usage tracking:
ctx := context.Background()
// Create or update a customer
customer, err := client.Customers.Upsert(ctx, sdk.CustomerUpsertRequest{
ExternalID: sdk.NewCustomerExternalID("your-user-id"),
Email: "user@example.com",
})
if err != nil {
return err
}
// Create a Stripe checkout session for subscription billing
session, err := client.Customers.Subscribe(ctx, customer.Customer.ID, sdk.CustomerSubscribeRequest{
TierID: uuid.MustParse("tier-uuid"),
SuccessURL: "https://myapp.com/success",
CancelURL: "https://myapp.com/cancel",
})
if err != nil {
return err
}
// Redirect user to session.URL for payment
// Check subscription status
status, err := client.Customers.GetSubscription(ctx, customer.Customer.ID)
if err != nil {
return err
}
fmt.Printf("Active: %v\n", status.Active)
Token Providers
Use token providers for automatic bearer token management:
Mint Customer Tokens
Backend mints tokens for frontend use:
ctx := context.Background()
secret, _ := sdk.ParseSecretKey(os.Getenv("MODELRELAY_API_KEY"))
customerID := uuid.MustParse(os.Getenv("MODELRELAY_CUSTOMER_ID"))
provider, err := sdk.NewCustomerTokenProvider(sdk.CustomerTokenProviderConfig{
SecretKey: secret,
Request: sdk.NewCustomerTokenRequestForCustomerID(customerID),
})
if err != nil {
return err
}
client, err := sdk.NewClientWithTokenProvider(provider)
if err != nil {
return err
}
// Token is automatically obtained and refreshed
text, err := client.Responses.Text(ctx, sdk.NewModelID("claude-sonnet-4-5"), "You are helpful.", "Hello!")
Service Clients
The SDK organizes functionality into service clients:
| Client | Description |
|---|---|
client.Responses |
Create AI responses, streaming |
client.Customers |
Customer CRUD, checkout sessions |
client.Tiers |
List pricing tiers |
client.Models |
List available models |
client.Auth |
Token minting |
client.Usage |
Usage statistics |
client.Runs |
Workflow execution |
client.Workflows |
Workflow management |
Next Steps
- First Request - Make your first API call
- Streaming - Real-time response streaming
- Tool Use - Let models call functions
- Structured Output - Get typed JSON responses