Go client source
The Go client is internal-only. It remains tested because mrl, API tooling,
and the dashboard depend on it, but it is not published or supported as a
standalone SDK product. External applications should use the supported
REST/OpenAPI contract. See the
authoritative client support matrix for lifecycle definitions.
The remaining examples are retained as an internal implementation reference.
They are not an external compatibility commitment.
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-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-5", "What is 2 + 2?", nil)
fmt.Println(answer) // "4"
Chat — Full Response with Metadata
response, _ := client.Chat(ctx, "claude-sonnet-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-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-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-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-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-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-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-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-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 |
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