gpt-live-1-codex 语音接入指南:第三方客户端如何调用 Live
Posted September 10, 2026 by XAI 技术团队 ‐ 12 min read

想在第三方客户端中使用 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=v2。gpt-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 |
| 加入同一会话的 sideband | wss://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 是如何工作的
- 浏览器或原生客户端获取麦克风,创建带音轨和
oai-events数据通道的 WebRTC offer SDP。 - 应用后端携带 Key 和协议头,以 multipart 表单向
/v1/live提交sdp与session。session.model明确填写gpt-live-1-codex。 - 成功响应为 HTTP
201,正文是 answer SDP,Location中包含新建会话的call_id。 - 应用后端通过
/v1/live/{call_id}建立 sideband,等待session.started;浏览器应用 answer SDP,完成 WebRTC 连接。 - 音频走 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_Key1. 创建后端 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,点击“开始语音”并允许麦克风,然后按下面的顺序检查:
- 后端出现
sideband: session.started。 - 页面出现
WebRTC: connected。 - 对麦克风说“你好,请回答语音连接成功”,检查页面是否出现用户和助手的转写,并听到实际回复。
- 体验结束后点击“结束语音”,释放会话和麦克风。
仅收到 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 配置不再附带语音开关。需要语音的用户,应在支持本文协议的第三方客户端或自己的应用中单独接入。