Files
puter/src/backend/services/realtime

OpenAI Realtime relay

The relay serves /puterai/openai/v1/realtime on the API host. The SDK exposes it through puter.ai.convo().

Beta: This API is incomplete. Its methods, options, events, and behavior may change abruptly, including breaking changes without notice.

Direct clients are documented in the Realtime WebSocket API reference.

Transport

The relay forwards native OpenAI JSON events unchanged after validating the event envelope, except that rate_limits.updated (Puter's provider account) is dropped and provider organization IDs are removed from diagnostic messages in error, response.done.status_details.error, and conversation.item.input_audio_transcription.failed.error, and from provider close reasons. Conversation content is preserved. An event must be an object with a nonempty string type of at most 128 characters. Binary messages and malformed envelopes close the session. Usage fields consumed by metering are validated before use. Event field order does not affect billing.

Clients authenticate with a Puter token through the Authorization header, OpenAI's browser key subprotocol, or auth_token. Server clients should use the header because URL and subprotocol logging can expose credentials.

The socket opens before authentication completes. Authentication and model failures arrive as OpenAI-shaped error events followed by a close frame.

const conversation = await puter.ai.convo({ output: ['text'] });
conversation.on('error', (event) => console.error(event));
conversation.on('text', ({ delta }) => console.log(delta));
conversation.on('turn.done', () => conversation.close());
await conversation.connect();
conversation.sendText('Hello!');

Limits and lifecycle

All transport limits are published in rate-limits-and-quotas.md. Message size includes reassembled fragments. Client messages are capped at 64 KiB; upstream messages at 4 MiB, since OpenAI events can carry whole items. Read backpressure is supplemented by hard queue admission limits because a paused socket can still deliver messages already in its receive buffer.

Each frame is parsed once and the original bytes are forwarded. Upstream traffic is not rate limited: OpenAI sends audio faster than real time, and backpressure bounds what a slow client can queue. Usage is priced as each event arrives, before any forwarding decision, and recorded serially off the forwarding path through the AI cost-factor hook. Pricing follows the model the session reports. A missing model uses the last known model, while an explicitly unpriced model closes the connection; a client session.update that changes the model is refused. So are conversation.item.retrieve and MCP tools in session.update or response.create, since a retrieved item or MCP output can exceed the upstream message cap.

When a session closes with a response still generating, the relay cancels it and keeps the upstream open for up to five seconds so the cancelled response reports, and is billed for, what it generated. A response the client requested that starts during that window is cancelled the same way. When the upstream closes, for any reason, with a started response that hasn't reported usage, that response is billed its full hold through the same metering path and a warning alarm is raised. A response the client requested but that never started isn't billed.

Each response holds a fixed amount of the account's budget (RESPONSE_HOLD_*_TOKENS at the model's audio rates) from response.created until its usage is recorded, so concurrent sessions can't overdraw it; a session opens only if one hold fits. Input transcription is priced at the costlier of the transcription models in force at commit and at completion. Session attempts are limited per credential after signature verification. Local, bounded IP and node admission limits protect signature verification and authentication lookups. Upgrades use Express's configured proxy trust policy and the ip.validate gate. Authentication uses touch: false on both initial connection and rechecks, so opening or maintaining a realtime socket does not update session activity metadata.

Input transcription holds credit per item, at a fixed budget for the selected transcription model and that model's cost factor. Audio and the configuration that enables transcription are forwarded only while a reserved spare item exists. Each provider commit takes one, and the pool refills to two spares in the background, up to 64 pending plus spare items. A commit with no spare left reserves its own item. A model change, requested or reported, releases the spares and reserves new ones at the new model's price. Committed items keep their hold and model. Disabling transcription keeps the spares until the connection ends. If the account can't cover another item, client reads pause while the session still holds items that may free credit, and the session is refused with insufficient_funds once it holds none.

Audio the provider may commit without the relay seeing the acknowledgement is tracked apart from the pool, as a duration at the session's input format. Without turn detection that is all audio since the last acknowledged commit or clear. With server turn detection it is only speech from speech_started, since the provider drops silence, plus the buffer after an explicit commit. Each started 160 seconds of it claims one held item, and an acknowledged commit, a clear, or an input_audio_buffer_commit_empty error returns those items to the spares. The session is never closed for its length. If the upstream closes first, each claimed item is billed. Each committed item retains its model until completion, failure, or bounded settlement. Missing usage is billed the item's fixed budget. Overflow closes the connection without evicting existing pending identities.

The post-record continuation check asks whether settled credit remains. It excludes outstanding holds and accepts fractional positive credit. Admission of new billable work continues to use the atomic hold path, which includes every outstanding reservation.

Text, transcripts and raw audio are forwarded without new content logging. The relay does not invoke ai.prompt.validate. A realtime adapter needs the host listeners' chat payload and denial contracts before that hook can safely apply to instructions and conversation text. Raw audio and generated transcripts need separate policy decisions.

Credential checks run in batches of at most 16 with no overlapping sweeps. Session and access-token revocation events close affected sockets and invalidate their auth cache entries. Subscription eligibility is checked independently of authentication and after subscription invalidations. A failed verification check after a 60-second retry grace from the first failed lookup closes with a retryable internal error. Periodic checks remain the missed-event fallback. When the drain starts, open sessions get a puter.reconnect event and new sessions receive server_restarting and close with 1012. Open sessions can finish an in-flight response for up to five seconds at shutdown, then close with 1012. A host extension can announce a restart early by emitting serverRestartAnnounced (optionally with closeInMs); that only sends the notice, so a false alarm leaves the server working normally. Each session gets the notice at most once a minute.

Configuration

The provider key resolves through openai-realtime, openai-completion, then openai. The default upstream is OpenAI. A deployment may override realtime.upstreamUrl; the tests point it at an in-process fake provider (the SDK tests live in src/puter-js/tests/api/realtime). The per-user connection cap is configured with realtime.maxConnectionsPerUser and defaults to 20.

Unit tests use a local WebSocket fixture. Optional live integration tests use PUTER_TEST_AI_OPENAI_API_KEY.

Remaining constraints

Responses and input transcriptions are billed after completion; holds bound what is in flight. Sessions require a paid plan (subscription.gate.realtime.openai can waive it) and accept session, app and full-access API tokens. Conversation state lives upstream and is lost when a connection ends; the SDK reconnects with the same settings and replays its recent items, with assistant replies cut to what the user heard.