calesthio/openai-realtime-voice
Build production OpenAI Realtime voice agents and low-latency spoken interactions with live audio sessions, WebRTC or WebSocket transport, voice activity detection, tool/function calling, prompt design, logging, consent, privacy, safety, latency, and cost controls. Use for speech-to-speech agents and live voice UX; do not use for separate request-based transcription, text-to-speech, or offline audio generation work.
npx skills add https://github.com/calesthio/generative-media-skills --skill openai-realtime-voice
Use this skill when the deliverable is a live spoken interaction: a browser voice assistant, phone-like agent, low-latency speech-to-speech support flow, guided interview, tutoring conversation, live kiosk, or realtime voice UI that listens, reasons, speaks, interrupts, and may call tools.
Do not use this skill for batch speech-to-text, offline TTS, voiceover generation, audio editing, dubbing, music, or file-based audio tasks. Those belong to the separate OpenAI audio/request APIs or other speech/audio skills. The boundary is simple: if a persistent realtime session and turn-taking behavior are part of the product, use this skill; if the job is "turn this file/text into text/audio," do not.
All OpenAI API facts below were verified against first-party OpenAI documentation on 2026-07-10. Re-check model identifiers, prices, session limits, voices, API object fields, data retention, and policy requirements before production release because Realtime surfaces change quickly.
Documented facts:
/v1/realtime. Translation and transcription sessions are separate Realtime session types. Source: Realtime and audio overview.RealtimeAgent and RealtimeSession; server-to-server integrations use WebSocket. Source: Voice agents, WebRTC guide, WebSocket guide.session.created, the client can use session.update. Most session properties can be updated at any time, but the voice cannot be changed after the model has responded with audio once in the session. The documented maximum Realtime session duration is 60 minutes. Source: Realtime conversations.session.audio.input.turn_detection. Documented VAD modes are server_vad and semantic_vad; server VAD can tune threshold, prefix padding, and silence duration. Source: Voice activity detection.Production heuristics in this skill are not OpenAI promises. They are practical design guidance for agents building voice products from the documented API surface.
Choose the architecture before writing prompts or code.
Use a speech-to-speech Realtime voice-agent session when:
Use a chained STT -> text agent -> TTS pipeline instead when:
Use Realtime transcription mode only when the product needs streaming transcript deltas and no model-generated voice response. Use Realtime translation only when the product is an interpreter. Do not smuggle those into a voice-agent architecture.
Design every implementation around three boundaries: client media, trusted server, and OpenAI Realtime session.
For browser or mobile:
For server-to-server:
wss://api.openai.com/v1/realtime?model=... with a standard API key only from the secure server.Do not put a standard API key in a browser, mobile app, desktop package, or downloadable client. Treat WebSocket-from-browser as a special-case prototype unless the authentication and media tradeoffs are deliberately accepted.
Verified on 2026-07-10: OpenAI's current voice-agent examples use gpt-realtime-2.1. Older or alternate realtime model names may exist in docs and model pages; select the model from the current model reference at implementation time and record the exact model in logs and artifacts.
Minimum session decisions to log before launch:
model: exact Realtime model identifier and verification date;transport: WebRTC, WebSocket, or SIP/telephony bridge if applicable;session_type: voice agent, transcription, or translation;voice: exact voice identifier; record that voice cannot be changed after the first audio response in a session;modalities: whether output is audio, text, or both;input_audio_format and output_audio_format when manually handling audio;turn_detection: server_vad, semantic_vad, or disabled/manual;input_audio_transcription: whether enabled, which ASR model, and whether transcripts are user-visible;tools: tool list, schemas, execution owner, approval policy, and side effects;retention/logging: whether raw audio, transcripts, events, and tool payloads are stored, for how long, and who can access them.Heuristic: set the voice at session creation, not after connection. Treat a voice change request mid-call as "start a new session with the new voice" unless current docs explicitly change the documented immutability after first audio.
Voice UX fails when the system does not make turn state visible. Implement the state machine explicitly:
Use server_vad for a default conversational assistant in normal rooms. Tune:
silence_duration_ms when users expect quick back-and-forth and interruptions are common;silence_duration_ms for users who pause mid-thought, accessibility contexts, language learners, or complex support cases;threshold in noisy rooms;prefix_padding_ms to avoid clipping first syllables.Consider semantic_vad when silence-based chunking cuts off users who use long pauses or when phrase completeness matters more than speed. Test carefully with real accents, background noise, and domain vocabulary.
Disable automatic VAD and commit turns manually only when the UI has a deliberate push-to-talk or operator-controlled workflow. If VAD is off, the application owns turn boundaries and must avoid sending accidental background speech as user input.
Interruption rules:
Realtime voice prompts need to constrain behavior that text prompts often leave implicit.
Include these instruction blocks when relevant:
10. Output style: no markdown, no lists unless verbally natural, no "as an AI language model," and no pretending to have completed a tool action before the tool result arrives.
Spoken-agent heuristics:
Attach tools only when the voice agent needs current data or side effects. Tools are not a substitute for prompt instructions.
Use application-executed function tools when:
Use MCP or connector tools only when the organization has approved the remote tool server/connector, the authorization boundary is understood, and logging includes what data may leave the application boundary.
Tool design rules:
Example tool approval policy:
Read-only tools may run automatically after user intent is clear.
Drafting tools may create but not send.
Side-effecting tools require explicit user confirmation in the same turn as execution.
High-impact tools require human operator approval outside the model.
The assistant must never claim completion until function_call_output confirms success.
Latency budget is an experience requirement, not just an engineering metric. Track at least:
Cost controls:
Heuristic: in production dashboards, split cost by session, user turn, assistant audio duration, tool time, and abandoned/idle time. Voice teams often discover that long assistant monologues are both the most expensive and the least usable part of the UX.
Before capturing voice:
For generated voice:
For privacy:
For safety:
For any production voice agent, create an auditable run record. At minimum, store:
Per session, log:
Never store evaluation or scoring artifacts in production agent context. Keep product prompts, test fixtures, and scoring materials separated.
Example intent: a logged-in ecommerce user can ask about order status by voice, interrupt the assistant, and request a human handoff. The assistant may look up orders but cannot cancel or refund without explicit confirmation and server approval.
Documented API choices verified 2026-07-10:
gpt-realtime-2.1 if still current in model docs at build time;server_vad, tuned after noisy-room tests;lookup_order, create_refund_draft, request_human_handoff;Voice-agent instructions:
# Role & objective
You are Acme Retail's voice support assistant. Help authenticated customers understand orders, shipping, returns, and simple account questions.
# Spoken style
Use a warm, calm retail-support voice. Keep answers to 1-2 short sentences, then ask one useful follow-up question. Do not use markdown or numbered lists unless the user asks for steps.
# Audio uncertainty
If a name, order number, address, or amount is uncertain, ask the user to confirm it before using a tool. If background noise may have changed intent, ask a clarifying question.
# Tools
Use lookup_order for order status. It is read-only and may run automatically after intent is clear.
Use create_refund_draft only after explaining the refund reason you heard. It creates a draft, not a refund.
Before any cancellation, refund, message send, or account change, ask for explicit confirmation. Never say an action is complete until the tool result says success.
# Privacy
Do not read full addresses, payment details, tokens, or secrets aloud. Mask sensitive fields. If the user starts sharing a card number or password, stop them and explain that you do not need it.
# Boundaries
For threats, self-harm, legal claims, medical issues, or harassment, move to the approved escalation route. If the user wants a human, call request_human_handoff.
Server-side ephemeral session creation pattern:
// Example only. Re-check current OpenAI SDK and REST shapes before production.
import express from "express";
import OpenAI from "openai";
const app = express();
app.use(express.json());
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
app.post("/voice/session", requireLoggedInUser, async (req, res) => {
const realtimeSession = await openai.beta.realtime.sessions.create({
model: "gpt-realtime-2.1",
voice: "alloy",
instructions: SUPPORT_VOICE_INSTRUCTIONS,
audio: {
input: {
turn_detection: {
type: "server_vad",
threshold: 0.55,
prefix_padding_ms: 300,
silence_duration_ms: 650
}
}
},
tools: [
{
type: "function",
name: "lookup_order",
description: "Look up an authenticated customer's order status.",
parameters: {
type: "object",
properties: {
order_number: { type: "string" }
},
required: ["order_number"]
}
}
]
});
audit.log("realtime_session_created", {
user_hash: hashUser(req.user.id),
model: "gpt-realtime-2.1",
voice: "alloy",
raw_audio_retention: "off"
});
res.json({ client_secret: realtimeSession.client_secret });
});
Browser connection pattern:
// Example only. Prefer current Agents SDK docs at implementation time.
import { RealtimeAgent, RealtimeSession } from "@openai/agents/realtime";
const agent = new RealtimeAgent({
name: "Acme Support",
instructions: SUPPORT_VOICE_INSTRUCTIONS,
});
const session = new RealtimeSession(agent, {
model: "gpt-realtime-2.1",
});
async function startVoice() {
await showConsentAndGetMicPermission();
const auth = await fetch("/voice/session", { method: "POST" }).then(r => r.json());
await session.connect({ apiKey: auth.client_secret.value });
ui.setState("listening");
}
session.on("transport_event", (event) => {
auditClientEvent(event);
updateVoiceUI(event);
});
Why this example is structured this way:
Likely failure modes:
Example intent: a research team runs a consented voice interview for product feedback. The agent asks prepared questions, follows up naturally, tags answers, and calls save_answer after each section. It must not make claims that the user will receive compensation unless the backend confirms eligibility.
Architecture:
semantic_vad if current docs and model support it for the selected session, otherwise tuned server_vad;Instructions:
# Role & objective
You are a voice research moderator. Run a 12-minute interview about a beta productivity app. Ask one question at a time, listen actively, and save concise answer notes after each topic.
# Interview method
Use the approved topic order: onboarding, first useful moment, confusing moments, trust concerns, pricing reaction, final recommendation.
Ask one follow-up when an answer is vague. Do not argue, persuade, or defend the product.
# Spoken style
Sound curious and neutral. Keep your turns under 12 seconds. Leave space for the participant to think.
# Consent and privacy
If the participant asks about recording, say: "We record only if you opted in on the consent screen. You can stop at any time." Do not request passwords, payment details, or medical information.
# Tools
After each topic, call save_answer with topic, concise_summary, notable_quote_if_any, and sentiment. Do not invent quotes. If you are uncertain, omit the quote.
Call flag_for_human_review if the participant withdraws consent, reports a safety issue, or asks to delete data.
Server WebSocket pattern:
// Example only. Match event names and fields to the current Realtime API reference.
import WebSocket from "ws";
const ws = new WebSocket("wss://api.openai.com/v1/realtime?model=gpt-realtime-2.1", {
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"OpenAI-Safety-Identifier": hashParticipant(participantId),
},
});
ws.on("open", () => {
ws.send(JSON.stringify({
type: "session.update",
session: {
type: "realtime",
model: "gpt-realtime-2.1",
voice: "alloy",
instructions: INTERVIEW_INSTRUCTIONS,
audio: {
input: {
turn_detection: {
type: "server_vad",
threshold: 0.5,
prefix_padding_ms: 400,
silence_duration_ms: 900
}
}
},
tools: [{
type: "function",
name: "save_answer",
description: "Save a concise research note for the current interview topic.",
parameters: {
type: "object",
properties: {
topic: { type: "string" },
concise_summary: { type: "string" },
notable_quote_if_any: { type: "string" },
sentiment: { type: "string", enum: ["positive", "neutral", "negative", "mixed"] }
},
required: ["topic", "concise_summary", "sentiment"]
}
}]
}
}));
});
ws.on("message", async (raw) => {
const event = JSON.parse(raw.toString());
logRealtimeEventMetadata(event);
if (event.type === "response.function_call_arguments.done") {
const result = await executeApprovedTool(event.name, event.arguments);
ws.send(JSON.stringify({
type: "conversation.item.create",
item: {
type: "function_call_output",
call_id: event.call_id,
output: JSON.stringify(result)
}
}));
ws.send(JSON.stringify({ type: "response.create" }));
}
});
Why this example is structured this way:
silence_duration_ms gives participants thinking time.save_answer is additive and auditable, not a hidden decision engine.Likely failure modes:
Block release until these are true:
If the agent talks over users:
silence_duration_ms, consider semantic VAD, shorten assistant answers, and ensure barge-in cancels audio playback.If first syllables are clipped:
prefix_padding_ms, inspect client microphone buffering, and test with quiet speakers.If the agent is slow:
If transcripts disagree with what the model did:
If costs rise over the session:
If a tool action is unsafe:
If users distrust the voice:
Take calesthio/openai-realtime-voice from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.