Skip to main content
The inference plane listens on 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:
Relay also reads the credential from 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

The model 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:
When several hosts serve a model, append @<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:
  • start carries the HTTP status and content type the same request would get over HTTP.
  • chunk frames carry the response body as a byte stream. Concatenate data across 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.
  • end closes the request. Frames for different ids interleave.
Errors use the same frames: 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: Anthropic cache_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_control breakpoint. A ttl over 5 minutes selects the 1-hour tier. Anthropic allows 4 breakpoints; when anchors exceed that, the oldest anchors are dropped. key is ignored.
  • OpenAI: caching is automatic, so the flags and anchors are ignored. key becomes prompt_cache_key; a ttl over 10 minutes becomes prompt_cache_retention: "24h", otherwise "in_memory".
  • Gemini: cache_config is ignored.
Inbound Anthropic 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 marks temperature, 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:
The request still succeeds. Each drop is also counted in the relay_dropped_params_total metric.

System messages

Start-of-conversation system instructions map to the upstream’s native form: the top-level system 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:
The marker round-trips: a user turn that is exactly this form is restored to a system message when Relay parses the conversation back. A <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.
In Go, 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.
Relay does not fail over mid-stream. Failover across the keys of the chosen host happens before the first byte reaches you. Once bytes flow, an upstream error reaches you as the upstream sent it.