For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
Navigation principale
Theme

WebRTC

Connectez vos applications vocales avec WebRTC.

Choisissez votre API pour consulter les étapes de connexion et les événements de session correspondants.

Connectez un navigateur à GPT-Live

Nous recommandons WebRTC pour les connexions audio à GPT-Live depuis un navigateur, une application mobile ou une application côté serveur. Ce guide présente un exemple dans un navigateur. Le son du microphone et la parole générée transitent sur les pistes multimédias négociées par WebRTC. Un canal de données transporte les événements JSON liés aux transcriptions, aux mises à jour de la session et aux tâches déléguées.

Votre navigateur crée une offre SDP (Session Description Protocol). Votre serveur d’application envoie cette offre avec POST /v1/live/sessions en utilisant la clé API du projet et reçoit une réponse SDP. Conservez la clé et la configuration de la session sur votre serveur de confiance.

Prérequis

Vous aurez besoin des éléments suivants :

  • Une clé API de projet donnant accès à GPT-Live.
  • Un environnement d’exécution côté serveur adapté à l’exemple de SDK choisi. L’exemple Node.js nécessite Node.js 22.6 ou une version ultérieure.
  • Un navigateur autorisé à utiliser le microphone et dans lequel l’application s’ouvre en HTTPS ou sur localhost.

L’exemple utilise la délégation via Responses avec gpt-5.6-terra et la recherche web hébergée. Pour les instructions côté serveur et les outils de l’application, consultez Délégation et outils. Pour en savoir plus sur l’utilisation des services vocaux et du backend, consultez Optimisation des coûts.

Déroulement de la connexion

  1. En réponse à une action de l’utilisateur, demandez l’accès au microphone et ajoutez ses pistes à une connexion pair à pair.
  2. Créez le canal de données et enregistrez les écouteurs d’événements avant de créer l’offre SDP.
  3. Définissez la description locale, attendez la fin de la collecte des candidats ICE, puis envoyez l’offre à votre serveur.
  4. Depuis votre serveur, envoyez à OpenAI une requête POST dont le corps JSON contient session et transport: { type: "webrtc", sdp: ... }.
  5. Utilisez la réponse SDP reçue comme description distante. Attendez de recevoir session.started sur le canal de données avant d’envoyer des commandes de l’application.

La requête HTTP démarre la session. N’envoyez pas session.start sur le canal de données. Dans l’exemple, la chaîne oai-events est le libellé du canal de données.

Lorsque vous créez une session WebRTC avec POST /v1/live/sessions, 15 secondes d’utilisation vocale sont facturées à l’initialisation. Une fois la session démarrée, ce montant est déduit des frais liés à sa durée. Ces 15 secondes ne s’ajoutent donc pas à la durée facturée. Pour le détail du calcul, consultez Frais d’initialisation de WebRTC.

Créez le serveur d’application

Enregistrez l’exemple de serveur dans un nouveau répertoire et définissez OPENAI_API_KEY dans l’environnement du serveur. Pour Node.js, utilisez le fichier server.mjs et installez openai et express avec npm install openai express. Pour Python, installez openai. Dans cet exemple, le serveur écoute sur 127.0.0.1, accepte les demandes de session provenant de http://localhost:3000 et sert le fichier index.html depuis le répertoire où vous le lancez.

Pour Go, enregistrez l’exemple sous le nom main.go, initialisez un module avec go mod init live-demo et installez github.com/openai/openai-go/v3@v3.85.1 avec go get. Exécutez go run . avec Go 1.26 ou une version ultérieure. Pour Java, utilisez Java 17 ou une version ultérieure, ajoutez la dépendance Maven com.openai:openai-java:4.95.0 et exécutez la classe LiveConnectionWebrtcExample. Pour Ruby 3.3 ou une version ultérieure, installez openai en version 0.110.0 et webrick avec gem install openai -v 0.110.0 et gem install webrick, enregistrez l’exemple sous le nom server.rb, puis exécutez ruby server.rb.

Choisissez ci-dessous le langage du serveur. Chaque variante sert le fichier index.html et expose le même point de terminaison /api/session sur le port 3000. Utilisez une version du SDK qui prend en charge Live. N’exécutez qu’une variante à la fois.

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}`));

Avant de rendre le serveur accessible à d’autres utilisateurs, protégez /api/session en appliquant les mécanismes d’authentification et d’autorisation de votre application, en limitant les requêtes et en utilisant HTTPS. La vérification de l’origine dans cet exemple local n’authentifie pas les utilisateurs.

Créez le client dans le navigateur

Créez le fichier index.html dans le répertoire où vous lancez le serveur :

<!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>

Collez le code suivant dans la balise de script du module. Il ajoute les commandes qui permettent de démarrer et de terminer la conversation, connecte le microphone et la sortie audio, et gère les événements de session. /api/session est une route de votre serveur d’application.

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());
  const closingEvents = events;
  events = undefined;
  closingEvents?.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);
});

Lancez le serveur choisi (node server.mjs ou python server.py), ouvrez http://localhost:3000 et sélectionnez Start conversation. Lorsque l’état passe à Connected, posez une question qui nécessite des informations à jour pour tester la recherche web hébergée. Utilisez les commandes audio si votre navigateur bloque la lecture automatique.

Lisez la réponse de création de session

Si la requête aboutit, le serveur renvoie le code HTTP 201 et un objet JSON contenant l’identifiant de session et la réponse SDP :

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

Lisez result.session.id et passez result.transport.sdp à setRemoteDescription. Traitez l’identifiant de session comme une valeur opaque et conservez-le tel quel, préfixe compris.

Pour réduire le délai d’établissement de la connexion, consultez WebRTC avec WARP.

Gérez les flux multimédias et les événements

Utilisez les pistes multimédias pour envoyer le son du microphone et recevoir la parole générée. WebRTC négocie le format audio via SDP. Omettez donc audio.format dans la configuration de la session. Sur le canal de données, n’envoyez pas session.input_audio.append et ne vous attendez pas à recevoir session.output_audio.delta.

Utilisez le canal de données pour les mises à jour incrémentales des transcriptions, les commandes de session et les messages response.event imbriqués. Consultez Gestion des sessions pour le traitement des transcriptions et les événements du cycle de vie, et Contrôles côté serveur si votre serveur a besoin de sa propre connexion pour les événements.

Pour terminer la conversation, envoyez session.close et continuez à recevoir les événements jusqu’à session.closed. Fermez ensuite la connexion pair à pair et arrêtez les pistes du microphone. L’exemple enregistre l’écouteur de l’événement final avant d’envoyer la commande. Si la connexion échoue ou si le délai d’attente expire avant la réception de cet événement, les données d’utilisation finales ne sont pas confirmées. Pour savoir comment les traiter, consultez Utilisation et fermeture propre de la session.