Stream ended without finish_reason 怎么办?pi 接第三方端点时先分清是断流还是端点不发
用 pi 接一个 OpenAI 兼容的端点(本地的 Ollama、vLLM,或者某个中转站),跑着跑着蹦出来这么一句:
Stream ended without finish_reason
如果你用的是 Gemini 这类 Google 模型,文案会是另一个样子:
Google stream ended without a finish reason
这两句的意思一样:流已经结束了,但 pi 没等到模型说「我说完了」的那个信号。
先把结论放前面:这句报错背后有两种完全不同的情况,处理方向正好相反。一种是流被半路掐断,该查网络;另一种是端点本来就不发这个字段,该改配置。分错了,要么白折腾网络,要么把真正的截断藏起来。
一、finish_reason 是什么,pi 为什么非要它
OpenAI 兼容接口的流式输出,是一小块一小块往回推的。模型说完以后,最后一块里会带一个 finish_reason 字段,常见值是 stop(正常说完)、length(到了长度上限)、tool_calls(要调工具)。
对一个编程 Agent 来说,这个字段决定下一步做什么:是这一轮结束了,还是该去执行工具,还是被截断了要续写。所以 pi 默认把它当成必须收到的信号。
pi 源码(packages/ai/src/api/openai-completions.ts)里,流读完之后有这么一个判断,意思翻成中文是:
- 如果这个端点被认为「会发 finish_reason」,但整条流里一次都没收到,就抛
Stream ended without finish_reason; - 如果停止原因到最后还处在「待定」状态,也抛这句。
Google 的实现(google-generative-ai.ts)逻辑类似:流结束时停止原因还是「待定」,就抛 Google stream ended without a finish reason。
关键在第一条里那个「被认为会发」。它对应 pi 的一个兼容开关 supportsFinishReason,默认是 true。
二、两种情况,怎么一眼分开
情况一:流被半路掐断了。 模型本来会发 finish_reason,但连接在最后一块到达之前断了。这时回答通常是写到一半的,报错也是时有时无的:同一个端点,有时成功有时失败。
情况二:端点本来就不发。 有些本地推理服务和中转站,流式输出里干脆没有这个字段,或者只在非流式返回里有。这时的特征非常稳定:同一个端点,每次都报,而且你看到的回答往往是完整的。
所以判断的第一步不用看任何日志,只看一个问题:是偶尔报,还是每次都报?
如果不确定,可以绕过 pi 直接问端点。下面这条命令把流式原样打出来,看最后几行里有没有 finish_reason 且不为 null(地址、模型名和 key 换成你自己的):
curl -N https://你的端点/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"你的模型","stream":true,"messages":[{"role":"user","content":"说一个字"}]}'
连续跑几次,最后一块都带着 "finish_reason":"stop",就是情况一;每次都只有 null 或者根本没这个字段,就是情况二。
三、情况二的解法:在 models.json 里关掉这个开关
pi 从 coding-agent 0.84.0(2026-08-06)起支持一个兼容项:compat.supportsFinishReason。把它设成 false,pi 就不再等这个信号,而是在流结束时自己推断——内容里有工具调用就当 toolUse,没有就当 stop。
配置写在 ~/.pi/agent/models.json(目录可以用 PI_CODING_AGENT_DIR 改)。既可以放在整个提供方上,也可以只放在某个模型上:
{
"providers": {
"my-endpoint": {
"baseUrl": "http://localhost:8000/v1",
"api": "openai-completions",
"apiKey": "$MY_KEY",
"compat": { "supportsFinishReason": false },
"models": [{ "id": "my-model" }]
}
}
}
改完在 pi 里打开一次 /model 就会重新加载这个文件。
有一个小坑:0.84.0 刚加这个开关时,models.json 的类型定义漏了它,到 0.84.3(2026-08-24)才补上。如果你用的是这两个版本之间的 pi,建议先升级再配。
四、为什么情况一千万别用这个开关「修」
这是整篇最想说的一点。
看上一节 pi 的推断规则:关掉开关以后,流一结束,pi 就当它正常说完了。 它没有办法再区分「模型真的说完了」和「连接在中间断了」。
所以如果你的问题其实是情况一(网络不稳、代理掐连接),关掉开关以后报错确实消失了,但代价是:被截断的半截回答会被当成完整回答接着用。对写代码的 Agent 来说,这比报错糟糕得多——半截的文件改动、写了一半的命令,都会被当成完成了的结果往下走。
pi 自己的文档也专门提醒过这类兼容项:它们应该描述端点经过验证的行为差异,不要因为一个端点自称「兼容 OpenAI」就去开关它们。第二节那条 curl,就是为了让你先验证再改。
情况一该做的是:
- 换一个网络环境复现一次(比如关掉代理或换热点),看报错频率有没有变化;
- 如果走的是中转站,看它有没有流式超时或连接数限制;
- 本地推理服务的话,看服务端日志里这一次请求是不是异常结束了(显存不足、进程被杀都会让流中途停下)。
五、默认设置下,你看到它时重试已经用完了
有一点容易被忽略:pi 自己默认就会重试这类错误。 pi 的重试规则里,报错文本包含 ended without 的都算「可以重试的临时故障」;coding-agent 的自动重试默认开启、默认最多 3 次(设置项 retry.enabled 和 retry.maxRetries)。
所以在 pi 里最终看到这句报错,一般意味着自动重试已经用完了还是没成。这一点对判断很有帮助:偶发的断流通常在重试里就被消化掉了,能冒到你面前的,要么断得很频繁,要么就是第二节说的情况二(每次都不发,重试多少次都一样)。
deepseek-harness(dsh)里也是类似的逻辑。dsh 有一条通过 pi 的 AI 层(pi-ai 适配器)调模型的路径,据它仓库里 2026-07-22 的一篇设计笔记,这个适配器会把 Stream ended without finish_reason 以及同类的「流在终止事件之前就结束了」的报错归为传输层错误,而传输层错误在 dsh 里默认会被重试。dsh 的重试次数和超时怎么配,见下面的相关阅读。
六、Google 模型那句怎么处理
Google stream ended without a finish reason 走的是 pi 的 Google 实现,没有上面那个兼容开关可以关。它只在流结束时停止原因仍是「待定」才抛,所以基本可以直接按情况一处理:查网络、查代理、换个时间再试。
附:pi 里的同族报错对照
pi 给每一家模型接口各写了一套流式解析,所以「流结束了却没等到终止信号」这件事,在不同接口下是不同的文案。下面是 pi 源码(2026-09-28)里这一族的主要写法:
| 你看到的报错 | 走的是哪种接口 | 有没有兼容开关可关 |
|---|---|---|
Stream ended without finish_reason | OpenAI 兼容的 Chat Completions | 有,compat.supportsFinishReason |
OpenAI Responses stream ended without a stop reason | OpenAI Responses | 无 |
OpenAI Responses stream ended before a terminal response event | OpenAI Responses | 无 |
Azure OpenAI Responses stream ended without a stop reason | Azure OpenAI | 无 |
Codex stream ended without a stop reason | OpenAI Codex Responses(源码文件 openai-codex-responses.ts) | 无 |
Anthropic stream ended before message_stop | Anthropic Messages | 无 |
Anthropic stream ended without a stop reason | Anthropic Messages | 无 |
Google stream ended without a finish reason | Google Generative AI | 无 |
Google Vertex stream ended without a finish reason | Google Vertex | 无 |
Mistral stream ended without a finish reason | Mistral | 无 |
Bedrock stream ended without a stop reason | AWS Bedrock | 无 |
<提供方名> stream ended without a terminal event | pi 自己的消息接口(pi-messages.ts),开头是提供方名 | 无 |
这张表能帮你做两件事。第一,从报错反推你实际走的是哪条接口:比如你以为自己配的是 OpenAI 兼容端点,报出来的却是 Responses 那一句,说明 models.json 里的 api 字段不是你以为的那个。第二,判断有没有配置可以改:只有第一行那种有兼容开关,其余的都只能按「流被掐断」去查网络和上游。
七、一句话总结
- 偶尔报:流被掐了,查网络,别动开关;
- 每次都报、回答又是完整的:端点不发 finish_reason,先用 curl 验证,再在
models.json里对这个端点设compat.supportsFinishReason: false; - Google 那句:没有开关,按断流处理。
本文依据 pi 仓库截至 2026-09-28 的源码与文档(当时最新版本 v0.87.1),以及 deepseek-harness 仓库的设计笔记。
相关阅读
留言讨论
评论发布后会被人工复核,违规内容将被删除。
如果发表没有反应,可以前往联系我们告诉我们。