Model APIs / Realtime

# Realtime (WebSocket)

Hold a live text-and-audio session with a model over one WebSocket, in the OpenAI Realtime protocol, with the route, the usage and the final price reported by the gateway.

`GET` `/v1/realtime?model=…`WebSocket`inference:invoke`

Which models. Realtime is available only on models whose provider offers it directly. A model that supports it lists `realtime` in its `operations` (`GET /v1/models`, and a Realtime badge on its [catalog](https://yelno.ai/catalog) page); a model without it answers `4422` with the route that does serve it. A session runs on one endpoint from start to end: it is never moved to another provider mid-conversation. The [Realtime](https://yelno.ai/catalog?type=realtime) tab of the catalog lists every such model, and the console playground opens a voice session with any of them.

## Connect

Open a WebSocket to `/v1/realtime?model=MODEL` with your API key in the `Authorization: Bearer` header (or `x-api-key`). Browsers, which cannot set headers on a WebSocket, pass it as the subprotocol `openai-insecure-api-key.<key>` together with `realtime`. A key in the query string is refused: URLs end up in logs. The handshake response carries `x-request-id`; the same id is on every event the gateway sends and on the session's request in your usage.

Before anything reaches a provider the session passes the same checks an HTTP request does: key scopes and expiry, test keys against production projects, source allow-lists, rate limits (a session counts as one request), your routing policy (data retention, regions, providers, your own keys), budgets and a hold on your balance.

Connect

```
import asyncio, json, os
import websockets  # pip install websockets

URL = "wss://yelno.ai/v1/realtime?model=MODEL"

async def main():
    headers = {"Authorization": f"Bearer {os.environ['YELNO_API_KEY']}"}
    async with websockets.connect(URL, additional_headers=headers) as ws:
        await ws.send(json.dumps({"type": "conversation.item.create", "item": {
            "type": "message", "role": "user",
            "content": [{"type": "input_text", "text": "Say hello in one sentence."}]}}))
        await ws.send(json.dumps({"type": "response.create"}))
        async for message in ws:
            event = json.loads(message)
            if event["type"] == "response.done":
                await ws.send(json.dumps({"type": "yelno.session.close"}))
            elif event["type"] == "yelno.session" and event["status"] == "closed":
                print(event["cost"])  # {"state": "final", "currency": "USD", "customer_nanos": ...}

asyncio.run(main())
```

## Events

The session speaks the OpenAI Realtime event protocol in both directions: `session.update`, `input_audio_buffer.append` / `commit`, `conversation.item.create`, `response.create` and `response.cancel` from you; `session.created`, the response deltas and `response.done` (with its usage) back. Clients built for that protocol work unchanged; the beta interface is selected with the `openai-beta.realtime-v1` subprotocol or the `OpenAI-Beta: realtime=v1` header.

Refused, with an `error` event while the session continues:

- a `session.update` that names another model (`model_switch_unsupported`): open a new session instead;
- input audio transcription (`input_audio_transcription`) and transcription sessions: the provider bills them separately and they are not offered through the gateway yet;
- binary frames and events that are not JSON objects with a `type`.

Your organization's content guardrails apply to the text an event carries (message text, instructions, function output): a blocking policy refuses that event and sends nothing, a redacting policy rewrites it before it leaves, a flagging policy records it. Audio is never scanned or modified.

## Gateway events

`yelno.session` arrives first with `status: "open"`: the request id, the model, the provider, the endpoint, the route-plan versions and the session's limits. When the session ends and you are still connected, a second `yelno.session` with `status: "closed"` carries the reason, the duration, the usage and the final charge. Send `{"type": "yelno.session.close"}` to end a session and receive it. `yelno.warning` reports guardrail redactions and flags.

yelno.session · closed

```
{
  "type": "yelno.session",
  "status": "closed",
  "request_id": "req_…",
  "model": "MODEL",
  "provider": "…",
  "reason": "client_requested",
  "duration_seconds": 42,
  "responses": 3,
  "request_state": "financially_finalized",
  "usage": { "units": { "input_uncached": 812, "input_audio": 1290, "output": 96, "output_audio": 2410, "session_second": 42 }, "source": "provider_final" },
  "cost": { "state": "final", "currency": "USD", "customer_nanos": 218752000 }
}
```

A refusal is an `error` event whose `error` is the standard [error envelope](https://yelno.ai/docs/errors) (with `event_id` when it answers one of your events), followed by the close.

## Close codes

| Code | Meaning |
| --- | --- |
| 1000 | Normal end: you sent yelno.session.close, or the provider ended the session. |
| 4400 | Invalid request, for example no model in the URL or a key in the query string. |
| 4401 | Authentication: missing, invalid or expired key, or the provider refused the platform credential. |
| 4402 | Insufficient funds: the opening hold or a later extension could not be placed, a budget is exhausted, or the session reached your price ceiling. |
| 4403 | Permission: scope, test key on a production project, suspension, or a source address outside an allow-list. |
| 4408 | The session reached its maximum duration or its idle timeout. |
| 4422 | No route: the model is not offered for realtime, or your policy leaves no eligible endpoint. |
| 4429 | Rate limit or concurrency limit. |
| 4502 · 4503 | The provider failed or could not be reached. |
| 1011 | Internal error. |

## How a session is charged

You pay the model's listed final price for what the session used: text and audio tokens as the provider reports them for each response, cached input at its cached price, and connected time where the model is priced per minute. Nothing is added on top.

- A hold is placed when the session opens and grows while it runs, so it always covers what the session has used plus a minute ahead. If your balance, credit limit or a budget cannot cover the next extension, you get an `insufficient_funds` error and the session closes with `4402`, charged for what it used.
- The session is settled once when it ends, whatever ends it. A response still being generated is cancelled first and its usage collected; if the provider never reports it, the output relayed to you is charged as a disclosed estimate.
- A session that used nothing billable is not charged and its hold is returned.

## Limits

A session lasts at most 30 minutes and ends after 5 minutes without an event from you while no response is in progress (the exact values for your deployment are in the opening `yelno.session`, under `limits`). Each open session holds one of your concurrency slots until it closes. A session also ends if the provider ends it; it is never resumed on another endpoint.

Source: https://yelno.ai/docs/realtime
