ComfyPilotComfyPilot
文档 / 开发 / Comfy Router 实时语音会话

Comfy Router 实时语音会话

通过 Comfy Router 打开 Grok Voice 实时 WebSocket 会话:端点、认证、按秒计费、会话限制和关闭代码。

开发 realtime 更新于 2026-11-12

查看官方原文

Comfy Router 实时语音会话

通过 Comfy Router 打开 Grok Voice 实时 WebSocket 会话:端点、认证、按秒计费、会话限制和关闭代码。

Comfy Router 中继与 xAI Grok Voice 的实时语音到语音会话,Grok Voice 由 fal 提供。你使用 Comfy API 密钥向 Router 打开一个 WebSocket。Router 会检查你的账户和积分,在上游打开会话,然后双向中继事件。会话按秒从你的 Comfy 积分中扣费。

端点

wss://api.comfy.org/v2/realtime/fal/xai/grok-voice/realtime

该请求是一次 WebSocket 升级(GET,并带有 Connection: Upgrade 和 Upgrade: websocket)。其他任何请求都会以 426 拒绝。

认证

在升级请求的请求头中发送你的 Comfy API 密钥,可以使用 X-API-Key: comfyui-... 或 Authorization: Bearer comfyui-...。Router 不会从查询字符串或 WebSocket 子协议中读取密钥。

[!WARNING] 浏览器无法为 WebSocket 设置请求头,并且 Comfy API 密钥绝不能传到浏览器。请从你的服务器打开会话,并自行将音频中继到浏览器。

连接

升级成功后,即可发送和接收 xAI 为 Grok Voice 所记录的实时事件。下面这个示例会配置会话、发送一轮文本对话,并打印接收到的每个事件类型。

# pip install websockets msgpack
import asyncio
import os

import msgpack
import websockets

URL = "wss://api.comfy.org/v2/realtime/fal/xai/grok-voice/realtime"


async def send(ws, event):
    await ws.send(msgpack.packb(event))


async def main():
    async with websockets.connect(
        URL,
        additional_headers={"X-API-Key": os.environ["COMFY_API_KEY"]},
    ) as ws:
        await send(ws, {
            "type": "session.update",
            "session": {
                "voice": "eve",
                "instructions": "You are a helpful assistant.",
                "turn_detection": {"type": "server_vad"},
            },
        })
        await send(ws, {
            "type": "conversation.item.create",
            "item": {
                "type": "message",
                "role": "user",
                "content": [{"type": "input_text", "text": "Hello!"}],
            },
        })
        await send(ws, {"type": "response.create"})

        async for message in ws:
            if isinstance(message, str):
                continue  # fal 自己的 x-fal-message 帧
            event = msgpack.unpackb(message)
            print(event["type"])
            if event["type"] == "response.done":
                break


asyncio.run(main())
// npm install ws @msgpack/msgpack
import WebSocket from "ws";
import { encode, decode } from "@msgpack/msgpack";

const ws = new WebSocket(
  "wss://api.comfy.org/v2/realtime/fal/xai/grok-voice/realtime",
  { headers: { "X-API-Key": process.env.COMFY_API_KEY } },
);

ws.on("unexpected-response", (req, res) => {
  // 升级请求被拒绝。JSON 响应体会说明原因。
  let body = "";
  res.on("data", (chunk) => (body += chunk));
  res.on("end", () => console.error(res.statusCode, body));
});

const send = (event) => ws.send(encode(event));

ws.on("open", () => {
  send({
    type: "session.update",
    session: {
      voice: "eve",
      instructions: "You are a helpful assistant.",
      turn_detection: { type: "server_vad" },
    },
  });
  send({
    type: "conversation.item.create",
    item: {
      type: "message",
      role: "user",
      content: [{ type: "input_text", text: "Hello!" }],
    },
  });
  send({ type: "response.create" });
});

ws.on("message", (data, isBinary) => {
  if (!isBinary) return; // fal 自己的 x-fal-message 帧
  const event = decode(data);
  console.log(event.type);
  if (event.type === "response.done") ws.close();
});

ws.on("close", (code) => console.log("closed", code));

完成后请立即关闭套接字。计费会持续到会话关闭为止,而不是到最后一个响应结束。

事件

Router 不定义自己的事件协议。服务器事件会完全按照 fal 发送它们的方式送达你手上,而 Router 接受的客户端事件也会原样转发。有关事件列表、会话字段、音色和音频格式,请参阅:

像示例中那样,把每个客户端事件作为一条 msgpack 二进制消息发送。这是 fal 的 realtime 客户端所使用的分帧方式,模型的事件也会以同样的方式返回,即从 session.created 开始的 msgpack 二进制消息。fal 还会发送自己的、类型为 x-fal-message 的 JSON 文本消息(计时信息),你可以跳过它们。Router 的检查也接受把客户端事件作为一条 JSON 文本消息发送,但除非你已经用你的客户端测试过 JSON,否则请使用 msgpack。

Router 在转发每个客户端事件之前都会对其进行检查:

  • 事件类型: session.update、input_audio_buffer.append、input_audio_buffer.commit、input_audio_buffer.clear、conversation.item.create、conversation.item.delete、conversation.item.truncate、response.create、response.cancel,以及 fal 的 x-fal-session.configure。
  • 模型: session.update 中的 model 必须是 grok-voice-latest、grok-voice-3.0 或 grok-voice-2.0。
  • 工具: 允许使用由你的客户端执行的函数工具。服务器端工具(网络搜索、X 搜索、文件搜索和 MCP)会被拒绝。
  • 媒体: 图像和视频输入会被拒绝。会话仅支持语音和文本。
  • 大小: 单条客户端消息最大为 1 MiB。

被拒绝的事件不会被转发,Router 会以关闭码 1008 关闭会话。

定价

一次会话的费用为\\每秒 $0.0011869**(约每分钟 $0.0712,或每秒 0.2504 积分、约每分钟 15.0 积分,按 211 积分 = 1 美元计算)。会话开启期间以 15 秒为一个增量计费,最后不足一个增量的部分按秒计费并向上取整。计费从升级开始,一直到 socket 关闭为止,无论期间是否有音频传输。

限制

限制值
每个账户的会话数同一时间只能有一个打开的会话。打开第二个会话会返回 429,并带有 router.realtime.session_already_live。
会话时长最多 30 分钟,且绝不会超过连接时你的积分余额所能覆盖的时长。
会话期间的积分Router 每 15 秒重新检查你的余额,当余额无法再覆盖下一秒时结束会话。
连接速率每次升级尝试都会计入你的每个调用方的请求速率,并与其他 Router 调用共享。

当会话达到时间限制或积分已用完时,连接可能会在没有关闭帧的情况下关闭(通常表现为关闭代码 1006)。请仅在充值之后重新连接;如果需要更多时间,请启动新会话。

错误

升级之前

如果 Router 拒绝连接,它不会切换协议,而是返回 HTTP 状态码和 JSON 响应体:

{
  "detail": "Insufficient credits to open a realtime session.",
  "error_type": "insufficient_credits",
  "code": "router.realtime.insufficient_credits"
}

根据 code 进行判断;detail 供人阅读。

状态码code原因处理方式
426router.realtime.upgrade_required请求不是 WebSocket 升级请求。使用 WebSocket 客户端连接。
401router.realtime.unauthorizedAPI 密钥缺失或无效。在 X-API-Key 或 Authorization: Bearer 中发送有效密钥。
402router.realtime.insufficient_credits你的余额不足以支付至少一秒的会话费用。添加积分,然后重新连接。
402router.realtime.spend_denied该会话将要从其扣费的钱包没有足够资金。响应体中的 denied_reason 和 billed_workspace 会指明是哪个钱包。向该工作区添加积分,然后重新连接。
403router.realtime.provider_disabled你的工作区的合作伙伴提供商策略不允许使用此提供商。请工作区管理员允许使用。
404router.realtime.model_not_allowedURL 中的模型不可用于实时会话。检查 URL。
429router.realtime.rate_limited你超出了请求速率限制。等待 Retry-After,然后重试。
429router.realtime.session_already_live你的账户下已有另一个会话处于打开状态。先关闭它。
429router.realtime.concurrency_limit并发合作伙伴模型调用过多。减少并发任务后重试。
502router.realtime.upstream_refused、router.realtime.token_mint_failed上游提供商未能打开会话。使用退避策略重试。
503其他任何 router.realtime.* 代码实时会话暂时不可用。使用退避策略重试。

升级之后

关闭代码含义处理方式
1008客户端事件被拒绝(参见事件)。修正该事件。会话已结束。
1012Router 正在重启。重新连接。会启动新会话,并从新的升级开始计费。
1006(没有关闭帧)当会话达到时间限制或积分用完时,客户端通常会上报此代码。网络中断看起来也一样。添加积分或启动新会话。

任何其他关闭代码都来自 fal 或 xAI。请参阅 事件 下链接的文档。

下一步

  • 计费:Router 如何收费以及如何核对用量。
  • 错误与重试:其他 Router 调用返回的错误分类。
  • 限制:请求速率及其他限制。