Skip to content
// Node.js 可以用 ws 库,能自定义请求头
import WebSocket from 'ws'

const ws = new WebSocket(
  'wss://api.wxiai.com/xai/v1/realtime?model=grok-voice-latest',
  { headers: { Authorization: 'Bearer YOUR_API_KEY' } },
)

ws.on('open', () => {
  // 建连后第一件事:配置会话
  ws.send(JSON.stringify({
    type: 'session.update',
    session: {
      voice: 'eve',
      instructions: '你是一个耐心的中文客服助手。',
      turn_detection: { type: 'server_vad' },
      audio: {
        input:  { format: { type: 'audio/pcm', rate: 24000 } },
        output: { format: { type: 'audio/pcm', rate: 24000 } },
      },
    },
  }))
})

ws.on('message', (raw) => {
  const event = JSON.parse(raw.toString())
  if (event.type === 'response.output_audio_transcript.delta') {
    process.stdout.write(event.delta)
  }
  if (event.type === 'response.output_audio.delta') {
    // event.delta 是 base64 音频,解码后播放
  }
})
import WebSocket from 'ws'

const ws = new WebSocket(
  'wss://api.wxiai.com/v1/realtime?model=grok-voice-latest',
  { headers: { Authorization: 'Bearer YOUR_API_KEY' } },
)

ws.on('open', () => {
  ws.send(JSON.stringify({
    type: 'session.update',
    session: {
      voice: 'eve',
      instructions: '你是一个耐心的中文客服助手。',
      turn_detection: { type: 'server_vad' },
    },
  }))
})
# 浏览器的 WebSocket API 不能自定义请求头,所以先用你的 Key
# 换一个短期临时凭证出来,再拿它建连。这个端点本身不计费。
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 } }'
// 子协议字段是浏览器唯一能自定义的握手信息
const ws = new WebSocket('wss://api.wxiai.com/xai/v1/realtime', [
  `xai-client-secret.${ephemeralToken}`,
])
// 服务端 VAD 模式下,只管持续追加音频,不用自己提交
ws.send(JSON.stringify({
  type: 'input_audio_buffer.append',
  audio: base64AudioChunk,   // base64 编码的 PCM16 分片
}))

// 手动模式(turn_detection: null)才需要自己提交
ws.send(JSON.stringify({ type: 'input_audio_buffer.commit' }))
{ "type": "session.created" }
{ "type": "conversation.created" }
{ "type": "session.updated" }
{ "type": "input_audio_buffer.speech_started" }
{ "type": "conversation.item.input_audio_transcription.completed", "transcript": "你好" }
{ "type": "response.created" }
{ "type": "response.output_audio_transcript.delta", "delta": "你好" }
{ "type": "response.output_audio.delta", "delta": "<base64 音频>" }
{ "type": "response.output_audio.done" }
{ "type": "response.done" }
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-latestOpenAI 兼容,协议与 OpenAI Realtime 一致
POST /xai/v1/realtime/client_secrets换临时凭证(浏览器用,不计费)
POST /v1/realtime/client_secrets同上,兼容层路径

两条路径的事件协议完全相同,除路径前缀外没有差异——Grok 的实时语音本身就是 OpenAI Realtime 协议。具体建连代码见独立页:

浏览器怎么连(重要) ​

浏览器原生的 WebSocket 构造函数不支持自定义请求头,没法带 Authorization——这是硬限制。

xAI 的官方做法是两步走:

  1. 你的后端用 API Key 调 POST /xai/v1/realtime/client_secrets,换一个短期临时凭证
  2. 前端拿这个凭证,用子协议字段建连
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。

参数类型说明
instructionsstring系统提示词。用第二人称写,见官方 Prompting Guide
voicestring音色。内置音色(如 eve)或自定义音色 ID
reasoning.effortstringhigh(默认)或 none,是否让模型先推理
toolsarray给语音助手挂工具,见工具
turn_detection.typestring | nullserver_vad 自动断句;null 手动控制轮次
turn_detection.thresholdnumberVAD 触发门限 0.1–0.9,默认 0.85。越大越要求音量大
turn_detection.silence_duration_msnumber静音多久算说完,0–10000 ms
turn_detection.prefix_padding_msnumber语音起点前多带多少 ms 音频,0–10000,默认 333。防止第一个字被切掉
turn_detection.idle_timeout_msnumber助手说完后,用户静音超过这个时长就主动搭话。默认 null(不开启)
resumption.enabledboolean断线重连时用 conversation_id 回放历史轮次。默认 false
audio.input.format.typestring输入编码:audio/pcm / audio/pcmu / audio/pcma / audio/opus
audio.input.format.ratenumber输入采样率(仅 PCM):8000 / 16000 / 22050 / 24000 / 32000 / 44100 / 48000
audio.input.transportstringjson(默认,base64)或 binary(裸字节走二进制帧)
audio.output.format.typestring输出编码,取值同上
audio.output.format.ratenumber输出采样率(仅 PCM):取值同上
audio.output.transportstringjson(默认)或 binary。改它会在下一个响应边界生效
audio.input.transcription.language_hintstringBCP-47 语言码,给转写做偏置。可中途改
audio.input.transcription.keytermsarray关键词偏置,最多 100 个、每个 ≤ 50 字符。可中途改
audio.output.speednumber助手语速,0.7–1.5,默认 1.0
replaceobject发音替换表,作用于模型输出文本转语音之前。见发音替换
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/pcmuG.711 μ-law固定 8000 Hz
audio/pcmaG.711 A-law固定 8000 Hz
audio/opusOpus固定 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_stoppedVAD 检测到说话开始 / 结束。仅 server VAD 模式
input_audio_buffer.committed音频已提交成用户消息
input_audio_buffer.timeout_triggeredidle_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 缓存对话轮次,重连时回放。注意两点:

  1. 重连必须带上 ?conversation_id=<你的会话 ID> query 参数,否则回放不了;
  2. 历史在30 分钟不活动后丢弃,超过这个窗口的重连等于新会话。
计费项说明
音频按分钟计费,发送和接收都算——你说的和它说的都计费
文本消息按 conversation.item.create 事件计费(每条消息一次)。response.create 不计费;function_call_output 类型的条目不计费;内容是 input_audio / audio 的条目走音频计费

省钱的两个实际动作

  1. 不说话时及时关掉连接。按分钟计费意味着挂着不用的连接也在烧钱。用户离开页面就该断开。
  2. 别把音频当背景音一直传。环境噪音会被当成有效音频计费。

具体单价见 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 分钟上限 + 网络抖动,断开是常态不是异常。

相关页 ​

基于 Apache-2.0 许可发布