Otari Go Client SDK

June 8, 2026 · View on GitHub

otari logo

Otari Go Client SDK

Go 1.25+ Discord

Go client for otari, the open-source core that powers otari.ai. Communicate with any LLM provider through otari using a single, typed interface.

Python SDK | TypeScript SDK | Documentation | Platform (Beta)

New to otari? The otari repo explains what it is and why you’d use it.

Quickstart

go get github.com/mozilla-ai/otari-sdk-go/otari

Generate an API token at otari.ai/organization-settings/api-tokens, then add a provider key (e.g. OpenAI) at otari.ai/organization-settings/provider-keys so the gateway can route requests to that provider. Then use the client:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/mozilla-ai/otari-sdk-go/otari"
)

func main() {
    client, err := otari.New(
        otari.WithAPIKey("tk_your_api_token"),
        otari.WithPlatformMode(),
    )
    if err != nil {
        log.Fatal(err)
    }

    resp, err := client.Completion(context.Background(), otari.CompletionParams{
        Model:    "openai:gpt-4o-mini",
        Messages: []otari.Message{{Role: otari.RoleUser, Content: "Hello!"}},
    })
    if err != nil {
        log.Fatal(err)
    }

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

That's it: the client defaults to the hosted gateway at https://api.otari.ai, so you don't need to set a base URL. Change the model string to switch between LLM providers through the gateway.

Installation

Requirements

  • Go 1.25 or newer
  • An otari.ai account, or a running self-hosted otari instance

Install

go get github.com/mozilla-ai/otari-sdk-go/otari

Setting up credentials

For the hosted platform, set the platform-token environment variable:

export OTARI_AI_TOKEN="tk_your_api_token"
# GATEWAY_PLATFORM_TOKEN is still accepted as a legacy alias.

For a self-hosted gateway, set the base URL and gateway API key:

export GATEWAY_API_BASE="http://localhost:8000"
export GATEWAY_API_KEY="your-key-here"

Alternatively, pass credentials directly when creating the client (see Authentication).

Authentication

The client supports two authentication modes, matching the Python and TypeScript SDKs.

Platform mode (recommended) uses a platform token as standard Authorization: Bearer auth. On the hosted platform, generate an API token at otari.ai/organization-settings/api-tokens and add a provider key (e.g. OpenAI) at otari.ai/organization-settings/provider-keys so the gateway can route requests to that provider. The base URL defaults to the hosted gateway at https://api.otari.ai, so it can be omitted:

client, err := otari.New(
    otari.WithAPIKey("tk_your_api_token"),
    otari.WithPlatformMode(),
)

The token can also be supplied via the OTARI_AI_TOKEN environment variable (legacy alias GATEWAY_PLATFORM_TOKEN), in which case otari.New(otari.WithPlatformMode()) picks it up automatically.

Self-hosted mode sends the API key via the custom Otari-Key: Bearer … header and requires a base URL. Run the gateway yourself following the setup in the otari repo, make sure it has provider keys configured, then point the SDK at it:

client, err := otari.New(
    otari.WithBaseURL("http://localhost:8000"), // or wherever you host the gateway
    otari.WithOtariKey("your-gateway-api-key"),
)

The base URL and key can also come from the GATEWAY_API_BASE and GATEWAY_API_KEY environment variables. When no explicit credentials are provided, otari.New() resolves the mode from the environment: a platform token (OTARI_AI_TOKEN / GATEWAY_PLATFORM_TOKEN) selects platform mode against the hosted gateway, otherwise GATEWAY_API_BASE + GATEWAY_API_KEY select self-hosted mode.

Usage

Chat completions

resp, err := client.Completion(ctx, otari.CompletionParams{
    Model:    "openai:gpt-4o-mini",
    Messages: []otari.Message{{Role: otari.RoleUser, Content: "Hello!"}},
})
if err != nil {
    log.Fatal(err)
}
fmt.Println(resp.Choices[0].Message.ContentString())

Streaming

Streaming methods return a channel of typed chunks and a buffered error channel. Drain the chunk channel, then check the error channel once it closes.

chunks, errs := client.CompletionStream(ctx, otari.CompletionParams{
    Model:    "openai:gpt-4o-mini",
    Messages: []otari.Message{{Role: otari.RoleUser, Content: "Tell me a story."}},
})

for chunk := range chunks {
    if len(chunk.Choices) > 0 {
        fmt.Print(chunk.Choices[0].Delta.Content)
    }
}

if err := <-errs; err != nil {
    log.Fatal(err)
}

Responses API

The OpenAI-style Responses API is exposed via Response (and ResponseStream). The gateway returns an opaque payload, so the decoded JSON is delivered as a map[string]any. Input accepts any JSON-serializable value (a string, a message list, and so on); Extra forwards additional /responses fields verbatim.

resp, err := client.Response(ctx, otari.ResponseParams{
    Model: "openai:gpt-4o-mini",
    Input: "Write a haiku about Go.",
})
if err != nil {
    log.Fatal(err)
}
fmt.Println(resp["output_text"])

Streaming delivers each event as a raw decoded JSON map:

events, errs := client.ResponseStream(ctx, otari.ResponseParams{
    Model: "openai:gpt-4o-mini",
    Input: "Write a haiku about Go.",
})

for event := range events {
    fmt.Println(event["type"])
}

if err := <-errs; err != nil {
    log.Fatal(err)
}

Messages API

The Anthropic-shaped Messages API is exposed via Message (and MessageStream). MaxTokens is required by the gateway. The response is opaque, so the decoded JSON is delivered as a map[string]any; Extra forwards additional /messages fields (system, tools, thinking, and so on) verbatim.

resp, err := client.Message(ctx, otari.MessageParams{
    Model:     "anthropic:claude-3-5-sonnet",
    MaxTokens: 1024,
    Messages: []map[string]any{
        {"role": "user", "content": "Hello!"},
    },
})
if err != nil {
    log.Fatal(err)
}
fmt.Println(resp["content"])

Streaming delivers each event as a raw decoded JSON map:

events, errs := client.MessageStream(ctx, otari.MessageParams{
    Model:     "anthropic:claude-3-5-sonnet",
    MaxTokens: 1024,
    Messages: []map[string]any{
        {"role": "user", "content": "Tell me a story."},
    },
})

for event := range events {
    fmt.Println(event["type"])
}

if err := <-errs; err != nil {
    log.Fatal(err)
}

Embeddings

result, err := client.Embedding(ctx, otari.EmbeddingParams{
    Model: "openai:text-embedding-3-small",
    Input: "Hello world",
})
if err != nil {
    log.Fatal(err)
}
fmt.Println(result.Data[0].Embedding)

Listing models

models, err := client.ListModels(ctx)
if err != nil {
    log.Fatal(err)
}
for _, m := range models.Data {
    fmt.Println(m.ID)
}

Moderation

result, err := client.Moderation(ctx, otari.ModerationParams{
    Model: "openai:omni-moderation-latest",
    Input: "some content to check",
})
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Flagged: %v\n", result.Results[0].Flagged)

Reranking

result, err := client.Rerank(ctx, otari.RerankParams{
    Model:     "cohere:rerank-v3.5",
    Query:     "What is machine learning?",
    Documents: []string{"ML is a subset of AI", "The weather is nice"},
})
if err != nil {
    log.Fatal(err)
}
for _, r := range result.Results {
    fmt.Printf("doc[%d] score=%.3f\n", r.Index, r.RelevanceScore)
}

Batch operations

Create a batch, poll it with RetrieveBatch, cancel it with CancelBatch, enumerate jobs with ListBatches, and fetch results with RetrieveBatchResults. Each lookup takes the batch ID and the upstream provider.

// Create a batch.
batch, err := client.CreateBatch(ctx, otari.CreateBatchParams{
    Model: "openai:gpt-4o-mini",
    Requests: []otari.BatchRequestItem{
        {CustomID: "req-1", Body: map[string]any{"messages": []any{
            map[string]any{"role": "user", "content": "Hello"},
        }}},
    },
    CompletionWindow: "24h",
})
if err != nil {
    log.Fatal(err)
}

// Check status later.
status, err := client.RetrieveBatch(ctx, batch.ID, batch.Provider)
if err != nil {
    log.Fatal(err)
}
fmt.Println(status.Status)

// Retrieve results once complete.
results, err := client.RetrieveBatchResults(ctx, batch.ID, batch.Provider)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%d results\n", len(results.Results))

Error handling

All errors are typed. Use errors.Is for sentinel checks and errors.As for typed details:

import "errors"

resp, err := client.Completion(ctx, params)
if err != nil {
    switch {
    case errors.Is(err, otari.ErrAuthentication):
        log.Fatal("Invalid credentials")
    case errors.Is(err, otari.ErrRateLimit):
        log.Fatal("Rate limited, try again later")
    case errors.Is(err, otari.ErrInsufficientFunds):
        log.Fatal("Budget exceeded")
    default:
        log.Fatal(err)
    }
}
HTTP StatusError TypeSentinelDescription
400 (capability)*UnsupportedCapabilityErrorErrUnsupportedProvider does not support the requested capability
401, 403*AuthenticationErrorErrAuthenticationInvalid or missing credentials
402*InsufficientFundsErrorErrInsufficientFundsBudget or credits exhausted
404*ModelNotFoundErrorErrModelNotFoundModel not found, or no provider key configured for the requested provider; the error carries the gateway's detail
409*BatchNotCompleteErrorErrBatchNotCompleteBatch not yet finished (includes BatchID, Status)
429*RateLimitErrorErrRateLimitRate limit exceeded
502*UpstreamProviderErrorErrUpstreamProviderUpstream provider unreachable
504*TimeoutErrorErrTimeoutGateway timed out waiting for provider

Project Structure

otari-sdk-go/
├── otari/                  # Main package (hand-written ergonomic shell)
│   ├── client.go           # Client struct, constructor, chat/messages/responses methods
│   ├── config.go           # Functional options (WithBaseURL, WithAPIKey, WithPlatformMode, ...)
│   ├── streaming.go        # Hand-written SSE decoder (channel-based streaming)
│   ├── errors.go           # Typed error hierarchy with sentinel errors
│   ├── types.go            # Request/response types
│   ├── batch.go            # Batch API methods
│   ├── moderation.go       # Content moderation
│   ├── rerank.go           # Document reranking
│   ├── control_plane.go    # Control-plane API (keys/users/budgets/pricing/usage)
│   ├── conversion.go       # Type conversions
│   ├── request.go          # HTTP request helpers
│   ├── transport.go        # HTTP transport helpers
│   ├── otari.go            # Package documentation
│   ├── client/             # Generated OpenAPI core (do not hand-edit; regenerated from the spec)
│   └── *_test.go           # Unit, integration, and endpoint-coverage tests
├── examples/
│   └── quickstart/         # Quick smoke test
├── sdk-endpoints.txt       # Endpoint-coverage manifest (drift gate)
├── go.mod
├── go.sum
└── README.md

Development

# Run tests
go test ./...

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

# Build
go build ./...

# Vet
go vet ./...

Documentation

Contributing

We welcome contributions from developers of all skill levels! Please see the Contributing Guide or open an issue to discuss changes.

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.