--enable_llm_proxy:不认证、不限流,官方自己说的
speech-to-speech 这个仓库里,有一个开关的官方说明写得比大多数开源项目都坦白。--enable_llm_proxy 打开之后,实时服务会把它自己配置的那个远端 LLM 也当成一个普通的 OpenAI 兼容端点暴露出来,好让客户端跑一些侧边任务——生成摘要、给这段对话起个标题、喂给某个后台 agent——带工具、带流式,与语音对话完全并发,且不会被新的说话打断。
听上去很方便。紧接着 README 就补了一句,原文的意思一点没绕:服务端自己不做任何认证,也不做任何限流。只在受信网络上启用该代理,或者把服务部署在一个自己掌管访问控制的网关后面。
这句话不是免责声明式的客套,它直接决定了这个开关该怎么用、什么时候不该用。下面按「命令怎么写 → 产出物长什么样 → 怎么验收 → 什么情况不适用」四段走一遍。全文事实来自仓库 README 与 src/speech_to_speech/arguments_classes/ 下的参数定义,我们没有安装、部署或调用过这个服务。
一、先看开启条件:它默认是关的,而且挑后端
两条硬前提,写命令之前必须先确认:
- 代理默认关闭。
--enable_llm_proxy类型bool,默认False。不显式打开就没有这两个路径。 - 它要求远端后端。 也就是
--llm_backend必须是chat-completions或responses-api之一,否则返回 501 并附原因。
第 2 条是最容易忽略的一条。如果你在 Apple Silicon 上用 --llm_backend mlx-lm、在 CUDA / CPU 上用 --llm_backend transformers,模型是在进程内跑的,压根没有一个「上游 OpenAI 兼容服务」可以转发过去,代理这条路自然走不通。这不是配置写错的报错,是设计上的边界——想用代理,LLM 那一环就得是 API 形态。
路径与后端是一一对应的,别记混:
| 你跑的后端 | 代理暴露出来的路径 |
|---|---|
--llm_backend chat-completions | POST /v1/chat/completions |
--llm_backend responses-api | POST /v1/responses |
也就是说,同一时刻只会有一条路径可用,取决于你用哪个后端起的服务。
二、完整命令:先给最保守的那一版
先说一个容易被忽略的默认值:serve 的 --host 默认是 127.0.0.1,--port 默认是 8765。参数的 help 原文写得很有意思——要暴露到网络必须显式传 0.0.0.0,帮助文本里那句话直接用了 “expose the unauthenticated API on the network”(把未认证的 API 暴露到网络上)。项目自己在参数说明里就把 unauthenticated 这个词写进去了。
所以第一版命令,全程留在环回接口上:
export OPENAI_API_KEY=<YOUR_API_KEY>
speech-to-speech serve \
--llm_backend responses-api \
--model_name gpt-5.4-mini \
--enable_llm_proxy \
--llm_proxy_connect_timeout_s 10.0 \
--num_pipelines 1
逐个说为什么在这:
--llm_backend responses-api:这是默认值,也是代理能开起来的两个合法后端之一。选了它,代理路径就是POST /v1/responses。--model_name gpt-5.4-mini:responses_api_language_model_arguments.py里的默认值。这个值同时是代理会强制覆写成的那个模型名,下一节细说。--enable_llm_proxy:开关本体,默认False。--llm_proxy_connect_timeout_s 10.0:默认就是10.0。参数 help 原文说明得很清楚,这是连接上游服务商的超时秒数,读取没有超时(因为生成可能耗时几分钟)。写出来是为了提醒自己:这个数字管的是「连不上」,不管「生成慢」。--num_pipelines 1:模块级参数,默认就是1。它的 help 说的是流水线池大小——一个 uvicorn 服务监听--port,把每个进来的客户端路由到下一个空闲流水线,每个流水线有自己的 VAD / STT / LM / TTS handler 与对话状态;最大并发 WebSocket 会话数等于num_pipelines,再多的连接会被拒绝。默认只有 1,这一点很多人第一次看到会愣一下。
没有显式写 --host,就是走默认的 127.0.0.1。这一版命令的定位是:本机自己调,别人连不上。
如果你把 LLM 也放到本地,比如 README「Fully Local」那套用独立 llama.cpp 进程的做法,代理同样可以开:
speech-to-speech serve \
--stt parakeet-tdt \
--llm_backend responses-api \
--tts qwen3 \
--model_name "ggml-org/gemma-4-E4B-it-GGUF" \
--responses_api_base_url "http://127.0.0.1:8080/v1" \
--responses_api_api_key "" \
--responses_api_stream \
--enable_llm_proxy
这里 8080 是 llama.cpp 那一侧的端口(docker compose 里也是这么分的),实时服务自己仍然在 8765。这两个端口别写反,它们是两个进程。
以上为按官方参数语义组合的示例,未逐项实测,以官方文档与 --help 的实际输出为准;speech-to-speech serve -h 会按当前选中的后端组合给出默认值与专属参数。
三、开起来之后,客户端那一侧长什么样
README 给了一段可以整段照抄的调用示例:
from openai import OpenAI
llm = OpenAI(base_url="http://localhost:8765/v1", api_key="unused")
completion = llm.chat.completions.create(
model="anything", # 会被忽略:服务端强制使用它配置的 --model_name
messages=[{"role": "user", "content": "Summarize the conversation so far: ..."}],
)
这段代码里有两处「看着像随便写的、其实是在演示行为」的地方,正好对应 README 列出的行为细节:
api_key="unused":这个服务忽略客户端传的 API key。README 的说法是,前面那个网关才决定这个 key 必须是什么——服务端自己不看。代理转发到上游时用的是服务端持有的 key,而这个 key 永不到达客户端。这个设计本身是合理的(不让前端拿到上游密钥),但它的另一面就是:谁能连上这个端口,谁就能免费用你的上游额度。model="anything":model字段总是被覆写成服务端配置的--model_name。客户端选不了模型,这既是一层约束,也意味着你不能指望用同一个端点跑「便宜模型做摘要、贵模型做对话」的分流。
另外两条行为特征:请求是无状态的,每次要发完整的消息列表;代理与语音对话完全并发,不会被新的说话打断——后面这一点和语音链路里的打断机制(cancel_scope 那一套世代计数)是分开的两条路,侧边任务不受 barge-in 影响。
产出物这一侧我们能确定的就是这些:多出一个 HTTP 路径、model 被覆写、key 被忽略、无状态、后端不满足条件时返回 501 并附原因。具体的响应体字段、日志行长什么样,我们没有调用过,不编。
四、怎么验收:四处逐条确认
第一处,确认路径确实存在,而且是你以为的那一条。 用哪个 --llm_backend 起的服务,就只有对应的那一条路径。如果你按 responses-api 起服务却去打 /v1/chat/completions,那不是「代理没开」,是走错门了。反过来,如果你用的是进程内后端(transformers / mlx-lm),无论怎么打都是 501 并附原因——看到 501,先回头看 --llm_backend,而不是怀疑开关没生效。
第二处,确认 model 覆写是你能接受的。 客户端随便传一个 model 值也不会报错,因为它被覆写了。这意味着「调用成功」不等于「用的是你想用的模型」——真正生效的永远是服务端 --model_name 的值。要改就得重启服务改参数,客户端侧改不动。
第三处,也是最该停下来想一下的:确认监听面。 检查启动命令里有没有 --host 0.0.0.0。如果有,那你暴露出去的就是一个不认证、不限流的 LLM 端点。这不是危言耸听,是参数 help 与 README 两处都写明的口径。顺带一提,local 命令的行为不一样:它永远绑定环回,并把自带客户端连到 ws://127.0.0.1:<port>/v1/realtime,所以用 local 起的服务不存在「不小心暴露到公网」这个问题;会出事的是 serve 加显式 0.0.0.0。
第四处,确认前面到底有没有东西挡着。 README 给的正例是 s2s-endpoint 的计算副本充当这样的网关:它只对「用 HF token 创建了会话的客户端」开放这些路径,按该 token 校验 API key,并按用户限流。请注意这三件事——路径级放行、按 token 校验、按用户限流——全部发生在 speech-to-speech 之外。如果你的部署里没有一个东西在做这三件事,那这三件事就是没做。
至于具体怎么做:反向代理加鉴权、防火墙只放行内网网段、放到网关或 API 管理层后面,这些都是通用运维做法,不是该项目官方文档的内容,本文不展开,也不构成安全方案建议。这里只强调一件事:speech-to-speech 自己不提供这一层,别指望开关里藏着什么隐式保护。
五、什么情况别开它
- LLM 跑在进程内(
--llm_backend transformers或mlx-lm):开了也是 501,别折腾。 - 要按用户计费、按用户限流、区分租户:服务端明确不做限流,也不看客户端的 key,这些需求它一条都满足不了,必须在外面做。
- 要让客户端自己挑模型:
model字段被覆写,做不到。 - 需要服务端记住上下文的侧边任务:代理请求是无状态的,每次得把完整消息列表发过来。想让它「接着刚才那段对话继续」,得由客户端自己把上下文拼好——语音对话的会话状态在流水线那一侧,不会自动流进代理请求。
- 服务要暴露到不完全受信的网络:那就先解决访问控制,再考虑要不要开这个开关。顺序反了,等于把上游 API key 的使用权拱手让给任何能连上这个端口的人。
最后
这个开关的价值不在于「省了一次配置」,而在于:语音对话本来就已经在服务端配好了一个上游 LLM 连接与一个密钥,代理让你复用这条现成的连接去做别的事,而不必在客户端再存一份密钥。理解成「一个复用上游连接的转发器」比理解成「一个 LLM 网关」更准确——它没有网关该有的认证与限流,官方自己也是这么说的。
延伸阅读
本文依据 speech-to-speech 官方仓库(github.com/huggingface/speech-to-speech)的 README、
src/speech_to_speech/arguments_classes/ 下的参数定义与 Realtime Engine 架构文档整理,核对日 2026-08-09。
本文内容为仓库源码与文档口径,我们没有安装、部署或调用过该服务,文中毫秒值均为参数默认值而非实测延迟。
参数与默认值随版本变动,请以 speech-to-speech serve -h 的实际输出为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。