Skip to content

[Bug] OpenRouter 流式请求全部失败:上游帧尾非标准 event: data / data: [DONE] 触发严格 SSE 校验,gateway 以 upstream SSE protocol error 终止 #78

Description

@NewEnglandSurvivorKing

CSSwitch 版本

v0.8.1 Linux x64

macOS 版本 + 芯片

Ubuntu 24.04 x86_64 glibc

用的是哪个 provider

自定义 / 其它

复现步骤

现象

第三方模式下 Science 聊天一律失败,与模型无关(deepseek/deepseek-v4-flash-0731anthropic/claude-sonnet-5@preset/fable2 均复现)。Science 服务端日志表现为:

[LLM] Streaming sse_server error (attempt N), retrying: Error: {"type":"error","error":{"type":"api_error","message":"upstream SSE protocol error"}}

重试 5+ 次后整帧报错。同一 profile 的非流式请求(validate_connectionactivate_profilePOST /v1/messages scratch probe、GET /v1/models)全部 outcome=Ok,即认证、目录、非流式链路均正常,只有流式(stream=true)触发。

复现与根因证据

1. 直连上游(绕过 gateway)流式正常。 用 profile 内同一 key 对 https://openrouter.ai/api/v1/messagesstream=true 带 tools 请求,HTTP 200 text/event-stream,正文完整。但其帧尾为:

event: message_delta
data: {"type":"message_delta","delta":{...,"stop_reason":"end_turn",...},"usage":{...}}

event: message_stop
data: {"type":"message_stop"}

event: data
data: [DONE]

最后一段 event: data + data: [DONE] 是 OpenAI 风格结束标记,不属于 Anthropic Messages SSE 协议(data 不是合法 JSON 事件体)。

2. 经 gateway 的受控复现。 用与正式 gateway 完全相同的环境变量(CSSWITCH_*,含 anthropic-relay contract、CSSWITCH_RELAY_THINKING=adaptive)在另一端口启动第二个 csswitch-gateway 实例,按 Science 的真实访问方式(ANTHROPIC_BASE_URL=http://127.0.0.1:<port>/<path_secret>)发同样的流式请求。gateway 将上游事件原样转发到 message_delta 为止,随后就地发出:

event: error
data: {"type":"error","error":{"type":"api_error","message":"upstream SSE protocol error"}}

且不转发 message_stop(与 0.8.0 changelog「严格 SSE 生命周期校验:失败或截断的流只发一个终止 error、绝不合成 message_stop」的设计一致)。

期望结果 / 实际结果(含完整报错文字)

gateway 的 Anthropic SSE 校验在收到合法 message_stop 后进入「终止容忍」状态:之后的 data: [DONE](或 event: data 包裹的 [DONE])直接忽略并正常收尾,而不是整流判违规。

日志(可选,强烈建议)

POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=false msgs=1 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=false msgs=1 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=false msgs=1 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=@preset/fable2 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=@preset/fable2 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=@preset/fable2 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize
POST /v1/messages relay target=deepseek/deepseek-v4-flash-0731 stream=true msgs=2 rules=tool.relay.input-schema-normalize

[oauth] 虚拟登录已就绪(Rust,零 node;action=Created;isolated=true)
隔离运行状态已由 CSSwitch 准备(路径已隐藏)

启动隔离沙箱 Science(虚拟登录)
  HOME     = [CSSwitch isolated]
  data-dir = [CSSwitch isolated Science data]
  端口     = 8990   (真实实例 8765 不受影响)
  预览端口 = 8991   (显式固定,供本机 Science 预览使用)
  二进制   = backend-selected runtime
  系统 SSH = 未授权
  推理指向 = http://127.0.0.1:18991/****
  账号     = virtual@localhost.invalid (本地假账号,不用真实凭证)
  外联防卡 = Anthropic HTTPS fast-fail(经 http://127.0.0.1:18991,no_proxy=127.0.0.1,localhost,::1)


已后台启动。验证:
  健康:   curl -s http://127.0.0.1:8990/health || true
  状态:   请使用 CSSwitch 状态灯确认
停止:     请使用 CSSwitch「停止全部」

确认

  • 我已确认贴出的内容里不含 API key、令牌等任何密钥

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions