跳转到主要内容
🎉 限时免费 — MiniMax: MiniMax M3 Free 现在体验 🔥

实时语音(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
Querymodel=<逻辑模型名>(必填)
鉴权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:

python
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):

python
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):

js
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:

python
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 驱动轮次。用户再次开口会触发:

  1. 服务端下发 input_audio_buffer.speech_started;
  2. 客户端停止播放已缓冲的回复音频,并发送 response.cancel;
  3. 服务端随后下发 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
最大输出 Token65,536
音频历史最多 100 轮 / 累计 600 秒
视频历史最多 50 轮 / 累计 240 秒
上游限流60 RPM

下一步