Skip to main content

Go SDK

This guide shows how to install the Go SDK (module github.com/enterprise-ai/enterprise-ai-go, package eai), connect it to the Control Plane of the Navin Platform and call every resource group of API v1 with typed errors, retries, pagination and streaming. Source: sdks/go. Every type, function and field is listed in the generated reference.

Version française : go.fr.md

Overview​

eai.NewClient(opts...) returns a *eai.Client with one service field per resource group of API v1 (client.Agents, client.Gateway, client.FinOps...). Every method takes a context.Context, maps one documented route and returns json.RawMessage, a typed struct or a *eai.Page. Errors are *eai.APIError values, matched with errors.As, errors.Is and the Err* sentinels. Sub-packages add building blocks for services:

PackagePurpose
eai (module root)The Control Plane client, models, errors, pagination, SSE, contracts.
managerThe Navin Manager client (Navin Manager SDK).
gatewaymwnet/http middleware: policy check, quota reservation, release.
workerAsynchronous media and tool workers with a kill switch.
usageBuilder and batched emitter of Usage Events.
realtimeWebSocket client of voice live sessions (Realtime guide).

Requirements and compatibility​

ItemSupported
Go1.26 or later (go 1.26.0 in sdks/go/go.mod)
Operating systemsEvery platform supported by the Go toolchain (Linux, macOS, Windows; amd64 and arm64)
Dependenciesgithub.com/santhosh-tekuri/jsonschema/v6 (contracts), golang.org/x/net (WebSocket)
Control Plane APIv1 (eai.Version is the SDK version, 1.0.2)

Installation​

  1. Download the module from Harbor. The SDK packages are hosted in the sdk project of the Navinspire Harbor registry (artifact sdk/go, tag = SDK version). The Navinspire registry is registry.navinspire.ai; log in with a robot account that can pull from the sdk project (given by the platform team). If your organization mirrors the sdk project in its own Harbor, use the address of that mirror instead.

    oras login registry.navinspire.ai
    oras pull registry.navinspire.ai/sdk/go:1.0.2 -o third_party/enterprise-ai-go

    Expected result: third_party/enterprise-ai-go/go.mod exists.

  2. Add the module to your project with a replace directive that points to the downloaded folder.

    go mod edit -require=github.com/enterprise-ai/enterprise-ai-go@v1.0.2
    go mod edit -replace=github.com/enterprise-ai/enterprise-ai-go=./third_party/enterprise-ai-go
    go mod tidy

    Expected result: go.mod lists the module and its replace line, and go build ./... succeeds. The name github.com/enterprise-ai/enterprise-ai-go is the module name declared in the go.mod of the SDK and used in the imports. It is an identifier, not a download address: nothing is fetched from it.

    If it fails: check go version (1.26 or later) and the path of the replace directive.

Offline install​

  1. Vendor the module. On a connected machine go mod vendor copies every dependency, the SDK included, into vendor/; the offline build then reads only that folder.

    go mod vendor
    go build -mod=vendor ./...
  2. Or point at a copy of this repository with a replace directive in go.mod: replace github.com/enterprise-ai/enterprise-ai-go => ../sdks/go.

Quickstart​

This example builds a client, reads the caller identity and sends one chat completion through the gateway.

client, err := eai.NewClient(eai.WithBaseURL("<control-plane-url>"), eai.WithAPIKey(os.Getenv("EAI_API_KEY")))
if err != nil {
return err
}
me, err := client.Meta.Me(ctx)
if err != nil {
return err
}
reply, err := client.Gateway.ChatCompletions(ctx, eai.ChatRequest{Model: "gpt-4o-mini", Messages: []eai.ChatMessage{{Role: "user", Content: "Bonjour"}}})
if err != nil {
return err
}
fmt.Println(string(me), reply.Content, reply.CostMeasured)

What happens: NewClient checks the options (a base URL is required, https except on localhost), every call sends the key as X-API-Key and a W3C traceparent, and the completion is posted with an Idempotency-Key. Expected result: the identity document, then the text of the answer and its measured cost.

Authentication​

The server resolves the tenant from the credential. The SDK reads no environment variable: pass the values to the options, for example with os.Getenv.

MethodOptionHeader sent
Tenant API key eai_<prefix>_<secret>eai.WithAPIKey(key)X-API-Key
OIDC access tokeneai.WithToken(token)Authorization: Bearer <token>
Development mode (server started with EAI_AUTH_DEV=1), only without credentialeai.WithTenant(slug)X-Dev-Tenant
Workload identity, mTLSeai.WithHTTPClient(...) with a client certificateTLS client certificate
import eai "github.com/enterprise-ai/enterprise-ai-go"

client, err := eai.NewClient(
eai.WithBaseURL("<control-plane-url>"),
eai.WithAPIKey(os.Getenv("EAI_API_KEY")),
eai.WithTenant("acme"),
eai.WithLocale("en"),
)

Client configuration​

eai.NewClient(opts ...eai.Option) (*eai.Client, error):

OptionTypeDefaultDescription
WithBaseURL(u)stringnone, requiredControl Plane URL without /v1; https except for localhost, 127.0.0.1, ::1.
WithAPIKey(k)stringnoneTenant API key, sent as X-API-Key.
WithToken(t)stringnoneOIDC access token, sent as Authorization: Bearer.
WithTenant(t)stringnoneTenant slug; sent as X-Dev-Tenant only without credential.
WithProject(p)stringnoneSent as X-Project.
WithEnvironment(e)stringnonedev, test, staging or prod, sent as X-Environment.
WithLocale(l)string"en"en or fr (any value starting with fr gives fr); sent as Accept-Language.
WithRetries(n)int2Retries after the first attempt; negative values are refused.
WithRetryBaseDelay(d)time.Duration250msFirst backoff delay, doubled at each attempt.
WithHTTPClient(h)*http.Client&http.Client{Timeout: 60 * time.Second}Custom client (timeout, proxy, mTLS).
WithHeader(name, value)string, stringnoneHeader sent on every request.
WithTraceparent(f)func() stringrandom per requestReturns the W3C traceparent of the current span.
WithCABundle(pem)[]bytesystem poolExtra trusted CAs (PEM); verification stays on.
WithDangerouslyDisableTLSVerificationForLocalhost()noneoffDisables certificate checks; refused unless the host is local.

Per call options (eai.CallOption, last arguments of every method):

OptionDescription
WithQuery(map[string]string)Query string parameters (filters).
WithLimit(n), WithCursor(c)Page size and cursor of a list call.
WithIdempotencyKey(k)Idempotency-Key of a POST; otherwise one is generated and kept across retries.
WithCallTraceparent(tp)W3C traceparent of this call.
WithCallHeader(name, value)Extra header of this call.
tuned, err := eai.NewClient(
eai.WithBaseURL("<control-plane-url>"),
eai.WithToken(os.Getenv("EAI_TOKEN")),
eai.WithProject("invoices"),
eai.WithEnvironment("prod"),
eai.WithRetries(4),
eai.WithRetryBaseDelay(500*time.Millisecond),
eai.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
eai.WithHeader("X-Client", "billing"),
)
if err != nil {
return err
}
agent, err := tuned.Agents.Get(ctx, "agt_ap", eai.WithCallHeader("X-Correlation-Id", "c-1"))

fmt.Println(client) masks the API key and the token. Redirects are never followed and TLS 1.2 is the minimum.

Requests and responses​

Most methods take a body any (a struct or a map[string]any, encoded as JSON) and return json.RawMessage; decode it into your own type with eai.Decode[T] or json.Unmarshal. Typed helpers return structs: Gateway.ChatCompletions returns *eai.ChatResponse with Content, Usage, DecisionID, CostEstimated and CostMeasured.

resp, err := client.Gateway.ChatCompletions(ctx, eai.ChatRequest{Model: "gpt-4o-mini", Messages: []eai.ChatMessage{{Role: "user", Content: "Bonjour"}}, Eai: &eai.EaiExtension{Effort: eai.EffortLow, AgentID: "agt_ap"}})
if err != nil { return err }
fmt.Println(resp.CostMeasured, resp.DecisionID)
raw, err := client.Agents.Get(ctx, "agt_ap")
if err != nil {
return err
}
type agentView struct {
ID string `json:"id"`
Status string `json:"status"`
}
decode := eai.Decode[agentView]
view, err := decode(raw)
fmt.Println(view.ID, view.Status)

Errors​

Every error answer uses the envelope {"error": {"code", "message", "message_i18n", "details", "request_id"}}. The SDK returns an *eai.APIError with Status (0 for a network error), Code, Message (in the client locale), MessageI18n, Details (secret looking keys masked), RequestID, RetryAfter and HasRetryAfter. Without an envelope the code comes from the status: 400 and 422 validation_error, 401 unauthenticated, 403 forbidden, 404 not_found, 409 conflict, 429 rate_limited, 5xx server_error.

Sentinel or helperMatchesRetried by the SDK
ErrPolicyDenied, IsPolicyDeniedpolicy_deniedno
ErrQuotaExceeded, IsQuotaExceededquota_exceeded, budget_exceededyes when the status is 429, then returned
ErrEffortNotSupported, IsEffortNotSupportedeffort_not_supportedno
ErrContextOverflow, IsContextOverflowcontext_overflowno
ErrApprovalRequired, IsApprovalRequired, APIError.ApprovalID()approval_requiredno
ErrNotFound, IsNotFoundnot_found or HTTP 404no
ErrAuth, IsAuthunauthenticated, forbidden, HTTP 401, HTTP 403 other than policy_deniedno
ErrRateLimitedrate_limitedyes, then returned
ErrEgressDenied, IsEgressDenied, EgressDeniedReasonegress_deniedno
ErrPromptInjectionBlocked, IsPromptInjectionBlockedprompt_injection_blockedno
ErrPayloadTooLarge, IsPayloadTooLargepayload_too_largeno
Code == "network_error"no answer (errors.Unwrap gives the cause)yes, then returned
Code == "server_error" and other 5xxHTTP 502, 503, 504 retried; 500 returned at oncepartly
_, err = client.Gateway.ChatCompletions(ctx, req)
var apiErr *eai.APIError
if errors.As(err, &apiErr) && apiErr.Code == "quota_exceeded" { log.Println(apiErr.MessageI18n["fr"], apiErr.RequestID) }
if errors.Is(err, eai.ErrQuotaExceeded) { /* same check */ }

Retries and timeouts​

A network error and HTTP 408, 429, 502, 503 and 504 are retried up to WithRetries times (default 2), for every method: a POST keeps its Idempotency-Key across retries, so the server deduplicates it. The delay starts at WithRetryBaseDelay (250 ms), doubles at each attempt up to 20 s and adds up to 20 % of jitter; a Retry-After header wins, capped at 60 s. The context deadline bounds the whole call, retries included; the http.Client timeout (60 s by default) bounds each attempt. A cancelled context stops at once.

callCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
_, err = client.Meta.Ready(callCtx)

Pagination​

List methods return a *eai.Page (Items []json.RawMessage, NextCursor, HasNext(), Next(ctx)). Iter() walks every item across pages, fetching lazily; All(ctx) collects them.

run, err := client.Runs.Start(ctx, "agt_ap", map[string]any{"task": "Process invoice med_01HZX3K9"}, eai.WithIdempotencyKey("inv-2291"))
page, err := client.Runs.List(ctx, eai.WithLimit(50))
it := page.Iter()
for it.Next(ctx) { fmt.Println(string(it.Item())) }
first, err := client.Agents.List(ctx, eai.WithLimit(100), eai.WithQuery(map[string]string{"status": "active"}))
if err != nil {
return err
}
all, err := first.Iter().All(ctx)
fmt.Println(len(all), first.HasNext())

Check it.Err() after the loop: it holds the error that stopped the iteration.

Idempotency and long operations​

Every POST sends an Idempotency-Key: the WithIdempotencyKey value, else a generated UUID kept across the retries of the call. The Control Plane replays the first answer for a key it already saw. A long creation answers with the Operation-Id and Operation-Location headers; client.Operations.Get(ctx, id) reads the operation, its tracked resource and its live state. client.Exchange(...) returns the status and the headers of any call.

op, err := client.Operations.Get(ctx, "op_1")
execution, err := client.Sandbox.Get(ctx, "sbx_1")
fmt.Println(string(op), string(execution))

Async and concurrency​

*eai.Client is safe for concurrent use: share one client between goroutines. Cancellation and deadlines travel in the context.Context of each call. The worker package runs asynchronous jobs with a bounded concurrency.

import "github.com/enterprise-ai/enterprise-ai-go/worker"

jobs := make(chan *worker.Job)
w, err := worker.New(worker.Config{
Client: client,
Source: &worker.ChanSource{Jobs: jobs},
Concurrency: 4,
Handler: func(ctx context.Context, job *worker.Job) (any, error) {
return map[string]any{"frames": 118}, nil
},
})
err = w.Run(ctx)

Streaming and realtime​

Gateway.ChatCompletionsStream returns a *eai.ChatStream over Server-Sent Events: Next(), Chunk() (with Delta, FinishReason, Usage), Err(), Close() and CollectText(). Voice live sessions use the realtime package (Realtime guide).

stream, err := client.Gateway.ChatCompletionsStream(ctx, eai.ChatRequest{Model: "gpt-4o-mini", Messages: []eai.ChatMessage{{Role: "user", Content: "Bonjour"}}})
if err != nil {
return err
}
defer stream.Close()
for stream.Next() {
fmt.Print(stream.Chunk().Delta)
}
err = stream.Err()
import "github.com/enterprise-ai/enterprise-ai-go/realtime"

raw, err := client.Realtime.CreateSession(ctx, map[string]any{"pipeline": "stt", "config": map[string]any{"agent_id": "agt_voice", "language": "fr"}})
var sess struct{ SessionID string `json:"session_id"` }
err = json.Unmarshal(raw, &sess)
defer client.Realtime.EndSession(ctx, sess.SessionID)
conn, err := realtime.Dial(ctx, client, sess.SessionID)
defer conn.Close()
for { ev, err := conn.Recv(); if err != nil { break }; if ev.Type == realtime.UsageUpdate { log.Println(string(ev.Raw)) } }

Logging and debugging​

The SDK writes no log. client.Exchange returns the status and the headers of any call, among them the X-Request-Id to give to the platform team; fmt.Println(client) prints the settings with the secrets masked.

resp, err := client.Exchange(ctx, "GET", "/v1/meta", nil, nil)
if err != nil {
return err
}
log.Printf("status=%d request_id=%s", resp.Status, resp.Header.Get("X-Request-Id"))
fmt.Println(client)

To log every call, give eai.WithHTTPClient an http.Client whose Transport wraps http.DefaultTransport and logs the method, the URL, the status and the X-Request-Id; never log the request headers, which hold the credentials.

Testing​

Point the client at an httptest server: the real transport runs (headers, retries, errors), on 127.0.0.1.

import "net/http/httptest"

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.URL.Path == "/v1/agents/missing" {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"error":{"code":"not_found","message":"Resource not found."}}`))
return
}
_, _ = w.Write([]byte(`{"items":[{"id":"agt_1"}],"next_cursor":null}`))
}))
defer srv.Close()
testClient, err := eai.NewClient(eai.WithBaseURL(srv.URL), eai.WithAPIKey("eai_test_key"), eai.WithRetries(0))
if err != nil {
return err
}
listed, err := testClient.Agents.List(ctx)
_, err = testClient.Agents.Get(ctx, "missing")
if !eai.IsNotFound(err) {
return err
}
fmt.Println(len(listed.Items))

Versioning and compatibility​

  • The module follows semantic versioning: v1.x keeps every exported name; a breaking change would use a /v2 module path.
  • It targets API v1 and the contracts v1; json.RawMessage results keep the fields a newer server adds.
  • eai.Version and the User-Agent header identify the SDK in the audit trail.
  • Pin the version in go.mod and read the release notes before an upgrade.

Resource guides​

One subsection per resource group, with one example per common call. Every method is in the generated reference.

System, identity and tenancy​

Meta, Catalog, Roadmap, Operations, Contracts, Manifests, Tenants, Projects, Members, APIKeys, ServiceIdentities, Authz, Secrets.

meta, err := client.Meta.Get(ctx)
project, err := client.Projects.Create(ctx, map[string]any{"slug": "invoices", "name": "Invoices"})
key, err := client.APIKeys.Create(ctx, map[string]any{"name": "ci-pipeline", "scopes": []string{"agents:run"}})
fmt.Println(string(meta), string(project), string(key))

Gateway, models and FinOps​

Providers, Models, Routes, LiteLLM, Adapters, Context, Gateway, FinOps, Quotas.

est, _ := client.FinOps.Estimate(ctx, map[string]any{"usage_type": "stt", "provider": "local", "model_id": "faster-whisper-large-v3", "units": map[string]any{"audio_seconds": 600}})
v, _ := client.Audit.Verify(ctx)
g, _ := client.Evaluations.CheckGate(ctx, map[string]any{"target": map[string]any{"type": "agent", "ref": "agt_ap"}})

Gateway middleware​

gatewaymw.New wraps an http.Handler: it evaluates the policy, reserves the quota, calls the handler and releases the reservation unless the handler committed it.

import "github.com/enterprise-ai/enterprise-ai-go/gatewaymw"

// Evaluate the policy, reserve the quota, call the wrapped handler, release the reservation unless the handler committed it.
h := gatewaymw.New(gatewaymw.Config{
Client: client,
Action: func(r *http.Request) string { return "model.invoke" },
Reserve: func(r *http.Request) map[string]any { return map[string]any{"tokens": 4000, "scope": map[string]any{"agent_id": r.Header.Get("X-Agent-Id")}} },
})(upstream)
http.ListenAndServe(":8081", h)

Usage events​

The usage package builds Usage Events and sends them in batches (POST /v1/finops/usage/batch).

import "github.com/enterprise-ai/enterprise-ai-go/usage"

ev, err := usage.NewEvent("acme", "gpu_local", "runtime").Model("local", "llama-70b-local", "", "").Execution("agt_ap", "", "", "").Unit("gpu_seconds", 12.4).Unit("energy_wh", 1.7).Build()
em := usage.NewEmitter(client, usage.Options{BatchSize: 500, FlushInterval: 5 * time.Second}) // POST /v1/finops/usage/batch
defer em.Close(ctx)
err = em.Emit(ev)

Agents, runs, webhooks and heartbeats​

Agents, Runs, Checkpoints, Heartbeats, A2A, Sessions, Schedules, Triggers, Events, Sandbox, Webhooks. A heartbeat answer with Stop() true means: save a checkpoint and exit.

h := eai.WebhookHeaders(raw, secret, "acme", "evt-42", time.Now())
_, err := c.Webhooks.DeliverSigned(ctx, "sap", raw, secret, "acme", "")
hb, err := c.Heartbeats.Heartbeat(ctx, runID, map[string]any{"progress": 0.5})
if hb.Stop() { /* checkpoint and exit */ }
b, err := usage.NewEvent(tenantID, "llm", "runtime").TokenTotals(1200, 300, 800, 120)
ks := worker.RunKillSwitch(c, runID) // trips on action "stop"

Licence, SOC, context, marketplace and policies​

License, SOC, Context, Delegations, Mandates, BreakGlass, Marketplace, Evidence, Storage, Policies, Approvals.

lic, err := c.License.Get(ctx)
order, err := c.SOC.CreateOrder(ctx, map[string]any{"incident_id": incID, "action": "kill_switch"})
_, err = c.SOC.ApproveOrder(ctx, orderID) // a second approver, four eyes
proof, err := c.SOC.VerifyKillSwitch(ctx, switchID)
tok, err := c.Context.Issue(ctx, map[string]any{"subject": "agent:billing", "purpose": "invoice"})
pre, err := c.Marketplace.Preflight(ctx, map[string]any{"package": "crm-sync", "version": "1.2.0"})
archive, err := c.Policies.DownloadBundle(ctx, revision) // []byte, tar.gz
res, err := eai.ValidateContract("en", "storage-profile", profile)

Architecture interfaces​

Named methods over documented routes. A refusal propagates as an *eai.APIError (relation_required, approval_closed, storage_object_protected), and an answer that does not confirm the result is returned as an error, never as a success.

assumed, err := c.ServiceIdentities.AssumeService(ctx, "si_billing", eai.AssumeRequest{Purpose: "invoice sync", Audience: []string{"erp"}, TTLSeconds: 600})
var ae *eai.APIError
if errors.As(err, &ae) && ae.Code == "relation_required" {
return err // write the can_assume relation first (POST /v1/authz/tuples)
}
_, err = c.Audit.Record(ctx, map[string]any{"action": "app.invoice.validated", "resource": map[string]any{"type": "invoice", "id": "inv-42"}, "outcome": "success"})
_, err = c.Approvals.Cancel(ctx, "apr_1", map[string]any{"reason": "request withdrawn"})
objects, err := c.Storage.List(ctx, "stp_1", eai.WithQuery(map[string]string{"prefix": "exports/"}), eai.WithLimit(100))
_, err = c.Storage.Delete(ctx, "stp_1", "exports/old.json", "")

Platform Manager space (platform_admin)​

client.PlatformManager reaches the Platform Manager space of the console, relayed by the Control Plane on behalf of the caller.

summary, err := c.PlatformManager.Summary(ctx)
services, err := c.PlatformManager.Services(ctx, "c1")
op, err := c.PlatformManager.RequestOperation(ctx, "c1", "opa", map[string]any{"action": "rolling_restart", "batch_size": 1})
restore, err := c.PlatformManager.PlanRestore(ctx, map[string]any{"artifact_id": "b1", "environment": "isolated"}) // ADR-0162
exercises, err := c.PlatformManager.DRExercises(ctx) // observed RPO and RTO

The manager package calls the Navin Manager API directly with a manager token nmt_<12 hex>_<secret>; GET, PUT and DELETE are retried, POST and PATCH never. The full guide is Navin Manager SDK.

import "github.com/enterprise-ai/enterprise-ai-go/manager"

nm, err := manager.New(manager.WithBaseURL("<manager-url>"), manager.WithToken(os.Getenv("NM_MANAGER_TOKEN")), manager.WithOnBehalfOf("alice"))
if err != nil {
return err
}
action, err := nm.Services.Action(ctx, "c1", "kafka", map[string]any{"action": "restart"})
fmt.Println(string(action))

Reference​