/v1/audio/speech, /v1/audio/transcriptions, /v1/chat/completions and /v1/realtime speak the OpenAI wire protocol, request and error envelope alike. An existing integration moves with a base URL and a key. That gets you running in a minute. The half a migration guide usually omits is the three places your client still needs a branch — they are at the bottom of this page, and skipping them is how a migration passes in staging and fails on a real call.

The two lines

Note the /v1 here. The OpenAI clients take a base URL that already includes the version segment; the EESI SDKs take the bare origin and append it themselves.

What resolves to what

Send an OpenAI model id and it resolves to the EESI model that serves that job. You do not have to change a model string to get running.
Move to the real ids before you ship. An alias resolves to whichever EESI model currently serves that job, so it follows a decision you did not make. whisper-1 and whisper-large-v3 are not aliases here: transcription is nur-stt-v1, named. See Models.
Read GET /v1/audio/models rather than hardcoding either spelling: backends are opt-in per environment, so a model with no backend configured here answers 503 model_unavailable and one that is not in the catalog at all answers 404 model_not_found.

What you gain

Extra fields on the same request, ignored by models that do not support them, so adding one never breaks a fallback path. And two response headers with no OpenAI counterpart:
  • X-Synthetic-Audio — every audio response declares itself machine-generated. You cannot turn it off. See Synthetic audio marking.
  • X-EESI-Generation-Id — the history entry this audio became, readable later through GET /v1/generations.
You also get voice cloning, diarized transcripts with speaker labels and an audibility read, and a replayable record of every generation and every live session — none of which have an OpenAI equivalent, and none of which you reach through an OpenAI client.

The three places your client needs a branch

1. A voice is an id, not a name

This is the one real difference in the request body. OpenAI ships a fixed set of voice names. Here every voice — built-in or cloned — is a stored reference clip, and voice takes its id:
The OpenAI names still work as a compatibility shim: alloy, nova, shimmer and the rest resolve to the model’s own default voice so a ported request runs unchanged. They do not select different voices. A name that resolves to neither an id nor an alias is 400 unknown_voice rather than audio in the wrong voice — which is the failure this check exists to prevent, because the synthesis backend conditions on reference audio and would otherwise drop an unrecognized name and return 200 OK in the default voice. That invariant is why a long call keeps one voice: without reference audio the model samples a new one on every generation. See Voices.

2. response_format is per model, and the enum lies

The request schema advertises six formats. What you can actually ask for is whatever the serving model advertises, and nur-tts-v1 advertises mp3, wav and pcm only. Anything else is a 400 whose message lists the ones that would have worked. mp3 is the safe default. Read formats from GET /v1/audio/models before you request opus, aac or flac anywhere.

3. The realtime dialect

The socket speaks the OpenAI Realtime protocol, with three additions and one spelling rule.
response.output_audio.delta, response.output_audio.done, response.output_audio_transcript.delta and response.output_audio_transcript.done. The legacy spellings without output_response.audio.delta and friends — are accepted as aliases on the way in but never emitted, so a client listening only for the old names goes silent. Fold both onto the canonical name at your decoder’s edge; that is what the shared decoder in this repository does, and spec/realtime/v1/ is the corpus that pins it.
Beside the OpenAI-shaped session object it carries eesi_protocol_version and a capabilities block — audio_input_rates (the first is the default), audio_formats, and an events map of the optional features this gateway honours. It is additive, so an OpenAI client that ignores it is still correct. Read audio_input_rates rather than guessing the wire rate: guessing is what produced audio playing half again too fast. An absent eesi_protocol_version means a gateway predating versioning — treat it as version 0, never as an empty string.
eesi.synthetic_audio declares the session’s output machine-generated before any other frame. eesi.session hands you the session_id and resume_token a reconnect needs, and reappears before a 1008 close to name a limit reached mid-call. eesi.recording_disclosure carries the wording to show a person when the session is recorded. eesi.keepalive is the one client-to-server event this gateway defines. All four are vendor-prefixed so they can never collide with an OpenAI event name, and an OpenAI client ignoring them is still correct. See Realtime events.
Browser realtime clients also differ in how they authenticate. OpenAI’s browser pattern smuggles the key through a openai-insecure-api-key.<key> subprotocol, which this gateway accepts so an unmodified client connects — but its own name says what it is. Mint a single-use ticket instead. See Authentication.

Errors are the same shape

Every OpenAI-compatible path answers in the OpenAI error envelope, including authentication failures, so a typed SDK error surfaces the reason without a translation layer:
Request validation is a 400, not FastAPI’s 422, for the same reason. What differs is which errors exist: an exhausted prepaid balance is 429 insufficient_quota before the work rather than an invoice afterwards, and a never-paid account also carries a daily ceiling that arrives as 429 free_tier_daily_limit. See Errors and Pricing and limits.

What has no OpenAI path at all

Duet, live translation, voices, sessions, transcription runs, story and memory are EESI routes with no OpenAI counterpart, so an OpenAI client cannot reach them. Use the EESI SDKs or raw HTTP for those — the same key works for both.

Next

Models

The real ids, and why you pin the version.

Realtime events

Every event this server sends, and the two accumulation rules.

Voices

Why a voice is a resource rather than a string.

SDKs

The typed clients that reach the rest of the platform.