Aller au contenu principal

SDK Go

Ce guide montre comment installer le SDK Go (module github.com/enterprise-ai/enterprise-ai-go, paquet eai), le connecter au Control Plane de la Navin Platform et appeler chaque groupe de ressources de l'API v1 avec des erreurs typées, des reprises, la pagination et le streaming. Source : sdks/go. Chaque type, fonction et champ figure dans la référence générée.

English version: go.md

Vue d'ensemble​

eai.NewClient(opts...) renvoie un *eai.Client avec un champ de service par groupe de ressources de l'API v1 (client.Agents, client.Gateway, client.FinOps...). Chaque méthode prend un context.Context, correspond à une route documentée et renvoie un json.RawMessage, une structure typée ou une *eai.Page. Les erreurs sont des valeurs *eai.APIError, reconnues avec errors.As, errors.Is et les sentinelles Err*. Des sous-paquets ajoutent des briques pour les services :

PaquetRôle
eai (racine du module)Le client du Control Plane, les modèles, les erreurs, la pagination, SSE, les contrats.
managerLe client du Navin Manager (SDK du Navin Manager).
gatewaymwMiddleware net/http : contrôle de politique, réservation de quota, libération.
workerWorkers asynchrones de médias et d'outils avec un kill switch.
usageConstruction et émission par lots des événements d'usage.
realtimeClient WebSocket des sessions vocales en direct (guide temps réel).

Prérequis et compatibilité​

ÉlémentPris en charge
Go1.26 ou plus (go 1.26.0 dans sdks/go/go.mod)
Systèmes d'exploitationChaque plateforme prise en charge par la chaîne Go (Linux, macOS, Windows ; amd64 et arm64)
Dépendancesgithub.com/santhosh-tekuri/jsonschema/v6 (contrats), golang.org/x/net (WebSocket)
API du Control Planev1 (eai.Version est la version du SDK, 1.0.2)

Installation​

  1. Téléchargez le module depuis Harbor. Les paquets des SDK sont hébergés dans le projet sdk du registre Harbor de Navinspire (artefact sdk/go, tag = version du SDK). Le registre Navinspire est registry.navinspire.ai ; connectez-vous avec un compte robot qui peut lire (pull) le projet sdk (fourni par l'équipe plateforme). Si votre organisation réplique le projet sdk dans son propre Harbor, utilisez l'adresse de ce miroir.

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

    Résultat attendu : third_party/enterprise-ai-go/go.mod existe.

  2. Ajoutez le module à votre projet avec une directive replace qui pointe vers le dossier téléchargé.

    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

    Résultat attendu : go.mod liste le module et sa ligne replace, et go build ./... réussit. Le nom github.com/enterprise-ai/enterprise-ai-go est le nom de module déclaré dans le go.mod du SDK et utilisé dans les imports. C'est un identifiant, pas une adresse de téléchargement : rien n'est récupéré à cette adresse.

    En cas d'échec : vérifiez go version (1.26 ou plus) et le chemin de la directive replace.

Installation hors ligne​

  1. Intégrez le module au dépôt (vendoring). Sur une machine connectée, go mod vendor copie chaque dépendance, SDK compris, dans vendor/ ; la construction hors ligne ne lit ensuite que ce dossier.

    go mod vendor
    go build -mod=vendor ./...
  2. Ou pointez vers une copie de ce dépôt avec une directive replace dans go.mod : replace github.com/enterprise-ai/enterprise-ai-go => ../sdks/go.

Démarrage rapide​

Cet exemple construit un client, lit l'identité de l'appelant et envoie une complétion de chat par le 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)

Ce qui se passe : NewClient vérifie les options (une URL de base est obligatoire, https sauf sur localhost), chaque appel envoie la clé dans X-API-Key et un traceparent W3C, et la complétion part avec une Idempotency-Key. Résultat attendu : le document d'identité, puis le texte de la réponse et son coût mesuré.

Authentification​

Le serveur résout le tenant à partir de l'identifiant. Le SDK ne lit aucune variable d'environnement : passez les valeurs aux options, par exemple avec os.Getenv.

MéthodeOptionEn-tête envoyé
Clé API de tenant eai_<prefix>_<secret>eai.WithAPIKey(key)X-API-Key
Jeton d'accès OIDCeai.WithToken(token)Authorization: Bearer <token>
Mode développement (serveur lancé avec EAI_AUTH_DEV=1), seulement sans identifianteai.WithTenant(slug)X-Dev-Tenant
Identité de workload, mTLSeai.WithHTTPClient(...) avec un certificat clientcertificat client TLS
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"),
)

Configuration du client​

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

OptionTypeDéfautDescription
WithBaseURL(u)stringaucun, obligatoireURL du Control Plane sans /v1 ; https sauf pour localhost, 127.0.0.1, ::1.
WithAPIKey(k)stringaucunClé API du tenant, envoyée dans X-API-Key.
WithToken(t)stringaucunJeton d'accès OIDC, envoyé dans Authorization: Bearer.
WithTenant(t)stringaucunSlug du tenant ; envoyé dans X-Dev-Tenant seulement sans identifiant.
WithProject(p)stringaucunEnvoyé dans X-Project.
WithEnvironment(e)stringaucundev, test, staging ou prod, envoyé dans X-Environment.
WithLocale(l)string"en"en ou fr (toute valeur qui commence par fr donne fr) ; envoyé dans Accept-Language.
WithRetries(n)int2Reprises après la première tentative ; les valeurs négatives sont refusées.
WithRetryBaseDelay(d)time.Duration250msPremier délai d'attente, doublé à chaque tentative.
WithHTTPClient(h)*http.Client&http.Client{Timeout: 60 * time.Second}Client personnalisé (délai, proxy, mTLS).
WithHeader(name, value)string, stringaucunEn-tête envoyé avec chaque requête.
WithTraceparent(f)func() stringaléatoire par requêteRenvoie le traceparent W3C du span courant.
WithCABundle(pem)[]bytemagasin du systèmeAutorités de certification supplémentaires (PEM) ; la vérification reste active.
WithDangerouslyDisableTLSVerificationForLocalhost()aucundésactivéDésactive le contrôle des certificats ; refusé sauf si l'hôte est local.

Options par appel (eai.CallOption, derniers arguments de chaque méthode) :

OptionDescription
WithQuery(map[string]string)Paramètres de la chaîne de requête (filtres).
WithLimit(n), WithCursor(c)Taille de page et curseur d'un appel de liste.
WithIdempotencyKey(k)Idempotency-Key d'un POST ; sinon une clé est générée et conservée entre les reprises.
WithCallTraceparent(tp)Traceparent W3C de cet appel.
WithCallHeader(name, value)En-tête supplémentaire de cet appel.
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) masque la clé API et le jeton. Les redirections ne sont jamais suivies et TLS 1.2 est le minimum.

Requêtes et réponses​

La plupart des méthodes prennent un corps any (une structure ou une map[string]any, encodée en JSON) et renvoient un json.RawMessage ; décodez-le dans votre propre type avec eai.Decode[T] ou json.Unmarshal. Des aides typées renvoient des structures : Gateway.ChatCompletions renvoie un *eai.ChatResponse avec Content, Usage, DecisionID, CostEstimated et 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)

Erreurs​

Chaque réponse d'erreur utilise l'enveloppe {"error": {"code", "message", "message_i18n", "details", "request_id"}}. Le SDK renvoie un *eai.APIError avec Status (0 pour une erreur réseau), Code, Message (dans la langue du client), MessageI18n, Details (clés d'allure secrète masquées), RequestID, RetryAfter et HasRetryAfter. Sans enveloppe, le code vient du statut : 400 et 422 validation_error, 401 unauthenticated, 403 forbidden, 404 not_found, 409 conflict, 429 rate_limited, 5xx server_error.

Sentinelle ou aideReconnaîtReprise par le SDK
ErrPolicyDenied, IsPolicyDeniedpolicy_deniednon
ErrQuotaExceeded, IsQuotaExceededquota_exceeded, budget_exceededoui quand le statut est 429, puis renvoyée
ErrEffortNotSupported, IsEffortNotSupportedeffort_not_supportednon
ErrContextOverflow, IsContextOverflowcontext_overflownon
ErrApprovalRequired, IsApprovalRequired, APIError.ApprovalID()approval_requirednon
ErrNotFound, IsNotFoundnot_found ou HTTP 404non
ErrAuth, IsAuthunauthenticated, forbidden, HTTP 401, HTTP 403 autre que policy_deniednon
ErrRateLimitedrate_limitedoui, puis renvoyée
ErrEgressDenied, IsEgressDenied, EgressDeniedReasonegress_deniednon
ErrPromptInjectionBlocked, IsPromptInjectionBlockedprompt_injection_blockednon
ErrPayloadTooLarge, IsPayloadTooLargepayload_too_largenon
Code == "network_error"pas de réponse (errors.Unwrap donne la cause)oui, puis renvoyée
Code == "server_error" et autres 5xxHTTP 502, 503, 504 repris ; 500 renvoyé aussitôten partie
_, 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 */ }

Reprises et délais​

Une erreur réseau et les statuts HTTP 408, 429, 502, 503 et 504 sont repris jusqu'à WithRetries fois (2 par défaut), pour toutes les méthodes : un POST garde son Idempotency-Key entre les reprises, donc le serveur le dédoublonne. Le délai commence à WithRetryBaseDelay (250 ms), double à chaque tentative jusqu'à 20 s et ajoute jusqu'à 20 % d'aléa ; un en-tête Retry-After l'emporte, plafonné à 60 s. L'échéance du contexte borne l'appel entier, reprises comprises ; le délai du http.Client (60 s par défaut) borne chaque tentative. Un contexte annulé arrête l'appel aussitôt.

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

Pagination​

Les méthodes de liste renvoient une *eai.Page (Items []json.RawMessage, NextCursor, HasNext(), Next(ctx)). Iter() parcourt chaque élément à travers les pages, en les chargeant à la demande ; All(ctx) les rassemble.

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())

Vérifiez it.Err() après la boucle : il contient l'erreur qui a arrêté le parcours.

Idempotence et opérations longues​

Chaque POST envoie une Idempotency-Key : la valeur de WithIdempotencyKey, sinon un UUID généré et conservé entre les reprises de l'appel. Le Control Plane rejoue la première réponse pour une clé déjà vue. Une création longue répond avec les en-têtes Operation-Id et Operation-Location ; client.Operations.Get(ctx, id) lit l'opération, sa ressource suivie et son état courant. client.Exchange(...) renvoie le statut et les en-têtes de n'importe quel appel.

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

Asynchrone et concurrence​

*eai.Client est sûr en usage concurrent : partagez un seul client entre les goroutines. L'annulation et les échéances voyagent dans le context.Context de chaque appel. Le paquet worker exécute des tâches asynchrones avec une concurrence bornée.

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 et temps réel​

Gateway.ChatCompletionsStream renvoie un *eai.ChatStream sur Server-Sent Events : Next(), Chunk() (avec Delta, FinishReason, Usage), Err(), Close() et CollectText(). Les sessions vocales en direct utilisent le paquet realtime (guide temps réel).

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)) } }

Journalisation et débogage​

Le SDK n'écrit aucun journal. client.Exchange renvoie le statut et les en-têtes de n'importe quel appel, dont le X-Request-Id à donner à l'équipe plateforme ; fmt.Println(client) affiche les réglages avec les secrets masqués.

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)

Pour journaliser chaque appel, donnez à eai.WithHTTPClient un http.Client dont le Transport enveloppe http.DefaultTransport et journalise la méthode, l'URL, le statut et le X-Request-Id ; ne journalisez jamais les en-têtes de la requête, qui portent les identifiants.

Tests​

Dirigez le client vers un serveur httptest : le vrai transport s'exécute (en-têtes, reprises, erreurs), sur 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))

Versions et compatibilité​

  • Le module suit le versionnage sémantique : v1.x garde chaque nom exporté ; un changement incompatible utiliserait un chemin de module /v2.
  • Il cible l'API v1 et les contrats v1 ; les résultats json.RawMessage gardent les champs qu'un serveur plus récent ajoute.
  • eai.Version et l'en-tête User-Agent identifient le SDK dans la piste d'audit.
  • Fixez la version dans go.mod et lisez les notes de version avant une mise à jour.

Guides des ressources​

Une sous-section par groupe de ressources, avec un exemple par appel courant. Chaque méthode est dans la référence générée.

Système, identité et 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, modèles et 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"}})

Middleware du gateway​

gatewaymw.New enveloppe un http.Handler : il évalue la politique, réserve le quota, appelle le handler et libère la réservation sauf si le handler l'a confirmée.

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)

Événements d'usage​

Le paquet usage construit les événements d'usage et les envoie par lots (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 et heartbeats​

Agents, Runs, Checkpoints, Heartbeats, A2A, Sessions, Schedules, Triggers, Events, Sandbox, Webhooks. Une réponse de heartbeat avec Stop() vrai signifie : enregistrer un point de reprise et sortir.

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, contexte, place de marché et politiques​

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)

Interfaces de l'architecture​

Méthodes nommées sur des routes documentées. Un refus se propage en *eai.APIError (relation_required, approval_closed, storage_object_protected), et une réponse qui ne confirme pas le résultat est renvoyée comme une erreur, jamais comme un succès.

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", "")

Espace Platform Manager (platform_admin)​

client.PlatformManager atteint l'espace Platform Manager de la console, relayé par le Control Plane au nom de l'appelant.

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

Client du Navin Manager​

Le paquet manager appelle directement l'API du Navin Manager avec un jeton du manager nmt_<12 hex>_<secret> ; GET, PUT et DELETE sont repris, POST et PATCH jamais. Le guide complet est SDK du Navin Manager.

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))

Référence​