Skip to content

feat: add Force Stream channel setting for stream-only upstreams#6102

Open
xcrong wants to merge 5 commits into
QuantumNous:mainfrom
WebAgentLoop:feat/force-stream-buffer
Open

feat: add Force Stream channel setting for stream-only upstreams#6102
xcrong wants to merge 5 commits into
QuantumNous:mainfrom
WebAgentLoop:feat/force-stream-buffer

Conversation

@xcrong

@xcrong xcrong commented Jul 11, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

本 PR 代码由人工设计并评审,实现过程有 AI 辅助(详见各 commit footer 的 This code was AI-assisted.)。以下描述由作者本人整理撰写。

📝 变更描述 / Description

背景:部分上游 provider(以及某些自建网关)只支持流式响应,不接受 stream:false。现有渠道无法把这类上游透明地暴露给发非流式请求的客户端。

方案:新增渠道级 Force Stream 开关。开启后,当客户端请求为非流式时,系统会:

  1. 强制向上游发送 stream:true
  2. 用新增的 OaiStreamBufferHandler 把上游 SSE 增量(content / reasoning_content / tool_calls)累积成一条完整的 OpenAITextResponse
  3. 以普通 application/json 单体响应返回给客户端,客户端无感知。

后端

  • dto/channel_settings.go:新增 ChannelSettings.ForceStream
  • relay/common/relay_info.go:新增 RelayInfo.ForceStreamBuffer 标志
  • relay/compatible_handler.go:抽出公共函数 ApplyForceStream,在 TextHelper(正常请求路径)中调用
  • relay/channel/openai/relay-stream-buffer.go:新增 OaiStreamBufferHandler,累积 SSE delta 为单体响应
  • relay/channel/openai/adaptor.goDoResponseForceStreamBuffer 时路由到 buffer handler
  • relay/channel/openai/helper.go:抽出 markContentFilterReject / marshalTextResponse 供两个 handler 复用
  • controller/channel-test.go渠道测试路径同样调用 ApplyForceStream——之前测试路径独立于 TextHelper,ForceStream 渠道在手动/自动健康检查时会用非流式探测只支持流式的上游而失败,甚至被自动禁用

前端web/default):

  • 渠道表单 schema / defaults / transform / build 逻辑
  • 高级设置抽屉里的 Switch 开关
  • 7 种语言(en / zh / zh-TW / fr / ja / ru / vi)的 i18n 翻译

为什么渠道测试也要改testChannel 自己跑 buildTestRequest → ConvertOpenAIRequest → DoRequest → DoResponse,不经过 TextHelper,所以最初遗漏了 ForceStream 逻辑。由于 OaiStreamBufferHandler 返回的是带非 nil usage 的单体 JSON,渠道测试下游的 readTestResponseBody / validateTestResponseBody / coerceTestUsage 在原始 isStream=false 下行为完全正确,无需额外改动。

🚀 变更类型 / Type of change

  • ✨ 新功能 (New feature)
  • 🐛 Bug 修复 (Bug fix)
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

  • 暂无关联 Issue。

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 已搜索现有 Issues / PRs,未发现重复的 ForceStream / stream-only upstream 方案。
  • Bug fix 说明: N/A(本 PR 为新功能;渠道测试路径的修复属于本功能自身闭环)
  • 变更理解: 已理解——ForceStream 仅对 OpenAI chat completions 格式生效(OaiStreamBufferHandler 只解析该格式的 SSE delta);与 pass-through 模式互斥。
  • 范围聚焦: 仅触及 ForceStream 功能相关文件,无无关改动。
  • 本地验证: 见下方运行证明。
  • 安全合规: 无敏感凭据;遵循项目 JSON 包装(common.*)、跨数据库兼容、计费安全等规范。

📸 运行证明 / Proof of Work

单元测试OaiStreamBufferHandler 累积逻辑 + ApplyForceStream):

$ go test ./relay/channel/openai/ -count=1
ok  github.com/QuantumNous/new-api/relay/channel/openai  0.516s

$ go test ./relay/ -run TestApplyForceStream -v
=== RUN   TestApplyForceStream
=== RUN   TestApplyForceStream/active_when_force_stream_and_non-stream_client
=== RUN   TestApplyForceStream/active_when_force_stream_and_stream_nil
=== RUN   TestApplyForceStream/inactive_when_client_already_streaming
=== RUN   TestApplyForceStream/inactive_when_force_stream_disabled
=== RUN   TestApplyForceStream/inactive_when_channel_pass-through_enabled
=== RUN   TestApplyForceStream/inactive_when_global_pass-through_enabled
--- PASS: TestApplyForceStream (0.00s)
PASS
ok  github.com/QuantumNous/new-api/relay  0.532s
  • TestApplyForceStream 覆盖激活分支(非流式 / stream=nil)与全部四个抑制条件(客户端已流式 / 开关关闭 / 渠道 pass-through / 全局 pass-through),断言 ForceStreamBuffer 标志与 request.Stream 的精确状态。
  • OaiStreamBufferHandler 的 14 条表驱动测试覆盖 content / reasoning 累积、tool_calls delta 合并、多 choice、usage 提取(含 cached token)、错误检测、SSE 解析。

编译与回归go build ./... 通过;go test ./relay/... ./controller/... 全绿。

Summary by CodeRabbit

  • New Features
    • Added a Force Stream (force_stream) channel setting, with UI + i18n support.
    • When enabled, compatible chat/completions requests will buffer upstream streaming and return a single non-streaming JSON response.
  • Bug Fixes
    • Improved forced buffering routing and OpenAI content-filter rejection marking for buffered responses.
    • Ensures the forced stream/buffer flag is correctly cleared on retries.
  • Tests
    • Added coverage for SSE chunk accumulation, usage extraction, upstream error handling, and buffered JSON assembly.
  • Documentation
    • Updated the Force Stream help text, including incompatibility with Pass Through Body.

xcrong added 2 commits July 10, 2026 23:25
Add a per-channel 'Force Stream' toggle that forces stream:true upstream
for non-streaming client requests, buffers the SSE response, and returns
it as a single non-streaming JSON response. This enables channels backed
by upstream providers that only support streaming to serve non-streaming
clients transparently.

Backend:
- New ChannelSettings.ForceStream field (dto/channel_settings.go)
- New RelayInfo.ForceStreamBuffer flag (relay/common/relay_info.go)
- Force stream upstream in TextHelper when setting is active and
  pass-through is disabled (relay/compatible_handler.go)
- New OaiStreamBufferHandler that accumulates SSE deltas (content,
  reasoning_content, tool_calls) into an OpenAITextResponse
  (relay/channel/openai/relay-stream-buffer.go)
- Response dispatch routes to buffer handler when ForceStreamBuffer
  is set (relay/channel/openai/adaptor.go)
- Extracted shared helpers markContentFilterReject and
  marshalTextResponse used by both OpenaiHandler and
  OaiStreamBufferHandler (relay/channel/openai/helper.go)

Frontend:
- Channel form schema, defaults, transform, and build logic
- Switch toggle in channel advanced settings drawer
- i18n translations for all 7 locales (en, zh, zh-TW, fr, ja, ru, vi)

Tests:
- 14 deterministic table tests covering content/reasoning accumulation,
  tool-call delta merging, multi-choice responses, usage extraction
  with cached-token data, error detection, and SSE parsing

This code was AI-assisted.
The ForceStream 'stream-to-nonstream' logic only existed in TextHelper
(relay path). The channel test path (testChannel) builds and runs the
adaptor flow independently, so ForceStream channels were never exercised
correctly during manual/automatic tests: a non-streaming probe hit an
upstream that only supports streaming and failed, causing auto-ban.

Extract the shared logic into ApplyForceStream and invoke it from both
TextHelper and testChannel. Only GeneralOpenAIRequest is handled, since
OaiStreamBufferHandler only parses OpenAI chat-completion SSE deltas.

The buffered handler returns a single JSON with non-nil usage, so the
existing downstream helpers (readTestResponseBody, validateTestResponseBody,
coerceTestUsage) work unchanged with the original isStream=false.

Added table-driven tests for ApplyForceStream covering the active branch
and all four suppression conditions.

This code was AI-assisted.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7201c45d-09c0-4f45-b5f0-439b7a2ede1a

📥 Commits

Reviewing files that changed from the base of the PR and between 646e2a9 and 83cc024.

📒 Files selected for processing (1)
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx

Walkthrough

Adds a configurable ForceStream setting for forcing upstream streaming on non-streaming requests, buffering OpenAI SSE responses into JSON, preserving usage and tool-call data, and exposing the setting through channel forms and localized UI text.

Changes

Force-stream configuration and UI

Layer / File(s) Summary
Setting contract and persistence
dto/channel_settings.go, web/default/src/features/channels/lib/..., web/default/src/features/channels/types.ts
Adds force_stream to backend and frontend settings, form defaults, parsing, validation, and serialized channel settings.
Channel editor integration
web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx, web/default/src/i18n/locales/*
Adds the Force Stream toggle, sensitive-field handling, advanced-setting validation, and localized descriptions.

Request-path integration

Layer / File(s) Summary
Force-stream decision and wiring
relay/common/relay_info.go, relay/compatible_handler.go, controller/channel-test.go, relay/compatible_handler_test.go
Activates forced streaming when pass-through is disabled and the request is non-streaming, records buffering state, applies the logic to channel tests, and tests retry and activation behavior.
Response conversion helpers
relay/channel/openai/helper.go, relay/channel/openai/relay-openai.go
Shares content-filter rejection marking and relay-format response marshaling across OpenAI response handling.

Buffered OpenAI responses

Layer / File(s) Summary
SSE accumulation and output
relay/channel/openai/relay-stream-buffer.go, relay/channel/openai/adaptor.go
Parses and merges streamed chunks, reconstructs responses, and emits buffered JSON when enabled.
Accumulator validation
relay/channel/openai/relay-stream-buffer_test.go
Tests content, reasoning, tool calls, multiple choices, usage, errors, empty streams, SSE parsing, and assembled JSON.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant TextHelper
  participant ApplyForceStream
  participant Upstream
  participant OaiStreamBufferHandler
  Client->>TextHelper: Send non-streaming GeneralOpenAIRequest
  TextHelper->>ApplyForceStream: Evaluate ForceStream and pass-through settings
  ApplyForceStream->>TextHelper: Enable streaming and ForceStreamBuffer
  TextHelper->>Upstream: Forward streaming request
  Upstream->>OaiStreamBufferHandler: Return SSE chunks
  OaiStreamBufferHandler->>OaiStreamBufferHandler: Merge chunks and assemble JSON
  OaiStreamBufferHandler->>Client: Return non-streaming JSON
Loading

Possibly related PRs

Suggested reviewers: seefs001

Poem

I’m a rabbit buffering streams in a queue,
Turning SSE bubbles to JSON anew.
Tool calls hop softly, usage counts grow,
Force Stream keeps the data in flow.
Nibble, merge, respond! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a Force Stream channel setting for stream-only upstream providers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
web/default/src/features/channels/lib/channel-form.ts (1)

214-294: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider validating the force_stream + pass_through_body_enabled conflict.

The description text states Force Stream is "Incompatible with Pass Through Body," but the schema's superRefine has no check for this. Enabling both silently no-ops Force Stream on the backend (per ApplyForceStream), which could confuse admins.

♻️ Suggested validation
   .superRefine((data, ctx) => {
+    if (data.force_stream && data.pass_through_body_enabled) {
+      addRequiredIssue(
+        ctx,
+        'force_stream',
+        'Force Stream is incompatible with Pass Through Body'
+      )
+    }
+
     if ([3, 8, 36, 45].includes(data.type) && !data.base_url?.trim()) {

Also applies to: 188-194

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/channels/lib/channel-form.ts` around lines 214 -
294, Add validation in the schema’s superRefine callback to reject
configurations where both force_stream and pass_through_body_enabled are
enabled, using addRequiredIssue on the relevant field with a clear
incompatibility message. Locate the existing channel-specific checks within
superRefine and ensure the rule applies consistently to the corresponding fields
noted in the comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/compatible_handler.go`:
- Around line 25-38: ApplyForceStream must recompute info.ForceStreamBuffer on
every invocation rather than only setting it true: assign it directly from the
full force-stream condition, then set request.Stream to true only when that
condition is satisfied. This clears stale state when RelayInfo is reused for
retries on channels that do not force streaming.

In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Around line 4095-4119: Only render the force_stream FormField when the current
channel type is OpenAI-compatible, matching the channels handled by
ForceStreamBuffer in the OpenAI adaptor; use the existing channel-type state or
helper near the drawer component to gate the Switch and its label, while
preserving form behavior for supported channels.

In `@web/default/src/i18n/locales/zh-TW.json`:
- Line 1505: Restore the Traditional Chinese translation for the specified
referral-rewards localization key in zh-TW.json, matching the correct existing
translation in zh.json rather than leaving the value identical to the English
key.

---

Nitpick comments:
In `@web/default/src/features/channels/lib/channel-form.ts`:
- Around line 214-294: Add validation in the schema’s superRefine callback to
reject configurations where both force_stream and pass_through_body_enabled are
enabled, using addRequiredIssue on the relevant field with a clear
incompatibility message. Locate the existing channel-specific checks within
superRefine and ensure the rule applies consistently to the corresponding fields
noted in the comment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7138f991-12e9-4f74-bc6b-f07a96c2e633

📥 Commits

Reviewing files that changed from the base of the PR and between dad57a6 and 0c4a856.

📒 Files selected for processing (21)
  • controller/channel-test.go
  • dto/channel_settings.go
  • relay/channel/openai/adaptor.go
  • relay/channel/openai/helper.go
  • relay/channel/openai/relay-openai.go
  • relay/channel/openai/relay-stream-buffer.go
  • relay/channel/openai/relay-stream-buffer_test.go
  • relay/common/relay_info.go
  • relay/compatible_handler.go
  • relay/compatible_handler_test.go
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/lib/channel-form-errors.ts
  • web/default/src/features/channels/lib/channel-form.ts
  • web/default/src/features/channels/types.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh-TW.json
  • web/default/src/i18n/locales/zh.json

Comment thread relay/compatible_handler.go
Comment thread web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx Outdated
Comment thread web/default/src/i18n/locales/zh-TW.json Outdated
xcrong and others added 3 commits July 11, 2026 16:55
- Clear stale ForceStreamBuffer on retry: ApplyForceStream now
  reassigns the flag unconditionally, so a RelayInfo reused across a
  retry onto a non-ForceStream channel no longer keeps buffering on
  (which would route the upstream's plain JSON into OaiStreamBufferHandler
  and misparse it as SSE).
- Restore the zh-TW translation for the referral-rewards key that was
  overwritten with the untranslated English source.
- Gate the Force Stream toggle to OpenAI/Azure/Custom channel types,
  matching the adaptor that actually consumes ForceStreamBuffer; avoids
  a misleading switch on Claude/Gemini channels where it would break the
  non-streaming response.

Adds a regression test for the retry flag-clearing invariant.

This code was AI-assisted.
The Force Stream toggle was gated to channel types [1, 3, 8] (OpenAI,
Azure, Custom), hiding it for advanced custom channels. The buffering
logic runs on the advanced custom native OpenAI-format path as well
(ApplyForceStream in TextHelper + ForceStreamBuffer consumed in the OpenAI
adaptor default branch), so include CHANNEL_TYPE_ADVANCED_CUSTOM in the
visibility gate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant