--- title: Realtime 兼容协议设计 description: 面向集成方的 OpenAI Realtime 兼容 WebRTC、WebSocket 与扩展事件协议。 icon: radio --- # Realtime 兼容协议设计 > 当前实现版本为 `Interactive Media Realtime Extensions v1`。OpenAI 兼容接口与现有 `/api/webrtc/offer`、`/ws/voice`、`/ws/stream`、RTVI 消息并行提供;RTVI 不是待删除的迁移接口。 ## 设计目标 AI 视频助手以 OpenAI Realtime 协议作为对外接口,同时保留助手、知识库、工具、工作流、动态变量和视频输入等现有能力。 协议适配层只负责连接、事件和 Pipecat Frame 之间的转换,不引入第二套语音运行时: ```text OpenAI Realtime 客户端 │ │ WebRTC / WebSocket ▼ Realtime 协议适配层 │ │ Pipecat transport / frame ▼ 现有 run_pipeline() │ ├── Prompt / Workflow / External Agent ├── ASR / LLM / TTS / Realtime Model ├── Knowledge / System、HTTP、Client、MCP Tools └── History / Handoff / End Call ``` 协议分为两层: | 层级 | 约束 | 面向对象 | | --- | --- | --- | | OpenAI Realtime Core | 保持标准事件名称、字段含义和生命周期,不改变标准事件语义 | 任意 OpenAI Realtime 兼容客户端 | | `x.interactive_media.*` Extensions | 所有项目扩展均有命名空间和能力协商 | 了解 Interactive Media 扩展的客户端 | 普通兼容客户端不声明扩展能力时,服务端不会发送 `x.interactive_media.*` 事件。它仍可使用音频、文本、图片、转写、回复流、打断和函数调用,但不会获得工作流节点、动态变量、连续视频等项目专属状态。 ## 接口概览 假设服务地址为 `https://api.example.com`: | 接口 | 用途 | 推荐客户端 | | --- | --- | --- | | `POST /v1/realtime/client_secrets` | 创建短期、绑定助手的临时令牌 | 浏览器或移动端 | | `POST /v1/realtime/calls` | 用 SDP Offer 创建 WebRTC 会话并返回 SDP Answer | 浏览器、移动端 | | `wss://api.example.com/v1/realtime?model=assistant:` | 创建 WebSocket Realtime 会话 | 服务端、话务网关、自定义客户端 | `model` 使用 `assistant:`,其中 `id` 是当前数据库中的助手 ID,例如: ```text assistant:asst_xxx ``` 这里的 `model` 代表一个完整助手,而不是直接选择底层模型。助手绑定的系统提示词、模型凭证、知识库、服务端工具、工作流和运行模式仍由服务端控制。 ### 鉴权 - 长期 API Key 仅供可信服务端使用,通过 `Authorization: Bearer ` 发送。 - 浏览器先由自己的业务后端创建临时令牌,再用临时令牌连接 Realtime 接口。 - 临时令牌绑定助手和会话配置,默认有效期为 60 秒;首版不保证单次使用,令牌只用于建连,建连后到期不会中止会话。 - 父长期 Key 被撤销或过期后,尚未使用的临时令牌立即失效。 - 管理后台 Cookie 不属于公开兼容协议,也不应作为第三方客户端的鉴权方式。 - 会话和事件不会返回底层模型密钥、助手系统提示词或工具密钥。 ### 长期 API Key 管理 Realtime Key 存在数据库中,每个 Key 默认可访问当前及未来创建的全部助手。管理接口仅允许后台管理员 Cookie 或 Basic Auth 调用: | 接口 | 行为 | | --- | --- | | `POST /api/realtime/api-keys` | 创建 Key;完整 `sk-rt-...` 只在本次响应显示一次 | | `GET /api/realtime/api-keys` | 查看名称、前缀、有效期、最后使用时间和状态,不返回密钥 | | `DELETE /api/realtime/api-keys/{id}` | 逻辑撤销并保留审计记录 | 轮换方式为先创建新 Key,再撤销旧 Key。服务端只保存使用部署级 pepper 计算的 HMAC-SHA256,不保存可恢复的 Key 明文。 ## 会话配置 首次建连配置以及后续 `session.update` 使用标准 Realtime session 结构: ```json { "type": "realtime", "model": "assistant:asst_xxx", "output_modalities": ["audio"], "audio": { "input": { "format": { "type": "audio/pcm", "rate": 24000 }, "turn_detection": { "type": "server_vad" } }, "output": { "format": { "type": "audio/pcm" } } } } ``` 客户端可修改输出模态,以及输入端 Server VAD / Push-to-Talk 参数。服务端在 `session.updated` 中返回**实际生效**的配置。 以下内容始终以服务端助手配置为准: - 助手身份和基础系统提示词; - Pipeline 或 Realtime 运行模式; - 底层模型资源及凭证; - 知识库和服务端 System、HTTP、MCP 工具; - 工作流图、结束条件和接管策略。 - voice、工具定义、系统提示词和每次回复的临时指令。 服务端不能静默接受一个实际未生效的字段。字段不支持、被锁定或没有权限时,应返回标准 `error` 事件。 ## WebRTC WebRTC 是浏览器和移动端实时音视频交互的首选通道。音频和可选视频走媒体轨道,JSON 事件走 DataChannel。 ### 建连方式一:统一接口 可信业务后端向 `/v1/realtime/calls` 发送 `multipart/form-data`: | 表单字段 | 类型 | 说明 | | --- | --- | --- | | `sdp` | string | 浏览器生成的 SDP Offer | | `session` | JSON string | Realtime session 配置,必须选择一个已授权助手 | 请求使用长期 API Key 鉴权。成功响应为 `Content-Type: application/sdp` 的 SDP Answer。 ```bash curl -X POST "https://api.example.com/v1/realtime/calls" \ -H "Authorization: Bearer $REALTIME_API_KEY" \ -F 'sdp= { if (event.track.kind === "audio") { remoteAudio.srcObject = event.streams[0]; } }; const localStream = await navigator.mediaDevices.getUserMedia({ audio: true }); pc.addTrack(localStream.getAudioTracks()[0], localStream); const events = pc.createDataChannel("oai-events"); events.onmessage = (event) => { const serverEvent = JSON.parse(event.data); console.log(serverEvent); }; const offer = await pc.createOffer(); await pc.setLocalDescription(offer); const answerSdp = await fetch("https://api.example.com/v1/realtime/calls", { method: "POST", headers: { Authorization: `Bearer ${ephemeralToken}`, "Content-Type": "application/sdp" }, body: offer.sdp }).then((response) => response.text()); await pc.setRemoteDescription({ type: "answer", sdp: answerSdp }); ``` ### WebRTC 数据分工 | 数据 | 通道 | 说明 | | --- | --- | --- | | 用户麦克风 | WebRTC audio track | 不再通过 JSON 重复发送音频块 | | 助手语音 | WebRTC remote audio track | 浏览器直接播放,不依赖 `response.output_audio.delta` 拼接 | | 标准和扩展事件 | `oai-events` DataChannel | 使用 UTF-8 JSON 文本,保持可靠、有序传输 | | 用户摄像头 | 可选 WebRTC video track | 通过 `video_track` capability 协商,非 OpenAI 标准能力 | 摄像头轨道只在助手启用视觉、令牌允许且 SDP 协商成功时接收。普通 OpenAI 兼容客户端不添加视频轨道,仍可正常完成语音和文本会话。 切换摄像头、静音、音量控制和 `RTCPeerConnection.getStats()` 属于客户端 WebRTC 能力,不需要新增协议事件。切换设备时可使用 `RTCRtpSender.replaceTrack()`,无需重建会话。 ## WebSocket WebSocket 适合服务到服务、话务网关和希望自行处理音频缓冲的客户端。连接时在 query 中指定助手: ```text wss://api.example.com/v1/realtime?model=assistant%3Aasst_xxx ``` 可信服务端在握手请求中发送: ```http Authorization: Bearer ``` 浏览器无法设置 `Authorization` 握手头时,可同时发送 `realtime` 与 `openai-insecure-api-key.` WebSocket 子协议;服务端选择 `realtime`。这只适用于短期令牌,不应把长期 Key 放入浏览器。 连接成功后,客户端和服务端都只发送 UTF-8 JSON 文本帧。浏览器无法安全保存长期 API Key,浏览器实时通话仍应优先使用 WebRTC。 ### 文本输入 先创建用户消息,再请求生成回复: ```json { "event_id": "evt_text_001", "type": "conversation.item.create", "item": { "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "请介绍办理流程" } ] } } ``` ```json { "event_id": "evt_response_001", "type": "response.create" } ``` ### 音频输入和输出 WebSocket 音频以 Base64 编码放入标准事件: ```json { "event_id": "evt_audio_001", "type": "input_audio_buffer.append", "audio": "" } ``` - 开启 Server VAD 时,服务端自动产生 `speech_started`、`speech_stopped` 并触发回复。 - 关闭 VAD 时,客户端发送 `input_audio_buffer.commit`,然后发送 `response.create`。 - 服务端通过 `response.output_audio.delta` 返回 Base64 音频块,并以 `response.output_audio.done` 结束。 - 助手文本或音频转写分别通过 `response.output_text.*` 和 `response.output_audio_transcript.*` 返回。 WebSocket MVP 不传输连续原始视频。视觉输入使用标准 `input_image`;需要连续摄像头的客户端使用 WebRTC 视频轨道。 ### 图片输入 图片和文本可放在同一个用户消息中: ```json { "type": "conversation.item.create", "item": { "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "画面里有什么?" }, { "type": "input_image", "image_url": "data:image/jpeg;base64," } ] } } ``` ## Push-to-Talk Push-to-Talk(按住说话)属于 OpenAI Realtime Core 兼容能力,不需要新增 `x.interactive_media.push_to_talk.*` 扩展事件。按钮是否按下、快捷键和麦克风 UI 状态由客户端本地管理,服务端只处理标准音频缓冲与回复事件。 ### 关闭自动 VAD 客户端建立会话后先通过 `session.update` 关闭自动轮次检测: ```json { "event_id": "evt_ptt_config_001", "type": "session.update", "session": { "type": "realtime", "audio": { "input": { "turn_detection": null } } } } ``` 关闭 VAD 后,服务端不会根据静音自动提交用户语音或创建回复。客户端必须在用户松开按钮时发送 `input_audio_buffer.commit`,然后发送 `response.create`。 ### WebSocket 时序 WebSocket 的音频和控制事件在同一条有序连接上传输: | 阶段 | 客户端行为 | | --- | --- | | 按下 | 开始在客户端录音;如果已有活动回复,发送 `response.cancel` | | 打断播放 | 立即停止本地音频播放,并发送 `conversation.item.truncate`,用 `audio_end_ms` 删除用户未听到的内容 | | 松开 | 通过一个或多个 `input_audio_buffer.append` 发送本次录音 | | 提交 | 依次发送 `input_audio_buffer.commit` 和 `response.create` | 完整的松开事件序列如下: ```json { "event_id": "evt_ptt_audio_001", "type": "input_audio_buffer.append", "audio": "" } ``` ```json { "event_id": "evt_ptt_commit_001", "type": "input_audio_buffer.commit" } ``` ```json { "event_id": "evt_ptt_response_001", "type": "response.create" } ``` 如果用户在录音完成前取消本次输入,客户端应丢弃本地录音;已经发送到服务端的未提交音频则使用 `input_audio_buffer.clear` 清除。 ### WebRTC 时序 WebRTC 的音频媒体轨道和 `oai-events` 控制事件属于不同通道,因此开始新一轮输入前必须显式清理旧缓冲: | 阶段 | 客户端行为 | | --- | --- | | 按下 | 先发送 `input_audio_buffer.clear`,再开始或放开本地麦克风输入门控 | | 打断生成 | 如果已有活动回复,发送 `response.cancel` | | 打断播放 | 如果助手音频仍在播放,发送 `output_audio_buffer.clear`;服务端同时截断未播放的对话内容 | | 松开 | 关闭本地麦克风输入门控,依次发送 `input_audio_buffer.commit` 和 `response.create` | ```js function sendEvent(event) { dataChannel.send(JSON.stringify(event)); } function onPushDown() { sendEvent({ type: "input_audio_buffer.clear" }); if (hasActiveResponse) { sendEvent({ type: "response.cancel" }); } if (isAssistantAudioPlaying) { sendEvent({ type: "output_audio_buffer.clear" }); } microphoneTrack.enabled = true; } function onPushUp() { microphoneTrack.enabled = false; sendEvent({ type: "input_audio_buffer.commit" }); sendEvent({ type: "response.create" }); } ``` 客户端应处理指针移出、窗口失焦和权限撤销等情况,确保一次按下只产生一次提交。没有有效音频时不要发送 `input_audio_buffer.commit`。 ### 保留 VAD、手动触发回复 如果只想由客户端决定何时生成回复,但仍希望服务端判断说话开始和结束,可以保留 VAD,并关闭自动回复及自动打断: ```json { "type": "session.update", "session": { "type": "realtime", "audio": { "input": { "turn_detection": { "type": "server_vad", "interrupt_response": false, "create_response": false } } } } } ``` 这种模式适合在提交前进行审核、输入校验或知识检索,不等同于严格的按住说话。 ## 标准事件范围 首版兼容层至少支持以下 OpenAI Realtime 事件。 ### 客户端发送 | 事件 | 用途 | | --- | --- | | `session.update` | 更新允许覆盖的会话配置 | | `conversation.item.create` | 发送文本、整段音频、图片或函数结果 | | `conversation.item.truncate` | 打断后删除未播放的助手内容 | | `input_audio_buffer.append` | WebSocket 追加音频 | | `input_audio_buffer.commit` | 无 VAD 时提交音频 | | `input_audio_buffer.clear` | 清除未提交音频 | | `output_audio_buffer.clear` | WebRTC/SIP 清除未播放音频并截断上下文 | | `response.create` | 请求助手生成回复 | | `response.cancel` | 取消正在生成的回复 | ### 服务端发送 | 事件组 | 事件 | | --- | --- | | 会话 | `session.created`、`session.updated`、`error` | | 用户语音 | `input_audio_buffer.speech_started`、`input_audio_buffer.speech_stopped`、`input_audio_buffer.committed` | | 用户转写 | `conversation.item.input_audio_transcription.completed` | | 对话项 | `conversation.item.added`、`conversation.item.done` | | 回复生命周期 | `response.created`、`response.output_item.added`、`response.output_item.done`、`response.done` | | 文本 | `response.output_text.delta`、`response.output_text.done` | | 音频 | `response.output_audio.delta`、`response.output_audio.done` | | 音频转写 | `response.output_audio_transcript.delta`、`response.output_audio_transcript.done` | | 函数调用 | `response.function_call_arguments.delta`、`response.function_call_arguments.done` | WebRTC 中的助手音频本体走媒体轨道,因此客户端不应依赖 `response.output_audio.delta` 播放声音;音频生命周期和转写事件仍通过 `oai-events` 发送。 ## 首版兼容矩阵 | 分类 | 状态 | 说明 | | --- | --- | --- | | WebRTC 音频、`oai-events` DataChannel | 支持 | 复用 SmallWebRTC 和现有 Pipeline | | WebSocket 文本与 24 kHz PCM16 mono Base64 音频 | 支持 | 只接受 UTF-8 JSON 文本帧 | | 文本轮次、Server VAD、PTT、取消和清空输出 | 支持 | `turn_detection: null` 时由 `commit` + `response.create` 结束轮次 | | `input_image` Data URL | 条件支持 | 仅 Pipeline 模式且助手已授权视觉输入;Realtime 模式明确报错 | | Client Tool 标准 function call | 支持 | System、HTTP、MCP 工具仍在服务端内部执行 | | `x.interactive_media.*` | 协商后支持 | 未协商的客户端不会收到扩展事件 | | `model`、instructions、voice、工具和系统提示词 | 锁定 | 由助手配置决定,覆盖请求返回标准 `error` | | WebSocket 其他采样率、压缩音频、连续视频 | 暂不支持 | WebRTC 音频由 SDP 协商;连续视频仅走 WebRTC | | 公开助手别名、断线续传、并发回复、SIP | 暂不支持 | `model` 必须使用实际的 `assistant:asst_xxx` | Qwen Realtime provider 只允许在 provider 会话首次配置前选择 PTT 或自动轮次检测;建立连接后再次切换会返回明确错误。Pipeline 与支持动态更新的 Realtime provider 可在会话内切换。 ## 工具调用 服务端 System、HTTP 和 MCP 工具在现有运行时内部执行。Client 工具使用标准函数调用协议,不再建立一套平行的 `client-tool-call` 公开协议。 服务端完成函数参数后发送标准函数调用事件;客户端执行函数并回传: ```json { "type": "conversation.item.create", "item": { "type": "function_call_output", "call_id": "call_abc123", "output": "{\"ok\":true,\"result\":{\"ticket_id\":\"T-1001\"}}" } } ``` 随后发送 `response.create` 让助手基于工具结果继续当前轮次。`call_id` 在整个会话内唯一,客户端应原样返回。 ## Interactive Media 扩展 ### 能力协商 客户端收到 `session.created` 后主动声明所需扩展: ```json { "event_id": "evt_capabilities_001", "type": "x.interactive_media.capabilities.update", "capabilities": [ "workflow_events", "dynamic_variables", "handoff", "video_track" ] } ``` 服务端返回实际接受的能力: ```json { "event_id": "evt_server_001", "type": "x.interactive_media.capabilities.updated", "capabilities": [ "workflow_events", "dynamic_variables", "handoff", "video_track" ], "rejected": [] } ``` 服务端只发送已接受能力对应的扩展事件。扩展版本与 OpenAI Realtime Core 独立演进。 ### 扩展事件目录 | 方向 | 事件 | 用途 | | --- | --- | --- | | Client → Server | `x.interactive_media.capabilities.update` | 请求扩展能力 | | Server → Client | `x.interactive_media.capabilities.updated` | 返回允许的能力 | | Client → Server | `x.interactive_media.session.variables.update` | 静默更新动态变量,不创建用户消息 | | Server → Client | `x.interactive_media.session.variables.updated` | 确认变量更新状态 | | Server → Client | `x.interactive_media.workflow.node_active` | 当前工作流节点变化 | | Server → Client | `x.interactive_media.workflow.variables.updated` | 工作流变量变化 | | Server → Client | `x.interactive_media.workflow.event` | 工作流业务事件 | | Server → Client | `x.interactive_media.workflow.error` | 工作流执行失败 | | Server → Client | `x.interactive_media.call.handoff_requested` | 助手请求人工接管 | | Server → Client | `x.interactive_media.call.ended` | 会话结束及原因 | 动态变量更新示例: ```json { "event_id": "evt_variables_001", "type": "x.interactive_media.session.variables.update", "update_id": "update_001", "variables": { "user_name": "王先生", "region": "上海" } } ``` ```json { "event_id": "evt_server_002", "type": "x.interactive_media.session.variables.updated", "update_id": "update_001", "status": "accepted" } ``` 工作流节点事件示例: ```json { "event_id": "evt_server_003", "type": "x.interactive_media.workflow.node_active", "nodeId": "collect_materials" } ``` 会话结束示例: ```json { "event_id": "evt_server_004", "type": "x.interactive_media.call.ended", "reason": "assistant_completed" } ``` `x.interactive_media.call.ended` 发出后,服务端停止产生新回复并正常关闭媒体与信令通道。未启用扩展的客户端只观察到标准回复结束和连接关闭。 ## 打断和回复并发 - 默认同一会话只允许一个活动回复。 - 用户开始说话且助手允许打断时,服务端取消当前生成并停止后续音频输出。 - 客户端可显式发送 `response.cancel`。 - WebSocket 客户端自行管理音频播放,因此停止播放后应发送 `conversation.item.truncate`,用 `audio_end_ms` 告诉服务端用户实际听到的位置。 - WebRTC/SIP 的输出音频由服务端缓冲;VAD 打断时由服务端自动截断,显式 Push-to-Talk 打断使用 `output_audio_buffer.clear`。 - 助手配置禁止打断时,服务端继续当前回复,并对不允许的显式取消返回 `error`。 ## 错误格式 协议错误使用标准 `error` 事件,不使用只存在于某个传输的自定义错误结构: ```json { "event_id": "evt_server_error_001", "type": "error", "error": { "type": "invalid_request_error", "code": "session_field_locked", "message": "The assistant does not allow overriding instructions.", "param": "session.instructions", "event_id": "evt_update_001" } } ``` 常见错误码包括: | code | 含义 | | --- | --- | | `authentication_failed` | API Key 或临时令牌无效 | | `assistant_not_found` | 助手不存在、未发布或不可访问 | | `session_field_locked` | 客户端尝试覆盖服务端锁定配置 | | `unsupported_event` | 当前协议版本不支持该事件 | | `capability_not_enabled` | 未协商或无权使用某项扩展 | | `response_in_progress` | 已有活动回复且当前请求不能并发执行 | | `tool_result_timeout` | Client 工具未在规定时间内返回 | | `internal_error` | 运行时出现不可恢复错误 | 错误只终止相关事件;只有鉴权失败、协议严重错误或运行时不可恢复时才关闭整个连接。 ## 事件顺序和重连 - WebSocket 文本帧和 `oai-events` DataChannel 都按连接内的发送顺序处理。 - 客户端事件建议携带唯一 `event_id`;服务端错误通过 `error.event_id` 指回原事件。 - `session.created` 是连接建立后的第一个业务事件。 - 每个回复由 `response.created` 开始,以 `response.done` 结束;Delta 必须携带所属的 response、item 和 content 索引。 - `Interactive Media Realtime Extensions v1` 不支持会话断线续传。重连会创建新会话;服务端历史记录仍按平台策略持久化。 - 客户端不得把收到的旧连接事件写入新连接。 ## 现有能力映射 | 现有能力或旧事件 | 新协议 | | --- | --- | | WebRTC 麦克风与助手音频 | WebRTC audio track | | 摄像头连续输入 | 可选 WebRTC video track + `video_track` capability | | `user-input` 文本或图片 | `conversation.item.create` + `response.create` | | `session-update` | `x.interactive_media.session.variables.update` | | `transcript` 用户转写 | `conversation.item.input_audio_transcription.*` | | `assistant-text-start/delta/end` | `response.output_text.*` 或 `response.output_audio_transcript.*` | | `client-tool-call/result` | 标准 function call + `function_call_output` | | `node-active` | `x.interactive_media.workflow.node_active` | | `workflow-variables` | `x.interactive_media.workflow.variables.updated` | | `workflow-event` | `x.interactive_media.workflow.event` | | `call-ended` | `x.interactive_media.call.ended` | | 知识库、服务端工具、工作流执行 | 继续在现有运行时内部完成 | Pipeline 与 Realtime 两种助手运行模式对客户端使用同一套北向事件。协议适配层负责把不同内部模型事件归一化,客户端不需要根据运行模式切换协议。 原有 RTVI 入口和消息格式继续并行保留,当前前端不需要迁移;新兼容接口面向新的第三方集成。 ## 传输选择 | 场景 | 推荐传输 | 原因 | | --- | --- | --- | | 浏览器实时语音、打断和摄像头 | WebRTC | 媒体自适应、播放简单、支持视频轨道 | | 移动端实时语音 | WebRTC | 更适合不稳定网络和双向媒体 | | 服务端 Agent 或批处理式实时流 | WebSocket | JSON 与音频缓冲可完全由调用方控制 | | 电话网关 | WebSocket | 易于接入现有 PCM/PCMU 音频桥接 | | 纯文本 Realtime | WebSocket | 无需建立媒体轨道 | ## 兼容性原则 1. 不修改标准事件的名称和含义。 2. 所有项目扩展使用 `x.interactive_media.*` 命名空间。 3. 扩展必须先协商再发送,普通客户端只看到标准事件。 4. 未支持字段明确报错,不静默伪装为成功。 5. OpenAI Realtime Core 和 Interactive Media 扩展分别进行版本管理。 6. 新协议只替换北向连接层,不改变现有 Pipecat 运行时和助手配置模型。 ## 参考 - [OpenAI Realtime API with WebRTC](https://developers.openai.com/api/docs/guides/realtime-webrtc) - [OpenAI Realtime API with WebSocket](https://developers.openai.com/api/docs/guides/realtime-websocket) - [OpenAI Realtime conversations](https://developers.openai.com/api/docs/guides/realtime-conversations) - [OpenAI Realtime Push-to-Talk](https://developers.openai.com/api/docs/guides/realtime-conversations#push-to-talk)