fix(core): stream chat-compression side-query to survive gateway timeout - #5865
Conversation
The context-compression side-query runs non-streaming, so the whole LLM
inference must finish before the first HTTP byte arrives. Behind a BFF
gateway whose `proxy_read_timeout` is ~60s, a long compression inference
(observed ~1.9 min) is killed at 60s with a 504 — surfaced to qwen-code
as a 422 — which fails compression and breaks the session.
Add an opt-in `stream?: boolean` to the text side-query path
(`BaseLlmClient.generateText` -> `runSideQuery`) and have only
`ChatCompressionService.compress` opt in. Streaming keeps the connection
alive (the first delta arrives within seconds, resetting the gateway
timeout), so a slow inference no longer trips the gateway. Streamed
deltas are collected into the same `{ text, usage }` result, so every
other side-query caller keeps the unchanged non-streaming path.
Refs QwenLM#5861
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
|
Thanks for the PR! Template looks good ✓ — all required sections present with substantive content. On direction: this solves a real production problem — 504 gateway timeouts killing compression inference behind BFF proxies (DataWorks ACP / DQ_AGENT). Sessions break mid-conversation despite the model itself succeeding. Directly aligned with reliability. Closes #5861, which has clear user impact. On approach: scope is tight and well-bounded. Only the compression caller opts in to streaming; every other side-query keeps the unchanged non-streaming path. Reuses the existing Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 方向:解决真实的生产问题——BFF 网关(DataWorks ACP / DQ_AGENT)的 504 超时导致压缩推理中断会话。与可靠性目标直接对齐,关联 #5861。 方案:范围紧凑,只有压缩调用方 opt-in 流式;其余 side-query 保持非流式。复用已有的 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
|
Code review: no blockers. The streaming branch in One minor note (non-blocking): the error catch block logs Tests are thorough: 7 new streaming tests covering delta concatenation, thought-part filtering, usage capture (separate chunk and same-chunk), mid-stream error, mid-stream abort, empty stream, and per-model generator streaming. Plus 2 Tmux smoke test: this is internal plumbing — the streaming path only activates during context compression behind a BFF gateway with Installed buildDev build (main branch, without PR)No regression. The streaming behavior requires a gateway repro that the PR's E2E section explicitly marks as optional. Unit tests (127 on main, 132 on PR per CI) and CI gates are the appropriate verification here. 中文说明**代码审查:**无阻塞问题。 一个小建议(非阻塞):错误 catch 块的日志 测试覆盖充分:7 个新流式测试 + 2 个 **Tmux 冒烟测试:**这是内部链路改动,流式路径只在 BFF 网关后触发,本地 tmux 无法复现。已验证安装版和开发版对简单查询响应正常。 — Qwen Code · qwen3.7-max |
|
This PR does exactly what it says: adds a minimal streaming opt-in to keep the HTTP connection alive during slow compression inference behind BFF gateways. The implementation matches my independent proposal — extend The code is straightforward — one conditional in CI is green (ubuntu tests, typecheck, lint). The existing Approving. ✅ 中文说明这个 PR 完全兑现了承诺:以最小改动为压缩推理增加流式 opt-in,在 BFF 网关后保持 HTTP 连接存活。实现与我的独立方案一致——扩展 代码简洁直接,测试充分覆盖了重要的边界情况。CI 全绿。此前的 批准。✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Keep the full gateway-timeout rationale canonical on `GenerateTextOptions.stream` and slim `SideQueryTextOptions.stream` to a brief opt-in note that links to it, instead of repeating the same ~7-line block in both. Also harmonize "Defaults to falsy" -> "Defaults to `false`" for the typed boolean. Addresses review feedback on QwenLM#5865. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
chiga0
left a comment
There was a problem hiding this comment.
Overview
Final Verdict: Approve — Clean, focused bug fix that solves a real production issue (504 gateway timeout killing slow compression inference). The streaming opt-in is minimally invasive — only the compression caller opts in, all other side-queries keep the unchanged non-streaming path. Both branches resolve to the same { text, usage } shape under a single retryWithBackoff, preserving retry semantics.
Findings Summary
- Critical/Major: 0 items
- Minor: 0 items
- Nit: 0 items
Key Observations
The design is correct: streaming keeps the HTTP connection alive by returning the first delta within seconds, which resets the gateway's proxy_read_timeout on every chunk. The entire stream is consumed inside retryWithBackoff, so a mid-stream failure retries the whole idempotent request — matching non-streaming retry semantics.
Cross-Validation
| Finding | Other Reviewer | My Assessment |
|---|---|---|
JSDoc duplication on stream across layers |
qwen-code-ci-bot | Fixed at HEAD (commit 7de4967b) — canonical rationale on GenerateTextOptions.stream, slim {@link} note on SideQueryTextOptions.stream |
Additional Audit Coverage
- Trim behavior change: Previously
getResponseText(result).trim()on raw response; nowresult.text.trim()after stream concatenation. Correct — trim is applied once on the full concatenated text, not per-delta. Test confirms:' Hello' + ', ' + 'world '→'Hello, world'. - Usage "last one wins":
if (chunk.usageMetadata) { usage = chunk.usageMetadata }— overwrites on each chunk with usage, so final chunk's usage is captured. Matches streaming pipeline convention where usage rides the trailing chunk. - Abort signal propagation:
for awaiton the response stream respects abort — ifabortSignalfires mid-stream, the async iterator throws, which propagates toretryWithBackoff's catch. The existingif (abortSignal.aborted)check in the outer catch handles it correctly. - Stream inactivity timeout: The existing
withStreamInactivityTimeoutwrapsgenerateContentStreaminternally, providing a per-chunk 120s watchdog. This is strictly better than the non-streaming all-or-nothing wait. - Conditional spread in
sideQuery.ts:...(options.stream !== undefined && { stream: options.stream })correctly omits the field when not set. Test confirms:expect(callArg).not.toHaveProperty('stream'). maxAttempts: 1for compression: Compression is best-effort (next turn re-triggers), so the existing single-attempt limit is preserved. Streaming doesn't change this — a stream failure just means compression returnsNOOPfaster.- Backward compatibility:
streamdefaults to falsy. All existingrunSideQuerycallers omit it. JSON-mode side-queries are untouched (thestreamoption is only onSideQueryTextOptions).
This review was generated by QoderWork AI
doudouOUC
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: self-PR; CI still running.
— qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings. LGTM. Downgraded from Approve to Comment: CI still running (30 checks pending).
The change is clean and well-scoped: the stream?: boolean opt-in threads correctly through ChatCompressionService → runSideQuery → BaseLlmClient.generateText, the streaming closure routes through generateContentStream and accumulates deltas into the same { text, usage } shape, and retryWithBackoff wraps the whole call so mid-stream failures retry cleanly. Abort propagation through the SDK transport is intact (no need for an explicit in-loop check — matches the established pattern in geminiChat.ts). Tests cover the new paths thoroughly (multi-chunk concatenation + trim, thought-part filtering, usage capture, forwarding + backward compat).
— qwen3.7-max via Qwen Code /review
| // the final chunk (last one wins), matching the non-streaming read. | ||
| let text = ''; | ||
| let usage: GenerateContentResponseUsageMetadata | undefined; | ||
| for await (const chunk of responseStream) { |
There was a problem hiding this comment.
[Critical] The for await loop has no inter-chunk idle timeout. The PR description claims "Streaming arms a per-chunk inactivity watchdog (withStreamInactivityTimeout, default 120s between chunks)" — but that wrapper only exists for the OpenAI provider (openaiContentGenerator/pipeline.ts). The Gemini (geminiContentGenerator.ts) and Anthropic (anthropicContentGenerator.ts) content generators return raw streams with no inactivity timeout.
For the chat compression use case (the PR's sole motivation, which runs on Gemini via the BFF gateway), a stalled model that stops emitting chunks but keeps the TCP connection open would cause this loop to block indefinitely — worse than the pre-PR behavior where the gateway's 60s timeout at least produced a catchable error that triggered the NOOP fallback.
Consider wrapping responseStream with withStreamInactivityTimeout (or an equivalent) at this level so all providers benefit uniformly, regardless of which content generator is resolved by resolveForModel.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Thanks — I checked this carefully. Two parts of the observation are correct, but the severity premise isn't, so I've corrected the PR description rather than adding the wrapper.
Correct: withStreamInactivityTimeout lives only in the OpenAI pipeline (openaiContentGenerator/pipeline.ts:120, applied at :265); GeminiContentGenerator.generateContentStream returns the raw googleGenAI stream and AnthropicContentGenerator returns a raw Anthropic stream — neither has a per-chunk idle timeout.
But the motivating path is not Gemini. The DataWorks ACP / DQ_AGENT BFF is OpenAI-compatible — issue #5861's own trace shows openai.chat.completions.create. That resolves to OpenAIContentGenerator (or QwenContentGenerator, whose generateContentStream override just wraps super.generateContentStream in credential management — qwenContentGenerator.ts:167), so compression behind the BFF does route through the pipeline and is covered by the watchdog. The "blocks indefinitely / worse than pre-PR" scenario doesn't apply where this PR actually runs, and the 60s gateway timeout you cite only exists in that BFF deployment.
For native USE_GEMINI / USE_ANTHROPIC: the raw-stream point is fair, but that's the exact contract the main chat loop already consumes on every turn (geminiChat.ts iterates the same generateContentStream, e.g. :2013, :2994), bounded by the SDK transport timeout + abortSignal. This side-query reuses that existing invariant; it isn't a new exposure introduced here.
Changed: corrected the PR description's risk note, which overclaimed a universal 120s watchdog — it's now scoped to the OpenAI/Qwen pipeline, with the Gemini/Anthropic contract spelled out.
Not changed: I didn't wrap responseStream at the baseLlmClient level. A provider-agnostic idle timeout belongs at the ContentGenerator layer so the main loop benefits too; adding it only to this side-query would double-wrap OpenAI/Qwen and give the side-query a guarantee the main turn doesn't have. Happy to do that as a separate, broader change if maintainers want it — leaving this thread open for a maintainer to weigh in.
🤖 Addressed by Qwen Code
wenshao
left a comment
There was a problem hiding this comment.
No new findings. The streaming error-path test gap (mid-stream failure, abort during iteration) was already flagged in a prior review. Production code is clean and correct — the stream?: boolean opt-in threads properly through the three layers, retryWithBackoff wraps both branches uniformly, and all 132 tests pass. LGTM. ✅
— qwen3.7-max via Qwen Code /review
Add two regression tests for the streaming branch of generateText: a mid-stream throw rejects the whole call (partial text is never returned as success) and is reported, and an abort firing mid-stream surfaces the original error unwrapped while skipping reportError via the `abortSignal.aborted` guard. The first is the gateway-timeout-mid- inference scenario this PR targets. Addresses review feedback on QwenLM#5865. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No high-confidence findings. Downgraded from Approve to Comment: CI still running.
Needs Human Review (low-confidence, not blockers):
baseLlmClient.ts:365-382— No partial text length or chunk count in error context when streaming fails mid-stream. The accumulated partial text is silently discarded.baseLlmClient.test.ts— No test for retry-then-success path for streaming. The comment claims retries work but this path is untested.baseLlmClient.test.ts— No test for empty stream (zero chunks). Handled correctly by construction but untested boundary case.
— qwen3.7-max via Qwen Code /review
Add a boundary test for a stream that yields zero chunks: generateText must resolve to an empty string with undefined usage rather than throw. Addresses review feedback on QwenLM#5865. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
|
Thanks for the low-confidence notes. Triaged the three:
🤖 Addressed by Qwen Code |
Two streaming-branch gaps surfaced in review: - The per-model `fastContentGenerator` mock had no `generateContentStream`, so no test exercised streaming against a `resolveForModel`-selected generator — the exact `model` + `stream: true` combination compression uses. Add that mock method and a per-model streaming test asserting the resolved generator's stream is consumed (not the injected default). - `mockTextStream` only emitted usage on a separate trailing chunk. Add a test where the final content-bearing chunk carries both a text delta and usageMetadata (the realistic Gemini/OpenAI shape), guarding against a refactor that skips text extraction on usage-bearing chunks. Addresses review feedback on QwenLM#5865. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
wenshao
left a comment
There was a problem hiding this comment.
[Suggestion] baseLlmClient.ts:430 — The error catch block is identical for streaming and non-streaming paths. reportError logs 'Error generating text content via API.' with no indication that streaming was active, no partial text length, and no chunk count. The stream flag is destructured at line 335 and in scope here but unused. An oncall engineer debugging a stalled compression at 3 AM cannot distinguish a streaming mid-stream failure from the original non-streaming 504 this PR fixes.
Consider adding streaming context to the error message:
const streamContext = stream ? ` [streaming]` : '';
await reportError(
error,
`Error generating text content via API${streamContext}.`,
contents,
'generateText-api',
);— qwen3.7-max via Qwen Code /review
✅ Verification report — stream chat-compression side-queryVerdict: works as designed. I reproduced the #5861 gateway timeout on I built a real end-to-end reproduction of the BFF gateway timeout and ran a BASE-vs-PR A/B against it (real 1. Unit tests (PR head) — all greenReverse-audit — do the new tests actually pin the new behaviour? Reverting only the 3 source files to the merge-base while keeping the PR's test files makes 9 tests fail (1 × 2. End-to-end — gateway-timeout reproduction (the core of #5861)Harness. A zero-dep OpenAI-compatible mock that also models a BFF gateway:
A/B. Dist-level swap of only the 3 changed core files ( Result.
The PR flips the compression side-query to Raw evidence (mock request log + TUI)// PR arm — one streamed request, survives the 9s inference
{"requestIndex":2,"stream":true,"hasStreamKey":true,"stream_options":{"include_usage":true},"isCompression":true,"nMessages":4}
STREAM_START_200 → STREAM_DONE_200 elapsedMs=9059
// TUI: ✦ Chat history compressed from 1200 to 1060 tokens.
// BASE arm — non-streaming, killed 4× by the gateway
{"requestIndex":2,"stream":"(absent)","hasStreamKey":false,"stream_options":null,"isCompression":true,"nMessages":4}
{"requestIndex":3,"stream":"(absent)","hasStreamKey":false,...}
{"requestIndex":4,"stream":"(absent)","hasStreamKey":false,...}
{"requestIndex":5,"stream":"(absent)","hasStreamKey":false,...}
GATEWAY_TIMEOUT_504 × 4
// TUI: ✕ Failed to compress chat history: ... Request timeout after 15s.Notes (non-blocking)
中文版(点击展开)✅ 验证报告 —— 压缩 side-query 流式化结论:行为符合预期。我在 我用真实 1. 单元测试(PR head)—— 全绿逆向审计(这些新测试是否真的钉住了新行为?):只把 3 个源文件回退到 merge-base、保留 PR 的测试文件后,有 9 个测试失败(1 个 2. 端到端 —— 网关超时复现(#5861 的核心)测试装置: 一个零依赖的 OpenAI 兼容 mock,同时模拟 BFF 网关:
A/B: 仅在 dist 层替换 3 个改动文件( 结果:
PR 正如设计把压缩 side-query 切到 备注(不阻塞)
|
|
@qwen-code /triage |
Surface a `[streaming]` marker in the generateText error report when the streaming branch was active, so an oncall can tell a mid-stream failure apart from the original non-streaming gateway timeout this PR fixes. The already-destructured `stream` flag drives it; partial-text length / chunk count are intentionally left out (they would require hoisting mutable per-attempt state across the retryWithBackoff boundary). Also assert the request shape (model, contents, abortSignal-bearing config, promptId) in the main and per-model streaming tests, matching the non-streaming tests so a request-construction regression is caught. Addresses review feedback on QwenLM#5865. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
|
@wenshao thanks for the thorough end-to-end verification — the dist-level swap isolating only the 3 changed files, and the BASE (504 ×4) vs PR (streamed 9.06s, On your 🤖 Addressed by Qwen Code |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
The mid-stream-error test verified reportError was called but not with what message. Assert the `[streaming]` marker so a regression dropping it (the signal that distinguishes a mid-stream failure from the original 504) is caught. Addresses review feedback on QwenLM#5865. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
What this PR does
Adds an opt-in streaming path to the text side-query so the context-compression summary request can keep its HTTP connection alive while the model is still thinking. A new
stream?: booleanoption flows fromChatCompressionService.compressthroughrunSideQuerydown toBaseLlmClient.generateText; when set, the call routes through the already-existinggenerateContentStreamand the streamed deltas are collected back into the same{ text, usage }result the non-streaming path returns. Only the compression caller opts in — every other side-query keeps the unchanged non-streaming behaviour.Why it's needed
When the context window approaches capacity, compression fires a summarization side-query that today runs non-streaming, so the entire LLM inference must finish before the first HTTP byte arrives. Behind a BFF gateway (e.g. DataWorks ACP / DQ_AGENT) whose
proxy_read_timeoutis ~60s, a long compression inference — a real production trace measured ~1.9 minutes — is killed by the gateway at 60s with a504 Gateway Timeout, surfaced to qwen-code asUnprocessableEntityError: 422. Compression then fails, the turn errors out, and the session breaks even though the model itself would have succeeded. Streaming fixes this because the server returns HTTP 200 and an initial delta within seconds, which resets the gateway's read timeout on every chunk, so a slow inference can no longer trip the 60s ceiling.Reviewer Test Plan
How to verify
Unit:
cd packages/core && npx vitest run src/core/baseLlmClient.test.ts src/utils/sideQuery.test.ts src/services/chatCompressionService.test.ts— expect all green. New coverage asserts thatstream: trueroutes throughgenerateContentStream, concatenates multi-chunk deltas, trims, drops thought parts, and captures usage from the final chunk; that omittingstreamstill uses the non-streaminggenerateContent; thatrunSideQueryforwards the flag only when set; and that compression callsgenerateTextwithstream: true.End-to-end (optional, needs a gateway): drive a long session against an OpenAI-compatible endpoint behind a ≤60s
proxy_read_timeoutproxy, force compression, and confirm the compression request now streams and completes where the non-streaming path previously failed with a 504/422.Evidence (Before & After)
Non-user-visible change (core inference plumbing), so no UI before/after. Local unit run:
Test Files 3 passed (3) · Tests 132 passed (132). Repo gates pass:npm run typecheck(all workspaces) andnpm run lintboth exit 0; the pre-commit hook (prettier+eslint --max-warnings 0) passed on the six changed files.Tested on
Environment (optional)
Unit tests only (
vitest), plusnpm run typecheck/npm run lint. No runtime/sandbox needed.Risk & Scope
OpenAIContentGenerator/QwenContentGenerator) routes through the OpenAI pipeline, which already wraps the stream in a per-chunk inactivity watchdog (withStreamInactivityTimeout, tunable viaQWEN_STREAM_IDLE_TIMEOUT_MS/streamIdleTimeoutMs,0disables) — so a stream that returns 200 then goes silent is bounded there, not the old all-or-nothing non-streaming wait. NativeUSE_GEMINI/USE_ANTHROPICstreams have no per-chunk idle timeout at the provider layer, but that is the exact contract the main chat loop already consumes for every turn (geminiChat.tsiterates the same rawgenerateContentStream), bounded by the SDK transport timeout plusabortSignal; this side-query reuses that existing invariant rather than introducing a new exposure. The whole stream is consumed insideretryWithBackoff, so a mid-stream failure retries the entire idempotent request, matching the non-streaming retry semantics. A provider-agnostic idle timeout for Gemini/Anthropic would belong at theContentGeneratorlayer (so the main loop benefits too) and is out of scope here.thinkingConfig: { includeThoughts: false }andapplyThinkingDefaultdefaults every side-query to it (the issue'sthoughts_token_count=1was from an older preview build). "Use the fast model for compression" is a summarization-quality / model-selection change unrelated to the timeout and is left as a possible follow-up (see the related Disable reasoning on all fastModel side queries (follow-up to #3759) #3760 for the broader fastModel-reasoning topic). The end-to-end gateway repro above was not run locally.stream?: booleanis an additive optional field defaulting to falsy; all existing text-moderunSideQuerycallers omit it and keep the non-streaming path, and JSON-mode side-queries are untouched.Linked Issues
Closes #5861
中文说明
这个 PR 做了什么
为文本 side-query 增加一个可选的流式路径,使上下文压缩的摘要请求在模型仍在推理时也能保持 HTTP 连接存活。新增的
stream?: boolean选项从ChatCompressionService.compress经runSideQuery一直传到BaseLlmClient.generateText;开启后请求改走已存在的generateContentStream,流式增量再被收集回与非流式路径相同的{ text, usage }结果。只有压缩这一个调用方 opt-in,其余 side-query 保持原有的非流式行为不变。为什么需要
上下文窗口接近上限时,压缩会发起一个摘要 side-query,而它当前是非流式的,因此整个 LLM 推理必须全部完成后第一个 HTTP 字节才会返回。在 BFF 网关(如 DataWorks ACP / DQ_AGENT,
proxy_read_timeout约 60s)后面,较长的压缩推理(生产 trace 实测约 1.9 分钟)会在 60s 被网关以504 Gateway Timeout杀掉,到 qwen-code 侧表现为UnprocessableEntityError: 422。于是压缩失败、本轮 turn 报错、会话中断——尽管模型本身其实会成功。流式能解决:服务端会在数秒内返回 HTTP 200 和首个增量 chunk,每个 chunk 都会重置网关的读超时,慢推理因此不再触发 60s 上限。Reviewer Test Plan
如何验证
单测:
cd packages/core && npx vitest run src/core/baseLlmClient.test.ts src/utils/sideQuery.test.ts src/services/chatCompressionService.test.ts,预期全绿。新增用例断言:stream: true走generateContentStream、拼接多 chunk 增量、trim、过滤 thought part、并从最后一个 chunk 取 usage;省略stream时仍走非流式generateContent;runSideQuery仅在设置时转发该 flag;压缩以stream: true调用generateText。端到端(可选,需要网关):用 OpenAI 兼容端点 + 一个
proxy_read_timeout≤60s 的代理跑一个长会话,强制触发压缩,确认压缩请求现在以流式完成,而此前非流式路径会以 504/422 失败。证据(Before & After)
非用户可见改动(核心推理链路),无 UI before/after。本地单测:
Test Files 3 passed (3) · Tests 132 passed (132)。仓库门禁通过:npm run typecheck(全 workspace)与npm run lint均 exit 0;pre-commit(prettier+eslint --max-warnings 0)在 6 个改动文件上通过。测试平台
仅 macOS 本地验证;Windows / Linux 未本地测试,交由 CI 覆盖。
运行环境(可选)
仅单测(
vitest)+npm run typecheck/npm run lint,无需运行时/沙箱。风险与范围
OpenAIContentGenerator/QwenContentGenerator承接)走的是 OpenAI pipeline,该 pipeline 已把流包进按 chunk 计的空闲看门狗(withStreamInactivityTimeout,可经QWEN_STREAM_IDLE_TIMEOUT_MS/streamIdleTimeoutMs调节,0关闭)——所以「返回 200 后转入静默」的流在那里是有界的,而非旧的「全有或全无」非流式等待。原生USE_GEMINI/USE_ANTHROPIC流在 provider 层没有按 chunk 的空闲超时,但这正是主对话循环每一轮已经在消费的同一契约(geminiChat.ts迭代的是同一个原始generateContentStream),由 SDK 传输超时 +abortSignal兜底;本 side-query 复用了这个既有不变量,并未引入新的暴露面。整段流在retryWithBackoff内消费,中途失败会重试整个幂等请求,与非流式的重试语义一致。若要为 Gemini/Anthropic 加一个 provider 无关的空闲超时,应放在ContentGenerator层(这样主循环也受益),不在本 PR 范围内。thinkingConfig: { includeThoughts: false },且applyThinkingDefault把每个 side-query 默认设为该值(issue 里的thoughts_token_count=1来自较旧的 preview 构建)。「为压缩使用 fast model」是与超时无关的摘要质量 / 选模行为变更,留作可能的 follow-up(fastModel-reasoning 的更大话题见关联的 Disable reasoning on all fastModel side queries (follow-up to #3759) #3760)。上面的端到端网关复现未在本地执行。stream?: boolean是默认 falsy 的新增可选字段;所有现有文本模式runSideQuery调用方都不传它、保持非流式路径,JSON 模式 side-query 不受影响。关联 Issue
Closes #5861
🤖 Generated with Qwen Code