Cloud SDK: OpenAI Realtime adapter

OpenAI Realtime fires on ambient speech. Selective Auditory Attention (SAA) filters it first.

Realtime's built-in VAD cannot tell whether a speaker is addressing the agent or someone nearby.

OpenAI Realtime's server VAD fires on any voice above a noise floor, with no addressee signal.

In a drive-thru lane, meeting room, or clinic, the VAD fires on every voice it hears. Each crossing bills a full model call: audio tokens, generation, TTS.

SAA (Selective Auditory Attention) is the hosted addressee gate between mic capture and the WebSocket. See what addressee detection is and how it differs from VAD.

What fires OpenAI Realtime's server VAD in a multi-party environment In a multi-party environment, the principal speaker (addressed speech), ambient talkers, background media, and side conversations all cross the VAD energy threshold and trigger Realtime API calls. Without an addressee gate, only one of these four sources should trigger a model call, but all four do. Addressed speech principal speaker Ambient talker nearby conversation Background media TV, hold music Side conversation supervisor aside OpenAI Realtime server-side VAD All four fire model call on every turn tokens charged regardless only 1 of 4 should trigger
Lime marks the one source that should trigger a model call. With no addressee layer, the Realtime VAD fires on all four. Non-addressed turns run the model and charge tokens.

token waste

Model calls for speech no one directed at the agent

Each non-addressed utterance runs a full billed model turn, whether the agent replies to no one or talks over a real user.

false barge-in and agent interruption

Background voices cancel real responses mid-generation

Server VAD also drives barge-in: any energy spike mid-response triggers response.cancel, so a cough or a TV ad cuts off a legitimate reply.

SAA is the L5 addressee gate. VAD answers a different question entirely.

A standard voice pipeline has five layers. None of the first four decide whether a turn is addressed to the agent. SAA is the layer that does. See the pre-ASR pipeline layer and the 5-layer voice stack.

Layer Question it answers When it runs Answers "is this turn addressed to the agent?"
L1: Mic capture + AEC Is the signal clean? Pre-VAD No
L2: Energy-threshold VAD Is someone speaking? Pre-ASR No
L3: Endpointing Has the turn ended? Pre-ASR No
L4: ASR What was said? Pre-model No
L5: SAA addressee gate Is this turn addressed to the agent, right now? Pre-WebSocket, pre-ASR Yes

See the engagement-control layer SAA implements.

AttentionClient streams mic and camera to SAA. turnReady delivers audio ready for the Realtime API.

AttentionClient streams mic and webcam audio to SAA. Addressed utterances emit turnReady as base64 PCM16, OpenAI's native input_audio format. Non-addressed speech never reaches the WebSocket.

Data flow from AttentionClient through SAA to the OpenAI Realtime WebSocket AttentionClient acquires mic and camera. Audio streams to the SAA hosted service which decides addressee per utterance. Addressed utterances are delivered as a turnReady event containing base64 PCM16 at 24000 Hz. The application sends a single conversation.item.create message to the OpenAI Realtime WebSocket followed by response.create. Non-addressed speech is suppressed before the WebSocket. AttentionClient mic + camera audio stream audio SAA addressee decision per utterance fails closed turnReady base64 PCM16 ยท 24000 Hz conversation.item.create Not addressed suppressed before WebSocket no model call, no tokens charged
SAA sits between mic capture and the Realtime WebSocket. Only addressed utterances reach conversation.item.create. Non-addressed audio is suppressed before the connection.
client.on('turnReady', ({ audio, frames }) => {
  // audio = base64-encoded PCM16 at 24000 Hz -- native Realtime input_audio format
  // No transcoding needed: pass the payload directly into conversation.item.create
  const content = [{ type: 'input_audio', audio }];

  // Optional: JPEG frames from the webcam map to input_image content parts
  if (frames?.length) {
    frames.forEach(f => content.push({ type: 'input_image', image: f }));
  }

  ws.send(JSON.stringify({
    type: 'conversation.item.create',
    item: { type: 'message', role: 'user', content }
  }));
  ws.send(JSON.stringify({ type: 'response.create' }));
});

Set turn_detection: null in session.update to hand turn detection to SAA.

Setting turn_detection: null in session.update disables OpenAI's server VAD entirely; SAA decides each turn boundary instead. Other session fields are unchanged.

ws.addEventListener('open', () => {
  // Disable OpenAI server VAD; hand the turn boundary decision to SAA
  ws.send(JSON.stringify({
    type: 'session.update',
    session: {
      turn_detection: null,       // critical: prevents OpenAI from acting on ambient speech
      input_audio_format: 'pcm16',
      modalities: ['text', 'audio']
      // other session fields (voice, instructions, tools) unchanged
    }
  }));
});

without turn_detection: null

Continuous stream, per-frame VAD, ambient triggers

OpenAI fires response.create on every VAD event, ambient or addressed. Spend tracks the acoustic environment, not user intent.

with turn_detection: null

Per-utterance messages, addressee-gated, fails closed

The WebSocket receives one conversation.item.create per real user turn. When uncertain, SAA suppresses rather than forwards.

Mute the mic during playback so the agent's own TTS never re-enters the addressee model.

Without this, TTS re-enters the mic and can restart the loop. Call client.mute() and client.markResponding(true) at playback start; hold ~400ms after it ends, then unmute.

ws.addEventListener('message', ({ data }) => {
  const ev = JSON.parse(data);

  if (ev.type === 'response.audio.delta') {
    // Playback started: suspend SAA addressee predictions
    client.mute();
    client.markResponding(true);
  }

  if (ev.type === 'response.audio.done') {
    // Playback ended: hold ~400ms to absorb room reverb and speaker tail,
    // then resume addressee detection
    setTimeout(() => {
      client.unmute();
      client.markResponding(false);
    }, 400);
  }
});

SAA's interrupt event wires to response.cancel and a gain fade for clean barge-in.

SAA keeps listening while the agent talks. On barge-in it emits interrupt with a fadeMs value: fade playback, send response.cancel, and unmute the mic for the next turnReady.

// SAA emits an interrupt event when it detects addressed speech mid-response
client.on('interrupt', (e) => bridge.interrupt(e.fadeMs));

function interrupt(fadeMs) {
  // Fade and stop Realtime audio playback over fadeMs milliseconds
  gainNode.gain.linearRampToValueAtTime(0, ctx.currentTime + fadeMs / 1000);

  setTimeout(() => {
    audioBufferQueue.length = 0;

    // Halt upstream model generation
    ws.send(JSON.stringify({ type: 'response.cancel' }));

    // Unmute mic so the user's new utterance enters the next turnReady turn
    client.unmute();
    client.markResponding(false);
  }, fadeMs);
}

Every suppressed utterance is a model call that never runs.

Spend tracks user intent, not acoustic activity. A TV, a door, or a second speaker all clear VAD but never reach the WebSocket. See when your agent responds to the wrong person.

suppressed before the WebSocket

Non-addressed speech: no model call, no bill

Ambient talkers, background media, side conversations, coaching asides: none reach conversation.item.create.

passes through to conversation.item.create

Addressed speech: one clean turn per model call

Delivered as one complete utterance per turnReady event, not fragmented streaming or per-frame VAD jitter.

Get an SAA token, install the Cloud SDK, wire prewarm() for near-instant first turns.

Sign up at the dashboard for a free, self-serve SAA token, then install @attenlabs/saa-js (npm) or attenlabs-saa (PyPI). prewarm() opens the WebSocket early for a near-instant first turn.

Install

# JavaScript (npm) -- for browser-side Realtime API adapter
npm install @attenlabs/saa-js

# Python (PyPI) -- for server-side addressee gating
pip install attenlabs-saa

Initialize and prewarm

import { AttentionClient } from '@attenlabs/saa-js';

const client = new AttentionClient({ token: SAA_TOKEN });

// prewarm() opens the Realtime WebSocket during SAA model warmup to absorb
// handshake and session.update latency before the first addressed utterance
let ws;
client.prewarm(() => {
  ws = new WebSocket(
    'wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview',
    ['realtime', `openai-insecure-api-key.${OPENAI_KEY}`, 'openai-beta.realtime-v1']
  );
  // Disable server VAD on open
  ws.addEventListener('open', () => {
    ws.send(JSON.stringify({
      type: 'session.update',
      session: { turn_detection: null, input_audio_format: 'pcm16', modalities: ['text', 'audio'] }
    }));
  });
});

// Acquire mic (and optionally webcam) and start the addressee model
await client.start({ video: true });

SAA token

Self-serve, separate from your OpenAI key

Billed independently from OpenAI. In production, proxy the Realtime WebSocket server-side so your OpenAI key never reaches the browser.

free eval kit

Score addressee decisions on your own audio

Sample scoring scripts and per-utterance addressee decisions on audio from your environment, no commitment required.

Request the eval kit
Built for

Teams building on OpenAI Realtime wherever ambient speech reaches the mic.

What developers building on the OpenAI Realtime API ask before integrating SAA.

Why does OpenAI Realtime respond to background talkers?

OpenAI Realtime's server VAD triggers a response on any speech that clears an energy threshold, with no addressee signal: it cannot tell whether someone is talking to the agent or to someone nearby. In a drive-thru lane, a conference room, or anywhere ambient speech reaches the mic, every utterance that clears VAD fires an API call and charges tokens.

How does SAA replace OpenAI's built-in turn detection?

SAA sends turn_detection: null in the session.update handshake, disabling OpenAI's server VAD entirely, then evaluates each utterance itself and emits turnReady only when speech is confidently addressed to the agent. The application sends one conversation.item.create followed by response.create. SAA fails closed: when uncertain, it suppresses rather than forwards.

What audio format does SAA deliver, and does it need transcoding?

turnReady delivers the utterance as base64 PCM16 at 24000 Hz, OpenAI Realtime's native input_audio format, so no transcoding is needed: pass the payload directly into a conversation.item.create content array. Optional webcam JPEG frames map to input_image parts in the same message. See arXiv:2604.08412 for the addressee model's evaluation.

How do I stop the agent from hearing its own TTS?

Call client.mute() and client.markResponding(true) when playback starts, and unmute plus markResponding(false) when it ends. This suspends addressee predictions so the agent's own TTS never re-enters the model. Hold ~400ms after playback to absorb room reverb before detection resumes.

Can a user interrupt the agent mid-response?

Yes. SAA keeps monitoring for addressed speech while the agent talks. On barge-in it emits interrupt with a fadeMs value: wire client.on('interrupt', (e) => bridge.interrupt(e.fadeMs)) to fade playback, send response.cancel, and unmute the mic. SAA pre-rolls the barge-in audio so the next turn carries the user's actual question.

Does SAA require changes to my OpenAI account?

No changes to your OpenAI account are needed. SAA uses its own API key, self-serve from the dashboard. Pass the SAA token to AttentionClient and your OpenAI key to the WebSocket; the two services are billed independently. In production, proxy the WebSocket server-side so the OpenAI key never reaches the browser.

Get your API key and start building today.

Sign up at the dashboard for a free, self-serve API token, no approval needed. Want to test first? Request a free eval kit.

Related: what addressee detection is and how it differs from VAD · the pre-ASR pipeline layer and the 5-layer voice stack · when your agent responds to the wrong person · LiveKit integration guide · Pipecat integration guide