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:
| Package | Purpose |
|---|---|
eai (module root) | The Control Plane client, models, errors, pagination, SSE, contracts. |
manager | The Navin Manager client (Navin Manager SDK). |
gatewaymw | net/http middleware: policy check, quota reservation, release. |
worker | Asynchronous media and tool workers with a kill switch. |
usage | Builder and batched emitter of Usage Events. |
realtime | WebSocket client of voice live sessions (Realtime guide). |
Requirements and compatibility
| Item | Supported |
|---|---|
| Go | 1.26 or later (go 1.26.0 in sdks/go/go.mod) |
| Operating systems | Every platform supported by the Go toolchain (Linux, macOS, Windows; amd64 and arm64) |
| Dependencies | github.com/santhosh-tekuri/jsonschema/v6 (contracts), golang.org/x/net (WebSocket) |
| Control Plane API | v1 (eai.Version is the SDK version, 1.0.2) |
Installation
-
Download the module from Harbor. The SDK packages are hosted in the
sdkproject of the Navinspire Harbor registry (artifactsdk/go, tag = SDK version). The Navinspire registry isregistry.navinspire.ai; log in with a robot account that can pull from thesdkproject (given by the platform team). If your organization mirrors thesdkproject in its own Harbor, use the address of that mirror instead.oras login registry.navinspire.aioras pull registry.navinspire.ai/sdk/go:1.0.2 -o third_party/enterprise-ai-goExpected result:
third_party/enterprise-ai-go/go.modexists. -
Add the module to your project with a
replacedirective that points to the downloaded folder.go mod edit -require=github.com/enterprise-ai/enterprise-ai-go@v1.0.2go mod edit -replace=github.com/enterprise-ai/enterprise-ai-go=./third_party/enterprise-ai-gogo mod tidyExpected result:
go.modlists the module and itsreplaceline, andgo build ./...succeeds. The namegithub.com/enterprise-ai/enterprise-ai-gois the module name declared in thego.modof 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 thereplacedirective.
Offline install
-
Vendor the module. On a connected machine
go mod vendorcopies every dependency, the SDK included, intovendor/; the offline build then reads only that folder.go mod vendorgo build -mod=vendor ./... -
Or point at a copy of this repository with a
replacedirective ingo.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.
| Method | Option | Header sent |
|---|---|---|
Tenant API key eai_<prefix>_<secret> | eai.WithAPIKey(key) | X-API-Key |
| OIDC access token | eai.WithToken(token) | Authorization: Bearer <token> |
Development mode (server started with EAI_AUTH_DEV=1), only without credential | eai.WithTenant(slug) | X-Dev-Tenant |
| Workload identity, mTLS | eai.WithHTTPClient(...) with a client certificate | TLS 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):
| Option | Type | Default | Description |
|---|---|---|---|
WithBaseURL(u) | string | none, required | Control Plane URL without /v1; https except for localhost, 127.0.0.1, ::1. |
WithAPIKey(k) | string | none | Tenant API key, sent as X-API-Key. |
WithToken(t) | string | none | OIDC access token, sent as Authorization: Bearer. |
WithTenant(t) | string | none | Tenant slug; sent as X-Dev-Tenant only without credential. |
WithProject(p) | string | none | Sent as X-Project. |
WithEnvironment(e) | string | none | dev, 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) | int | 2 | Retries after the first attempt; negative values are refused. |
WithRetryBaseDelay(d) | time.Duration | 250ms | First 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, string | none | Header sent on every request. |
WithTraceparent(f) | func() string | random per request | Returns the W3C traceparent of the current span. |
WithCABundle(pem) | []byte | system pool | Extra trusted CAs (PEM); verification stays on. |
WithDangerouslyDisableTLSVerificationForLocalhost() | none | off | Disables certificate checks; refused unless the host is local. |
Per call options (eai.CallOption, last arguments of every method):
| Option | Description |
|---|---|
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 helper | Matches | Retried by the SDK |
|---|---|---|
ErrPolicyDenied, IsPolicyDenied | policy_denied | no |
ErrQuotaExceeded, IsQuotaExceeded | quota_exceeded, budget_exceeded | yes when the status is 429, then returned |
ErrEffortNotSupported, IsEffortNotSupported | effort_not_supported | no |
ErrContextOverflow, IsContextOverflow | context_overflow | no |
ErrApprovalRequired, IsApprovalRequired, APIError.ApprovalID() | approval_required | no |
ErrNotFound, IsNotFound | not_found or HTTP 404 | no |
ErrAuth, IsAuth | unauthenticated, forbidden, HTTP 401, HTTP 403 other than policy_denied | no |
ErrRateLimited | rate_limited | yes, then returned |
ErrEgressDenied, IsEgressDenied, EgressDeniedReason | egress_denied | no |
ErrPromptInjectionBlocked, IsPromptInjectionBlocked | prompt_injection_blocked | no |
ErrPayloadTooLarge, IsPayloadTooLarge | payload_too_large | no |
Code == "network_error" | no answer (errors.Unwrap gives the cause) | yes, then returned |
Code == "server_error" and other 5xx | HTTP 502, 503, 504 retried; 500 returned at once | partly |
_, 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.xkeeps every exported name; a breaking change would use a/v2module path. - It targets API
v1and the contractsv1;json.RawMessageresults keep the fields a newer server adds. eai.Versionand theUser-Agentheader identify the SDK in the audit trail.- Pin the version in
go.modand 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
Navin Manager client
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
- Generated Go reference: every type, function, method and field.
- API v1 of the Control Plane, Contracts v1, Realtime guide.
- Other SDKs.