Realtime API
AISIX AI 网关在 GET /v1/realtime 通过 WebSocket 转发 OpenAI Realtime API。网关会在接受连接前完成客户端认证、模型别名解析、访问控制和限流策略检查,然后在客户端与服务提供方之间双向转发事件。
本指南将演示如何通过网关连接 Realtime 客户端,并说明该端点的关键会话行为。
前提条件
开始前请准备:
- 一个可处理代理请求的 AISIX 网关。
- 一个可访问目标模型别名的调用方 API Key。
- 一个由支持 OpenAI Realtime 协议的服务提供方支持的模型别名。
导出服务器端示例使用的网关连接和请求值:
# AISIX_PROXY 使用 http 或 https,且不含尾部斜杠或端点路径。
# 本地快速入门使用 http://127.0.0.1:3000。
export AISIX_PROXY="YOUR_AISIX_GATEWAY_URL"
export AISIX_API_KEY="YOUR_CALLER_API_KEY"
export AISIX_MODEL="realtime-prod"
目前支持兼容 OpenAI 的服务提供方(适配器为 openai,包括自定义 api_base 部署)和 Azure OpenAI(适配器为 azure-openai,使用 /openai/realtime 和 api-key 认证)。Gemini Live 和 Bedrock 使用不同的实时事件协议,不通过该端点提供。
连接客户端
通过 model 查询参数选择模型。服务端客户端使用标准请求头认证:
import WebSocket from "ws";
const realtimeUrl = new URL("/v1/realtime", process.env.AISIX_PROXY);
realtimeUrl.protocol = realtimeUrl.protocol === "https:" ? "wss:" : "ws:";
realtimeUrl.searchParams.set("model", process.env.AISIX_MODEL);
const ws = new WebSocket(realtimeUrl, {
headers: { Authorization: `Bearer ${process.env.AISIX_API_KEY}` },
});
浏览器客户端无法设置 WebSocket 请求头。可以像 OpenAI 浏览器示例一样,把调用方 API Key 作为子协议项传入。网关会回显 realtime 子协议:
// 使用不含端点路径的绝对 HTTP(S) 网关 URL,例如 https://gateway.example.com
const aisixProxy = "YOUR_AISIX_GATEWAY_URL";
const aisixApiKey = "YOUR_CALLER_API_KEY";
const aisixModel = "realtime-prod";
const realtimeUrl = new URL("/v1/realtime", aisixProxy);
realtimeUrl.protocol = realtimeUrl.protocol === "https:" ? "wss:" : "ws:";
realtimeUrl.searchParams.set("model", aisixModel);
const ws = new WebSocket(realtimeUrl, [
"realtime",
`openai-insecure-api-key.${aisixApiKey}`,
]);
连接建立后,可以像直接连接服务提供方一样收发 Realtime 事件。AISIX 转发 session.update、音频缓冲区、response.create 和服务端事件等事件时不会改变其结构。
AISIX 唯一会改写的值是会话对象中的模型名称。session.created 和 session.updated 事件中的模型名称是客户端连接时使用的模型别名,而不是服务提供方自身的模型 ID。客户端把该别名通过 session.update 发回时,会在转发到上游前被转换回服务提供方的模型 ID,因此客户端可以直接回传收到的会话对象。
认证与策略
认证、模型访问检查、客户端 IP 限制和限流会在 WebSocket 升级完成前执行。预算检查也在此时执行。任何一项检查失败都会在 HTTP 握手阶段被拒绝(401、403 或 429),客户端会看到连接建立失败。
会话期间,已配置的安全护栏会扫描双向文本事件。被阻断的事件会产生 OpenAI 风格的 error 事件,随后连接关闭。
用量跟踪
网关会从服务提供方的 response.done 事件中提取用量信息;转录会话还会从转录完成事件中提取用量。每个会话会记录一个聚合用量事件,包括缓存 Token 数。会话总 Token 会计入基于 Token 的限流。
会话限制
AISIX 会对两个方向的事件间隔应用空闲上限。系统依次从直接模型的 stream_timeout、该模型的 timeout,以及部署级 upstream.stream_timeout_ms 或 upstream.timeout_ms 默认值解析此上限。部署默认上限为 6000 秒。静默时间超过解析后截止时间的会话将以代码 1001 和原因 idle timeout 关闭。
要让某个模型不受部署级兜底值影响,请设置 timeout: 0,并确保该模型未设置非零 stream_timeout。部署运维人员也可以将两个上游超时默认值均设置为 0。有关完整优先级规则,请参阅超时之间的关系。上游连接失败会以代码 1011 关闭会话,参见上游连接失败。打开该上游连接本身也有时间预算:upstream.connect_timeout_ms(默认 5 秒)限制整个拨号过程,其中包括 WebSocket 升级握手,因此始终不回应的上游会在此处失败,而不会让会话一直挂着。
上游连接失败
AISIX 先接受客户端的 WebSocket 升级,再拨号上游,因此拨号失败时,客户端会收到一个类型为 upstream_error 的 error 帧,随后是代码 1011、原因 upstream connect failed 的关闭。其余内容取决于拨号失败的方式:
| 拨号失败的方式 | 帧中的 error.message | 记录的状态 | error_class | 冷却设置 |
|---|---|---|---|---|
上游以 4xx 拒绝升级。 | 上游的文本。上游发送了 code 和 param 时,帧中也会带上。 | 该状态 | upstream_status | trigger_statuses |
上游以 5xx 拒绝升级。 | 上游的文本。 | 502 | upstream_status | trigger_statuses |
上游以其他状态应答,例如不是升级的 200。 | 上游的文本。 | 502 | upstream_status | trigger_statuses |
| 没有收到 HTTP 响应,例如 DNS、连接或 TLS 失败。 | failed to connect to the upstream realtime endpoint | 502 | transport | trigger_on_transport |
拨号超过 upstream.connect_timeout_ms,或内核报告连接超时。 | failed to connect to the upstream realtime endpoint | 504 | timeout | trigger_on_timeout |
记录的状态是用量事件、访问日志,以及请求指标 status 和 outcome 标签上的状态。记录的文本仍然写明上游实际返回的内容,例如 upstream returned HTTP 500: <text>;trigger_statuses 和 honor_retry_after 读取的是上游自己的状态和 Retry-After,因此在默认列表下,以 400 拒绝不会让模型进入冷却。
AISIX 按 HTTP 路由的方式读取上游文本,上限为 1 KiB。没有文本可引用时,帧中改用状态,例如 upstream returned 500 Internal Server Error 或 upstream returned 200 OK,且不带 code 或 param。这包括正文为空或经过压缩,以及正文停滞或被截断的情况。AISIX 在模型的流式预算内读取正文,因此停滞的正文最多会让这个帧在拨号之后再晚一个预算发出。没有预算的模型(timeout: 0 且未设置 stream_timeout)会一直等待,直到上游不再停滞或客户端离开。
azure-openai 服务提供方密钥的拒绝响应按 OpenAI 错误读取,因此 4xx 携带的是上游自己的 code,不做转换:是 DeploymentNotFound,而 chat completions 上回答的是 model_not_found。
1.8.0 及更早版本的网关在所有情况下都发送固定文本,并把每次失败的拨号记录为 502、error_class 为 transport,由 trigger_on_transport 决定冷却。
升级之后的上游拒绝
上游可能先完成 101 升级,再拒绝会话。OpenAI 对错误的 API 密钥就是这样应答的:发送一个 error 事件,然后以代码 3000 关闭。
当上游一侧结束时只发送过 error 事件、没有其他帧,客户端只会收到上游的帧及其关闭代码和原因,不会收到网关的帧。没有 Close 帧就断开的连接仍以 1011 关闭客户端,同样没有网关的帧。
AISIX 把这样的会话记录为失败,error_class 为 upstream_in_band,文本为 upstream reported an in-band error: <message>。该事件不带 HTTP 状态,因此记录的状态 从事件中读取:
| error 事件 | 记录的状态 |
|---|---|
code 为 invalid_api_key | 401 |
code 为 invalid_model 或 model_not_found | 404 |
code 为 rate_limit_exceeded 或 insufficient_quota | 429 |
type 为 invalid_request_error,code 为其他值 | 400 |
code 是 4xx 范围内的数字 | 该值 |
| 其他情况 | 502 |
冷却按同一状态走 trigger_statuses。映射不到状态的事件走 trigger_on_transport。
会话从第一个不是 error 事件的帧起变为可用。在该帧之后的 error 事件不算拒绝:OpenAI 会对错误的客户端事件发送这种事件,会话保持打开。1.8.0 及更早版本的网关把被拒绝的会话记录为 200,并在上游的帧之后多给客户端发送一个 server_error 帧。
连接关闭
AISIX 会在两个方向上完成 WebSocket 关闭握手:
- 客户端关闭会话时,客户端会收到自己发送的关闭代码和原因,上游也会收到同样的代码和原因。
- 上游关闭会话时,客户端会收到上游的关闭代码和原因。原因按上游发送的原样转发,截断为 123 字节。
- 无法在 Close 帧中发送的关闭代码,例如缺少状态码或
1006,会以1011送达另一方。恰好为1002、原因为Protocol violation的 Close 帧也会变为1011,因为 AISIX 无法把它与 WebSocket 库替换无效代码后生成的帧区分开。原因不同的1002会照常转发。 - 除正常关闭(
1000)外,任何结束之前都会先发送一个类型为server_error的 Realtimeerror事件,说明发生了什么:上游的关闭代码和原因、AISIX 无法转发的 Close 帧、不带状态码的关闭、会话期间失败的连接,或者没有 Close 帧就结束的连接。例外是上游在升级之后拒绝的会话,它自己的error事件已经告知了客户端。拨号失败时发送的则是上游连接失败中说明的upstream_error帧。
关闭过程中到达的用量仍会计入。会话在访问日志中的状态不取决于关闭代码。
网关关闭时,会像对待流式响应一样,等待打开的 Realtime 会话关闭后再退出,以便记录其用量。请按预期最长的会话时长设置网关的终止宽限期。
通过代理访问上游
对于 wss:// 服务提供方,AISIX 通过 HTTPS_PROXY 打开上游连接;对于 ws:// 服务提供方,则通过 HTTP_PROXY,两者都回退到 ALL_PROXY,除非 NO_PROXY 覆盖该主机。代理必须是支持 CONNECT 的 HTTP 代理。不支持 SOCKS 代理:会话会以一个 error 事件和关闭代码 1011 失败,访问日志记录 502。1.6.x 及更早版本的网关始终直接连接上游。
下一步
你已经通过网关连接了 Realtime 客户端。接下来可阅读语音与音频,了解非 Realtime 音频端点。