Grok Voice 3.0 是 xAI 的实时语音对话模型(speech-to-speech),2026 年 10 月 14 日发布,面向语音智能体。你通过一条 WebSocket 把语音发进去,模型直接用语音回答:28 个内置音色,20 多种语言自动识别(含中文),支持函数调用。按会话时长每分钟 $0.104,按秒计费。
wss://api.apimodels.app/v1/realtime?model=grok-voice-3.0连接时在请求头里带你的 apimodels.app API Key:
Authorization: Bearer YOUR_API_KEY浏览器不能给 WebSocket 设请求头。由你的服务端换一个临时令牌,浏览器用子协议带上它。API Key 永远不要放进前端。
curl -X POST https://api.apimodels.app/v1/realtime/client_secrets \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"expires_after": {"seconds": 60}}'
# { "value": "<token>", "expires_at": 1792130400 }| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| expires_after.seconds | 否 | number | 令牌有效期,10 到 600 秒,默认 60。只影响能否用它建立连接。 |
返回 { "value": "<令牌>", "expires_at": <unix 秒> }。
new WebSocket("wss://api.apimodels.app/v1/realtime?model=grok-voice-3.0", ["xai-client-secret." + value])连上之后先发一条 session.update。所有字段都可选,之后可以随时再发。
{
"type": "session.update",
"session": {
"voice": "eve",
"instructions": "You are Grok Voice 3.0, a realtime voice assistant built by xAI. You are a friendly support agent. Keep answers short.",
"turn_detection": { "type": "server_vad" },
"audio": {
"input": { "format": { "type": "audio/pcm", "rate": 24000 },
"transcription": { "language_hint": "zh-CN" } },
"output": { "format": { "type": "audio/pcm", "rate": 24000 }, "speed": 1.0 }
},
"tools": [
{
"type": "function",
"name": "get_order_status",
"description": "Look up an order by its id",
"parameters": {
"type": "object",
"properties": { "order_id": { "type": "string" } },
"required": ["order_id"]
}
}
]
}
}| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| voice | 否 | string | 28 个内置音色之一,默认 eve。见下方音色表。 |
| instructions | 否 | string | 系统提示词:角色、语气、要做什么。 |
| turn_detection | 否 | object | null | {"type":"server_vad"} 由服务端判断你什么时候说完;null 为手动轮次(commit + response.create)。 |
| audio.input.format | 否 | object | 默认 {"type":"audio/pcm","rate":24000};也可用 audio/pcmu、audio/pcma、audio/opus。 |
| audio.output.format | 否 | object | 同上,决定模型回给你的音频格式。 |
| audio.output.speed | 否 | number | 语速,0.7 到 1.5。 |
| audio.input.transcription.language_hint | 否 | string | BCP-47 语言标签,如 zh-CN、en-US。知道用户语言时设一下。 |
| tools | 否 | array | 函数工具({"type":"function", name, description, parameters})。apimodels.app 目前只开放函数工具。 |
web_search、x_search、file_search、mcp 这类服务端工具暂不开放:session.update 里带了它们会收到 error 事件。函数调用本身不额外收费。
默认格式:16 位 PCM、24 kHz、单声道、小端序,base64 编码后放进 JSON 事件。
// with turn_detection {"type":"server_vad"}: just keep appending,
// the server decides when you stopped talking and answers.
{ "type": "input_audio_buffer.append", "audio": "<base64 16-bit PCM, 24 kHz, mono>" }
// with turn_detection null (manual turns): append, then commit, then ask for a reply
{ "type": "input_audio_buffer.append", "audio": "<base64 ...>" }
{ "type": "input_audio_buffer.commit" }
{ "type": "response.create" }
// throw away audio you have not committed yet
{ "type": "input_audio_buffer.clear" }不用麦克风也能对话:发一条文本消息再要回答。每条 input_text 文本消息 $0.0052。
{
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [{ "type": "input_text", "text": "What can you do?" }]
}
}
{ "type": "response.create" }// server -> you
{ "type": "response.function_call_arguments.done",
"call_id": "call_123", "name": "get_order_status", "arguments": "{\"order_id\":\"A-1001\"}" }
// you -> server: the result, then ask the model to continue speaking
{ "type": "conversation.item.create",
"item": { "type": "function_call_output", "call_id": "call_123", "output": "{\"status\":\"shipped\"}" } }
{ "type": "response.create" }# pip install websockets (websockets 14+; on older versions pass extra_headers= instead)
import asyncio, base64, json, os, wave
import websockets
URL = "wss://api.apimodels.app/v1/realtime?model=grok-voice-3.0"
API_KEY = os.environ["APIMODELS_API_KEY"]
async def main():
headers = {"Authorization": f"Bearer {API_KEY}"}
pcm = bytearray()
asked = False
async with websockets.connect(URL, additional_headers=headers) as ws:
# 1. Configure the session. turn_detection None = manual turns (no microphone here).
await ws.send(json.dumps({
"type": "session.update",
"session": {
"voice": "eve",
"instructions": "You are Grok Voice 3.0, a realtime voice assistant built by xAI. Keep answers short.",
"turn_detection": None,
"audio": {"output": {"format": {"type": "audio/pcm", "rate": 24000}}},
},
}))
async for raw in ws:
event = json.loads(raw)
kind = event["type"]
if kind == "session.updated" and not asked:
asked = True
# 2. One text message ($0.0052), then ask for a spoken reply.
await ws.send(json.dumps({
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Introduce yourself in one sentence."}],
},
}))
await ws.send(json.dumps({"type": "response.create"}))
elif kind == "response.output_audio.delta":
pcm += base64.b64decode(event["delta"])
elif kind == "response.output_audio_transcript.delta":
print(event["delta"], end="", flush=True)
elif kind == "error":
raise RuntimeError(event)
elif kind == "response.done":
break
# 3. The audio is 16-bit PCM, 24 kHz, mono: wrap it in a WAV header.
with wave.open("reply.wav", "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(24000)
wav.writeframes(bytes(pcm))
print()
print("saved reply.wav")
asyncio.run(main())// npm install ws (Node 18+, save as reply.mjs)
import fs from "node:fs";
import WebSocket from "ws";
const ws = new WebSocket("wss://api.apimodels.app/v1/realtime?model=grok-voice-3.0", {
headers: { Authorization: "Bearer " + process.env.APIMODELS_API_KEY },
});
const chunks = [];
let asked = false;
ws.on("open", () => {
ws.send(JSON.stringify({
type: "session.update",
session: {
voice: "eve",
instructions: "You are Grok Voice 3.0, a realtime voice assistant built by xAI. Keep answers short.",
turn_detection: null,
},
}));
});
ws.on("message", (data) => {
const event = JSON.parse(data.toString());
if (event.type === "session.updated" && !asked) {
asked = true;
ws.send(JSON.stringify({
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [{ type: "input_text", text: "Introduce yourself in one sentence." }],
},
}));
ws.send(JSON.stringify({ type: "response.create" }));
} else if (event.type === "response.output_audio.delta") {
chunks.push(Buffer.from(event.delta, "base64"));
} else if (event.type === "error") {
console.error(event);
} else if (event.type === "response.done") {
// Raw 16-bit PCM, 24 kHz, mono. To get a WAV:
// ffmpeg -f s16le -ar 24000 -ac 1 -i reply.pcm reply.wav
fs.writeFileSync("reply.pcm", Buffer.concat(chunks));
ws.close();
}
});// ── On your server (Node 18+, any framework). The API key never leaves the server. ──
export async function mintRealtimeToken() {
const res = await fetch("https://api.apimodels.app/v1/realtime/client_secrets", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.APIMODELS_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ expires_after: { seconds: 60 } }), // 10 to 600, default 60
});
return res.json(); // { value, expires_at }
}
// Expose it to your own logged-in users, e.g. POST /api/realtime-token -> mintRealtimeToken()// ── In the browser. Call start() from a click: audio only starts after a user gesture. ──
async function start() {
const { value } = await (await fetch("/api/realtime-token", { method: "POST" })).json();
const ws = new WebSocket(
"wss://api.apimodels.app/v1/realtime?model=grok-voice-3.0",
["xai-client-secret." + value],
);
const ctx = new AudioContext({ sampleRate: 24000 });
let playAt = 0;
ws.onopen = async () => {
ws.send(JSON.stringify({
type: "session.update",
session: {
voice: "eve",
instructions: "You are Grok Voice 3.0, a realtime voice assistant built by xAI.",
turn_detection: { type: "server_vad" },
},
}));
// Microphone -> 16-bit PCM -> input_audio_buffer.append.
// ScriptProcessorNode keeps the example short; use an AudioWorklet in production.
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mic = ctx.createMediaStreamSource(stream);
const proc = ctx.createScriptProcessor(4096, 1, 1);
proc.onaudioprocess = (e) => {
const f32 = e.inputBuffer.getChannelData(0);
const pcm = new Int16Array(f32.length);
for (let i = 0; i < f32.length; i++) {
const s = Math.max(-1, Math.min(1, f32[i]));
pcm[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
}
const bytes = new Uint8Array(pcm.buffer);
let bin = "";
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "input_audio_buffer.append", audio: btoa(bin) }));
}
};
mic.connect(proc);
proc.connect(ctx.destination);
};
// Model audio -> speakers, queued back to back.
ws.onmessage = (e) => {
const event = JSON.parse(e.data);
if (event.type === "error") console.error(event);
if (event.type !== "response.output_audio.delta") return;
const bin = atob(event.delta);
const buf = ctx.createBuffer(1, bin.length / 2, 24000);
const out = buf.getChannelData(0);
for (let i = 0; i < out.length; i++) {
const v = bin.charCodeAt(2 * i) | (bin.charCodeAt(2 * i + 1) << 8);
out[i] = (v >= 0x8000 ? v - 0x10000 : v) / 32768;
}
const src = ctx.createBufferSource();
src.buffer = buf;
src.connect(ctx.destination);
playAt = Math.max(playAt, ctx.currentTime);
src.start(playAt);
playAt += buf.duration;
};
}| 事件 | 说明 |
|---|---|
| session.update | 设置音色、指令、轮次检测、音频格式和函数工具。随时可以再发一次。 |
| conversation.item.create | 加一条文本消息(input_text,每条 $0.0052),或回传函数结果(function_call_output)。 |
| response.create | 让模型开口回答。手动轮次、发完文本或函数结果之后都要发。 |
| input_audio_buffer.append | 发送一段 base64 音频(字段名 audio)。 |
| input_audio_buffer.commit | 手动轮次(turn_detection 为 null)下结束用户这一轮。 |
| input_audio_buffer.clear | 丢弃尚未提交的音频。 |
| 事件 | 说明 |
|---|---|
| session.created | 连接已就绪,附带当前会话配置。 |
| session.updated | 你的 session.update 已生效。 |
| conversation.created | 会话已创建。 |
| conversation.item.added | 一条消息或函数调用已加入对话。 |
| conversation.item.input_audio_transcription.updated | 用户语音的转写,累积形式:每次给出到目前为止的完整文本,不是增量。 |
| response.created | 模型开始一次回答。 |
| response.output_audio.delta | 一段 base64 音频,在 delta 字段里。 |
| response.output_audio_transcript.delta | 模型正在说的话的文字,增量。 |
| response.output_audio.done | 这次回答的音频已全部发出。 |
| response.function_call_arguments.done | 模型要调用函数:带 call_id、name 和 JSON 字符串形式的 arguments。 |
| response.done | 这次回答结束。 |
| error | 请求出错。余额用完时错误类型为 insufficient_balance,随后会话关闭。 |
28 个内置音色,每一个都能说所有支持的语言 —— 不需要为中文另挑音色。在 session.update 里用 voice 指定,默认 eve。
| 类型 | voice |
|---|---|
| 女声(9) | ara, aurora, carina, celeste, eve (default), iris, liora, luna, ursa |
| 男声(19) | altair, atlas, castor, cosmo, helios, helix, kepler, leo, lumen, lux, naksh, orion, perseus, rex, rigel, sal, sirius, zagan, zenith |
语言:20 多种,口音地道,自动识别,包括简体中文。
| 项目 | 价格 |
|---|---|
| 会话时长 | $0.104 / 分钟,按秒计费 |
| 文本消息(conversation.item.create 的 input_text) | $0.0052 / 条 |
| 函数调用 | 不额外收费 |
| 示例:90 秒的会话 | $0.156 |
事件模型大体一致:session.update、conversation.item.create、response.create、input_audio_buffer.* 以及 response.output_audio.delta 的用法相同,OpenAI Realtime 的客户端改动不多就能迁过来。需要注意:
想先听听效果:在模型页用浏览器直接试(同样按 $0.104/分钟计费)。
用 WebSocket 连 wss://api.apimodels.app/v1/realtime?model=grok-voice-3.0。服务端在请求头带 Authorization: Bearer 你的 API Key;浏览器则由你的服务端调 POST https://api.apimodels.app/v1/realtime/client_secrets 换临时令牌(有效期 10 到 600 秒,默认 60),再用子协议 xai-client-secret.<value> 连接。这个端点只在 api.apimodels.app 上,apimodels.app/api/v1 下面没有。
按连接时长每分钟 $0.104,按秒结算,90 秒的会话 $0.156。用 conversation.item.create 发送的每条 input_text 文本消息 $0.0052;函数调用不收费。连接时余额至少要够一分钟,每分钟开始时扣费,结束时退回没用完的秒数,连接失败不扣费。
单次会话最长 60 分钟,每个账号最多 5 路并发,10 分钟没有客户端事件会被关闭。音频默认 16 位 PCM、24 kHz、单声道,base64 放在 JSON 里;也支持 audio/pcmu、audio/pcma、audio/opus。28 个内置音色(默认 eve)都能说全部 20 多种语言。