切换日光/暗黑模式
Grok API 调用
实时会话
WebSocket 长连接,支持双向实时语音:你说它听、它说你听,不用等一轮请求结束。模型是 grok-voice-latest。
WSS
/wss:/api.wxiai.com/xai/v1/realtime什么情况该用它
| 你要做的 | 用哪个 |
|---|---|
| 语音对话助手(像打电话一样) | 用这个 |
| 一次性的「录音 → 转文字」 | 语音识别,更简单 |
| 普通的文本流式输出 | 对话补全 就够,不用上 WebSocket |
只有需要「边说边听、随时打断」的场景,才值得引入 WebSocket。 它的连接管理和状态同步成本比普通 HTTP 接口高不少。
两个端点
| 端点 | 用途 |
|---|---|
WSS /xai/v1/realtime?model=grok-voice-latest | 原生透传,事件原样转发给 Grok |
WSS /v1/realtime?model=grok-voice-latest | OpenAI 兼容,协议与 OpenAI Realtime 一致 |
POST /xai/v1/realtime/client_secrets | 换临时凭证(浏览器用,不计费) |
POST /v1/realtime/client_secrets | 同上,兼容层路径 |
两条路径的事件协议完全相同,除路径前缀外没有差异——Grok 的实时语音本身就是 OpenAI Realtime 协议。具体建连代码见独立页:
浏览器怎么连(重要)
浏览器原生的 WebSocket 构造函数不支持自定义请求头,没法带 Authorization——这是硬限制。
xAI 的官方做法是两步走:
- 你的后端用 API Key 调
POST /xai/v1/realtime/client_secrets,换一个短期临时凭证 - 前端拿这个凭证,用子协议字段建连
javascript
// 浏览器端
const ws = new WebSocket('wss://api.wxiai.com/xai/v1/realtime', [
`xai-client-secret.${ephemeralToken}`,
])网关同时支持两种子协议前缀:
| 前缀 | 用途 |
|---|---|
xai-client-secret.<临时凭证> | xAI 官方风格,配合 client_secrets 使用 |
openai-insecure-api-key.<密钥> | OpenAI 风格 |
换成临时凭证的请求:
bash
curl -X POST https://api.wxiai.com/xai/v1/realtime/client_secrets \
-H "Authorization: Bearer $WXIAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "expires_after": { "seconds": 300 } }'不要把长期 API Key 写进前端
那样任何访问者都能从源码里拿走它。client_secrets 这个端点存在的唯一目的就是避免这件事,而且它本身不计费。
模型
model 是 URL 查询参数,不是 session.update 里的字段。
wss://api.wxiai.com/xai/v1/realtime?model=grok-voice-latest| 模型 | 说明 |
|---|---|
grok-voice-latest | 别名,指向当前旗舰,推荐 |
grok-voice-think-fast-2.0 | 旗舰语音模型 |
grok-voice-think-fast-1.0 | 上一代语音模型 |
不传 model 时默认 grok-voice-latest。想锁定某个版本就用带版本号的完整名字。
session.update 参数
建连后第一件事就是发 session.update。
| 参数 | 类型 | 说明 |
|---|---|---|
instructions | string | 系统提示词。用第二人称写,见官方 Prompting Guide |
voice | string | 音色。内置音色(如 eve)或自定义音色 ID |
reasoning.effort | string | high(默认)或 none,是否让模型先推理 |
tools | array | 给语音助手挂工具,见工具 |
turn_detection.type | string | null | server_vad 自动断句;null 手动控制轮次 |
turn_detection.threshold | number | VAD 触发门限 0.1–0.9,默认 0.85。越大越要求音量大 |
turn_detection.silence_duration_ms | number | 静音多久算说完,0–10000 ms |
turn_detection.prefix_padding_ms | number | 语音起点前多带多少 ms 音频,0–10000,默认 333。防止第一个字被切掉 |
turn_detection.idle_timeout_ms | number | 助手说完后,用户静音超过这个时长就主动搭话。默认 null(不开启) |
resumption.enabled | boolean | 断线重连时用 conversation_id 回放历史轮次。默认 false |
audio.input.format.type | string | 输入编码:audio/pcm / audio/pcmu / audio/pcma / audio/opus |
audio.input.format.rate | number | 输入采样率(仅 PCM):8000 / 16000 / 22050 / 24000 / 32000 / 44100 / 48000 |
audio.input.transport | string | json(默认,base64)或 binary(裸字节走二进制帧) |
audio.output.format.type | string | 输出编码,取值同上 |
audio.output.format.rate | number | 输出采样率(仅 PCM):取值同上 |
audio.output.transport | string | json(默认)或 binary。改它会在下一个响应边界生效 |
audio.input.transcription.language_hint | string | BCP-47 语言码,给转写做偏置。可中途改 |
audio.input.transcription.keyterms | array | 关键词偏置,最多 100 个、每个 ≤ 50 字符。可中途改 |
audio.output.speed | number | 助手语速,0.7–1.5,默认 1.0 |
replace | object | 发音替换表,作用于模型输出文本转语音之前。见发音替换 |
javascript
ws.send(JSON.stringify({
type: 'session.update',
session: {
voice: 'eve',
instructions: '你是一个耐心的中文客服助手。',
turn_detection: {
type: 'server_vad',
threshold: 0.85,
silence_duration_ms: 600,
prefix_padding_ms: 333,
},
audio: {
input: { format: { type: 'audio/pcm', rate: 24000 } },
output: { format: { type: 'audio/pcm', rate: 24000 } },
},
},
}))音频格式与传输
编码(format.type):
| 值 | 编码 | 采样率 |
|---|---|---|
audio/pcm(默认) | Linear16 小端 | 可配置:8000 / 16000 / 22050 / 24000 / 32000 / 44100 / 48000 |
audio/pcmu | G.711 μ-law | 固定 8000 Hz |
audio/pcma | G.711 A-law | 固定 8000 Hz |
audio/opus | Opus | 固定 24000 Hz,一个 payload 一个包 |
输入和输出分别配置,不需要一致。
传输方式(transport):
| 方向 | json(默认) | binary |
|---|---|---|
| 输入 | input_audio_buffer.append 里放 base64 | 裸音频字节直接发二进制帧 |
| 输出 | response.output_audio.delta 里放 base64 | 裸音频字节走二进制帧;生命周期事件仍是 JSON |
输入双收:配了输入格式后,JSON 和二进制两种送法服务端都收。输出是严格的:只走 output.transport 指定的那一种,中途改会在下一个响应边界生效,保证一句话不会混两种格式。
事件表
你发过去的事件
| 事件 | 用途 |
|---|---|
session.update | 更新会话配置:提示词、音色、音频格式、断句、工具 |
input_audio_buffer.append | 追加一段音频(base64),服务端不回执 |
input_audio_buffer.commit | 把缓冲的音频提交成一条用户消息。仅 turn_detection 为 null 时可用 |
input_audio_buffer.clear | 丢弃还没提交的音频 |
conversation.item.create | 新建会话条目:用户文本、助手文本(预热历史)、函数调用及其结果 |
conversation.item.delete | 删除一条会话条目 |
conversation.item.truncate | 截断某条助手音频消息,丢掉指定时长之后的内容 |
response.create | 让服务端开始生成回应。用 server VAD 时是自动的 |
response.cancel | 取消正在生成的回应。VAD 模式下打断是自动的,手动模式才需要它 |
服务端发回来的事件
| 事件 | 说明 |
|---|---|
session.created | 建连后自动发,带会话配置 |
conversation.created | 会话已创建 |
session.updated | 确认 session.update |
input_audio_buffer.speech_started / .speech_stopped | VAD 检测到说话开始 / 结束。仅 server VAD 模式 |
input_audio_buffer.committed | 音频已提交成用户消息 |
input_audio_buffer.timeout_triggered | idle_timeout_ms 到了,用户一直没说话,服务端主动搭话 |
input_audio_buffer.cleared | 缓冲已清空 |
conversation.item.added / .deleted / .truncated | 会话条目变动 |
conversation.item.input_audio_transcription.completed | 用户这句话的转写完成 |
conversation.item.input_audio_transcription.updated | 边说边出的累计转写,可做实时字幕。需要把 audio.input.transcription.model 设为 grok-transcribe |
response.created | 一轮回应开始,之后的音频分片共享同一个 response_id |
response.output_audio.delta / .done | 助手音频分片 / 本段音频结束。分片事件也接受别名 response.audio.delta |
response.output_audio_transcript.delta / .done | 助手音频对应的文字分片 / 结束 |
response.output_text.delta / response.text.delta | 文本模式输出分片。两个名字功能相同,客户端两个都要处理 |
response.output_item.added / .done、response.content_part.added / .done | 输出条目的起止边界,做结构化渲染时用它 |
response.function_call_arguments.delta / .done | 函数调用参数流式输出 / 参数完整(该执行函数了) |
response.mcp_call.* / mcp_list_tools.* | MCP 工具发现与调用的过程事件 |
response.done | 本轮回应全部结束,可以开始下一轮 |
error | 出错。多数错误可恢复,连接不会断 |
工具
在 session.update 的 tools 里挂载,语音助手就能查资料、调你的函数:
| 类型 | 说明 |
|---|---|
file_search | 检索你上传的文档集合(需先用 Collections API 建集合) |
web_search | 联网搜索 |
x_search | 搜索 X(Twitter)上的帖子 |
mcp | 连接外部 MCP 服务器,用对方的工具 |
function | 自定义函数,用 JSON Schema 描述参数 |
javascript
ws.send(JSON.stringify({
type: 'session.update',
session: {
voice: 'eve',
instructions: '你是一个客服助手,需要时先查资料再回答。',
turn_detection: { type: 'server_vad' },
tools: [
{ type: 'web_search' },
{ type: 'x_search' },
],
},
}))function 类型的调用流程:收到 response.function_call_arguments.done → 在你的代码里执行函数 → 用 conversation.item.create(type: "function_call_output")把结果送回去 → 再发 response.create。
让它说一句固定的话(force_message)
合规播报、IVR 提示音、固定开场白这类必须一字不差的台词,不需要经过模型。用 conversation.item.create 发一条 force_message,它会直接走 TTS 念出来:
javascript
ws.send(JSON.stringify({
type: 'conversation.item.create',
item: {
type: 'force_message',
role: 'assistant',
interruptible: false,
content: [{ type: 'output_text', text: '本次通话将被录音。' }],
},
}))
// 不要再发 response.create —— force_message 本身就是一轮回应支持的语言
实时语音支持 20+ 种语言,模型自动识别输入语言并用同一种语言回答,不需要配置:
| 语言 | 代码 | 语言 | 代码 |
|---|---|---|---|
| 英语 | en | 日语 | ja |
| 中文(简体) | zh | 韩语 | ko |
| 阿拉伯语(埃及) | ar-EG | 葡萄牙语(巴西) | pt-BR |
| 阿拉伯语(沙特) | ar-SA | 葡萄牙语(葡萄牙) | pt-PT |
| 阿拉伯语(阿联酋) | ar-AE | 俄语 | ru |
| 孟加拉语 | bn | 西班牙语(墨西哥) | es-MX |
| 法语 | fr | 西班牙语(西班牙) | es-ES |
| 德语 | de | 土耳其语 | tr |
| 印地语 | hi | 越南语 | vi |
| 印尼语 | id | ||
| 意大利语 | it |
想让转写更准,用 audio.input.transcription.language_hint 指定语言。注意西班牙语和葡萄牙语必须带地区码(es-MX、pt-BR),裸 es、pt 不被接受;无法识别的码会被静默忽略并退回自动检测。
会话上限与计费
| 限制 | 值 |
|---|---|
| 单次会话时长 | 120 分钟 |
| 并发会话 | 50 / 团队 |
会话历史保留(开了 resumption) | 30 分钟不活动后丢弃 |
单次会话上限 120 分钟
一个 WebSocket 连接最长 120 分钟,到点会断开。
所以客户端必须实现重连逻辑:监听断开 → 重新建连 → 把必要的上下文补回去。
想少丢上下文就开 resumption.enabled:服务端会按 conversation_id 缓存对话轮次,重连时回放。注意两点:
- 重连必须带上
?conversation_id=<你的会话 ID>query 参数,否则回放不了; - 历史在30 分钟不活动后丢弃,超过这个窗口的重连等于新会话。
| 计费项 | 说明 |
|---|---|
| 音频 | 按分钟计费,发送和接收都算——你说的和它说的都计费 |
| 文本消息 | 按 conversation.item.create 事件计费(每条消息一次)。response.create 不计费;function_call_output 类型的条目不计费;内容是 input_audio / audio 的条目走音频计费 |
省钱的两个实际动作
- 不说话时及时关掉连接。按分钟计费意味着挂着不用的连接也在烧钱。用户离开页面就该断开。
- 别把音频当背景音一直传。环境噪音会被当成有效音频计费。
具体单价见 https://api.wxiai.com/models。
容易踩的坑
model放错地方:它是 URL 查询参数,不是session.update字段。- 在 server VAD 模式下自己发
commit:只有turn_detection为null时才需要手动提交。 - 只监听
response.output_text.delta:服务端两个名字(response.text.delta和response.output_text.delta)都可能发,两个都要处理。 - 等
response.completed:Grok 用的是response.done。 format和transport搞混:format选编码,transport选这些字节怎么在 WebSocket 上走。- 输出只认一种通道:
output.transport设成binary后,音频只在二进制帧里,别再去 JSON 分片里找。 - 断线不重连:120 分钟上限 + 网络抖动,断开是常态不是异常。
