2026-08-21 04:27:34 +08:00
# WebSocket transports
2026-08-23 02:51:49 +08:00
Aether exposes several independent WebSocket surfaces. They share transport
2026-08-21 04:27:34 +08:00
machinery, but not request schemas or continuation state:
| Public route | API format | Protocol |
| --- | --- | --- |
| `GET /v1/responses` | `openai:responses` | Responses WebSocket mode; every turn starts with `response.create` . |
| `GET /v1/realtime?model=...` | `openai:realtime` | OpenAI Realtime JSON events, including Base64 audio events. |
2026-08-23 02:51:49 +08:00
| `GET /v1/realtime?model=...` with a first-party Codex `originator` | `codex:live` | Current Codex Realtime v2 direct WebSocket transport. |
| `POST /v1/realtime/calls` , `GET /v1/realtime?intent=quicksilver&call_id=...` | `codex:live` | Codex Realtime v1 AVAS WebRTC call creation and sideband transport. |
| `GET/POST /v1/live[/{call_id}]` | `codex:live` | Legacy Codex Frameless direct and WebRTC compatibility transport. |
2026-08-21 04:27:34 +08:00
Do not point one surface at an endpoint configured for another. In particular,
a Realtime or Live event is not passed through the Responses
`response.create` state machine.
## Responses WebSocket mode
2026-07-31 10:59:15 +08:00
The Responses API supports a WebSocket mode for long-running, tool-call-heavy workflows. In this mode, you keep a persistent connection to `/v1/responses` and continue each turn by sending only new input items plus `previous_response_id` .
WebSocket mode is compatible with both Zero Data Retention (ZDR) and `store=false` .
2026-08-21 04:27:34 +08:00
## OpenAI Realtime WebSocket bridge
Configure an active `openai:realtime` provider endpoint, then connect to:
```text
wss://<aether-host>/v1/realtime?model=<authorized-global-model>
```
Aether authenticates and plans the request before returning the downstream
WebSocket upgrade. The global model alias is replaced in the upstream query,
while safe non-credential query parameters, provider authentication,
`header_rules` , and proxy settings continue to apply. Client credentials in
the query string are rejected or removed rather than forwarded upstream.
After the handshake, Aether relays text, binary, ping, pong, and close frames
one at a time. JSON events, Base64 audio payloads, and unknown future fields are
not rebuilt or coalesced. When an upstream `response.done` contains an
authoritative `response.usage` , its text/audio token counters are accumulated
for the connection's usage record. A session that closes without authoritative
usage is recorded as `usage_available=false` ; Aether does not estimate token
counts, audio duration, or cost from frame sizes.
`response.done` covers Realtime Response usage. Optional input transcription
is reported by a different event and can use a different transcription model;
it is not folded into the Response model's session row or priced as if it used
that model. Finite-balance Realtime access therefore remains fail-closed until
multi-event, multi-model settlement is implemented.
The upstream handshake is completed before Aether sends HTTP 101 to the
client. A provider authentication, TLS, proxy, or upgrade failure therefore
returns an ordinary bounded HTTP error instead of opening a socket that fails
immediately.
See the official [OpenAI Realtime WebSocket guide ](https://developers.openai.com/api/docs/guides/realtime-websocket )
for the current event contract.
2026-08-20 21:59:05 +08:00
## Experimental Codex Live bridge
Aether also exposes the Codex Frameless Bidi V3 transport used by current
Codex clients. It is related to the OpenAI Realtime API, but it is not the
Responses WebSocket protocol and never enters Aether's `response.create`
state machine:
2026-08-23 02:51:49 +08:00
- Current WebRTC call creation: `POST /v1/realtime/calls?intent=quicksilver&architecture=avas`
with bounded `sdp` and `session` multipart parts. Aether normalizes those
selectors, applies the global-to-provider mapping, and rewrites the upstream
`Location` to `/v1/realtime/calls/<call-id>` .
- Current WebRTC sideband: `GET /v1/realtime?intent=quicksilver&call_id=<call-id>` .
The unique `intent=quicksilver` selector classifies it as Codex Live;
`call_id` alone remains on the separate OpenAI Realtime surface.
- Current direct WebSocket (Realtime v2):
`GET /v1/realtime?model=<authorized-global-model>` . V2 intentionally omits
`intent=quicksilver` ; Aether uses the first-party Codex `originator` header
to distinguish it from ordinary OpenAI Realtime. The first client event is
still the opaque `session.update` frame generated by Codex; Aether forwards
it without converting it into a Responses `response.create` event.
- Realtime v1 direct WebSocket uses
`GET /v1/realtime?intent=quicksilver&model=<authorized-global-model>` and
sends `openai-alpha: quicksilver=v1` upstream. V2 does not send that header.
- Legacy direct WebSocket: `GET /v1/live?model=<global-model>` . The first client text
2026-08-20 21:59:05 +08:00
frame must be `session.update` ; later text, binary, ping, pong, and close
frames are relayed opaquely.
2026-08-23 02:51:49 +08:00
- Legacy WebRTC call creation: `POST /v1/live` with bounded `sdp` and `session`
2026-08-20 21:59:05 +08:00
multipart parts. Aether applies the existing global-to-provider model
mapping and rewrites the upstream `Location` to `/v1/live/<call-id>` .
2026-08-23 02:51:49 +08:00
- Legacy WebRTC sideband: `GET /v1/live/<call-id>` . Frameless sideband attaches to an
2026-08-20 21:59:05 +08:00
already initialized call, so Aether neither waits for nor sends a second
`session.update` frame.
2026-08-21 04:27:34 +08:00
The provider must expose an active, dedicated `codex:live` endpoint. Fixed
Codex providers receive this endpoint from the managed provider template;
custom providers can add it in the endpoint editor. The
`responses_websocket.enabled` provider option belongs only to
`openai:responses` WebSocket mode and is not reused as the Live permission.
2026-08-20 21:59:05 +08:00
2026-08-23 02:51:49 +08:00
Codex Live also requires an authorized model mapping; adding the endpoint alone
is not enough. For WebRTC, Codex sends the selected model in the multipart
`session.model` . Aether treats that value as the downstream global model,
selects an existing mapping whose provider endpoint is `codex:live` , and
rewrites only `session.model` to the mapped upstream model. Configure Codex's
realtime model selection to an authorized global alias (the current Codex
default may be `gpt-realtime-1.5` , but that value is client-version dependent),
or create an authorized mapping for the alias the client already sends. Aether
does not invent a Live model name or add a bundled hard-coded model merely
because the endpoint is enabled. For Codex providers only, an existing model mapping scoped to
`openai:responses` (including the historical `/v1/responses` alias) can be
reused for Live. The provider endpoint and key must still explicitly allow
`codex:live` ; OpenAI and custom providers do not receive this compatibility
rule.
For Codex Desktop/app-server, point both the call-creation and sideband
overrides at the same Aether origin when using a custom provider. Explicitly
setting both avoids a client-version-dependent fallback to the OpenAI origin:
```toml
model_provider = "aether"
experimental_realtime_webrtc_call_base_url = "https://<aether-host>/v1"
experimental_realtime_ws_base_url = "https://<aether-host>/v1"
experimental_realtime_ws_model = "<authorized-global-live-alias>"
[ model_providers . aether ]
name = "Aether"
base_url = "https://<aether-host>/v1"
wire_api = "responses"
```
The two experimental overrides are optional when the selected provider's
`base_url` already points at Aether, but if either is set they must resolve to
the same deployment so the authenticated call binding can be found. A missing
call-create request in Aether means the client did not select this provider or
failed before gateway routing; a call-create request without the matching
sideband usually means the sideband origin or credential differs. These
settings may change with newer Codex releases.
2026-08-20 21:59:05 +08:00
API-key and bearer providers can use direct WebSocket or WebRTC. ChatGPT OAuth
uses the official Codex backend for WebRTC call creation and the OpenAI Live
origin for its sideband; direct OAuth WebSocket and custom OAuth backend
origins fail closed. The call binding fixes the authenticated downstream
principal, provider/endpoint/key, mapped model, auth mode, account/FedRAMP
identity, session identity, and upstream origin. Raw call IDs are hashed in
RuntimeState keys, records expire after two hours, each principal retains at
most 64 call bindings, and one call permits only one renewable sideband
attachment at a time. The memory RuntimeState backend loses these bindings on
restart. The two-hour binding TTL and 64-record cap bound routing state and
abuse; they are not provider-concurrency reservations.
Frameless V3 currently has no stable usage object that Aether can settle into
its wallet pipeline. Aether therefore enables Live only for principals without
a finite `balance_remaining` ; finite-balance keys receive an explicit local
2026-08-21 04:27:34 +08:00
error instead of unmetered service. Aether writes one lifecycle record for each
relayed direct or sideband WebSocket connection, with frame/byte counts and
`usage_available=false` ; it does not create one database row per audio frame.
The synchronous WebRTC call-creation exchange keeps its ordinary HTTP record
and is also marked usage-unavailable. The WebRTC media leg itself does not
traverse Aether after call creation, so Aether cannot observe or invent a
separate audio-session usage record, token count, duration, or cost for it.
Aether-relayed direct and sideband WebSocket connections are limited to 60
minutes. The provider-pool and admission leases cover only the synchronous
HTTP call-creation exchange and are released after its SDP response. Aether
cannot infer media lifetime from the binding TTL or sideband lifetime, so a
created call that never attaches a sideband is not held against provider
concurrency after call creation.
2026-08-20 21:59:05 +08:00
For the public GA Realtime API's connection and session concepts, see the
2026-08-23 02:51:49 +08:00
[OpenAI Realtime guide ](https://developers.openai.com/api/docs/guides/realtime ),
[WebRTC connection guide ](https://developers.openai.com/api/docs/guides/realtime-webrtc ),
and [server-side controls guide ](https://developers.openai.com/api/docs/guides/realtime-server-controls ).
2026-08-20 21:59:05 +08:00
2026-08-20 08:51:16 +08:00
OpenAI's current WebSocket service supports named `stream_id` lanes: requests on
the same lane are FIFO, while different lanes may run concurrently. Aether's
bridge currently exposes only the implicit default lane and deliberately
rejects `response.create.stream_id` until per-lane binding, ordering, timeout,
usage, and error routing are implemented end to end. Use separate WebSocket
connections for parallel runs through Aether. A syntactically valid named
`stream_id` is rejected with `responses_websocket_named_stream_unsupported` ;
the error event echoes the validated ID so the client can associate the error
with its attempted lane. Invalid or untrusted IDs are not echoed.
2026-07-31 10:59:15 +08:00
## Why use WebSocket mode
WebSocket mode is most useful when a workflow involves many model-tool round trips (for example, agentic coding or orchestration loops with repeated tool calls).
2026-08-17 18:50:29 +08:00
Because the connection stays open and each turn sends only incremental input, WebSocket mode reduces per-turn continuation overhead and improves end-to-end latency across long chains. The [OpenAI WebSocket-mode guide ](https://developers.openai.com/api/docs/guides/websocket-mode ) reports up to roughly 40% faster end-to-end execution for workloads with 20 or more tool calls; this is an upstream product claim, not an Aether benchmark.
2026-07-31 10:59:15 +08:00
## Connect and create responses
In WebSocket mode, start each turn by sending a `response.create` event from the client. The payload mirrors the normal [Responses create body ](https://developers.openai.com/api/reference/resources/responses/methods/create ), except that transport-specific fields like `stream` and `background` are not used.
```python
from websocket import create_connection
import json
import os
ws = create_connection (
"wss://api.openai.com/v1/responses" ,
header = [
f "Authorization: Bearer { os . environ [ 'OPENAI_API_KEY' ] } " ,
],
)
ws . send (
json . dumps (
{
"type" : "response.create" ,
"model" : "gpt-5.6" ,
"store" : False ,
"input" : [
{
"type" : "message" ,
"role" : "user" ,
"content" : [{ "type" : "input_text" , "text" : "Find fizz_buzz()" }],
}
],
"tools" : [],
}
)
)
```
Clients can optionally warm up request state by sending `response.create` with `generate: false` . This is useful when you already know the tools, instructions, and/or custom messages you plan to send with an upcoming turn. `generate: false` does not return a model output, but prepares request state so the next generated turn can start faster. The warmup request returns a response ID that you can chain from with `previous_response_id` , including on later turns in a response chain. The next section explains how to continue a session using `previous_response_id` and incremental inputs.
## Continue with incremental inputs
To continue a run, send another `response.create` with:
- `previous_response_id` set to the prior response ID.
- `input` containing only new items (for example, tool outputs and the next user message).
```python
ws . send (
json . dumps (
{
"type" : "response.create" ,
"model" : "gpt-5.6" ,
"store" : False ,
"previous_response_id" : "resp_123" ,
"input" : [
{
"type" : "function_call_output" ,
"call_id" : "call_123" ,
"output" : "tool result" ,
},
{
"type" : "message" ,
"role" : "user" ,
"content" : [{ "type" : "input_text" , "text" : "Now optimize it." }],
},
],
"tools" : [],
}
)
)
```
## How continuation works
WebSocket mode uses the same `previous_response_id` chaining semantics as HTTP mode, but it adds a lower-latency continuation path on the active socket.
2026-08-20 08:51:16 +08:00
On an active Aether WebSocket connection, the selected upstream keeps the
previous-response state for the single default lane in its connection-local
cache. Continuing from that most recent response is fast because the service
can reuse connection-local state. Because the previous-response state is
retained only in memory and is not written to disk, you can use WebSocket mode
in a way that is compatible with `store=false` and Zero Data Retention (ZDR).
2026-07-31 10:59:15 +08:00
2026-08-20 08:51:16 +08:00
If a `previous_response_id` is not in the upstream connection's in-memory
cache, behavior depends on whether the upstream stored the response:
2026-07-31 10:59:15 +08:00
2026-08-20 08:51:16 +08:00
- With `store=true` , the upstream service may hydrate older response IDs from its persisted state when available. Continuation can still work, but it usually loses the in-memory latency benefit.
2026-07-31 10:59:15 +08:00
- With `store=false` (including ZDR), there is no persisted fallback. If the ID is uncached, the request returns `previous_response_not_found` .
2026-08-20 08:51:16 +08:00
For a new downstream WebSocket connection, Aether also has to prove that the
response belongs to the currently authenticated user/API key and to the exact
provider endpoint, key, credential generation, transport, adapter, model, and
normalization contract. Aether records this ownership only when the effective
provider `response.create` , after Aether's body rules and framing, explicitly
has `store=true` and a successful
`response.completed` , `response.done` , or non-error `response.incomplete`
terminal supplies a valid response ID. `store=false` , an omitted/overridden
`store` , failures, cancellations, malformed IDs, and ZDR turns never create
this registry state. This explicit-true rule is intentionally conservative:
Aether does not infer a provider default for an omitted `store` field.
The RuntimeState key contains a SHA-256 digest over length-delimited live
`user_id` , `api_key_id` , and the opaque response ID; raw response IDs and
credentials are not stored in keys or values. Records expire after 24 hours,
are capped at the 1,024 most recently registered IDs per user/API-key pair,
and are bounded in serialized size. The registry stores ownership/routing
proof and contract digests only; it does not store response contents and is
not a replacement for the upstream's `store=true` persistence. A registry
write failure does not turn a successful provider response into a failure, so
that terminal can reach the client but cannot later resume on a new socket.
With the Redis RuntimeState backend, ownership is shared across gateway
instances for the record TTL, subject to that Redis deployment's own
availability and persistence configuration. The memory backend is
process-local, is not shared between instances, and loses the registry on
restart. Expiry, per-principal eviction, a RuntimeState outage, or a memory
backend restart causes the first continuation on a new connection to fail
closed with `previous_response_not_found` , even if the upstream might still
retain the response. Aether never falls back to the ordinary scheduler for
such a miss and never sends the opaque response ID to a different provider or
key.
PII-redaction restore mappings intentionally remain connection-local and are
not persisted. If a stored response chain contains Aether PII sentinels, Aether
rejects cross-connection continuation rather than risk exposing those
sentinels without the original restore mapping. Start a new response with the
complete required context in that case.
If a continuation on the same lane fails (`4xx` or `5xx` ), the service evicts
the referenced `previous_response_id` from the connection-local cache. Aether
only supports the implicit default lane, so this same-lane rule applies to all
continuations it currently accepts. The upstream service preserves a shared
parent when a cross-lane fork fails, but Aether does not yet expose that named
lane behavior.
The continuation must keep the model selected for the response chain. Aether
rejects a model change with status `409` and code
`responses_continuation_model_change_unsupported` .
2026-07-31 10:59:15 +08:00
## Compaction and creating new responses
If you are using compaction, there are two different continuation patterns:
### Server-side compaction (`context_management`)
When you enable server-side compaction (`context_management` with `compact_threshold` ), compaction happens during normal `/responses` generation. In WebSocket mode, you continue the same way you normally do: send the next `response.create` with the latest `previous_response_id` and only new input items.
### Standalone `/responses/compact`
2026-08-20 08:51:16 +08:00
The standalone [`/responses/compact` endpoint ](https://developers.openai.com/api/reference/resources/responses/methods/compact ) returns a new compacted input window, not a response ID. After compaction, create a new response on your WebSocket connection using the compacted window as `input` (plus the next user/tool items).
2026-07-31 10:59:15 +08:00
Start a new chain by omitting `previous_response_id` or setting it to `null` . Pass the compacted output as-is; do not prune the returned window.
```python
# Compact your current window (HTTP call)
compacted = client . responses . compact (
model = "gpt-5.6" ,
input = long_input_items_array ,
)
# Start a new response on the WebSocket using the compacted window
ws . send (
json . dumps (
{
"type" : "response.create" ,
"model" : "gpt-5.6" ,
"store" : False ,
"input" : [
* compacted . output ,
{
"type" : "message" ,
"role" : "user" ,
"content" : [{ "type" : "input_text" , "text" : "Continue from here." }],
},
],
"tools" : [],
}
)
)
```
## Connection behavior and limits
- Server events and ordering match the existing Responses streaming event model.
2026-08-20 08:51:16 +08:00
- A single Aether WebSocket connection can receive multiple `response.create` messages over its lifetime, but the client must wait for a terminal event before sending the next one. Aether does not queue overlapping creates and returns `response_already_in_progress` while a turn is active.
- Named `stream_id` multiplexing is not exposed by Aether yet. Use multiple connections if you need parallel runs.
- The upstream OpenAI service allows at most 16 active/in-flight responses on one connection; additional `response.create` events are queued. It also allows at most 32 distinct named `stream_id` values per connection, and the implicit default lane does not count toward that 32-lane limit. These describe upstream multiplexing limits, not capabilities exposed by Aether's current single-lane bridge.
2026-07-31 10:59:15 +08:00
- Connection duration is limited to 60 minutes. Reconnect when the limit is reached.
- Aether binds each upstream WebSocket to one selected provider key. A provider must explicitly enable the standard Responses WebSocket capability and expose an `openai:responses` endpoint before it is eligible for this bridge.
- The Codex adapter additionally watches Codex quota events. A `usage_limit_reached` terminal error immediately marks the bound account unavailable. If the client has not received a standard `response.*` event and the request has no `previous_response_id` , Aether retries that one turn once on another eligible key without closing the public socket.
- After a standard response event has reached the client, after a retry has already been attempted, or for a request using `previous_response_id` , Aether forwards the provider terminal error and detaches only the exhausted upstream. If the upstream closes immediately after the quota signal, Aether emits a recoverable gateway error instead. The public WebSocket stays open so a later independent `response.create` can select another key.
- Aether does not transparently move an existing response chain to another provider key. Connection-local `previous_response_id` state cannot be transferred safely, especially with `store=false` /ZDR; send a new request with complete input after an exhausted continuation.
## Reconnect and recover
2026-08-20 08:51:16 +08:00
When a connection closes (or hits the 60-minute limit), open a new WebSocket
connection and continue with one of these patterns:
2026-07-31 10:59:15 +08:00
2026-08-20 08:51:16 +08:00
1. If the prior response is persisted (`store=true` ) and its response ID remains valid, continue with `previous_response_id` and only the new input items.
2. If the chain cannot be hydrated (for example, `store=false` /ZDR or `previous_response_not_found` ), start a new response by setting `previous_response_id` to `null` (or omitting it) and send the complete input context needed for the next turn.
2026-07-31 10:59:15 +08:00
3. If you compacted context with `/responses/compact` , use the returned compacted window as the base `input` for that new response, then append the latest user/tool items.
## Errors to handle
`previous_response_not_found`
```json
{
"type" : "error" ,
"status" : 400 ,
"error" : {
2026-08-20 08:51:16 +08:00
"type" : "invalid_request_error" ,
2026-07-31 10:59:15 +08:00
"code" : "previous_response_not_found" ,
"message" : "Previous response with id 'resp_abc' not found." ,
"param" : "previous_response_id"
}
}
```
`websocket_connection_limit_reached`
```json
{
"type" : "error" ,
"error" : {
"type" : "invalid_request_error" ,
"code" : "websocket_connection_limit_reached" ,
"message" : "Responses websocket connection limit reached (60 minutes). Create a new websocket connection to continue."
},
"status" : 400
}
```
## Related guides
- [Conversation state ](https://developers.openai.com/api/docs/guides/conversation-state )
- [Streaming API responses ](https://developers.openai.com/api/docs/guides/streaming-responses )
2026-08-20 08:51:16 +08:00
- [Responses streaming events reference ](https://developers.openai.com/api/reference/resources/responses )
- [Responses WebSocket events reference ](https://developers.openai.com/api/reference/resources/responses/websocket-events )