feat: implement OpenAI-compatible Realtime API with authentication and management features
- Added support for public Realtime API, including new routes for managing API keys and handling WebRTC connections. - Introduced RealtimeApiKey model and associated CRUD operations for admin management of API keys. - Implemented authentication mechanisms for API keys and client secrets. - Enhanced environment configuration with new secrets for Realtime API. - Created OpenAIRealtime session management and event processing for real-time interactions. - Updated schemas and settings to accommodate new features and ensure compatibility with existing systems.
This commit is contained in:
693
docs-developer/realtime-protocol.mdx
Normal file
693
docs-developer/realtime-protocol.mdx
Normal file
@@ -0,0 +1,693 @@
|
||||
---
|
||||
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:<id>` | 创建 WebSocket Realtime 会话 | 服务端、话务网关、自定义客户端 |
|
||||
|
||||
`model` 使用 `assistant:<id>`,其中 `id` 是当前数据库中的助手 ID,例如:
|
||||
|
||||
```text
|
||||
assistant:asst_xxx
|
||||
```
|
||||
|
||||
这里的 `model` 代表一个完整助手,而不是直接选择底层模型。助手绑定的系统提示词、模型凭证、知识库、服务端工具、工作流和运行模式仍由服务端控制。
|
||||
|
||||
### 鉴权
|
||||
|
||||
- 长期 API Key 仅供可信服务端使用,通过 `Authorization: Bearer <api-key>` 发送。
|
||||
- 浏览器先由自己的业务后端创建临时令牌,再用临时令牌连接 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=<offer.sdp' \
|
||||
-F 'session={"type":"realtime","model":"assistant:asst_xxx"}'
|
||||
```
|
||||
|
||||
业务浏览器不应直接持有示例中的长期 API Key。它通常把 Offer 发给自己的业务后端,由后端完成上述调用。
|
||||
|
||||
### 建连方式二:临时令牌
|
||||
|
||||
1. 可信业务后端使用长期 API Key 调用 `/v1/realtime/client_secrets`。
|
||||
2. 服务端返回绑定助手的短期令牌。
|
||||
3. 浏览器创建 `RTCPeerConnection`、音频轨道和 `oai-events` DataChannel。
|
||||
4. 浏览器以临时令牌调用 `/v1/realtime/calls`,请求体为原始 SDP Offer,类型为 `application/sdp`。
|
||||
5. 浏览器把响应中的 SDP Answer 设置为远端描述。
|
||||
|
||||
创建令牌的请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"session": {
|
||||
"type": "realtime",
|
||||
"model": "assistant:asst_xxx",
|
||||
"output_modalities": ["audio"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
浏览器侧的核心连接过程如下:
|
||||
|
||||
```js
|
||||
const pc = new RTCPeerConnection({ iceServers });
|
||||
const remoteAudio = new Audio();
|
||||
remoteAudio.autoplay = true;
|
||||
|
||||
pc.ontrack = (event) => {
|
||||
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 <api-key-or-ephemeral-token>
|
||||
```
|
||||
|
||||
浏览器无法设置 `Authorization` 握手头时,可同时发送 `realtime` 与 `openai-insecure-api-key.<token>` 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": "<base64-pcm-bytes>"
|
||||
}
|
||||
```
|
||||
|
||||
- 开启 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,<base64-image>"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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": "<base64-pcm-bytes>"
|
||||
}
|
||||
```
|
||||
|
||||
```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)
|
||||
Reference in New Issue
Block a user