Go gateway SDK — OpenAI-compatible client
osyra-go-sdk is an OpenAI-compatible Go client for Osyra Edge's verified
Chat Completions, Embeddings, and Models subset. Change one import and keep the
supported OpenAI-shaped request fields; enabled workspace controls add routing,
policy, cache, PII, usage, and evidence behavior. Additional fields may
serialize for source compatibility but are not server-capability claims. It
mirrors the Python SDK's transport decisions.
New to OSYRA? Start with the quickstart to get a workspace and a credential, then come back here.
Install
The source checkout lives at osyra-go-sdk/. Import its canonical module path:
import osyra "github.com/osyra-ai/osyra-go"Distribution status: the module is not yet published through a public package-release channel. Install from an authorized source checkout. Its canonical module path is
github.com/osyra-ai/osyra-go, and it is stdlib-only (net/http,encoding/json,bufio).
Drop-in swap: before / after
For the verified request subset, the integration is a one-import change. Here is OpenAI-shaped Go code before and after.
Before (native OpenAI-shaped client)
// Your existing OpenAI-shaped Go code.
resp, err := client.Chat.Completions.Create(ctx, ChatCompletionRequest{
Model: os.Getenv("OPENAI_MODEL"),
Messages: []Message{{Role: "user", Content: "Hello!"}},
})After (routed through OSYRA)
import (
"context"
"os"
osyra "github.com/osyra-ai/osyra-go"
)
client, err := osyra.NewClient(
osyra.WithAPIKey(os.Getenv("OSYRA_API_KEY")),
)
if err != nil {
// fail-closed: NewClient returns ErrNoAPIKey when no credential is configured.
}
resp, err := client.Chat.Completions.Create(context.Background(), osyra.ChatCompletionRequest{
Model: os.Getenv("OSYRA_MODEL"),
Messages: []osyra.Message{{Role: "user", Content: "Hello!"}},
})
if err != nil { /* handle */ }
println(resp.Choices[0].Message.Content)The request/response types are OpenAI-wire-shaped on purpose, so code written against an upstream OpenAI Go client parses OSYRA responses unchanged.
Configuration resolution
NewClient takes functional options and resolves configuration in this order:
| Setting | Option | Env fallback | Default |
|---|---|---|---|
| Credential | WithAPIKey(...) | OSYRA_API_KEY | none — NewClient returns ErrNoAPIKey |
| Base URL | WithBaseURL(...) | OSYRA_BASE_URL | https://api.osyra.ai/v1 |
| Timeout | WithTimeout(...) | — | 60s |
| Max retries | WithMaxRetries(...) | — | 2 |
NewClient is fail-closed: with no credential it returns osyra.ErrNoAPIKey
rather than building a silent unauthenticated client.
Other client options: WithHTTPClient(*http.Client) (inject your own transport),
WithUserAgent(string), WithDefaultHeader(key, value), and WithAllowInsecure()
(see Transport security below).
Authentication
The SDK emits exactly one credential channel:
| Configuration | Wire behavior |
|---|---|
| WithAPIKey("osy_<class>_<secret>") with the exact canonical grammar | X-Api-Key: ... |
| WithAccessToken(token) | Authorization: Bearer ... |
| WithTokenProvider(provider) | Authorization: Bearer ... |
| Another valid, unambiguous WithAPIKey(value) | Authorization: Bearer ... for source compatibility |
Canonical keys match
^osy_(sandbox|test|live)_[A-Za-z0-9_-]{43}$. A malformed osy_ value stays on
the opaque Bearer path. Generic header options cannot override
Authorization, X-Api-Key, or the unsupported X-Osyra-Api-Key alias.
API-key issuance is enabled per deployment. IAM must have API-key crypto and a
KMS HMAC key configured. Without it, management/minting fails closed with
501 OSY-SERVER-9050, while an Edge data-plane request that presents a key
currently returns 503 OSY-SERVER-9002 because the IAM verifier is unavailable.
The SDK transport being present does not by itself prove activation. Use
WithAccessToken/WithTokenProvider where API keys are not enabled.
Transport security
WithBaseURL must be https://. A non-HTTPS base URL is rejected unless you pass
WithAllowInsecure(), which permits http:// for localhost dev only:
client, _ := osyra.NewClient(
osyra.WithAPIKey(os.Getenv("OSYRA_API_KEY")),
osyra.WithBaseURL("http://localhost:8080/v1"),
osyra.WithAllowInsecure(), // localhost dev only
)The credential is never logged and never placed in a URL.
Chat completions
client.Chat.Completions.Create sends the exact OpenAI /v1/chat/completions
body and decodes into a typed *osyra.ChatCompletion:
resp, err := client.Chat.Completions.Create(ctx, osyra.ChatCompletionRequest{
Model: os.Getenv("OSYRA_MODEL"),
Messages: []osyra.Message{{Role: "user", Content: "Hello!"}},
})
if err != nil { /* handle */ }
choice := resp.Choices[0]
println(choice.Message.Content)
println(choice.FinishReason)
if resp.Usage != nil {
println(resp.Usage.TotalTokens)
}
println(resp.Provider) // OSYRA extension: which upstream provider served the callVerified request-field contract
Edge guarantees Model, non-empty string-role/string-content Messages,
MaxTokens, Temperature, TopP, Stop, Stream, and StreamOptions.
Sampling fields use pointers so an explicit zero is distinguishable from unset:
temp := 0.2
maxTok := 512
resp, err := client.Chat.Completions.Create(ctx, osyra.ChatCompletionRequest{
Model: os.Getenv("OSYRA_MODEL"),
Messages: msgs,
Temperature: &temp,
MaxTokens: &maxTok,
})The request type can serialize additional OpenAI-shaped fields and Extra
values for source compatibility. Serialization is not a server-capability
claim: Edge does not currently guarantee tools/function calling, multimodal
content, structured output, logprobs, or other extended semantics.
req := osyra.ChatCompletionRequest{
Model: os.Getenv("OSYRA_MODEL"),
Messages: msgs,
Extra: map[string]any{
"some_new_openai_param": true,
},
}Streaming
CreateStream sets stream:true, hits POST /v1/chat/completions/stream,
and parses the SSE data: chunks into *osyra.ChatCompletionChunk, terminating
on the [DONE] sentinel:
stream, err := client.Chat.Completions.CreateStream(ctx, osyra.ChatCompletionRequest{
Model: os.Getenv("OSYRA_MODEL"),
Messages: []osyra.Message{{Role: "user", Content: "Tell me a story."}},
})
if err != nil { /* handle */ }
defer stream.Close()
for {
chunk, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil { /* handle */ }
print(chunk.Choices[0].Delta.Content)
}The stream tolerates heartbeats, comment lines, and blank lines, delivers the
trailing chunk, and skips malformed lines. After the loop you can also check
stream.Err() for a terminal error.
Embeddings
client.Embeddings.Create hits /v1/embeddings and returns a
*osyra.CreateEmbeddingResponse. Input accepts a string or a []string
(OpenAI permits both):
emb, err := client.Embeddings.Create(ctx, osyra.EmbeddingRequest{
Model: os.Getenv("OSYRA_EMBEDDING_MODEL"),
Input: "hello world", // string or []string
})
if err != nil { /* handle */ }
println(len(emb.Data[0].Embedding))Listing models
client.Models.List sends a bodyless GET /v1/models and returns an
OpenAI-shaped *osyra.ModelList. The authenticated credential supplies the
authoritative workspace. RequestWithWorkspace or WithWorkspace adds an
optional equality assertion: Edge accepts it only when it equals the
credential's workspace and returns 403 on a mismatch. It cannot switch the
tenant and does not currently filter the broker catalog:
page, err := client.Models.List(ctx)
if err != nil { /* handle */ }
for _, m := range page.Data {
println(m.ID, m.OwnedBy)
}The live Edge response currently carries the OpenAI catalog fields ID,
Object, Created, and OwnedBy. The SDK type has optional extension slots
for richer broker metadata and tolerates both the bare-array and
{object,data} wire shapes, but those slots are not a current server claim.
In particular, the catalog does not identify chat-versus-embedding capability;
use operator-assigned OSYRA_MODEL and OSYRA_EMBEDDING_MODEL values and use
the catalog only to confirm deployment registration. A listed model can still
be denied by workspace policy; a successful authorized operation is the usable
model proof.
Native client.Osyra.* surface
client.Osyra is the OSYRA-native namespace with no OpenAI analog. It stays out
of the drop-in surface, so it vanishes if you point the base URL back at OpenAI.
// WIRED — same catalog as Models.List:
page, err := client.Osyra.Models(
ctx,
osyra.RequestWithWorkspace(os.Getenv("OSYRA_WORKSPACE_ID")),
)
// PENDING — broker RPCs exist but have no edge REST route yet:
_, err = client.Osyra.EstimateCost(ctx, osyra.CostEstimateRequest{
Model: os.Getenv("OSYRA_MODEL"),
Messages: msgs,
})
_, err = client.Osyra.RoutingDecision(ctx, osyra.RoutingDecisionRequest{Prompt: "..."})
_, err = client.Osyra.ProviderHealth(ctx)EstimateCost, RoutingDecision, and ProviderHealth return a
*osyra.NotImplementedError — the broker RPCs exist on AP00006 but have no edge
REST projection yet (AP00023 §12 OQ-2). No data is fabricated. Detect it via
the sentinel:
_, err := client.Osyra.EstimateCost(ctx, req)
if errors.Is(err, osyra.ErrNotImplemented) {
// pending route — handle gracefully (the error names the missing edge route)
}When those edge routes land, the method bodies fill in; the signatures are stable now.
OSYRA governance extras
The three OSYRA governance knobs — workspace, semantic cache, and PII masking —
are functional options, settable as a client default (WithXxx) or
per-call (RequestWithXxx). They project onto X-Osyra-* request headers and
never enter the OpenAI request body, so the same code still works if you point
the base URL back at a native OpenAI endpoint for A/B testing (the headers are
silently ignored there).
client, _ := osyra.NewClient(
osyra.WithAPIKey(os.Getenv("OSYRA_API_KEY")),
osyra.WithWorkspace(os.Getenv("OSYRA_WORKSPACE_ID")), // client default
)
resp, err := client.Chat.Completions.Create(ctx, req,
osyra.RequestWithCacheTTL(3600), // semantic-cache TTL (seconds)
osyra.RequestWithPIIMaskEnabled(true), // force PII masking (bool)
osyra.RequestWithPIIMask("redact"), // ...or a strategy string
)
// Alternative: explicitly disable cache for a call.
_, err = client.Chat.Completions.Create(ctx, req,
osyra.RequestWithCacheDisabled(),
)| option | wire header | meaning |
|---|---|---|
| WithWorkspace / RequestWithWorkspace | X-Osyra-Workspace | optional assertion that must equal the credential's authoritative workspace; it cannot switch tenants |
| WithCacheTTL / RequestWithCacheTTL | X-Osyra-Cache-TTL (+ X-Osyra-Cache: enabled) | semantic-cache freshness override |
| WithCacheDisabled / RequestWithCacheDisabled | X-Osyra-Cache: disabled | disables the semantic cache for the call |
| WithPIIMask / RequestWithPIIMask (string) | X-Osyra-PII-Mask | masking strategy |
| WithPIIMaskEnabled / RequestWithPIIMaskEnabled (bool) | X-Osyra-PII-Mask: true\|false | force PII masking on/off |
Passing no cache option emits neither cache header and inherits the
server/workspace behavior; it does not disable caching. An explicit disable
and an explicit TTL are alternatives. Per-call options override client-level
defaults field-by-field. You can also set an arbitrary per-call header via
RequestWithHeader(key, value).
Error handling
Non-2xx responses return an *osyra.APIError carrying .StatusCode, .Message,
.Code/.Type (OpenAI error envelope), .Body, .RequestID, and (for 429)
.RetryAfter. Specific conditions are distinguishable via errors.Is sentinels
and IsXxx helpers:
resp, err := client.Chat.Completions.Create(ctx, req)
switch {
case osyra.IsAuth(err): // 401 — fail-closed, never a silent unauthenticated call
case osyra.IsPermissionDenied(err): // 403
case osyra.IsRateLimit(err): // 429
if secs, ok := osyra.RetryAfter(err); ok { /* back off secs */ }
case osyra.IsNotFound(err): // 404
case osyra.IsBadRequest(err): // 400 / 422
case errors.Is(err, osyra.ErrInternalServer): // 5xx
default:
var apiErr *osyra.APIError
if errors.As(err, &apiErr) {
println(apiErr.StatusCode, string(apiErr.Body))
}
}Sentinels: ErrAuthentication, ErrPermissionDenied, ErrRateLimit,
ErrBadRequest, ErrNotFound, ErrInternalServer, plus the non-HTTP
ErrNoAPIKey and ErrNotImplemented. For the canonical OSYRA error catalog, see
the error reference.
Retry behavior
Automatic retry is limited to safe model-list GET requests. Inference,
streaming, and embedding POSTs are never replayed without an idempotency
contract. Eligible 429/503 retries are bounded by WithMaxRetries, honor
Retry-After, and otherwise use capped exponential backoff. All waits and calls
honor context cancellation and deadlines.
A note on async
There is no Async* surface in Go by design. The context.Context-aware
blocking API above, combined with goroutines, is the idiomatic Go concurrency
model — it is the analog of the Python port's AsyncOpenAI. (If you are coming
from osyra-python-sdk, that is where the async client lives; in Go you spawn a
goroutine per concurrent call.)
Migrating from the native OpenAI Go SDK
- Swap the import to
osyra "github.com/osyra-ai/osyra-go"and qualify the request/response types with theosyra.package (osyra.Message,osyra.ChatCompletionRequest, …). - Build the client with the credential enabled in your deployment. For an
activated API-key lifecycle, pass the one-time plaintext from Console to
osyra.NewClient(osyra.WithAPIKey(...)); an exact key is sent asX-Api-Key. Otherwise useWithAccessTokenorWithTokenProviderfor OAuth. - Use operator-assigned model IDs. Set chat
OSYRA_MODELand embeddingOSYRA_EMBEDDING_MODEL, then confirm each ID appears inModels.List. The current catalog does not expose operation capabilities, so do not infer modality or authorization from list order; prove each with an authorized operation. - Keep your call sites —
client.Chat.Completions.Create,...CreateStream, andclient.Embeddings.Createmirror the OpenAI shapes, and responses decode into OpenAI-wire-shaped structs. - Adopt the extras incrementally — add
RequestWithCacheTTL/RequestWithPIIMasketc. where you want OSYRA governance; they ride headers and are inert against a native OpenAI endpoint, so you can A/B test safely. - Update error handling to the
osyra.IsXxxhelpers /errors.Issentinels and the*osyra.APIErrortype.
What's implemented vs pending
The SDK is honest about scope. Implemented and tested (real wire contract):
NewClient with functional options + environment fallbacks;
Chat.Completions.Create with the Edge-verified field subset and
source-compatible serialization; streaming (CreateStream/Recv/Close);
Embeddings.Create; bodyless Models.List; client.Osyra.Models; the three
governance extras → X-Osyra-* headers; typed error mapping; safe-GET-only
context-aware retry; X-Request-Id correlation; the HTTPS-only guard; and
custom *http.Client injection.
Pending (flagged, never faked):
Osyra.EstimateCost/RoutingDecision/ProviderHealth— broker RPCs exist but have no edge REST projection yet; they return a*NotImplementedErrornaming the missing route (AP00023 §12 OQ-2).- Public package release — installation currently requires an authorized source checkout.