Skip to main content

Rolter is a high-performance AI gateway written in Rust that sits in front of your upstream model providers. Any SDK or HTTP client that already speaks the OpenAI or Anthropic API works with Rolter unchanged — point it at the gateway base URL and supply a Rolter virtual key. Rolter handles authentication, load balancing, retries, rate limiting, and spend enforcement before the request ever reaches an upstream.

Note

What this surface guarantees across upgrades — and what happens when OpenAI or Anthropic change their own dialect — is stated in Versioning & compatibility.

Base URL#

The default port is 4000, configurable via [server] port in rolter.toml.

Authentication#

Every inference endpoint requires a virtual key. You can pass it in either of two ways, matching the convention of the SDK you are using:

StyleHeader
OpenAI-styleAuthorization: Bearer <virtual-key>
Anthropic-stylex-api-key: <virtual-key>

When no virtual keys are configured in rolter.toml the gateway runs open, which is convenient for local development. In production, define at least one key under [[virtual_keys]].

Note

Virtual keys are stored as peppered digests — the plaintext key never persists in gateway memory after the initial handshake. Revoke a key instantly by setting disabled = true in your config, or set expires_at for time-bounded access.

Endpoints#

MethodPathDescription
POST/v1/chat/completionsOpenAI Chat Completions; streaming via "stream": true (SSE)
POST/v1/completionsOpenAI legacy text completions
POST/v1/messagesAnthropic Messages API; streaming supported
GET/v1/modelsList configured model route names your key can access
GET/healthzLiveness check — returns 200 OK when the gateway is running
GET/readyzReadiness check — 503 while the instance is draining
GET/metricsPrometheus metrics in exposition format

Routing via the model field#

The model field in your request body selects a route defined in rolter.toml, not necessarily a literal upstream model name. Rolter looks up the route, applies any admin-configured param defaults, picks a target according to the route’s balancing strategy, rewrites model to the target’s upstream model ID, and forwards the request with the provider’s credentials.

If the requested model name does not match any configured route, Rolter returns 404 with "code": "model_not_found".

Request headers#

Authorizationstring#

Bearer token carrying your virtual key: Authorization: Bearer <virtual-key>. Required on all /v1/* endpoints when virtual keys are configured.

x-api-keystring#

Alternative to Authorization. Accepted on all /v1/* endpoints; preferred by Anthropic SDKs.

x-session-idstring#

Pass a stable session identifier (e.g. a user or conversation ID) to enable session affinity. When present, the consistent-hash and cache-aware balancers pin the request to the same upstream replica, improving KV-cache hit rates across multi-turn conversations.

x-request-idstring#

An optional client-supplied request identifier. Rolter echoes it in response headers and includes it in request logs for correlation. A deployment can carry the id in another header, such as x-correlation-id, by setting the request ID header; x-request-id is then neither read nor echoed.

Response headers#

Every proxied response carries x-rolter-* headers describing the routing decision, so you can see which upstream served a request without enabling ClickHouse logging.

x-request-idstring#

The end-to-end request id — the value you sent, or one Rolter generated. Returned under the configured request ID header when the deployment sets one.

x-rolter-providerstring#

The provider whose target served the request (e.g. openai-prod).

x-rolter-targetstring#

The resolved upstream model the request was forwarded as.

x-rolter-modelstring#

The requested model name that was routed.

x-rolter-variantstring#

The A/B variant that served the request. Omitted on the classic single-pool path.

x-rolter-cachestring#

HIT when the response came from the cache, otherwise MISS.

x-rolter-param-adjustmentsstring#

The request parameters Rolter did not forward as you sent them, because the upstream that served the request speaks another dialect, as <parameter>=<disposition> entries separated by , . Omitted when every parameter went through. See Adjusted parameters.

Adjusted parameters#

When a route sends an OpenAI or Anthropic request to an upstream that speaks another dialect (an Anthropic, Gemini or Gemini Interactions provider), a parameter with no equivalent there cannot be forwarded. Rolter still answers the request, and names each such parameter in x-rolter-param-adjustments:

DispositionMeaning
droppedThe upstream has no equivalent, so the parameter was not sent. top_k to Gemini, presence_penalty to Anthropic.
clampedThe value was sent, limited to the range the upstream accepts: a temperature above 1 sent to Anthropic, which accepts 0 to 1, is sent as 1.
translatedThe parameter was sent as an approximation that loses part of its meaning: a tool_choice forcing one function, sent to Gemini as “call any function”.
  • Names, never values. The header carries parameter names from Rolter’s fixed list of dialect parameters and the disposition, nothing you sent. A field Rolter does not recognise is not echoed.
  • Absent when nothing changed. Its presence is the signal. A parameter that is only renamed on the way (max_completion_tokens to Anthropic’s max_tokens, stop to stop_sequences) keeps its meaning and is not listed, and neither are model, stream and stream_options, which Rolter carries itself.
  • Streams too. It is decided before the request is sent upstream, so a streamed response carries it in its headers like any other.
  • Cache hits too. A response served from Rolter’s response cache carries the header of the request that produced the cached answer, so a hit discloses what its miss did. An entry cached by a Rolter older than this carries none until it expires.
  • Recorded. The same entries are kept on the request’s log row, in the param_adjustments column of request_logs as <disposition>:<parameter> (dropped:top_k), once clickhouse/022_param_adjustments.sql is applied (see Upgrading). The LLM Logs drawer lists them under Parameters not sent as given, and the Compatibility screen and GET /api/v1/analytics/param-adjustments roll them up per route and provider, with the share of its traffic each one touched.
  • Informational. The header’s name and its <parameter>=<disposition> shape are stable. The parameters it can name and the dispositions it uses may grow in any release, so treat an unknown disposition as “not forwarded as sent” rather than failing on it.

Error responses#

Rolter returns OpenAI-style JSON error bodies on every error path, so existing error-handling code works unchanged.

StatusMeaning
401 UnauthorizedMissing or invalid virtual key (type authentication_error, code invalid_api_key)
403 ForbiddenKey is valid but not permitted to use the requested model
402 Payment RequiredA spend budget for the key, project, team, or org has been exceeded
429 Too Many RequestsA rate limit (RPM or TPM) has been reached, or every upstream target answered 429 (code upstream_rate_limited); check the Retry-After response header
404 Not FoundNo route matches the requested model name
502 Bad GatewayAn upstream target was unreachable on the last attempt (connection refused, reset or timed out). Credentials in the provider’s address are masked in the message
503 Service UnavailableRoute has no targets, or every upstream target failed with a retryable status and none is left to try (code upstream_unavailable; the message names the last upstream status)

When your route’s retry budget runs out on a retryable upstream status, you get the upstream’s own answer, status and body as it sent them. When rolter runs out of targets first, it answers itself: a 429 from the provider stays a 429 (code upstream_rate_limited, with the provider’s Retry-After when it sent one), and any other retryable failure is a 503 (code upstream_unavailable) whose message names the last upstream status. Both are logged with the upstream’s own reason; see What gets logged.