实时语音(Realtime)模型
Realtime 类模型不走 /v1/chat/completions,只能通过 WebSocket 长连接调用。网关把百炼 Qwen-Omni-Realtime 协议原样双向转发,不做任何协议转换:你按百炼协议发事件,也按百炼协议收事件。本文给出接入方式、可运行的最小示例与计费口径;逐字段的事件与错误定义见API 参考。
能做什么
- 实时语音对话:边说边回,端到端流式返回文本与音频。
- 语音打断:用户再次开口时,服务端下发
input_audio_buffer.speech_started;客户端停播并发response.cancel打断当前回复。 - 全模态输入:流式音频、图片(可由视频抽帧实时采集)、纯文本。
- 文本 + 音频输出:模型同时产出文本与语音。
适用模型
一个逻辑模型要能走本入口,必须在模型目录的 public_protocols 中声明 dashscope_realtime。目前上架的 realtime 模型是 qwen3.8-omni-flash-realtime(以模型广场实际上架为准)。可在 https://console.modelgo.com 的模型广场确认某个模型是否提供了该协议。
接入
| 项 | 值 |
|---|---|
| Endpoint | {{ ws_endpoint }}/api-ws/v1/realtime |
| Query | model=<逻辑模型名>(必填) |
| 鉴权 | Authorization: Bearer <mgk_ API Key> |
| 请求 ID | 可选 X-Request-ID;调用日志里 request_id 以 gw_rt_ 开头 |
握手阶段的失败会直接返回 HTTP 状态码(如 Key 无效 401 unauthorized、模型未声明 realtime 协议 403 model_protocol_not_offered、余额不足 402),完整清单见API 参考。
最小示例
下面示例在服务端运行:用 mgk_ API Key 建连,配置会话,推送一段 16 kHz PCM 语音,提交后请求回复,并把返回的 24 kHz PCM 写入 reply.pcm。
准备输入:把麦克风录音或一段 16 kHz、16-bit 小端、单声道、无文件头的 PCM 存为 input.pcm。若手上是 WAV,先提取裸 PCM:
import wave
from pathlib import Path
with wave.open("input.wav", "rb") as f:
assert (f.getnchannels(), f.getframerate(), f.getsampwidth(), f.getcomptype()) == (1, 16000, 2, "NONE")
Path("input.pcm").write_bytes(f.readframes(f.getnframes()))Python(pip install websocket-client):
import base64
import json
import os
from pathlib import Path
import websocket
API_KEY = os.environ["MODELGO_API_KEY"]
MODEL = "qwen3.8-omni-flash-realtime"
URL = "{{ ws_endpoint }}/api-ws/v1/realtime?model=" + MODEL
audio = Path("input.pcm").read_bytes() # 16 kHz / 16-bit LE / mono / headerless
if not audio or len(audio) % 2:
raise ValueError("provide non-empty 16-bit mono PCM audio")
ws = websocket.create_connection(
URL, header=["Authorization: Bearer " + API_KEY], timeout=30
)
def recv():
ev = json.loads(ws.recv())
if ev["type"] == "error":
raise RuntimeError(ev["error"])
return ev
try:
while recv()["type"] != "session.created":
pass
ws.send(json.dumps({
"type": "session.update",
"session": {
"modalities": ["text", "audio"],
"turn_detection": None,
"audio": {
"input": {"format": {"type": "pcm", "sample_rate": 16000}},
"output": {"format": {"type": "pcm", "sample_rate": 24000}},
},
},
}))
while recv()["type"] != "session.updated":
pass
for i in range(0, len(audio), 3200): # 100ms per frame
ws.send(json.dumps({
"type": "input_audio_buffer.append",
"audio": base64.b64encode(audio[i:i + 3200]).decode("ascii"),
}))
ws.send(json.dumps({"type": "input_audio_buffer.commit"}))
while recv()["type"] != "input_audio_buffer.committed":
pass
ws.send(json.dumps({"type": "response.create"}))
with open("reply.pcm", "wb") as out: # 24 kHz / 16-bit LE / mono
while True:
ev = recv()
if ev["type"] == "response.audio.delta":
out.write(base64.b64decode(ev["delta"]))
elif ev["type"] in ("response.audio_transcript.delta", "response.text.delta"):
print(ev["delta"], end="", flush=True)
elif ev["type"] == "response.done":
print("\nusage:", ev["response"].get("usage"))
break
finally:
ws.close()JavaScript / Node.js(npm install ws):
import fs from "node:fs";
import WebSocket from "ws";
const API_KEY = process.env.MODELGO_API_KEY;
const MODEL = "qwen3.8-omni-flash-realtime";
const url = "{{ ws_endpoint }}/api-ws/v1/realtime?model=" + MODEL;
const audio = fs.readFileSync("input.pcm"); // 16 kHz / 16-bit LE / mono
const out = fs.createWriteStream("reply.pcm"); // 24 kHz / 16-bit LE / mono
const ws = new WebSocket(url, { headers: { Authorization: `Bearer ${API_KEY}` } });
ws.on("message", (raw) => {
const ev = JSON.parse(raw.toString());
switch (ev.type) {
case "session.created":
ws.send(JSON.stringify({
type: "session.update",
session: {
modalities: ["text", "audio"],
turn_detection: null,
audio: {
input: { format: { type: "pcm", sample_rate: 16000 } },
output: { format: { type: "pcm", sample_rate: 24000 } },
},
},
}));
break;
case "session.updated":
for (let i = 0; i < audio.length; i += 3200) {
ws.send(JSON.stringify({
type: "input_audio_buffer.append",
audio: audio.subarray(i, i + 3200).toString("base64"),
}));
}
ws.send(JSON.stringify({ type: "input_audio_buffer.commit" }));
break;
case "input_audio_buffer.committed":
ws.send(JSON.stringify({ type: "response.create" }));
break;
case "response.audio.delta":
out.write(Buffer.from(ev.delta, "base64"));
break;
case "response.audio_transcript.delta":
case "response.text.delta":
process.stdout.write(ev.delta);
break;
case "response.done":
console.log("\nusage:", JSON.stringify(ev.response.usage));
ws.close();
out.end();
break;
case "error":
console.error("error:", ev.error);
ws.close();
break;
}
});
ws.on("error", console.error);把返回的 reply.pcm(24 kHz、16-bit、单声道)封装成可播放的 WAV:
import wave
from pathlib import Path
with wave.open("reply.wav", "wb") as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(24000)
f.writeframes(Path("reply.pcm").read_bytes())只想验证链路时,也可以不传音频:发送一条 conversation.item.create(content 为 input_text),再发 response.create,即可只拿文本回复。
会话配置
建连后服务端先发 session.created(含默认配置),客户端用 session.update 覆盖。常用字段:
| 字段 | 说明 |
|---|---|
modalities | ["text","audio"](默认,文本+音频)或 ["text"](仅文本) |
voice | 输出音色,默认 Tina;建议使用 audio.output.voice |
turn_detection | 服务端 VAD:{"type":"server_vad"} 或 {"type":"semantic_vad"};设为 null 由客户端手动 commit |
audio.input.format | 输入音频格式,如 {"type":"pcm","sample_rate":16000} |
audio.output.format | 输出音频格式,如 {"type":"pcm","sample_rate":24000} |
instructions | 系统人设 |
audio.input.transcription | 输入转写,服务端会自动开启(模型 qwen3-asr-flash-realtime) |
历史兼容字段 input_audio_format / output_audio_format 仍可用,但新接入建议使用 audio.input.format / audio.output.format。
打断
turn_detection 非 null 时由服务端 VAD 驱动轮次。用户再次开口会触发:
- 服务端下发
input_audio_buffer.speech_started; - 客户端停止播放已缓冲的回复音频,并发送
response.cancel; - 服务端随后下发
response.done(状态为 cancelled),开始新一轮。
手动模式(turn_detection: null)下,客户端负责在录音结束后发送 input_audio_buffer.commit 与 response.create。
计费
- 按 token 分模态计价:输入分文本/图片/视频与音频两档,输出分文本与音频两档。输出语音时,音频与其对应的文本分别计费。
- 连接时预授权:建连时按一个会话级预估冻结一笔余额(当前配置约 ¥1.2)。余额不足以覆盖预授权时,握手直接返回
402,不建立连接。 - 关闭连接后按实际用量结算:按本次连接内所有
response.done的usage累计结算,多退少补;整段会话没有产生任何用量时释放预授权、不扣费。
价格以控制台模型页为准;例如国内生产 qwen3.8-omni-flash-realtime 的单价(元/百万 token):输入 文本/图片/视频 1.5、输入音频 6、输出文本 4.5、输出音频 12。
限制
| 项 | 值 |
|---|---|
| 单连接最长时长 | 30 分钟(网关强制;到达上限即关闭连接并结算) |
| 全部输入 Token 上限 | 196,608 |
| 最大输出 Token | 65,536 |
| 音频历史 | 最多 100 轮 / 累计 600 秒 |
| 视频历史 | 最多 50 轮 / 累计 240 秒 |
| 上游限流 | 60 RPM |