Experimental. /v1/realtime carries the realtime
stability marker and may change shape in a minor release.
Budgets, rate limits, usage recording, guardrails, the guardrail webhook and
pre_upstream plugins apply to a session’s text events, and guardrails to
the arguments of the model’s function calls. Plugin calls made on a session
reach the request log on its next row, so the Plugins screen counts them. The PII sanitizer and
pre_route / post_response plugins do not run on a session. A
fail_closed PII sanitizer refuses the session with
sanitizer_unsupported_on_realtime; under fail_open the session is
admitted unsanitized.
/v1/realtime is a WebSocket relay compatible with the OpenAI Realtime API.
Rolter authenticates the request and selects an upstream before accepting the
client’s WebSocket upgrade, then pins that upstream and its provider key for
the life of the session.
Connect#
The gateway’s GET /openapi.json describes this upgrade as GET /v1/realtime,
including its required model query parameter, the 101 response and the HTTP
admission failures returned before a session is accepted.
Send the usual gateway virtual key and the public route model as a query parameter:
Authenticate with Authorization: Bearer <virtual-key> the same way you would
for /v1/chat/completions. The model value selects a route the same way it
does everywhere else in the gateway.
What is relayed#
Text, binary audio and WebSocket control frames are relayed in both
directions without application-level buffering, including session.update,
input_audio_buffer.*, response.* and function-call events.
If the upstream connection drops, the client must reconnect. Rolter does not fail a live session over to another target, because replaying audio or tool events mid-session is not safe to do automatically.
Budgets, rate limits and usage#
A realtime session meets the same budgets and rate limits as an HTTP request, on the same key, project, team and org scope.
Opening a session is checked before any upstream is dialled. A refused upgrade gets the HTTP status and error body a chat request would:
| Refusal | Status | error.code |
|---|---|---|
| a budget in the key’s scope is spent | 402 | insufficient_quota |
the model has no price and unpriced_policy resolves to block | 402 | model_unpriced |
an rpm or tpm window is full | 429 | rate_limit_exceeded |
| the gateway is shutting down | 503 | null |
Opening a session counts as one request against rpm, and a 429 carries
Retry-After.
While the session runs, rolter meters it per response. Each response.done
event reports the tokens that response used, and rolter turns it into one row in
the request log, priced and attributed like any other request, so realtime
spend appears in cost tracking and analytics.
Its cost is added to your budgets and its tokens to your tpm limits every
usage_flush_secs seconds (15 by default), not only when the session closes. A
session that stays open all day is accounted as it goes.
Audio and text tokens are priced apart. A response.done names the audio share of its input and output tokens, and when the model’s price row sets audio_input_per_mtok, audio_output_per_mtok or cached_audio_input_per_mtok those tokens are charged at them; a row without them prices every token at the text rates. See Audio and text tokens.
Input-audio transcription is metered as its own model. With input_audio_transcription on, the upstream runs a second model (whisper-1, gpt-4o-transcribe) over the caller’s audio and reports its usage on conversation.item.input_audio_transcription.completed. Each such event becomes one more row in the request log, and its cost is added to your budgets and its tokens to your tpm limits with the session’s turns. The row’s model is the transcription model the session named in session.update (session.audio.input.transcription.model, or session.input_audio_transcription.model in the beta shape) and it is priced from that model’s own price row:
- A model billed by tokens (
gpt-4o-transcribe) is priced from the event’s token usage, with its audio input ataudio_input_per_mtokwhen set. - A model billed by the minute (
whisper-1) is priced from the event’s duration ataudio_input_per_minute. - A model with no price row, a duration with no per-minute rate, or a session that never named a transcription model is logged
unpriced = 1with no cost. - An event that carries no usage is logged
usage_unknown = 1, unknown rather than free.
A full tpm window does not close a live session; it refuses the key’s next
session or request, as it would for HTTP.
When a budget runs out mid-session, rolter closes the session. This happens
at the next flush after the budget is spent, whether this session or other
traffic spent it. The client receives a Realtime error event first:
It then receives a close frame with code 1008 (policy violation). A session
can overspend by at most one flush window, plus the response in flight when the
budget ran out; lower usage_flush_secs to tighten that.
When the key is revoked mid-session, rolter closes the session too. Every flush
tick looks the session’s virtual key up again in the current configuration. A key
that was disabled, has expired or was deleted, a models list that no longer
names the session’s model, a route the key can no longer reach, and a
providers list that no longer includes the provider the session is connected
to all end the session, so revoking access is not left waiting on
max_session_secs. The client receives a Realtime error event with code
invalid_api_key (key gone), model_not_allowed, route_not_allowed or
provider_not_allowed, then a close frame with code 1008. The bound is one
flush interval, usage_flush_secs (one second when that is 0), plus the time
for the change to reach the gateway.
When the key is moved or its limits change mid-session, the same tick applies the change to the open session rather than leaving it on what it was opened with:
- Budgets, rate limits and prices edited while a session is open apply from its
next tick. A budget lowered below what its scope already spent closes the
session with
insufficient_quota. - A key moved to another team or project in the same organization keeps its session. Later turns are logged and charged under the new team and project, and a spent budget there closes the session.
- A key moved to another organization, or to a project whose
pre_upstreamplugins differ from the old one’s, closes the session with anerrorevent of codekey_scope_changedand a1008close. The client should open a new session, which is admitted under the key’s new scope. - A change to any other key leaves the session alone.
A response cut short (the client leaves, the upstream drops, or the session
is closed mid-answer) never reports its usage, although the provider bills
what it generated. Its row is written with usage_unknown = 1 so the gap is
visible rather than counted as free. See
Withheld responses for how
to query it.
Gateway restarts#
When a gateway receives SIGTERM (a rolling deploy, a scale-down) or Ctrl-C,
it closes every live session with WebSocket close code 1001 (going away) and
refuses new ones with 503. Reconnect, and the load balancer in front of the
fleet sends you to a gateway that is still running.
Before the process exits, each session’s usage so far is charged to budgets and
tpm, including turns still waiting for the next usage_flush_secs, and its
request-log rows are queued. A response in flight at that moment is logged with
status 503 and usage_unknown = 1. The gateway waits up to 10 seconds for
this, then up to 5 more seconds for the request-log writer, the health-event
writer and the budget and tpm recording queue to flush what they hold, so
give the process a termination grace period of at least 15 seconds on top of
your longest HTTP request. If ClickHouse or Redis is unreachable at that
moment, the rows and charges still queued are lost after those 5 seconds.
Current limits#
- WebRTC/browser ephemeral-token handoff is not exposed. Only the WebSocket-first flow is supported today.
- No failover. A dropped upstream ends the session; there is no session-affinity retry the way HTTP routes get.
- Content policy is narrower than on chat. Built-in guardrails, the
guardrail webhook and
pre_upstreamplugins check client text events (conversation.item.create,session.updateinstructions,response.create) and, for guardrails, the model’s text and transcript output. A blocked event is not forwarded and the client receives anerrorevent with codeguardrail_blocked(orplugin_blocked); the session stays open, and a blocked response is cancelled. Output is checked as it streams, so the part of a match before the delta that completes it can already have been delivered. The PII sanitizer andpre_route/post_responseplugins do not run on realtime events (afail_closedsanitizer refuses the session withsanitizer_unsupported_on_realtime), audio itself is not inspected, and a binary frame is refused while a policy applies. See Content policy for the detail. - Audio rates need the split. An audio rate applies only to the tokens a
response.donenames as audio. An upstream that reports totals alone is priced at the text rates. - A transcription model is named by the session. A session that turns
transcription on in its defaults, without a
session.updateor asession.updatednaming the model, has its transcription loggedunpriced.
[realtime] configuration#
The gateway’s [realtime] block holds per-process resource limits, which bound
what one gateway will hold rather than what a tenant may spend, and the
metering cadence:
max_connections(default1000) — concurrent sessions this gateway process admitsmax_session_secs(default3600) — hard session-duration limitidle_timeout_secs(default300) — closes a session when neither side sends a frameusage_flush_secs(default15) — how often finished responses are logged, charged to budgets andtpm, and followed by a budget check.0does this after every response instead of on a timer, and re-checks the budgets every second while the session is quiet
Set any of the first three to 0 to disable that limit. Metering itself cannot
be switched off. See Configuration for
where [realtime] lives in rolter.toml.