报错是这么冒出来的。浏览器端直连 wss 地址,用项目 key 拼在查询参数里,握手阶段就返回 401,原文 invalid_api_key,同一个 key 走 HTTP 接口完全正常。

中间走过弯路,把「临时凭证有有效期,过期之后重连一样是 401」当成主因,改完现象没变。

这类报错背后常见的原因有几处,逐个排除就行。浏览器直连不能携带长期 key,必须用服务端签发的短期凭证。也可能是查询参数名要写 model,拼错时服务端读不到模型直接拒绝握手。

不容易想到的是,反向代理去掉了 Authorization 头,握手时拿不到凭证。

第一件事是在服务端签发一次临时凭证

curl -s https://api.openai.com/v1/realtime/sessions -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" -d '{"model":"gpt-4o-realtime-preview"}' | jq -r '.client_secret.value'

接着用临时凭证发起 WebSocket 握手

wscat -c "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview" -H "Authorization: Bearer $EPHEMERAL"

然后确认凭证的有效期字段

curl -s https://api.openai.com/v1/realtime/sessions -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" -d '{"model":"gpt-4o-realtime-preview"}' | jq '.client_secret.expires_at'

等前面都确认过,再确认代理层放行了升级请求头

curl -sI https://api.openai.com/v1/realtime -H "Connection: Upgrade" -H "Upgrade: websocket" | head -3

怎么算真的好了,看的是同一批现象。握手返回 101 并立刻收到 session.created 事件,浏览器控制台没有 401,凭证过期后重新签发一次即可恢复。

补一句。长期 key 一旦进了浏览器就等于公开,临时凭证的签发放在自己后端,前端只拿短期的那个值。拖得越久,排查成本越高,因为现场状态会被后续操作覆盖。

作者 9823189