gpt-live-1-codex 语音接入指南:第三方客户端如何调用 Live

Posted September 10, 2026 by XAI 技术团队 ‐ 12 min read

OpenAI Realtime API 语音波形与在线助手界面
配图:OpenAI Realtime API

想在第三方客户端中使用 gpt-live-1-codex 实时语音,需要客户端支持 Codex Live 的 WebRTC 会话,以及连接同一个会话的 sideband WebSocket。只填写 API 地址、Key 和模型名称,还不足以让普通聊天客户端支持这套协议。

本文给出 XAI Router 的接入参数和一个可以在本机运行的浏览器语音示例。验证基于 2026 年 9 月 11 日的部署:WebRTC+sideband 已完成真实语音输入、用户转写、模型回复和音频接收验证;相同上游通道独立创建纯 WS 会话仍返回 Voice session access denied.。这是当前通道的验证结果,不代表所有第三方客户端、域名和上游账户都已逐一测试。

先确认你的客户端支持什么

客户端能力能否按本文方式使用
支持自定义 Live 地址、WebRTC SDP、同一 call_id 的 sideband,以及 Live V3 事件可以按下面的参数接入,仍需确认 Key 有模型访问权限
只有 Base URL、API Key、聊天模型设置需要客户端增加 Live 语音协议支持
只支持直接连接 WebSocket 创建新语音会话当前验证的 gpt-live-1-codex 上游通道不可用
只支持 OpenAI Realtime GA 协议需要确认是否也适配 Codex Live V3,不能直接混用请求体和事件
只有录音转文字或文字转语音功能属于独立的 ASR/TTS 接口,不等于 Live 实时对话
官方桌面客户端仅使用 API Key 登录当前不能据此启用官方桌面的 Voice 功能

这里的“V3”指 Codex 源码中的 Frameless Bidi 协议;请求头仍是 openai-alpha: quicksilver=v2gpt-live-1-codex 的会话结构可以对照 Codex Live 实现。OpenAI 的 Realtime WebRTC 文档适合理解 SDP 和媒体传输,但其中其他模型的参数不能直接照搬到本文的 Live 会话。

接入参数

先到 XAI Router 管理后台创建 API Key,确认当前渠道已启用 gpt-live-1-codex,并且 Key 的模型范围允许访问它。

参数本站取值
API 根地址https://api.xairouter.com
OpenAI 风格 Base URL(客户端要求带版本前缀时)https://api.xairouter.com/v1
API Key本站管理后台创建的 Key
模型gpt-live-1-codex
会话协议 / 音频传输Live V3 / WebRTC
创建会话POST https://api.xairouter.com/v1/live
加入同一会话的 sidebandwss://api.xairouter.com/v1/live/{call_id}
鉴权请求头Authorization: Bearer <你的 API Key>
协议请求头openai-alpha: quicksilver=v2
示例音色cove

上表列出了接口的完整路径。客户端如果自动追加 /v1/live,应填写 API 根地址,避免拼成 /v1/v1/live。创建和加入会话都使用同一个 API 入口与 Key;call_id 必须来自刚创建的会话,不能手填或复用旧值。

模型出现在列表中只说明模型目录已配置,不能代替实际语音验证。给客户端添加一个同名聊天模型,也不会让 /v1/chat/completions 或普通 /v1/responses 请求变成 Live 音频流。

WebRTC+sideband 是如何工作的

  1. 浏览器或原生客户端获取麦克风,创建带音轨和 oai-events 数据通道的 WebRTC offer SDP。
  2. 应用后端携带 Key 和协议头,以 multipart 表单向 /v1/live 提交 sdpsessionsession.model 明确填写 gpt-live-1-codex
  3. 成功响应为 HTTP 201,正文是 answer SDP,Location 中包含新建会话的 call_id
  4. 应用后端通过 /v1/live/{call_id} 建立 sideband,等待 session.started;浏览器应用 answer SDP,完成 WebRTC 连接。
  5. 音频走 WebRTC;应用通过数据通道和 sideband 接收会话事件、转写与控制消息。结束时发送 session.close,关闭连接并停止麦克风。

sideband 是加入已存在会话的控制连接。 它连接成功,不等于“独立创建纯 WS 语音会话”也成功。纯 WS 则需要通过一个新的 WebSocket 建立会话并传输音频;OpenAI 的 Realtime WebSocket 文档介绍了这种传输方式,但不能由此推断当前 Live 通道具备同样的访问权限。

浏览器原生 WebSocket 不能像 Node 客户端一样自由设置 Authorization 请求头。下面由应用后端建立带鉴权的 sideband,长期 Key 也只保存在后端。

本地体验:浏览器麦克风+Node 后端

这是一个最小的第三方语音客户端示例,适合先验证账号和协议。准备 Node.js 22 或更新版本,在本机创建目录:

mkdir live-voice-demo
cd live-voice-demo
npm init -y
npm install express@5 ws@8

在该目录新建 .env,填写本站 API 地址和你自己的 Key;将 .env 加入 .gitignore,不要放进前端代码:

ROUTER_API_BASE=https://api.xairouter.com
ROUTER_API_KEY=替换为你的_API_Key

1. 创建后端 server.mjs

后端负责提交 SDP、保留 session.model、连接同一会话的 sideband,并等到 session.started 才返回成功。示例每个会话最多保留 3 分钟,便于体验后自动清理;这只是示例设置,不是平台时长上限。

import express from "express";
import WebSocket from "ws";
import { randomUUID } from "node:crypto";
import { fileURLToPath } from "node:url";

const api = new URL(process.env.ROUTER_API_BASE);
const key = process.env.ROUTER_API_KEY;
if (!key) throw new Error("请在 .env 中设置 ROUTER_API_KEY");
const headers = {
  Authorization: `Bearer ${key}`,
  "openai-alpha": "quicksilver=v2",
};
const app = express();
const sessions = new Map();
app.use(express.text({ type: "application/sdp", limit: "1mb" }));
app.get("/", (_req, res) => {
  res.sendFile(fileURLToPath(new URL("./index.html", import.meta.url)));
});

function closeSocket(ws) {
  if (!ws) return;
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "session.close" }));
    ws.close();
  } else if (ws.readyState !== WebSocket.CLOSED) {
    ws.terminate();
  }
}

app.post("/session", async (req, res) => {
  if (typeof req.body !== "string" || !req.body.startsWith("v=0")) {
    return res.status(400).json({ error: "需要真实的 WebRTC offer SDP" });
  }
  let ws;
  try {
    const form = new FormData();
    form.set("sdp", new Blob([req.body], { type: "application/sdp" }));
    form.set("session", new Blob([JSON.stringify({
      model: "gpt-live-1-codex",
      instructions: "请用中文直接回答用户,保持简洁。不要委派给工具。",
      audio: { output: { voice: "cove" } },
      delegation: { type: "client" },
    })], { type: "application/json" }));
    const upstream = await fetch(new URL("/v1/live", api), {
      method: "POST", headers, body: form,
      signal: AbortSignal.timeout(30000), redirect: "error",
    });
    const sdp = await upstream.text();
    if (upstream.status !== 201 || !sdp.startsWith("v=0")) {
      throw new Error(`创建失败:HTTP ${upstream.status} ${sdp.slice(0, 300)}`);
    }
    const location = upstream.headers.get("location");
    if (!location) throw new Error("创建响应缺少 Location");
    const callId = new URL(location, api).pathname.split("/").filter(Boolean).at(-1);
    if (!callId) throw new Error("Location 中缺少 call_id");
    // 只取 call_id,始终向配置的 API 域名发送 Key。
    const sideband = new URL(`/v1/live/${encodeURIComponent(callId)}`, api);
    sideband.protocol = api.protocol === "https:" ? "wss:" : "ws:";
    ws = new WebSocket(sideband, { headers, handshakeTimeout: 15000 });

    let timer;
    try {
      await new Promise((resolve, reject) => {
        timer = setTimeout(() => reject(new Error("等待 session.started 超时")), 15000);
        ws.on("error", reject);
        ws.on("close", () => reject(new Error("sideband 已关闭")));
        ws.on("message", raw => {
          let event;
          try { event = JSON.parse(raw.toString()); } catch { return; }
          if (event.type === "session.started") resolve();
          if (event.type === "error") {
            reject(new Error(event.error?.message || "Live 会话错误"));
          }
        });
      });
    } finally {
      clearTimeout(timer);
    }
    console.log("sideband: session.started");
    const id = randomUUID();
    const expiry = setTimeout(() => closeSocket(ws), 3 * 60 * 1000);
    sessions.set(id, ws);
    ws.once("close", () => {
      clearTimeout(expiry);
      sessions.delete(id);
    });
    res.json({ id, sdp });
  } catch (error) {
    closeSocket(ws);
    res.status(502).json({ error: error.message });
  }
});

app.delete("/session/:id", (req, res) => {
  closeSocket(sessions.get(req.params.id));
  sessions.delete(req.params.id);
  res.sendStatus(204);
});
app.listen(3000, "127.0.0.1", () => console.log("打开 http://localhost:3000"));

2. 创建页面 index.html

页面通过同源 /session 与后端交换 SDP;麦克风和回复音频交给 WebRTC,字幕读取 Live V3 的 turn.done 事件。

<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<title>Live 语音体验</title>
<button id="start">开始语音</button>
<button id="stop" disabled>结束语音</button>
<audio id="audio" autoplay controls></audio>
<pre id="log" aria-live="polite"></pre>
<script type="module">
const startButton = document.querySelector("#start");
const stopButton = document.querySelector("#stop");
const audio = document.querySelector("#audio");
const output = document.querySelector("#log");
const log = text => { output.textContent += `${text}\n`; };
let pc, mic, sessionId;

async function stop() {
  mic?.getTracks().forEach(track => track.stop());
  pc?.close();
  audio.srcObject = null;
  const id = sessionId;
  pc = mic = sessionId = undefined;
  startButton.disabled = false;
  stopButton.disabled = true;
  if (id) {
    await fetch(`/session/${encodeURIComponent(id)}`, {
      method: "DELETE", keepalive: true,
    }).catch(() => log("关闭请求失败,服务端将在示例的 3 分钟上限内清理"));
  }
}

startButton.onclick = async () => {
  startButton.disabled = true;
  try {
    mic = await navigator.mediaDevices.getUserMedia({ audio: true });
    const connection = pc = new RTCPeerConnection();
    connection.ontrack = event => {
      audio.srcObject = event.streams[0] || new MediaStream([event.track]);
      audio.play().catch(() => log("请点击音频控件的播放按钮"));
    };
    connection.onconnectionstatechange = () => {
      log(`WebRTC: ${connection.connectionState}`);
    };
    const events = connection.createDataChannel("oai-events");
    events.onmessage = ({ data }) => {
      let event;
      try { event = JSON.parse(data); } catch { return; }
      if (event.type === "session.started") log("语音会话已开始");
      if (event.type === "turn.done" && event.turn?.transcript) {
        log(`${event.turn.role}: ${event.turn.transcript}`);
      }
      if (event.type === "error") log(`错误:${event.error?.message || data}`);
    };
    mic.getTracks().forEach(track => connection.addTrack(track, mic));
    await connection.setLocalDescription(await connection.createOffer());
    if (connection.iceGatheringState !== "complete") {
      await new Promise((resolve, reject) => {
        const timer = setTimeout(() => reject(new Error("ICE 收集超时")), 10000);
        connection.onicegatheringstatechange = () => {
          if (connection.iceGatheringState === "complete") {
            clearTimeout(timer);
            resolve();
          }
        };
      });
    }
    const response = await fetch("/session", {
      method: "POST",
      headers: { "Content-Type": "application/sdp" },
      body: connection.localDescription.sdp,
    });
    const result = await response.json();
    if (!response.ok) throw new Error(result.error);
    sessionId = result.id;
    await connection.setRemoteDescription({ type: "answer", sdp: result.sdp });
    stopButton.disabled = false;
    log("等待 WebRTC: connected 后,对麦克风说话");
  } catch (error) {
    log(error.message);
    await stop();
  }
};
stopButton.onclick = stop;
window.addEventListener("pagehide", () => { void stop(); });
</script>
</html>

3. 启动并说一句话

node --env-file=.env server.mjs

用浏览器打开 http://localhost:3000,点击“开始语音”并允许麦克风,然后按下面的顺序检查:

  1. 后端出现 sideband: session.started
  2. 页面出现 WebRTC: connected
  3. 对麦克风说“你好,请回答语音连接成功”,检查页面是否出现用户和助手的转写,并听到实际回复。
  4. 体验结束后点击“结束语音”,释放会话和麦克风。

仅收到 HTTP 201 或 WebSocket 101 Switching Protocols,都不能单独证明语音可用。完整成功标准是:会话开始、WebRTC 连通、用户语音被识别、回复音频能够播放。 如果浏览器拦截自动播放,请手动点击音频控件。

示例仅监听本机 127.0.0.1。部署为多人使用的网站时,浏览器页面应使用 HTTPS,后端还需为自己的用户添加登录校验、会话归属检查和用量限制。这里的示例只做直接语音对话;让语音助手执行工具或接入编程代理,需要另行实现客户端委派流程。

为什么不直接使用纯 WS

以下路径代表独立创建新会话,不是上面的 sideband:

wss://api.xairouter.com/v1/live?model=gpt-live-1-codex

在本次验证的上游通道中,连接可能先返回 101,随后收到:

{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "code": "forbidden",
    "message": "Voice session access denied."
  }
}

这条通道使用 ChatGPT/Codex OAuth 身份访问上游,用户填写的 Router API Key 用于网关入口鉴权。上游创建独立 WS 会话与加入已有 WebRTC 会话的授权结果可能不同;当前失败不能通过给模型列表添加名称、改成 wss:// 或重复重连解决,也不代表所有账户、所有 Realtime 模型都不支持纯 WS。

因此,只有纯 WS 新建能力的客户端,需要增加本文的 WebRTC+sideband 流程,或者使用已经适配该流程的客户端。把客户端纯 WS 自动转换为服务端 WebRTC 需要额外的音频桥接实现,本文接口没有自动完成这一转换。

常见问题

现象检查方向
HTTP 401,或 Key 无效检查 Key、Authorization 请求头和 API 站点是否匹配
模型不存在、无可用渠道或无模型权限检查渠道是否启用 gpt-live-1-codex,以及 Key 的模型范围
会话创建提示模型缺失或不可用在原始 HTTP 表单的 session JSON 中明确设置 model;不要删除 session.model,也不要依赖旧客户端默认模型
sideband 返回 404从本次创建响应的 Location 获取 call_id,使用相同 API 入口和 Key,并确认会话仍有效
101,随后 Voice session access denied.区分独立纯 WS 与已有会话的 sideband;前者在当前验证通道中受到上游访问限制
已连接但没有收音或回复检查麦克风权限、设备、输入音量、WebRTC/ICE 连接状态,以及浏览器播放限制
官方桌面提示“语音聊天不可用,你的账户或工作空间无法使用语音聊天”仅使用 API Key 登录不能启用官方桌面 Voice;这与第三方客户端是否能调用网关接口是两项独立检查

对于旧客户端的默认模型问题,OpenAI 的 Codex issue #40140默认模型更新 #40321可作为排查参考;更新默认模型并不等于解决独立 WS 的上游权限问题。

官方当前明确说明桌面 Voice 不支持通过 API Key 使用,参见 ChatGPT Voice in Desktop 说明。因此,本站管理页的默认 Codex API Key 配置不再附带语音开关。需要语音的用户,应在支持本文协议的第三方客户端或自己的应用中单独接入。