Skip to content

fix: 非流式请求(省略 stream 字段)被误判为流式,返回畸形 SSE 响应 #4

Description

@iMissNan

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,返回就是完全合法的纯 JSONcontent-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:114let 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions