RELAY_PORT (default 8080). It accepts the OpenAI and Anthropic wire shapes, Relay’s provider-neutral canonical shape, and a few Relay endpoints for models, batches and telemetry.
Authentication
Every inference request authenticates with a relay key or an inference token:X-WR-API-Key (checked first) and x-api-key (checked last), so stock OpenAI and Anthropic SDKs work with their own auth settings. See Headers.
Relay tells the two credentials apart by shape: an inference token looks like a JWT (starts ey, two . separators), a relay key never does. Both are accepted on every inference route. Relay keys are minted in the admin UI or with POST /api/keys; the plaintext is returned once and Relay stores only its SHA-256. Inference tokens are minted with POST /api/auth/token, are scoped to one project, and expire on their own. See Authentication.
Rotating a relay key (POST /api/keys/by-id/{id}/rotate) accepts {"graceSeconds": N}: the previous plaintext keeps authenticating for that long, so callers can be redeployed without a window of 401s. 0 cuts over immediately; the ceiling is the keyRotateMaxGrace setting (24 hours by default).
Endpoints
Each vendor wire shape has its own path prefix. The bare/v1 namespace belongs to Relay.
Wire shapes
Gemini is an upstream shape only: bindings with
adapter: gemini are reachable from any inbound shape above, but there is no inbound Gemini route.
Coding-agent clients
Each client gets a path prefix that mirrors its wire shape and adds what the client probes for. The routes authenticate like the shape routes, except the Claude Code startup probe.
The prefix also tells Relay which client is calling, which attributes the usage event to it. A request without the prefix is matched by its
User-Agent, or by an X-WR-Client header naming the client.
Relay endpoints
System endpoints
These need no credential.
Until the catalog has loaded, every authenticated route answers
503 catalog_unavailable with Retry-After: 5.
Examples
When the inbound shape matches the upstream’s, the response streams from the upstream byte for byte. Otherwise (OpenAI in, Anthropic upstream, for example) the request and every stream event are translated through Relay’s canonical shape.
Models
Themodel field is resolved against your catalog. A model is reachable only if the policy that governs your credential grants it and the model has an enabled host binding with a usable host key. List what your credential can reach:
@<host> to pin one ("model": "claude-sonnet-4-6@anthropic"), or send X-WR-Upstream-Host: <host>. An @host already in the model wins over the header.
Canonical shape
POST /v1/generate takes Relay’s own provider-neutral request and returns a canonical response, whatever shape the upstream speaks. It is the shape the Go SDK and the WebSocket transport use.
The response carries
id, model, status, finish_reason, output (items in the same form as input, so they can be appended to the next request) and usage, a map of token counts (input, output, cache_read, cache_creation, reasoning, …). A refusal comes back as a normal message item with finish_reason: "refusal".
With output_mode: "stream" the response is an SSE stream of six event types: generation.created, item.started, item.delta, item.completed, generation.completed and error.
Send X-WR-Usage: full to get a relay_usage block on the response (on the final event when streaming) with the request id, token counts, upstream attempts and timing. It is added to canonical responses only.
WebSocket
GET /v1/ws upgrades to a WebSocket that carries many canonical requests over one connection, so a client that sends many requests in a row pays the TLS and authentication handshake once.
The upgrade request authenticates with the same headers as any inference request. Every frame you send is one request:
id is yours to choose and correlates the response; payload is a /v1/generate body. Relay answers each request with frames tagged with that id, always in this order:
startcarries the HTTP status and content type the same request would get over HTTP.chunkframes carry the response body as a byte stream. Concatenatedataacross a request’s chunks: for a streamed request the result is the canonical SSE stream, otherwise the JSON response body. A chunk boundary is not an event boundary.endcloses the request. Frames for different ids interleave.
start carries the error status and the chunks carry the usual error envelope.
A text frame that is not JSON or has no
id is dropped without a reply; binary frames are ignored. Each frame is checked again when it arrives: a revoked or expired credential, a changed or disabled policy, catalog edits and a change to the proxy-mode setting all apply to the next frame on an open connection. Each frame is its own request for rate limits, the in-flight cap and usage.
The Go SDK’s client.RelayWS speaks this protocol. See Go SDK.
Prompt caching
Same-shape requests are forwarded with their cache fields untouched: Anthropiccache_control markers reach an Anthropic upstream, OpenAI prompt_cache_key and prompt_cache_retention reach an OpenAI upstream.
When a request crosses shapes, Relay carries cache intent through the canonical cache_config:
Each upstream applies what it can express:
- Anthropic: each flag and anchor becomes a
cache_controlbreakpoint. Attlover 5 minutes selects the 1-hour tier. Anthropic allows 4 breakpoints; when anchors exceed that, the oldest anchors are dropped.keyis ignored. - OpenAI: caching is automatic, so the flags and anchors are ignored.
keybecomesprompt_cache_key; attlover 10 minutes becomesprompt_cache_retention: "24h", otherwise"in_memory". - Gemini:
cache_configis ignored.
cache_control markers and inbound OpenAI prompt_cache_key / prompt_cache_retention are read into cache_config. cache_control keys inside an OpenAI Chat Completions body are not read, so they are lost on a cross-shape route.
Cache hits show up as cache_read and cache writes as cache_creation in the response usage and in usage events.
Unsupported parameters
When the catalog markstemperature, top_p or top_k as unsupported for the routed model, Relay removes it from the request instead of letting the upstream reject it, and reports the change on the response:
relay_dropped_params_total metric.
System messages
Start-of-conversation system instructions map to the upstream’s native form: the top-levelsystem field (Anthropic), systemInstruction (Gemini), or a leading system message (OpenAI-compatible).
System messages that appear mid-conversation keep their position. They are never merged into the start-of-conversation system prompt, so a late instruction does not invalidate the upstream’s prompt-cache prefix. Relay’s canonical semantics follow the OpenAI shape here: on OpenAI-compatible upstreams the message is sent as a real system turn in place. Anthropic and Gemini upstreams cannot express that on every model, so there the message is delivered in place as a user turn wholly wrapped in <system> markers:
<system> tag inside longer user text is never promoted. Inbound Anthropic requests may also carry role: "system" messages (supported by recent Claude models); Relay accepts them and keeps them positional.
A canonical system message can instead opt into full system authority with "hoist": true: it is merged into the upstream’s start-of-conversation system form on every shape. Hoisting changes the beginning of the prompt, so it invalidates the upstream’s cached prefix for that conversation. Use it for instructions that must override user content, not for routine mid-session notes.
Catalog
GET /v1/catalog returns the catalog document the Go SDK reads (hosts with their model bindings and pricing, plus models and providers metadata), built from this deployment’s live catalog and cut down to the bindings your credential can route to. It authenticates like /v1/models and lists the same models: every hosts[].models[].model is an id /v1/models returns for the same credential, and a credential without a policy gets the same 403 when policy-less traffic is off.
The document omits each host’s baseURL, path and consoleURL, since calls go through Relay rather than to the upstream. version is deployment@<tag> when the catalog was seeded from a release tag, else deployment; generatedAt is when the document was rendered.
The response carries a weak ETag and Cache-Control: private, no-cache. Send the tag back in If-None-Match to get 304 Not Modified while the catalog is unchanged. With Accept-Encoding: gzip the body is gzip-compressed.
client.Relay(...).Catalog(ctx) fetches and revalidates it, and Models(ctx) returns the /v1/models ids.
Errors
Relay writes errors in one envelope:
When an upstream failure is forwarded, Relay keeps only
Retry-After (rewritten to whole seconds), X-Should-Retry, X-Request-Id, Content-Type and the upstream’s rate-limit headers (Anthropic-Ratelimit-*, X-Ratelimit-*), and adds X-WR-Upstream-Status with the upstream’s status.
X-WR-Origin says who produced a response: relay when Relay wrote it, upstream when the status and body came from the provider. A 401 from Relay therefore means your relay key, while a 401 with X-WR-Origin: upstream means the provider rejected the host key or your own upstream key. Relay also sets X-Should-Retry: true|false on the errors it writes (true for 408, 429 and 5xx) unless the upstream already sent one. The 401 and 403 responses from the credential check carry neither header.