fix: 非流式请求(省略 stream 字段)被误判为流式,返回畸形 SSE 响应
提交到 https://github.com/techysy/10router/issues/new
标题栏填:fix: 非流式请求(省略 stream 字段)被误判为流式,返回畸形 SSE 响应
正文从下方「### 环境」开始整段粘贴。
环境
| 项 |
值 |
| 10router 版本 |
1.0.5(npm),同样复现于 1.0.3 |
| 部署形态 |
npm CLI 本地模式,端口 20128 |
| 操作系统 |
Windows 10 (19044) |
| 触发客户端 |
WorkBuddy(标准 OpenAI SDK 兼容客户端);curl 亦可稳定复现 |
| 相关源码 |
open-sse/handlers/chatCore.js |
问题描述
向 /v1/chat/completions 发送省略 stream 字段的非流式请求时(这是 OpenAI Chat Completions 规范里的默认非流式调用方式,官方 SDK 与多数客户端都这么做),10router 错误地按流式处理,返回:
content-type: text/event-stream(应为 application/json)
- body 是一个完整的
chat.completion JSON 对象,末尾紧跟一行 data: [DONE]
结果:任何严格 JSON 解析的客户端(OpenAI 官方 SDK、WorkBuddy 等)JSON.parse 直接失败,表现为"测试连接不通过 / 响应解析错误",导致自定义模型无法接入。
最小复现
启动 1.0.5,配置任意 combo 模型(下文以 my-combo 为例),发送不带 stream 字段、不带 Accept: application/json 的请求:
curl -s http://localhost:20128/v1/chat/completions \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"model":"my-combo","messages":[{"role":"user","content":"say ok"}],"max_tokens":20}'
实际返回:
HTTP/1.1 200 OK
content-type: text/event-stream ← 错误
{"id":"chatcmpl-...","object":"chat.completion","model":"my-combo","choices":[...]}data: [DONE]
↑ 尾部垃圾,JSON.parse 失败
期望返回:
HTTP/1.1 200 OK
content-type: application/json
{"id":"chatcmpl-...","object":"chat.completion","model":"my-combo","choices":[...]}
对照实验: 同样的请求,只要显式加上 "stream": false,返回就是完全合法的纯 JSON(content-type: application/json,无尾巴)。可见问题精确发生在"stream 字段被省略"这一条件上。
根因分析
open-sse/handlers/chatCore.js:114:
let stream = providerRequiresStreaming ? true : (body.stream !== false);
body.stream 省略时值为 undefined,而 undefined !== false === true → 默认走流式。这与 OpenAI Chat Completions 规范相反(规范里 stream 缺省即非流式)。
第 129–136 行确实有一个基于 Accept 头的兜底:
const clientPrefersJson = acceptHeader.includes("application/json");
const clientPrefersSSE = acceptHeader.includes("text/event-stream");
if (clientPrefersJson && !clientPrefersSSE && body.stream !== true && !providerRequiresStreaming) {
stream = false;
}
但它只在客户端显式发送 Accept: application/json 时才生效。OpenAI 官方 SDK 与 curl 默认发送的是 Accept: */*(不含 application/json 子串)→ 兜底条件不成立 → 仍然走流式 → 输出畸形 body。
影响面
所有以"省略 stream 字段"方式调用非流式接口的 OpenAI 兼容客户端:OpenAI 官方 SDK 默认行为、WorkBuddy、以及大量第三方集成。这些客户端在 10router 上会全部无法完成非流式对话。
建议修复
把默认语义从「opt-out 流式」改为「opt-in 流式」,与 OpenAI 规范对齐:
// 114 行
let stream = providerRequiresStreaming ? true : (body.stream === true);
第 112 行的 clientRequestedStreaming 已单独处理 ANTIGRAVITY / GEMINI / GEMINI_CLI 等强制流式格式,不受此改动影响;providerRequiresStreaming、图像生成模型(119–121)、deepseek-tui(127)等分支均保留。
我在本地对编译产物(.next-cli-build/server/chunks/8895.js 及 translator 的 send/translate 两个 route)按此逻辑打了补丁(!1!==x.stream → !0===x.stream,共 4 处),实测 WorkBuddy 的非流式与流式两种调用均恢复正常。以上仅为验证根因的临时补丁,正式修复建议由维护者在源码层评估更周全的方案(例如结合 Accept 头默认值或按 sourceFormat 判定)。
附加观察(P2,不影响主结论)
带 tools 参数的流式请求,响应结尾会输出两个 data: [DONE](正常应为一个)。主流 SDK 读到第一个即停止解析,暂不致命,一并供参考。
English TL;DR
Bug: Non-streaming requests to /v1/chat/completions that omit the stream field (the OpenAI spec default, used by the official SDK and most clients) are incorrectly treated as streaming. The server returns content-type: text/event-stream with a body of JSON + "data: [DONE]", which fails strict JSON parsing on the client.
Root cause: open-sse/handlers/chatCore.js:114 — let stream = providerRequiresStreaming ? true : (body.stream !== false);. When stream is omitted, undefined !== false is true, so it defaults to streaming. The Accept-header fallback (lines 129–136) only triggers on an explicit Accept: application/json, but SDKs/curl send Accept: */*.
Suggested fix: default to non-streaming — body.stream === true — while keeping the existing clientRequestedStreaming / providerRequiresStreaming / image-gen / deepseek-tui branches. Verified locally via a patched build; both streaming and non-streaming calls then work.
fix: 非流式请求(省略 stream 字段)被误判为流式,返回畸形 SSE 响应
环境
curl亦可稳定复现open-sse/handlers/chatCore.js问题描述
向
/v1/chat/completions发送省略stream字段的非流式请求时(这是 OpenAI Chat Completions 规范里的默认非流式调用方式,官方 SDK 与多数客户端都这么做),10router 错误地按流式处理,返回:content-type: text/event-stream(应为application/json)chat.completionJSON 对象,末尾紧跟一行data: [DONE]结果:任何严格 JSON 解析的客户端(OpenAI 官方 SDK、WorkBuddy 等)
JSON.parse直接失败,表现为"测试连接不通过 / 响应解析错误",导致自定义模型无法接入。最小复现
启动 1.0.5,配置任意 combo 模型(下文以
my-combo为例),发送不带stream字段、不带Accept: application/json的请求:实际返回:
期望返回:
对照实验: 同样的请求,只要显式加上
"stream": false,返回就是完全合法的纯 JSON(content-type: application/json,无尾巴)。可见问题精确发生在"stream字段被省略"这一条件上。根因分析
open-sse/handlers/chatCore.js:114:body.stream省略时值为undefined,而undefined !== false === true→ 默认走流式。这与 OpenAI Chat Completions 规范相反(规范里stream缺省即非流式)。第 129–136 行确实有一个基于
Accept头的兜底:但它只在客户端显式发送
Accept: application/json时才生效。OpenAI 官方 SDK 与curl默认发送的是Accept: */*(不含application/json子串)→ 兜底条件不成立 → 仍然走流式 → 输出畸形 body。影响面
所有以"省略
stream字段"方式调用非流式接口的 OpenAI 兼容客户端:OpenAI 官方 SDK 默认行为、WorkBuddy、以及大量第三方集成。这些客户端在 10router 上会全部无法完成非流式对话。建议修复
把默认语义从「opt-out 流式」改为「opt-in 流式」,与 OpenAI 规范对齐:
第 112 行的
clientRequestedStreaming已单独处理 ANTIGRAVITY / GEMINI / GEMINI_CLI 等强制流式格式,不受此改动影响;providerRequiresStreaming、图像生成模型(119–121)、deepseek-tui(127)等分支均保留。附加观察(P2,不影响主结论)
带
tools参数的流式请求,响应结尾会输出两个data: [DONE](正常应为一个)。主流 SDK 读到第一个即停止解析,暂不致命,一并供参考。English TL;DR
Bug: Non-streaming requests to
/v1/chat/completionsthat omit thestreamfield (the OpenAI spec default, used by the official SDK and most clients) are incorrectly treated as streaming. The server returnscontent-type: text/event-streamwith a body ofJSON + "data: [DONE]", which fails strict JSON parsing on the client.Root cause:
open-sse/handlers/chatCore.js:114—let stream = providerRequiresStreaming ? true : (body.stream !== false);. Whenstreamis omitted,undefined !== falseistrue, so it defaults to streaming. TheAccept-header fallback (lines 129–136) only triggers on an explicitAccept: application/json, but SDKs/curl sendAccept: */*.Suggested fix: default to non-streaming —
body.stream === true— while keeping the existingclientRequestedStreaming/providerRequiresStreaming/ image-gen / deepseek-tui branches. Verified locally via a patched build; both streaming and non-streaming calls then work.