Reading a transcript tells you what your agent said. Listening to it handle somebody difficult tells you whether it should have said that. A duet is two ordinary /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 streams response.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.
Even with both rules, a turn is occasionally missed. The console arms a timer when a pipe drains and sends the peer a 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 same pair_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.
It relays frames as they arrive and adds no pacing and no tail silence, so the two rules above become your job on whichever side can enforce them. If you have a client that can hold a paced buffer, do it there.

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.
paired_session_id is not only bookkeeping. The gateway starts its server-side cross-pipe on pair_id or on paired_session_id when source begins with duet — one expression decides both. So the two parameters this page suggests separately, paired_session_id for replay and source=duet for provenance, turn the server pipe on when you set them together.Run one pipe or the other. With your own pacedPipe also running, every frame reaches the peer twice: once paced by you and once relayed unpaced, and the endpointing the pacing existed to protect is gone. Either drop source=duet on a client-piped session, or drop the client pipe and let the gateway do it.

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.