Environment
-
Browser client for GPT Realtime (OpenAI Base, not Azure), ephemeral token minted right before the call.
-
ICE state: complete, offer SDP ≈2 KB with Opus 48 kHz in the m=audio line.
-
Broken request: POST https://api.openai.com/v1/realtime/calls?model=gpt-realtime → empty 400 Bad Request.
Symptoms
-
Network tab shows status 400 with no response body.
-
Same payload succeeds instantly if the query string is removed.
Diagnosis
-
GA docs (e.g., [Link removed by moderator]) describe /v1/realtime/calls with multipart sdp as the canonical endpoint; the ?model= suffix was only ever needed on early beta / Azure samples.
-
Known issue entry (process-modeler/docs/known-issues/chatkit_index.md:5-14) confirms GA now rejects /calls?model=… with an empty 400.
-
The ephemeral session already encodes the model/voice, so duplicating it in the URL or multipart body just re-triggers the failure.
Working request (GA)
js
await waitForIceComplete(peer); // ensure iceGatheringState === 'complete'
const fd = new FormData();
fd.set('sdp', peer.localDescription.sdp);
const res = await fetch('https://api.openai.com/v1/realtime/calls', {
method: 'POST',
headers: {
Authorization: `Bearer ${ephemeralToken}`, // ek_… < 60 s old
'OpenAI-Beta': 'realtime=v1', // required on some stacks, harmless on GA
},
body: fd, // let fetch set the multipart boundary
});
const answer = await res.text();
if (!res.ok) throw new Error(answer || `HTTP ${res.status}`);
await peer.setRemoteDescription({ type: 'answer', sdp: answer });
Key takeaways
-
Endpoint: POST https://api.openai.com/v1/realtime/calls (no query params).
-
Body: FormData with a single sdp part; don’t send JSON or session unless GA requires it again.
-
Headers: Only Authorization: Bearer ek_… plus optional OpenAI-Beta: realtime=v1.
-
Timing: Mint the ephemeral token immediately before the POST; TTL is ≈1 minute.
-
ICE: Wait for icegatheringstatechange to hit complete before posting.
Ask
Could the public docs / error text call out that GA rejects /v1/realtime/calls?model=…? Right now the server just returns an empty 400 while many legacy samples (and Azure’s realtimertc flow) still show the query string, so it’s an easy trap.
