/v1/realtime sessions with each
one’s output audio wired into the other’s input. Nothing about the endpoint is
special. What makes it a duet is the wiring, and what makes it useful is that
the failures it exposes are the ones a single-turn test cannot reach.
Why bother
Barge-in, endpointing and recovery are timing behaviours. They do not show up when you send one utterance and read one reply. They show up when a second live speaker interrupts mid-sentence, changes their mind, or refuses to confirm anything. Brief a counterpart to do exactly that and listen to the whole call. It is interaction simulation at a size you can run. Models conversing with models to find failures is how the conversation-native models here are trained. A duet is that loop against your own persona, on your own account. It is an honest load test. Every duet is two concurrent sessions doing real work, which stresses the serving pool the way production does. Replaying recorded audio does not.Wire it up
1
Open two sessions
Two
/v1/realtime connections, each with its own session.audio.output.voice
and its own instructions. Different voices, so you can tell them apart in
the recording.2
Cross-pipe the audio
Take each session’s
response.output_audio.delta, re-time it, and send it
to the peer as input_audio_buffer.append. Both sides believe they are
talking to a person.3
Give one side the first word
Send
response.create to whichever agent should open, or the two sit
waiting for each other forever.The part that is not obvious
A realtime backend streamsresponse.output_audio.delta much faster than
real time. Feeding those chunks straight into the peer’s input buffer races
its voice activity detector ahead of anything a listener would hear, and the
turn boundaries come out wrong.
Two rules fix it, and both are in the console’s own pipe:
Re-time the audio to wall clock. Buffer what arrives and release fixed
frames — roughly 100 ms of audio each — on a timer. The pipe below sends only
to the peer; fan send out to your local player as well and what you hear is
what the other agent hears, off the same clock.
Append explicit tail silence after each utterance. The server-side detector
counts silence samples, not wall-clock time, so a pipe that stops sending
when the source stops never ends the peer’s turn. About a second of silence
after response.output_audio.done is what closes it. An underrun in the middle
of an utterance sends nothing at all, which is correct: it must not end a turn
early.
Flush the last partial frame before that silence. An utterance almost never
divides evenly into frames, and whatever is left over is smaller than one. Hold
it and it goes out after the tail silence — the peer ends the turn on the
silence, then hears the last 50 ms of the previous sentence as the opening of
the next one. Pad it to a full frame and send it first.
response.create if no reply has
started after six seconds. Copy that; a duet that quietly stops is worse than
one that nudges.
Letting the gateway do it
Passing the samepair_id on both sockets makes the gateway cross-pipe them
server-side: it publishes each session’s output audio and injects it into the
peer’s upstream as input_audio_buffer.append. This exists for headless jobs
that have no browser to run a pipe in.
What it costs
A duet is two of everything.
If sessions refuse to start, a full pool is the first thing to check: the
server answers an
error with type session_limit_reached and closes 1013.
See Connections.
Set paired_session_id on both sockets — each pointing at the other side — so
the pair can be replayed together afterwards. The console’s session view links
one to the other from that field, and each side keeps its own view of the
conversation, which is the interesting part: two transcripts of one exchange,
each from inside one speaker’s turn-taking.
Try it without writing anything
The console has this built in at Studio → Duet: pick voices, give each seat a brief, and listen.Studio → Duet is an operator surface, not a customer one that can be unlocked.
The route sits behind a superuser check, so there is no plan, flag or account
contact that opens it — a customer account is sent back to its overview.The reason is capacity, not readiness: one duet holds two to four realtime
connections out of the deployment’s pool for as long as it runs. The pattern
above needs nothing but
/v1/realtime, so build the same thing on your own
account instead.Next
Turn-taking and barge-in
The behaviours a duet exists to exercise, and how to measure them.
Sessions
Both halves of the pair, linked by
paired_session_id.Live
The session protocol both sides speak.
Voices
Give each seat a voice you can tell apart.