アプリケーションで使用する API を選択してください。認証、セッション作成、イベントの仕様は API ごとに異なります。
ブラウザから GPT-Live への接続
ブラウザの音声アプリケーションには WebRTC を使用します。マイク入力と生成された音声は、ネゴシエーションで確立したメディアトラックを通じて送受信されます。文字起こし、セッションの更新、委任した作業に関する JSON イベントは、データチャネルで送受信されます。
ブラウザが Session Description Protocol(SDP)のオファーを作成します。アプリケーションサーバーはプロジェクトの API キーを使用し、POST /v1/live/sessions でこのオファーと引き換えにアンサーを取得します。キーとセッション構成は、信頼できるサーバー上に保持してください。
事前準備
必要なものは次のとおりです。
- GPT-Live にアクセスできるプロジェクトの API キー
- 選択した SDK のサンプルに対応するサーバーランタイム。Node.js のサンプルには Node.js 22.6 以降が必要です。
- HTTPS または localhost 上で動作し、マイクの使用が許可されたブラウザ
このサンプルでは、gpt-5.6-terra を使用した Responses への委任と、ホスト型のウェブ検索を利用します。バックエンドへの指示とアプリケーションのツールについては、委任とツールを参照してください。音声とバックエンドの使用量については、コストの最適化を参照してください。
接続の流れ
- ユーザーの操作をきっかけにマイクへのアクセスを要求し、マイクのトラックをピア接続に追加します。
- SDP オファーを作成する前に、データチャネルを作成してイベントリスナーを登録します。
- ローカル記述を設定し、ICE 候補の収集が完了するのを待ってから、オファーをサーバーに送信します。
- サーバーから、
sessionとtransport: { type: "webrtc", sdp: ... }を含む JSON を OpenAI に POST します。 - 返された SDP アンサーをリモート記述として適用します。データチャネルで
session.startedを受信してから、アプリケーションのコマンドを送信します。
セッションは HTTP リクエストによって開始されます。 データチャネルで session.start を送信しないでください。 サンプル内の oai-events という文字列は、データチャネルのラベルです。
POST /v1/live/sessions で WebRTC セッションを作成すると、初期化時に音声の利用時間 15 秒分が課金されます。この金額は、セッションの実行開始後に発生する利用時間の料金に充当されます。実行中のセッションに 15 秒分が追加で課金されるわけではありません。料金の計算については、WebRTC の初期化料金を参照してください。
アプリケーションサーバーの作成
サーバーのサンプルを新しいディレクトリに保存し、その環境で OPENAI_API_KEY を設定します。Node.js の場合は server.mjs を使用し、npm install openai express で openai と express をインストールします。Python の場合は openai をインストールします。このサンプルは 127.0.0.1 にバインドし、http://localhost:3000 からのセッションリクエストを受け付け、実行ディレクトリ内の index.html を配信します。
以下からサーバーの言語を選択してください。どのサンプルも、ポート 3000 で index.html と同じ /api/session エンドポイントを提供します。Live に対応したバージョンの SDK を使用してください。同時に実行するサンプルは 1 つだけにしてください。
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}`));他のユーザーがサーバーにアクセスできるようにする前に、アプリケーションの認証、認可、リクエスト制限、HTTPS で /api/session を保護してください。このローカル環境向けサンプルのオリジンチェックでは、ユーザー認証は行われません。
ブラウザクライアントの作成
サーバーを実行するディレクトリに index.html を作成します。
<!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>次のコードをモジュールスクリプト内に貼り付けます。このコードは、開始と終了のコントロールを追加し、マイクと音声出力を接続して、セッションイベントを処理します。/api/session はアプリケーションサーバー上のルートです。
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);
});選択したサーバーを実行し(node server.mjs または python server.py)、http://localhost:3000 を開いて、 会話を開始を選択します。ステータスが 接続済みに変わったら、最新情報を必要とする質問をして、ホスト型検索を試してください。ブラウザが自動再生をブロックする場合は、音声コントロールを使用してください。
セッションレスポンスの読み取り
リクエストが成功すると、HTTP 201 と、セッション ID および SDP アンサーを含む JSON が返されます。
{
"session": { "id": "live_123" },
"transport": { "type": "webrtc", "sdp": "<SDP answer>" }
}result.session.id を読み取り、result.transport.sdp を setRemoteDescription に渡します。セッション ID は内部構造を解釈せず、プレフィックスも含めて変更せずに保持してください。
メディアとイベントの処理
メディアトラックを通じてマイク音声を送信し、生成された音声を受信します。WebRTC は SDP を通じて音声形式をネゴシエーションするため、セッション構成では audio.format を省略してください。データチャネルで session.input_audio.append を送信しないでください。また、データチャネルで session.output_audio.delta が届くことを前提にしないでください。
文字起こしの差分、セッションコマンド、ネストされた response.event メッセージにはデータチャネルを使用します。文字起こしの処理とライフサイクルイベントについては、セッションの管理を参照してください。サーバー側で独自のイベント接続が必要な場合は、サーバー側の制御を参照してください。
会話を終了するには、session.close を送信し、session.closed を受信するまで受信処理を続けてから、ピア接続とマイクのトラックを閉じます。このサンプルでは、コマンドを送信する前に最終イベントのリスナーを登録します。その前に接続が失敗またはタイムアウトした場合、最終的な使用量は未確認のままです。最終的な使用量の処理については、使用量と正常な終了処理を参照してください。
WebRTC は、リアルタイムアプリケーションを構築するための強力な標準インターフェース群です。OpenAI Realtime API は、WebRTC のピア接続を通じたリアルタイムモデルへの接続に対応しています。
ブラウザベースの音声変換アプリケーションでは、まず音声エージェントを参照することをお勧めします。リアルタイムセッションを管理するための、Agents SDK の高水準のヘルパーと API を説明しています。WebRTC インターフェースは強力で柔軟ですが、Agents SDK よりも低水準です。
ウェブブラウザやモバイル端末などのクライアントから Realtime モデルに接続する場合は、パフォーマンスをより安定させるために、WebSockets よりも WebRTC の使用をお勧めします。
WebRTC を利用したユーザーインターフェースの構築について詳しくは、MDN のドキュメントを参照してください。
概要
Realtime API では、ブラウザから接続する方法として、OpenAI REST API で生成した一時 API キーを使用する方法と、新しい統合インターフェースを使用する方法の 2 つをサポートしています。一般に、統合インターフェースの方がシンプルですが、セッションの初期化にアプリケーションサーバーを経由する必要があります。
統合インターフェースを使用した接続
統合インターフェースを使用して WebRTC 接続を初期化する手順は次のとおりです(ウェブブラウザのクライアントを想定しています)。
- ブラウザは、WebRTC ピア接続の SDP データを使用して、開発者が管理するサーバーにリクエストを送信します。
- サーバーは、その SDP とセッション構成をマルチパートフォームにまとめ、標準の API キーで認証して OpenAI Realtime API に送信します。
統合インターフェースによるセッションの作成
統合インターフェースで Realtime API セッションを作成するには、/v1/realtime/calls にリクエストを送信する小規模なサーバー側アプリケーションを構築するか、既存のアプリケーションにその機能を組み込む必要があります。バックエンドサーバー上で、標準の API キーを使用してこのリクエストを認証します。
以下は、Realtime API セッションを作成する、Node.js の express を使ったシンプルなサーバーの例です。
import express from "express";
const app = express();
// Parse raw SDP payloads posted from the browser
app.use(express.text({ type: ["application/sdp", "text/plain"] }));
const sessionConfig = JSON.stringify({
type: "realtime",
model: "gpt-realtime-2.1",
audio: { output: { voice: "marin" } },
});
// An endpoint which creates a Realtime API session.
app.post("/session", async (req, res) => {
const fd = new FormData();
fd.set("sdp", req.body);
fd.set("session", sessionConfig);
try {
const r = await fetch("https://api.openai.com/v1/realtime/calls", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"OpenAI-Safety-Identifier": "hashed-user-id",
},
body: fd,
});
// Send back the SDP we received from the OpenAI REST API
const sdp = await r.text();
res.send(sdp);
} catch (error) {
console.error("Token generation error:", error);
res.status(500).json({ error: "Failed to generate token" });
}
});
app.listen(3000);アプリケーションでエンドユーザーごとに安全性識別子を割り当てている場合は、
このサーバー側リクエストの OpenAI-Safety-Identifier ヘッダーに含めてください。
内部ユーザー ID のハッシュ値など、
プライバシーを保護し、一貫して使用できる値を使用してください。このヘッダーはブラウザではなく、
信頼できるバックエンドで設定してください。
サーバーへの接続
ブラウザでは、標準の WebRTC API を使用して、アプリケーションサーバー経由で Realtime API に接続できます。クライアントは SDP データをサーバーに直接 POST します。
// Create a peer connection
const pc = new RTCPeerConnection();
// Set up to play remote audio from the model
audioElement.current = document.createElement("audio");
audioElement.current.autoplay = true;
pc.ontrack = (e) => (audioElement.current.srcObject = e.streams[0]);
// Add local audio track for microphone input in the browser
const ms = await navigator.mediaDevices.getUserMedia({
audio: true,
});
pc.addTrack(ms.getTracks()[0]);
// Set up data channel for sending and receiving events
const dc = pc.createDataChannel("oai-events");
// Start the session using the Session Description Protocol (SDP)
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const sdpResponse = await fetch("/session", {
method: "POST",
body: offer.sdp,
headers: {
"Content-Type": "application/sdp",
},
});
const answer = {
type: "answer",
sdp: await sdpResponse.text(),
};
await pc.setRemoteDescription(answer);一時トークンを使用した接続
一時 API キーを使って WebRTC 接続を初期化する手順は次のとおりです(ウェブブラウザのクライアントを想定しています)。
- ブラウザは、開発者が管理するサーバーに一時 API キーの発行をリクエストします。
- 開発者のサーバーは、標準の API キーを使って OpenAI REST API に一時キーをリクエストし、発行されたキーをブラウザに返します。
- ブラウザは一時キーを使い、OpenAI Realtime API とのセッションを WebRTC ピア接続として直接認証します。
一時トークンの作成
クライアント側で使用する一時トークンを作成するには、小規模なサーバー側アプリケーションを構築するか、既存のアプリケーションに機能を組み込み、OpenAI REST API に一時キーをリクエストする必要があります。このリクエストは、バックエンドサーバー上で標準の API キーを使って認証します。
以下は、REST API を使って一時 API キーを発行する、Node.js と express によるシンプルなサーバーの例です。
import express from "express";
const app = express();
const sessionConfig = JSON.stringify({
session: {
type: "realtime",
model: "gpt-realtime-2.1",
audio: {
output: {
voice: "marin",
},
},
},
});
// An endpoint which would work with the client code above - it returns
// the contents of a REST API request to this protected endpoint
app.get("/token", async (req, res) => {
try {
const response = await fetch(
"https://api.openai.com/v1/realtime/client_secrets",
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"OpenAI-Safety-Identifier": "hashed-user-id",
},
body: sessionConfig,
}
);
const data = await response.json();
res.json(data);
} catch (error) {
console.error("Token generation error:", error);
res.status(500).json({ error: "Failed to generate token" });
}
});
app.listen(3000);HTTP リクエストを送受信できるプラットフォームであれば、どのプラットフォームでもこのようなサーバーエンドポイントを作成できます。ただし、 標準の OpenAI API キーは必ずサーバー上でのみ使用し、ブラウザでは使用しないでください。
一時トークンを使用する場合は、クライアントシークレットを作成するサーバー側のリクエストに OpenAI-Safety-Identifier を設定します。
Realtime API は、この識別子を発行された一時トークンに紐づけます。
そのため、ブラウザが後でそのトークンを使って接続する際に、
安全性識別子を送信する必要はありません。
サーバーへの接続
ブラウザでは、標準の WebRTC API を使い、一時トークンで Realtime API に接続できます。クライアントはまずサーバーエンドポイントからトークンを取得し、次に SDP データを一時トークンとともに Realtime API に POST します。
// Get a session token for OpenAI Realtime API
const tokenResponse = await fetch("/token");
const data = await tokenResponse.json();
const EPHEMERAL_KEY = data.value;
// Create a peer connection
const pc = new RTCPeerConnection();
// Set up to play remote audio from the model
audioElement.current = document.createElement("audio");
audioElement.current.autoplay = true;
pc.ontrack = (e) => (audioElement.current.srcObject = e.streams[0]);
// Add local audio track for microphone input in the browser
const ms = await navigator.mediaDevices.getUserMedia({
audio: true,
});
pc.addTrack(ms.getTracks()[0]);
// Set up data channel for sending and receiving events
const dc = pc.createDataChannel("oai-events");
// Start the session using the Session Description Protocol (SDP)
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const sdpResponse = await fetch("https://api.openai.com/v1/realtime/calls", {
method: "POST",
body: offer.sdp,
headers: {
Authorization: `Bearer ${EPHEMERAL_KEY}`,
"Content-Type": "application/sdp",
},
});
const answer = {
type: "answer",
sdp: await sdpResponse.text(),
};
await pc.setRemoteDescription(answer);イベントの送受信
Realtime API のセッションは、開発者が送信するクライアントイベントと、セッションのライフサイクルに関するイベントを通知するために Realtime API が生成するサーバーイベントを組み合わせて管理します。
WebRTC 経由で Realtime モデルに接続する場合、WebSockets を使うときのように、モデルからの音声イベントを細かく処理する必要はありません。上記のように設定した WebRTC ピア接続オブジェクトが、すべての処理を代わりに行います。
その他のクライアントイベントやサーバーイベントを送受信するには、WebRTC ピア接続のデータチャネルを使用できます。
// This is the data channel set up in the browser code above...
const dc = pc.createDataChannel("oai-events");
// Listen for server events
dc.addEventListener("message", (e) => {
const event = JSON.parse(e.data);
console.log(event);
});
// Send client events
const event = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_text",
text: "hello there!",
},
],
},
};
dc.send(JSON.stringify(event));Realtime での会話の管理について詳しくは、Realtime の会話ガイドを参照してください。
この軽量なサンプルアプリで、WebRTC を使った Realtime API を試してみてください。