/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
/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.
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 throughGET /v1/generations.
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, andvoice takes its id:
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.This server emits the canonical event names
This server emits the canonical event names
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.session.created carries a capability handshake
session.created carries a capability handshake
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.Four eesi.* frames have no OpenAI counterpart
Four eesi.* frames have no OpenAI counterpart
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.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: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.