fix(core): support per-provider stream idle timeout - #9795
Conversation
|
✅ Qwen Triage finished — CI landed green on ✅ Qwen Triage 已完成 —— |
|
Thanks for the PR! Template looks good ✓ Problem: observed bug, not theoretical. #5975 reports Direction: aligned — per-provider timeout granularity is a natural fit for configs that mix fast cloud routes with a slow local server, and the PR explicitly scopes itself as the follow-up to #5975. CHANGELOG: no direct reference, but the area is clearly relevant. Size: core paths touched ( Approach: minimal and idiomatic. The production change is one field added to the existing Risk: no elevated risk signals — none of the changed files match the high-risk (revert-correlated) path list. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题: 是已观测到的 bug,不是理论性问题。#5975 报告在 OpenAI-compatible provider 上 100% 出现 方向: 对齐——对混合了快速云端路由和慢速本地服务的配置来说,按 provider 的超时粒度是自然的方案,且 PR 明确把自己定位为 #5975 的后续。CHANGELOG:无直接引用,但该领域明显相关。 规模: 触及核心路径( 方案: 最小且符合惯例。生产改动仅是向现有的 风险: 无升级风险信号——改动文件均未命中高风险(与 revert 相关的)路径。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewApproach matches what I'd have proposed: don't write new plumbing, just teach the existing generic machinery about the field. I traced every consumer of
No correctness, security, or convention findings. The change is the minimal wiring: 15 production lines, with the behavior itself already exercised by existing pipeline tests (disabled at Test evidence — the PR's own CI
Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 The PR body reports Sandboxed verification would settle the behavioural claim: 中文说明代码审查方案与我独立想到的做法一致:不写新的管道代码,只让现有的通用机制认识这个字段。我追踪了
没有正确性、安全性或规范问题。改动是最小接线:15 行生产代码,行为本身已由现有 pipeline 测试覆盖( 测试证据 —— PR 自己的 CI
PR 正文报告了三文件测试(153 通过)、pipeline 流空闲子集(34 通过)、 沙盒验证可以敲定行为性结论: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — clean, minimal wiring of a clearly-needed setting with every propagation path traced; the only thing missing is CI signal, not code confidence. Stepping back: this is the kind of PR that's easy to vouch for. The problem is a real, 100%-reproducible blocker from #5975 — users on queued local OpenAI-compatible servers can't relax the idle timeout without relaxing it for every provider — and the PR does exactly the follow-up that issue asks for, nothing more. Fifteen production lines that reuse the existing generic field machinery instead of adding parallel plumbing; the runtime semantics ( The reservation, plainly stated: this commit has zero CI evidence — the fork's workflow runs are waiting on maintainer approval, and the test numbers in the PR body are the author's word. The code review stands on its own, but approval should wait for the suite. Approval deferred until CI lands green on 中文说明置信度:4/5 —— 干净、最小的接线,设置项明显有必要,每条传播路径都已追踪;唯一缺的是 CI 信号,而不是对代码的信心。 退一步看:这是一个容易背书的 PR。问题是 #5975 中真实的、100% 可复现的阻塞——在排队的本地 OpenAI-compatible 服务上的用户无法只放宽某一个 provider 的空闲超时——而此 PR 做的正是该 issue 所期望的后续方案,不多不少。15 行生产代码复用了现有的通用字段机制,而不是另起炉灶;运行时语义( 明确说出保留意见:该提交没有任何 CI 证据——fork 的工作流运行正等待维护者批准,PR 正文中的测试数字是作者的自述。代码审查本身站得住,但批准应等测试套件跑完。 批准已推迟,等待 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
Test Plan (not a blocker): src/models/modelConfigResolver.test.ts — no such file or directory; src/models/modelRegistry.test.ts — no such file or directory; src/models/content-generator-config.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/config/settingsSchema.test.ts — no such file or directory; and 3 more.
中文说明
已审查——无阻断问题。 建议见行内评论。
Test Plan(非阻断):src/models/modelConfigResolver.test.ts — no such file or directory; src/models/modelRegistry.test.ts — no such file or directory; src/models/content-generator-config.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/config/settingsSchema.test.ts — no such file or directory; and 3 more。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| streamIdleTimeoutMs: { | ||
| type: 'integer', | ||
| label: 'Stream Idle Timeout', |
There was a problem hiding this comment.
[Suggestion] R1-1: The settings knob this PR adds takes precedence over QWEN_STREAM_IDLE_TIMEOUT_MS (per resolveStreamGuardMs in packages/core/src/core/openaiContentGenerator/pipeline.ts and the docs added here), but the StreamInactivityTimeoutError message in that same file (lines 192-196) still tells users to set the env var. That advice is silently ineffective whenever the timeout came from settings/modelProviders: an operator who sets model.generationConfig.streamIdleTimeoutMs: 300000 (the exact configuration the new docs recommend) and then hits a long SSE silence gets "Set QWEN_STREAM_IDLE_TIMEOUT_MS to increase this window (or 0 to disable it)" on every retry attempt; exporting that env var (even to 0) changes nothing because the explicit config field wins, and nothing hints that a settings value is overriding it. In a daemon deployment this turns a one-step fix into an open-ended debugging session.
Evidence: at the reviewed commit, npx vitest run src/core/openaiContentGenerator/pipeline.test.ts -t "stream is silent past the idle timeout|explicit streamIdleTimeoutMs config take precedence" in packages/core → Tests 2 passed | 158 skipped (160) — one test asserts the env-only advice string fires from an explicit-config timeout, and the other proves config beats env (env stubbed 1000 vs config 5000: the guard does not trip at 1000 ms and trips at 5000 ms).
Fix location: packages/core/src/core/openaiContentGenerator/pipeline.ts (~line 192) — name the settings knobs in the message, or make it source-aware (the pipeline resolves config.streamIdleTimeoutMs in its constructor and knows whether it was explicit):
super(
`No stream activity for ${idleMs}ms after ${chunksReceived} chunks ` +
`(stream lifetime: ${streamLifetimeMs}ms). Set ` +
`model.generationConfig.streamIdleTimeoutMs (or the modelProviders ` +
`entry's generationConfig.streamIdleTimeoutMs) — or ` +
`${QWEN_STREAM_IDLE_TIMEOUT_MS_ENV} when no settings value is set — ` +
`to increase this window (or 0 to disable it).`,
);中文说明
本 PR 新增的设置项优先级高于 QWEN_STREAM_IDLE_TIMEOUT_MS(见 packages/core/src/core/openaiContentGenerator/pipeline.ts 中的 resolveStreamGuardMs 以及本 PR 新增的文档),但同一文件中的 StreamInactivityTimeoutError 错误信息(第 192-196 行)仍然只提示用户设置环境变量。当超时值来自 settings/modelProviders 时,该提示会静默失效:运维人员按新文档推荐设置 model.generationConfig.streamIdleTimeoutMs: 300000 后,若遇到较长的 SSE 静默,每次重试都会看到 "Set QWEN_STREAM_IDLE_TIMEOUT_MS to increase this window (or 0 to disable it)";此时导出该环境变量(即使设为 0)也不会有任何效果,因为显式配置优先于环境变量,而错误信息中没有任何线索表明有设置值在覆盖它。在守护进程部署场景下,这会把一步就能解决的问题变成无期限的排查。
证据:在被审提交上运行 npx vitest run src/core/openaiContentGenerator/pipeline.test.ts -t "stream is silent past the idle timeout|explicit streamIdleTimeoutMs config take precedence"(位于 packages/core)→ Tests 2 passed | 158 skipped (160) —— 其中一个测试断言仅提及环境变量的提示文案会在显式配置超时场景下出现,另一个测试证明配置优先于环境变量(env 设为 1000、配置为 5000:守卫不在 1000 ms 触发,而在 5000 ms 触发)。
修复位置:packages/core/src/core/openaiContentGenerator/pipeline.ts(约第 192 行)—— 在错误信息中同时提及设置项,或让提示感知来源(pipeline 在构造函数中解析 config.streamIdleTimeoutMs,知道该值是否为显式配置),参考上方代码块。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| samplingParams: { temperature: 0.5 }, | ||
| streamIdleTimeoutMs: 600000, | ||
| contextWindowSize: 200000, |
There was a problem hiding this comment.
[Suggestion] R1-2: The registry-overlay path in applyResolvedModelConfig (packages/core/src/models/content-generator-config.ts, guarded by registryValue !== undefined) never receives a falsy registry value in any test. The documented streamIdleTimeoutMs: 0 disable case is pinned only in modelConfigResolver.test.ts, which exercises a different function (resolveGenerationConfig, guarded by field in). The code is correct today, but the one-line mutation registryValue !== undefined → registryValue survives the entire relevant test population green (measured: 153/153 pass under the mutation), which would silently drop a provider's 0 so agent content generators fall back to the env var / 240 s default and abort streams the operator explicitly told to leave unbounded.
Evidence (mutation probe in an isolated scratch tree): baseline 18/18; with the mutation, 153/153 passed across content-generator-config.test.ts + modelConfigResolver.test.ts + modelRegistry.test.ts; adding the streamIdleTimeoutMs: 0 case below fails with expected undefined to be +0 under the mutation (the probe flips) and passes against the real code.
Suggested fix — pin the falsy branch with a sibling case in the with registry-resolved model describe block:
it('should apply a falsy registry-resolved streamIdleTimeoutMs', () => {
const config = createMockConfig(parentConfig, {
...resolvedModel,
generationConfig: {
...resolvedModel.generationConfig,
streamIdleTimeoutMs: 0,
},
});
const result = buildAgentContentGeneratorConfig(
config,
'registry-model-id',
{ authType: 'anthropic' },
);
expect(result.streamIdleTimeoutMs).toBe(0);
});中文说明
applyResolvedModelConfig 中的 registry 覆盖路径(packages/core/src/models/content-generator-config.ts,守卫条件为 registryValue !== undefined)没有任何测试覆盖 falsy 值。文档中记载的 streamIdleTimeoutMs: 0 禁用场景只在 modelConfigResolver.test.ts 中被固定,而该文件测试的是另一个函数(resolveGenerationConfig,守卫条件为 field in)。代码目前是正确,但单行变异 registryValue !== undefined → registryValue 可以在全部相关测试保持绿色的情况下存活(实测:变异后 153/153 通过),这会静默丢弃 provider 配置的 0,导致 agent 内容生成器回退到环境变量 / 240 秒默认值,从而中止运维人员明确要求不设上限的流。
证据(在隔离 scratch tree 中进行的变异探针):基线 18/18;应用变异后,content-generator-config.test.ts + modelConfigResolver.test.ts + modelRegistry.test.ts 共 153/153 通过;加入下方的 streamIdleTimeoutMs: 0 用例后,在变异代码上以 expected undefined to be +0 失败(探针翻转),在真实代码上通过。
建议修复 —— 在 with registry-resolved model describe 块中新增一个同级用例来固定 falsy 分支(见上方代码块)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
✅ PR 9795 Local Validation ReportI built a real local verification environment for this PR and ran both the targeted unit tests and an end-to-end SSE scenario against a local OpenAI-compatible server. Environment
Unit tests (all passed)cd packages/core && npx vitest run src/models/modelConfigResolver.test.ts src/models/modelRegistry.test.ts src/models/content-generator-config.test.ts
# Test Files 3 passed (3)
# Tests 153 passed (153)
cd packages/core && npx vitest run src/core/openaiContentGenerator/pipeline.test.ts -t "stream inactivity timeout"
# Test Files 1 passed (1)
# Tests 34 passed | 126 skipped (160)
cd packages/cli && npx vitest run src/config/settingsSchema.test.ts
# Test Files 1 passed (1)
# Tests 43 passed (43)Real-world SSE verificationA local OpenAI-compatible SSE server was configured to send one Case 1 — provider
|
|
Released in v0.22.2. |


What this PR does
This exposes
streamIdleTimeoutMsas a model-scoped generation setting, includingmodelProviders[].generationConfig, and carries it through model registry resolution and agent configuration. It also adds the generated settings schema entry, regression coverage for provider precedence and provider switching, and documentation for the idle/lifetime guard interaction.Why it's needed
The global
QWEN_STREAM_IDLE_TIMEOUT_MSvariable is too broad when one Qwen Code configuration mixes responsive cloud providers with a queued local OpenAI-compatible server. Local queue wait plus prompt prefill can legitimately produce several minutes of SSE silence, while repeated watchdog retries re-enter the queue and add load. A per-provider setting keeps the stricter global default for cloud routes without making a slow local route unusable.Reviewer Test Plan
How to verify
generationConfig.streamIdleTimeoutMs: 1000and set the globalQWEN_STREAM_IDLE_TIMEOUT_MS=100.[DONE].npx vitest run src/models/modelConfigResolver.test.ts src/models/modelRegistry.test.ts src/models/content-generator-config.test.tsfrompackages/core(153 passed),npx vitest run src/core/openaiContentGenerator/pipeline.test.ts -t "stream inactivity timeout"frompackages/core(34 passed), andnpx vitest run src/config/settingsSchema.test.tsfrompackages/cli(43 passed).npm run preflightwith the repository's Node 22 toolchain. Clean/install, formatting, lint, build, and typecheck pass. The parallel full-test stage reaches an existingAuthDialogMiniMax keyboard-navigation failure; it reproduces identically on a clean detached worktree at base commit431a0bd. The original server failure passes in the three-file rerun, the two transient server assertions from that rerun pass 2/2 when targeted, andworkspace-agents.test.tspasses 49/49.Evidence (Before & After)
N/A — no visual UI change. In the local mock-SSE probe, the control configuration exited 1 with
No stream activity for 100ms after 1 chunks; the same request with the provider override exited 0 and printedprovider timeout works.Tested on
Environment (optional)
macOS arm64, Node.js v22.22.2, local source build, and a mock OpenAI-compatible SSE endpoint.
Risk & Scope
0through the platform timer maximum.0disables only the idle guard, not the separate total-lifetime guard.Linked Issues
Addresses the per-provider follow-up in #5975.
中文说明
此 PR 做了什么
此 PR 将
streamIdleTimeoutMs暴露为模型级生成配置,包括modelProviders[].generationConfig,并让该字段完整经过模型注册表解析和 Agent 配置链路。同时补充生成的 settings schema、provider 优先级与切换行为的回归测试,以及空闲守卫和总生命周期守卫交互关系的文档。为什么需要
当同一份 Qwen Code 配置同时包含响应较快的云端 provider 和需要排队的本地 OpenAI-compatible 服务时,全局
QWEN_STREAM_IDLE_TIMEOUT_MS过于粗粒度。本地服务的排队等待加上长提示词 prefill 可能合理地产生数分钟 SSE 静默,而 watchdog 的重复重试会重新进入队列并增加负载。按 provider 配置后,云端路由仍可保留更严格的全局默认值,同时允许较慢的本地路由正常工作。审阅者测试计划
验证方法
generationConfig.streamIdleTimeoutMs: 1000,同时设置全局QWEN_STREAM_IDLE_TIMEOUT_MS=100。[DONE]。packages/core运行npx vitest run src/models/modelConfigResolver.test.ts src/models/modelRegistry.test.ts src/models/content-generator-config.test.ts(153 项通过),在packages/core运行npx vitest run src/core/openaiContentGenerator/pipeline.test.ts -t "stream inactivity timeout"(34 项通过),并在packages/cli运行npx vitest run src/config/settingsSchema.test.ts(43 项通过)。npm run preflight。清理/安装、格式化、lint、构建和类型检查均通过。并行全量测试阶段命中一条现有的AuthDialogMiniMax 键盘导航失败;该失败可在基准提交431a0bd的干净 detached worktree 中完全一致地复现。原始 server 失败在三文件复跑中通过,该次复跑新增的两条瞬时 server 失败在定向运行时 2/2 通过,workspace-agents.test.ts也以 49/49 通过。前后证据
N/A — 没有可视化 UI 变化。在本地 mock SSE 探针中,对照配置以退出码 1 结束,并输出
No stream activity for 100ms after 1 chunks;加入 provider 覆盖后,同一请求以退出码 0 结束并输出provider timeout works。测试平台
环境(可选)
macOS arm64、Node.js v22.22.2、本地源码构建,以及 mock OpenAI-compatible SSE 端点。
风险与范围
0到平台定时器上限的整数。0只禁用空闲守卫,不会禁用独立的总生命周期守卫。关联 Issue
处理 #5975 中按 provider 配置的后续需求。