For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
Navegación principal

WebRTC

Conecta aplicaciones de voz para navegadores con WebRTC.

Elige la API que usa tu aplicación. Cada API tiene su propia autenticación, creación de sesiones y contrato de eventos.

Conectar un navegador a GPT-Live

Usa WebRTC para aplicaciones de voz en navegadores. La entrada del micrófono y la voz generada se transmiten por pistas multimedia negociadas. Un canal de datos transporta eventos JSON para las transcripciones, las actualizaciones de sesión y el trabajo delegado.

Tu navegador crea una oferta del protocolo de descripción de sesiones (SDP). El servidor de tu aplicación la intercambia por una respuesta mediante POST /v1/live/sessions, usando la clave de API del proyecto. Mantén la clave y la configuración de la sesión en tu servidor de confianza.

Antes de empezar

Necesitas:

  • Una clave de API del proyecto con acceso a GPT-Live.
  • Un entorno de ejecución de servidor para el ejemplo del SDK que elijas. El ejemplo de Node.js requiere Node.js 22.6 o posterior.
  • Un navegador con permiso para usar el micrófono, que funcione con HTTPS o localhost.

El ejemplo usa la delegación a Responses con gpt-5.6-terra y la búsqueda web alojada. Para obtener instrucciones del backend y herramientas de la aplicación, consulta Delegación y herramientas. Para conocer el uso de voz y del backend, consulta Optimización de costos.

Entender la secuencia de conexión

  1. Solicita acceso al micrófono a partir de una acción del usuario y agrega sus pistas a una conexión entre pares.
  2. Crea el canal de datos y registra los escuchadores de eventos antes de crear la oferta SDP.
  3. Establece la descripción local, espera a que se recopilen los candidatos ICE y envía la oferta a tu servidor.
  4. Haz que tu servidor envíe a OpenAI una solicitud POST con JSON que contenga session y transport: { type: "webrtc", sdp: ... }.
  5. Aplica la respuesta SDP recibida como descripción remota. Espera a recibir session.started en el canal de datos antes de enviar comandos de la aplicación.

La solicitud HTTP inicia la sesión. No envíes session.start por el canal de datos. La cadena oai-events del ejemplo es la etiqueta del canal de datos.

Al crear una sesión WebRTC con POST /v1/live/sessions, se facturan 15 segundos de duración de voz durante la inicialización. Ese importe se descuenta de los cargos por duración una vez que la sesión comienza a ejecutarse; no se agregan 15 segundos adicionales a la sesión en curso. Consulta Cargos de inicialización de WebRTC para conocer el cálculo de costos.

Crear el servidor de la aplicación

Guarda el ejemplo del servidor en un directorio nuevo y configura OPENAI_API_KEY en su entorno. Para Node.js, usa server.mjs e instala openai y express con npm install openai express. Para Python, instala openai. Este ejemplo escucha en 127.0.0.1, acepta solicitudes de sesión de http://localhost:3000 y sirve index.html desde el directorio donde lo ejecutas.

Elige un lenguaje de servidor a continuación; cada variante sirve index.html y el mismo punto de acceso /api/session en el puerto 3000. Usa una versión del SDK compatible con Live. Ejecuta solo una variante a la vez.

import express from "express";
import OpenAI from "openai";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";

const app = express();
const client = new OpenAI({ maxRetries: 0 });
const port = 3000;
const origin = `http://localhost:${port}`;
const indexPath = resolve("index.html");

app.use(express.json({ limit: "64kb" }));
app.get("/", async (_request, response) => {
  response.type("html").send(await readFile(indexPath, "utf8"));
});

// Local-only demo. Add your application's authentication and authorization
// before exposing session creation to other users.
app.post("/api/session", async (request, response) => {
  if (request.headers.origin !== origin) {
    response.status(403).json({ error: "Unexpected request origin" });
    return;
  }
  if (typeof request.body?.sdp !== "string" || !request.body.sdp.trim()) {
    response.status(400).json({ error: "An SDP offer is required" });
    return;
  }
  if (!process.env.OPENAI_API_KEY) {
    response.status(503).json({ error: "Set OPENAI_API_KEY on the server" });
    return;
  }

  try {
    const result = await client.live.create({
      session: {
        model: "gpt-live-1",
        instructions:
          "Be concise. Delegate requests needing current information to the backend, which can search the web.",
        delegation: {
          type: "responses",
          responses: {
            model: "gpt-5.6-terra",
            instructions:
              "Use web search when current facts are needed. Return concise, grounded results for a spoken conversation.",
            tools: [{ type: "web_search" }],
            tool_choice: "auto",
          },
        },
      },
      transport: {
        type: "webrtc",
        sdp: request.body.sdp,
      },
    });
    // Preserve the SDK's typed session ID and SDP answer.
    response.status(201).json(result);
  } catch (error) {
    if (!(error instanceof OpenAI.APIError)) throw error;
    console.error("Live session creation failed", error.status);
    response
      .status(error.status ?? 502)
      .json({ error: "Live session creation failed" });
  }
});

app.listen(port, "127.0.0.1", () => console.log(`Open ${origin}`));

Antes de permitir que otros usuarios accedan al servidor, protege /api/session con la autenticación, la autorización y los límites de solicitudes de tu aplicación, además de HTTPS. La comprobación de origen de este ejemplo local no autentica a los usuarios.

Crear el cliente del navegador

Crea index.html en el directorio donde ejecutas el servidor:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>GPT-Live connection</title>
  </head>
  <body>
    <script type="module">
      // Paste the browser code below here.
    </script>
  </body>
</html>

Pega el siguiente código dentro del script de módulo. Agrega controles de inicio y finalización, conecta el micrófono y el audio de salida, y maneja los eventos de sesión. /api/session es una ruta del servidor de tu aplicación.

const start = document.createElement("button");
start.textContent = "Start conversation";
const stop = document.createElement("button");
stop.textContent = "End conversation";
stop.disabled = true;
const status = document.createElement("p");
const audio = new Audio();
audio.autoplay = true;
audio.controls = true;
document.body.append(start, stop, status, audio);

let peer;

let events;

let microphone;

let closeTimeout;
let ready = false;
let finalized = false;

function cleanup() {
  clearTimeout(closeTimeout);
  microphone?.getTracks().forEach((track) => track.stop());
  events?.close();
  peer?.close();
  audio.srcObject = null;
  ready = false;
  start.disabled = false;
  stop.disabled = true;
}

start.addEventListener("click", async () => {
  start.disabled = true;
  finalized = false;
  status.textContent = "Connecting…";
  try {
    const connection = new RTCPeerConnection();
    peer = connection;
    connection.addEventListener("track", (event) => {
      audio.srcObject = new MediaStream([event.track]);
      audio.play().catch(() => {
        status.textContent =
          "Select play on the audio controls to hear the assistant.";
      });
    });
    microphone = await navigator.mediaDevices.getUserMedia({ audio: true });
    for (const track of microphone.getAudioTracks()) {
      connection.addTrack(track, microphone);
    }

    // Create the event channel before creating the SDP offer.
    events = connection.createDataChannel("oai-events");
    events.addEventListener("message", ({ data }) => {
      const event = JSON.parse(data);
      if (event.type === "session.started") {
        ready = true;
        stop.disabled = false;
        status.textContent = "Connected: " + event.session.id;
      } else if (event.type === "session.closed") {
        finalized = true;
        console.log("Final session usage", event.usage);
        status.textContent = "Conversation ended.";
        cleanup();
      } else {
        // Save transcript and nested Responses events as needed by your app.
        console.log(event);
      }
    });
    events.addEventListener("close", (event) => {
      if (event.target !== events) return;
      if (!finalized) {
        status.textContent = "Disconnected without final session usage.";
        cleanup();
      }
    });

    const offer = await connection.createOffer();
    await connection.setLocalDescription(offer);
    if (connection.iceGatheringState !== "complete") {
      await new Promise((resolve, reject) => {
        const timeout = setTimeout(() => {
          connection.removeEventListener("icegatheringstatechange", onState);
          reject(new Error("Timed out while gathering ICE candidates"));
        }, 10_000);
        function onState() {
          if (connection.iceGatheringState !== "complete") return;
          clearTimeout(timeout);
          connection.removeEventListener("icegatheringstatechange", onState);
          resolve(undefined);
        }
        connection.addEventListener("icegatheringstatechange", onState);
        onState();
      });
    }

    const sdp = connection.localDescription?.sdp;
    if (!sdp) throw new Error("Missing local SDP offer");
    const response = await fetch("/api/session", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ sdp }),
    });
    if (!response.ok) throw new Error(await response.text());

    const result = await response.json();
    console.log("Created session", result.session.id);
    await connection.setRemoteDescription({
      type: "answer",
      sdp: result.transport.sdp,
    });
    // The HTTP request started this session. Do not send session.start here.
  } catch (error) {
    status.textContent =
      error instanceof Error ? error.message : String(error);
    cleanup();
  }
});

stop.addEventListener("click", () => {
  if (!ready || !events || events.readyState !== "open") return;
  stop.disabled = true;
  status.textContent = "Finishing the conversation…";
  // The session.closed handler is already registered. Keep media and events
  // alive while pending work drains; only clean up after the final event.
  events.send(JSON.stringify({ type: "session.close" }));
  closeTimeout = setTimeout(() => {
    status.textContent = "Incomplete finalization: no session.closed event.";
    cleanup();
  }, 15_000);
});

Ejecuta el servidor que elegiste (node server.mjs o python server.py), abre http://localhost:3000 y selecciona Iniciar conversación. Cuando el estado cambie a Conectado, haz una pregunta que requiera información actual para probar la búsqueda alojada. Usa los controles de audio si tu navegador bloquea la reproducción automática.

Leer la respuesta de la sesión

Una solicitud exitosa devuelve HTTP 201 con JSON que contiene el ID de la sesión y la respuesta SDP:

{
  "session": { "id": "live_123" },
  "transport": { "type": "webrtc", "sdp": "<SDP answer>" }
}

Lee result.session.id y pasa result.transport.sdp a setRemoteDescription. Trata el ID de la sesión como un valor opaco y consérvalo sin cambios, incluido su prefijo.

Manejar contenido multimedia y eventos

Envía el audio del micrófono y recibe la voz generada a través de las pistas multimedia. WebRTC negocia el formato de audio mediante SDP, así que omite audio.format de la configuración de la sesión. No envíes session.input_audio.append ni esperes recibir session.output_audio.delta en el canal de datos.

Usa el canal de datos para los incrementos de transcripción, los comandos de sesión y los mensajes response.event anidados. Consulta Administración de sesiones para conocer el manejo de transcripciones y los eventos del ciclo de vida, y Controles del lado del servidor si tu servidor necesita su propia conexión de eventos.

Para finalizar la conversación, envía session.close y sigue recibiendo eventos hasta que llegue session.closed, antes de cerrar la conexión entre pares y las pistas del micrófono. El ejemplo registra el escuchador del evento final antes de enviar el comando. Si la conexión falla o se agota el tiempo de espera antes de eso, el uso final queda sin confirmar. Consulta Uso y cierre ordenado para conocer el manejo del uso final.