Skip to content

fix(core): clarify stream idle timeout guidance - #9896

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
yu-xin-c:codex/stream-idle-timeout-guidance
Aug 27, 2026
Merged

fix(core): clarify stream idle timeout guidance#9896
wenshao merged 4 commits into
QwenLM:mainfrom
yu-xin-c:codex/stream-idle-timeout-guidance

Conversation

@yu-xin-c

@yu-xin-c yu-xin-c commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Updates stream inactivity timeout errors to point users to both supported settings locations before the environment fallback, and adds a regression test proving that a registry-resolved streamIdleTimeoutMs: 0 remains intact.

Why it's needed

PR #9795 made explicit model and provider settings take precedence over QWEN_STREAM_IDLE_TIMEOUT_MS, but the timeout error still recommended only the environment variable. That advice can be ineffective when an explicit value is active. The missing zero-value case also left the registry merge vulnerable to a future truthiness regression.

Reviewer Test Plan

How to verify

  1. Run cd packages/core && npx vitest run src/core/openaiContentGenerator/pipeline.test.ts src/models/content-generator-config.test.ts --maxWorkers=1.
  2. Confirm a silent stream still throws retryable ETIMEDOUT, while the message names model.generationConfig.streamIdleTimeoutMs, modelProviders[providerId][].generationConfig.streamIdleTimeoutMs, and uses QWEN_STREAM_IDLE_TIMEOUT_MS only as the fallback.
  3. Confirm the registry-resolved model test preserves streamIdleTimeoutMs: 0 rather than falling back to the parent value.

Evidence (Before & After)

Before: with an explicit streamIdleTimeoutMs active, the error said only Set QWEN_STREAM_IDLE_TIMEOUT_MS to increase this window, even though that environment value would be overridden.

After: the error names both settings locations, explains when the environment fallback applies, and tells users to set the active value to 0 to disable the guard. The two affected test files pass with 181/181 tests.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Node.js 22.22.2 on macOS. Focused ESLint, workspace typecheck, and full build passed on the #9795 merge base. Repository preflight completed format, lint, build, and typecheck; its parallel test stage reported 29 CLI and 4 core timing/resource failures in unrelated files, and every originally failing test passed when rerun serially. After rebasing onto current main (including the #9888 build fix), the 181 affected tests and the full workspace typecheck pass again.

Risk & Scope

  • Main risk or tradeoff: The timeout message is longer so that its remediation stays correct for every configuration source.
  • Not validated / out of scope: Exact source-aware wording, streamMaxLifetimeMs, Windows/Linux runtime behavior, and a published-CLI E2E run (no global qwen executable is installed).
  • Breaking changes / migration notes: None. Runtime timeout selection and retry behavior are unchanged.

Linked Issues

Follow-up to #9795 and related to #5975. Addresses the two non-blocking review findings in R1-1 and R1-2.

中文说明

本 PR 的改动

更新流式响应无活动超时的错误提示:先指出两个受支持的设置位置,再说明环境变量回退;同时新增回归测试,证明从模型注册表解析出的 streamIdleTimeoutMs: 0 会被完整保留。

为什么需要

PR #9795 让显式模型设置和 provider 设置优先于 QWEN_STREAM_IDLE_TIMEOUT_MS,但超时错误仍只建议修改环境变量。当显式值生效时,这条建议可能完全不起作用。缺少零值用例也使注册表合并逻辑可能在未来被 truthiness 判断意外破坏。

审查测试计划

验证方法

  1. 运行 cd packages/core && npx vitest run src/core/openaiContentGenerator/pipeline.test.ts src/models/content-generator-config.test.ts --maxWorkers=1
  2. 确认静默流仍抛出可重试的 ETIMEDOUT,同时错误信息会列出 model.generationConfig.streamIdleTimeoutMsmodelProviders[providerId][].generationConfig.streamIdleTimeoutMs,并仅把 QWEN_STREAM_IDLE_TIMEOUT_MS 作为回退项。
  3. 确认注册表模型测试会保留 streamIdleTimeoutMs: 0,而不是退回父配置的值。

前后证据

修改前:即使显式 streamIdleTimeoutMs 已生效,错误仍只显示 Set QWEN_STREAM_IDLE_TIMEOUT_MS to increase this window,但该环境变量实际上会被覆盖。

修改后:错误信息会列出两个设置位置,说明环境变量何时作为回退生效,并提示将当前生效值设为 0 可禁用保护。两个相关测试文件共 181/181 条通过。

测试平台

OS 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

环境(可选)

macOS,Node.js 22.22.2。在 #9795 合并基线上,定向 ESLint、全工作区 typecheck 和完整 build 均通过。仓库 preflight 完成了 format、lint、build 和 typecheck;其并行测试阶段在无关文件中出现 29 条 CLI 与 4 条 core 时序/资源失败,所有原失败用例串行重跑后均通过。rebase 到包含 #9888 构建修复的当前 main 后,相关 181 条测试与全工作区 typecheck 再次全部通过。

风险与范围

  • 主要风险或取舍:为确保所有配置来源下的修复建议都正确,超时错误信息会更长。
  • 未验证 / 范围外:精确识别配置来源的动态文案、streamMaxLifetimeMs、Windows/Linux 运行时行为,以及已发布 CLI 的 E2E(本机未安装全局 qwen 命令)。
  • 破坏性变更 / 迁移说明:无。超时选择和重试行为均未改变。

关联问题

这是 #9795 的后续,并与 #5975 相关;处理了两个非阻塞审查意见:R1-1R1-2

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR! Clean follow-up to #9795.

Template looks good ✓

Problem: real and observed. Since #9795 (merged today), an explicit streamIdleTimeoutMs in model or provider settings wins over QWEN_STREAM_IDLE_TIMEOUT_MS — but the timeout error still told users to set only the env var, advice that silently does nothing when an explicit value is active. Verified in code: resolveStreamGuardMs in pipeline.ts gives the explicit config field precedence, so the old message was genuinely wrong guidance. This also addresses the two linked review threads (R1-1, R1-2) on #9795.

Direction: aligned — this is about remediation advice actually working for every configuration source. The reference CHANGELOG shows several recent stream-idle-timeout fixes, so the area is actively maintained and correct guidance matters.

Size: core paths touched. Production logic: 8 lines (5+/3- in pipeline.ts); tests: 31 lines; generated/schema: 0. Well under any threshold.

Approach: scope is right — the message rewrite (R1-1) and the zero-value regression test (R1-2) map one-to-one to the two review findings, and nothing else is in the diff. The new wording matches docs/users/configuration/settings.md exactly (model.generationConfig for runtime models, modelProviders[providerId][].generationConfig for provider-backed ones, env var as fallback, 0 disables), which I checked against the schema. No simpler path to suggest.

Risk: openaiContentGenerator is on this repo's high-risk path list (elevated post-merge revert correlation), so this gets full CI-evidence depth in the next stage before any approval. The production change is an error-message string only — timeout selection and retry behavior are untouched.

Moving on to code review. 🔍

中文说明

感谢贡献!这是对 #9795 的一次干净的后续修复。

模板完整 ✓

问题:真实且已观测。#9795(今日已合并)之后,模型或 provider 设置中显式的 streamIdleTimeoutMs 优先于 QWEN_STREAM_IDLE_TIMEOUT_MS,但超时错误仍然只建议用户设置环境变量——当显式值生效时,这条建议实际上不起作用。已在代码中核实:pipeline.tsresolveStreamGuardMs 确实让显式配置字段优先,所以旧的错误提示是错误引导。本 PR 同时处理了 #9795 上的两条关联审查意见(R1-1、R1-2)。

方向:对齐——目的是让修复建议在所有配置来源下都真正有效。参考 CHANGELOG 近期有多条 stream-idle-timeout 相关修复,说明该领域在活跃维护,正确的引导文案是有价值的。

规模:触及核心路径。生产逻辑 8 行(pipeline.ts 中 5+/3-);测试 31 行;生成/schema 文件 0 行。远低于任何阈值。

方案:范围合理——错误信息重写(R1-1)和零值回归测试(R1-2)与两条审查意见一一对应,diff 中没有其他内容。新文案与 docs/users/configuration/settings.md 完全一致(运行时模型用 model.generationConfig,provider 模型用 modelProviders[providerId][].generationConfig,环境变量作为回退,0 表示禁用),我已对照 schema 核实。没有更简的路径。

风险:openaiContentGenerator 在本仓库的高风险路径列表中(与合并后回滚相关性偏高),因此在给出任何批准前,下一阶段会要求完整的 CI 证据。生产改动仅是错误信息字符串——超时选择和重试行为均未改变。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 378d03bf83d7f1fa97a29ed6ac16553e00479365 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

Independent baseline first: from the title and motivation alone, I would rewrite the StreamInactivityTimeoutError message in pipeline.ts to point at the two documented settings locations ahead of the env fallback, update the exact-match assertion in pipeline.test.ts, and add the zero-value preservation test R1-2 asked for. The PR does exactly that — no more, no less.

No blockers found. Details worth noting:

  • The new wording is accurate. It matches the actual precedence in resolveStreamGuardMs (explicit config field > env knob > default) and docs/users/configuration/settings.md, which documents the same two locations — model.generationConfig for runtime models, modelProviders[providerId][].generationConfig for provider-backed ones, 0 disables. The message still uses the QWEN_STREAM_IDLE_TIMEOUT_MS_ENV constant, no hardcoded drift.
  • No orphaned references. The old "increase this window" phrase exists only in the two files the diff updates. All consumers of StreamInactivityTimeoutError classify by type or read idleMs/chunksReceived — nothing parses the message text, so retry/error-classification behavior is untouched (the PR's claim that timeout selection and retries are unchanged checks out).
  • The new zero-value test earns its place. applyResolvedModelConfig merges registry fields with a strict registryValue !== undefined check; the test proves a registry streamIdleTimeoutMs: 0 is copied through rather than falling back to the parent value. If that check ever degrades to truthiness, this test catches it — exactly the R1-2 concern.
  • Conventions are fine: tests colocated, no any in the production hunk, formatting consistent with the file.

Skipping the sequence diagram and the changed-files table — 3 files and 8 production lines leave nothing for them to add.

Testing evidence (this PR's own CI via API — no PR code executed here)

Check Conclusion
Test (ubuntu-latest, Node 22.x) ❌ failure
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ❌ failure
Test (macos-latest, Node 22.x) ⏭️ skipped
Test (windows-latest, Node 22.x) ⏭️ skipped
Integration Tests (CLI, No Sandbox) ⏭️ skipped
precheck-pr / precheck ✅ success
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04 / windows-2022) ✅ success
Secret scan (TruffleHog) ✅ success

Both red checks are pre-existing main breakage, not caused by this PR. They fail at tsc --build, before any test runs, with the identical error:

src/core/contentGenerator.test.ts(776,21): error TS2339: Property 'countTokens' does not exist on type 'ContentGenerator'.
Error: Command failed: tsc --build

Verified from git history rather than taken on faith: #9676 ("shrink the content generator interface", merged 08:30 today) removed countTokens from the interface, and #9888 (merged 09:24:26 — current main, 6a21c43) fixed exactly this test line. This PR's base is b5aec66 (09:11), roughly 20 seconds ahead of the fix, so it inherited the broken window. The failing file is not among the PR's 3 changed files.

Consequence: this PR's updated assertions have never executed in CI on this commit — the 181/181 pass figure in the description is the author's claim, not independently observed evidence. The macOS/Windows unit legs and the integration tests were skipped because the Linux leg failed first.

Sandboxed verification would settle the remaining gap after a rebase: once the branch picks up 6a21c43, the PR's own tests pin the whole claim (full-message equality plus zero preservation), which is sufficient for a message-wording change. The author lacks write access, so if a maintainer still wants runtime A/B proof of the rendered error, @qwen-code /verify is available as a sponsored run (pre-execution risk screen + full workspace wipe; read its report with the same skepticism as the fork's own CI logs).

Live-behavior testing is not driven on unattended CI runs — on this path the signal comes from the PR's CI and the lane above.

中文说明

代码审查

独立基线:仅凭标题和动机,我的方案就是重写 pipeline.tsStreamInactivityTimeoutError 的提示,先指向两个有文档记载的设置位置、再提环境变量回退,同时更新 pipeline.test.ts 的精确断言,并补上 R1-2 要求的零值保留测试。PR 的做法与此完全一致,没有多余内容。

未发现阻塞问题。要点:

  • 新文案准确。resolveStreamGuardMs 的实际优先级(显式配置字段 > 环境变量 > 默认值)以及 docs/users/configuration/settings.md 的记载一致;仍使用 QWEN_STREAM_IDLE_TIMEOUT_MS_ENV 常量,不会硬编码漂移。
  • 无遗留引用。 旧文案只出现在 diff 更新的两个文件中;所有错误消费方都按类型或字段处理,没有任何代码解析错误信息文本,重试与分类行为不受影响。
  • 新增零值测试有价值。 它钉住了 applyResolvedModelConfig 的严格 !== undefined 合并——注册表的 streamIdleTimeoutMs: 0 会被保留而不是回退到父值;将来若退化成 truthiness 判断,该测试会失败。
  • 规范无问题:测试同目录、生产代码无 any、格式与现有文件一致。

3 个文件、8 行生产改动,时序图和文件表没有可补充的信息,故省略。

测试证据(通过 API 读取本 PR 自身 CI——未在此执行任何 PR 代码)

两个红色检查均为 main 上已存在的构建破坏,并非本 PR 引起:它们在 tsc --build 阶段失败,错误相同(contentGenerator.test.ts:776 调用已被移除的 countTokens)。经 git 历史核实:#9676(今日 08:30 合并)移除了接口中的 countTokens#9888(09:24:26 合并,即当前 main 6a21c43)修复了该测试行;本 PR 的基线是 b5aec66(09:11),恰好早于修复约 20 秒,因此继承了这段破损窗口。失败文件不在本 PR 的 3 个改动文件之列。

后果:本 PR 更新的断言在该提交上从未被 CI 执行过——描述中 181/181 通过是作者自述,不是独立证据。rebase 到包含 6a21c43 的 main 后,PR 自带测试即可钉住全部声明(完整信息断言 + 零值保留),对文案类改动已足够;若维护者仍想要渲染错误的运行时 A/B 证明,可发起赞助式 @qwen-code /verify(作者无写权限;该运行带预执行风险筛查与工作区清理,报告应与 fork CI 日志同等审慎对待)。

无人值守 CI 不进行实机行为测试。

Qwen Code · qwen3.8-max

Reviewed at 378d03bf83d7f1fa97a29ed6ac16553e00479365 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review that would be a 4/5 on green CI; approval is held only because this head's CI is red on inherited main breakage and the PR's own tests have never executed here.

Stepping back: this is what a good follow-up PR looks like. The problem is real and verified in code (the old message recommended a knob that #9795 deliberately made second-best), the fix is the minimum that makes the advice correct for every configuration source, and the wording matches the docs character for character. The zero-value regression test is the kind that earns its keep — it pins a strict !== undefined merge that a future truthiness bug would silently break. No drive-by changes, no scope creep, nothing I'd maintain with a curse in six months. My independent proposal before reading the diff was the same change; there is no simpler path (a source-aware dynamic message would plumb config provenance into an error path for no real gain).

Why no approval yet: both unit-leg checks fail at tsc --build on a breakage that landed in main between #9676 and #9888 and is already fixed on current main — this branch's base missed the fix by about 20 seconds. Proven from git history and both job logs, so it is not the PR's fault. But it does mean the assertions this PR adds have never run in CI at this commit, and approving a red head would attest to tests that didn't execute. The fork-refactor guardrail doesn't apply (fix type, checked deterministically); this is purely about missing green evidence.

@yu-xin-c one mechanical step unblocks this: rebase onto current main (which contains 6a21c43, the #9888 fix for the unrelated countTokens build break). After that, CI will actually run your updated message assertions and the zero-value test, and this should be a straightforward approve — a maintainer can re-trigger @qwen-code /triage on the new head, or approve directly once the checks land green.

No deferred-approval marker on this run: CI already settled (red) on this head rather than still running, and the rebase moves the head — the green signal has to attach to the rebased commit.

中文说明

置信度:3/5 —— 审查本身干净,若 CI 为绿可给 4/5;暂不批准仅因为当前提交的 CI 因继承的 main 构建破坏而红,本 PR 自己的测试从未在此被执行。

整体来看:这是一个高质量后续 PR 的范本。问题真实且已在代码中核实(旧提示建议的环境变量在 #9795 之后已沦为次优配置来源),修复是让建议在所有配置来源下都正确的最小改动,文案与文档逐字一致。零值回归测试值得保留——它钉住了严格的 !== undefined 合并,未来任何 truthiness 退化都会被它抓住。没有顺手改动、没有范围蔓延。我在看 diff 之前独立构想的方案就是同样的改动,也没有更简路径(按配置来源动态生成文案会把配置溯源引入错误路径,得不偿失)。

为何暂不批准:两个单元检查都在 tsc --build 阶段失败,原因是 #9676#9888 之间落入 main 的构建破坏,当前 main 已修复——本分支基线恰好早于修复约 20 秒。已用 git 历史和两个 job 日志证实,责任不在本 PR。但这确实意味着本 PR 新增的断言在该提交上从未被 CI 执行,批准一个红色头部等于为未执行的测试背书。fork-refactor 护栏不适用(fix 类型,已确定性检查),此处纯粹是缺少绿色证据。

@yu-xin-c 只差一步机械操作:rebase 到当前 main(包含修复无关 countTokens 构建破坏的 6a21c43)。之后 CI 会真正执行你更新的信息断言和零值测试,应当可以直接批准——维护者可在新头部重新触发 @qwen-code /triage,或在检查变绿后直接批准。

本次不附延迟批准标记:该提交的 CI 已结束(红)而非仍在运行,且 rebase 会移动头部——绿色信号必须落在 rebase 后的提交上。

Qwen Code · qwen3.8-max

Reviewed at 378d03bf83d7f1fa97a29ed6ac16553e00479365 · re-run with @qwen-code /triage

@yu-xin-c
yu-xin-c force-pushed the codex/stream-idle-timeout-guidance branch from 378d03b to b75746e Compare August 24, 2026 09:48
@github-actions

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

@yu-xin-c

Copy link
Copy Markdown
Contributor Author

Rebased onto current main at 6a21c43 (including #9888), so the stale countTokens build failure is no longer in this branch. On new head b75746e, the two affected test files pass 181/181, and the full workspace npm run typecheck passes, including integration typechecking. Fresh CI is now running against the rebased commit.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

中文说明

已审查——无阻断问题。 建议见行内评论。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment thread packages/core/src/core/openaiContentGenerator/pipeline.ts Outdated
@yu-xin-c

Copy link
Copy Markdown
Contributor Author

Addressed the follow-up precedence suggestion in f438d4b and resolved the thread. The timeout hint now separates provider-backed and runtime-model configuration, leads with the provider path, states that provider configuration takes precedence, and uses the environment variable only when no explicit value is active. The two affected suites pass 181/181, Core typecheck and changed-file ESLint pass, and fresh CI is running.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

中文说明

已审查——无阻断问题。 建议见行内评论。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment thread packages/core/src/core/openaiContentGenerator/pipeline.ts Outdated
@yu-xin-c
yu-xin-c requested a review from qqqys as a code owner August 25, 2026 12:26
@yu-xin-c

Copy link
Copy Markdown
Contributor Author

Addressed the built-in Qwen OAuth configuration gap from round 2 in b643002 and resolved the thread. The hint now sends built-in OAuth users directly to QWEN_STREAM_IDLE_TIMEOUT_MS, while preserving provider-backed and runtime-model guidance. The affected suites still pass 181/181; Core typecheck, changed-file ESLint, Prettier, and diff checks pass. Fresh CI and automatic review are running on b643002.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Downgraded from Approve to Comment: CI failing: Test (windows-latest, Node 22.x). Reviewed.

Not explored to full depth (tool budget reached): "agent 6c": end-to-end runtime verification that syncAfterAuthRefresh actually routes the startup OAuth flow through applyResolvedModelDefaults (its body was unread whe…; "agent 1c": could not read PR #9896's existing comment thread ( gh token lacks read:org scope) — could not determine whether the OAuth phrasing was already discussed/res….

中文说明

⚠️ 已从批准降级为评论:CI failing: Test (windows-latest, Node 22.x)。 已审查。

未探索到全部深度(达到工具调用预算):"agent 6c"end-to-end runtime verification that syncAfterAuthRefresh actually routes the startup OAuth flow through applyResolvedModelDefaults (its body was unread whe…"agent 1c"could not read PR #9896's existing comment thread ( gh token lacks read:org scope) — could not determine whether the OAuth phrasing was already discussed/res…

— qwen3.8-max via Qwen Code /review (v0.22.0)

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅

中文说明

未发现问题。LGTM!✅

— qwen3.8-max via Qwen Code /review (v0.22.0)

@wenshao

wenshao commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Local verification on a real build — every remediation in the new message actually works

I built this PR locally and drove the real CLI against a live endpoint that answers a streaming request with SSE headers and then goes silent, so nothing but the client-side idle watchdog can end the turn. Verdict: all four claims in the new message reproduce, timeout behaviour is identical to the merge base, and the added test is a real regression guard. LGTM for merge. Four non-blocking observations at the end.

Setup
  • Worktree at PR head 8c7668d (merge of a770aef); npm run build && npm run bundledist/cli.js. The before arm is a second bundle built from the same tree with only packages/core/src/core/openaiContentGenerator/pipeline.ts restored to a770aef, so the two bundles differ in exactly this PR's source hunk.
  • Endpoint: a local server that answers POST /v1/chat/completions with 200 + text/event-stream, flushes headers, and then writes nothing (one variant wakes up after 12 s to complete the stream).
  • Judge: a server-side ledger recording response-start → client-abort per attempt, cross-checked against the No stream activity for Nms value the CLI itself prints. Retries make each arm three attempts; every attempt is listed.
  • macOS 15 (darwin 25.6.0), Node v24.18.1. QWEN_HOME isolated per arm; all *_PROXY vars stripped.

1. Does each location the message names actually change the effective timeout?

Arm Configuration QWEN_STREAM_IDLE_TIMEOUT_MS Expected CLI reported Server-side per attempt (ms)
A runtime model; model.generationConfig=3000 3000 3000ms 3013, 3002, 3002
B runtime model; model.generationConfig=8000 2000 8000 8000ms 8007, 8002, 8002
C runtime model; no explicit value 3000 3000 3000ms 3007, 3002, 3000
D provider entry =3000; model.generationConfig=15000 3000 3000ms 3009, 3005, 3003
E provider entry does not set the field; model.generationConfig=3000 7000 7000 7000ms 7009, 7003, 7004
F0 control: model.generationConfig=3000, endpoint wakes at 12 s 3000 3000ms 3020, 3003, 3004
F model.generationConfig=0, endpoint wakes at 12 s no timeout (none) 12010, 12004 → server-completed, model text printed
  • A / F0 — “For runtime models, increase model.generationConfig.streamIdleTimeoutMs” ✅
  • B — this is the bug the PR fixes: an explicit value of 8000 wins over QWEN_STREAM_IDLE_TIMEOUT_MS=2000, so the old message’s only advice would have had zero effect for this user. ✅
  • C — “or whenever no explicit value is active” ✅
  • D — “provider configuration takes precedence” ✅
  • E — “so model.generationConfig is ignored for those models” holds even for a field the provider entry doesn’t set: the env value wins, not the settings value. The CLI also prints its own startup warning here — Warning: model.generationConfig.streamIdleTimeoutMs is ignored for provider model "provider-test-model" from modelProviders.openai. Move this field to modelProviders.openai[].generationConfig …
  • F — “Set the active value to 0 to disable it” ✅, and F0 is the control proving the same endpoint would otherwise have tripped at 3 s.

verification matrix

2. Built-in Qwen OAuth models

End-to-end isn’t reachable (see observation 1), so I probed the real built core instead — seed the settings value, then select the model the way the CLI does:

runtime model  | resolver streamIdleTimeoutMs              3000
runtime model  | source        {"kind":"settings","settingsPath":"model.generationConfig.streamIdleTimeoutMs"}
qwen-oauth     | seeded from settings                      3000
qwen-oauth     | after selecting the built-in model        undefined      ← wiped
qwen-oauth     | source after selection                    {"kind":"modelProviders","authType":"qwen-oauth","modelId":"coder-model",...}
provider model | after selecting the provider model        5000

QWEN_OAUTH_MODELS carries no generationConfig, and applyResolvedModelDefaults overwrites every MODEL_GENERATION_CONFIG_FIELDS entry, so the settings value becomes undefined and the env var is indeed the only knob. The claim is accurate.

3. Before / after, in the real TUI

Same settings (model.generationConfig.streamIdleTimeoutMs = 3000), same endpoint, interactive session driven through a pty.

Before — merge base a770aef: the only advice is the env var, which this configuration overrides.

before

After — this PR: the user is pointed at the location that is actually live for them.

after

4. Behaviour is unchanged

The full 7-arm matrix was re-run on the before-bundle: identical reported idle values and per-attempt server-side timings (A 3010/3003/3003, B 8009/8002/8003, C 3006/3002/3001, D 3008/3002/3002, E 7008/7003/7001, F0 3012/3003/3004, F 12002/12001). This confirms the PR-body claim that runtime timeout selection and retry behaviour are untouched — only the string differs.

5. The added test has teeth

Mutating applyResolvedModelConfig in packages/core/src/models/content-generator-config.ts from registryValue !== undefined to a truthiness check:

Test file Result under the mutation
this PR’s content-generator-config.test.ts 1 failed / 18 passed — exactly should preserve a zero stream idle timeout from the registry
merge base’s content-generator-config.test.ts 18 passed — the regression would have shipped silently

And the mirror check: merge-base pipeline.ts + this PR’s pipeline.test.ts → 2 failed, so the new message text is pinned by both the exact-match and the toContain assertions.

6. Checks run

Check Result
PR’s stated command (pipeline.test.ts + content-generator-config.test.ts, --maxWorkers=1) 189 passed
Wider: src/models + src/core/openaiContentGenerator 25 files, 1120 passed
ESLint + Prettier on the three changed files clean
npm run build && npm run bundle (both arms) clean
Merge into current main (10c5b3e) no conflict; no drift in the touched files since a770aef

Non-blocking observations

  1. The Qwen OAuth sentence (144 chars) targets a path the CLI has refused since 2026-04-15. validateAuthMethod returns an error for AuthType.QWEN_OAUTH unconditionally (packages/cli/src/config/auth.ts:295), and it is called on both the interactive (gemini.tsx:579) and headless (validateNonInterActiveAuth.ts:40) startup paths. My arm with fabricated, unexpired cached credentials never issued a request — it exited 1 with “Qwen OAuth free tier was discontinued on 2026-04-15”. The sentence is still technically true for anything that reaches the core generator without passing that gate — §2 shows the mechanism — so this isn’t a correctness problem; it’s just the sentence with the least reach from the CLI today if the message length ever needs trimming.
  2. stream lifetime: 3001.0384169999998msperformance.now() - streamStartedAt is passed through raw (pipeline.ts:499; pre-existing, present in both arms and visible in both screenshots above). Since this PR is polishing exactly this string, a Math.round(...) would be a cheap readability win.
  3. “Set the active value to 0 to disable it” is only half the story. streamMaxLifetimeMs (default 900000) still caps the stream, which docs/users/configuration/settings.md already calls out but the error does not. I did not measure this (it needs a 15-minute run) — flagging it as a message/doc consistency point only.
  4. Cosmetic: the PR body says “181/181”; after the merge with main the same two files are 189/189.

For context on observation 1: for the provider-vs-settings case, the CLI already emits a targeted startup warning naming the exact fix (quoted in arm E). The timeout message grew from 149 to 529 characters; the parts that earn their keep in my runs are the provider-vs-runtime split and the “explicit value beats the env var” correction — both of which arm B and arm D show were genuinely wrong before.

中文版本

本地真实构建验证 —— 新错误信息里给出的每条修复建议都确实有效

我在本地构建了这个 PR,并让真实 CLI 去访问一个「先回 SSE 响应头、然后一直沉默」的本地端点,这样只有客户端的空闲看门狗能结束这一轮。结论:新信息中的四条论断全部复现,超时行为与合并基线完全一致,新增的测试也确实是一道有效的回归护栏。建议合入。 文末有四条非阻断性观察。

环境
  • worktree 位于 PR head 8c7668d(合并了 a770aef);npm run build && npm run bundle 产出 dist/cli.js修改前一臂是在同一棵树上、仅把 packages/core/src/core/openaiContentGenerator/pipeline.ts 还原到 a770aef 后重新构建的第二份 bundle,因此两份产物的差异恰好就是本 PR 的源码改动。
  • 端点:本地服务对 POST /v1/chat/completions 返回 200 + text/event-stream,flush 完响应头后不再写入任何内容(另有一个变体会在 12 秒后醒来把流补完)。
  • 判据:服务端台账记录每次尝试的「响应开始 → 客户端中止」耗时,并与 CLI 自己打印的 No stream activity for Nms 相互印证。重试会让每一臂发出三次请求,表中逐条列出。
  • macOS 15(darwin 25.6.0),Node v24.18.1。每一臂使用独立的 QWEN_HOME,并清空全部 *_PROXY

1. 信息里点名的每个配置位置,真的能改变生效的超时吗?

分组 配置 QWEN_STREAM_IDLE_TIMEOUT_MS 预期 CLI 实报 服务端逐次耗时(ms)
A runtime model;model.generationConfig=3000 3000 3000ms 3013, 3002, 3002
B runtime model;model.generationConfig=8000 2000 8000 8000ms 8007, 8002, 8002
C runtime model;未设显式值 3000 3000 3000ms 3007, 3002, 3000
D provider 条目 =3000model.generationConfig=15000 3000 3000ms 3009, 3005, 3003
E provider 条目设该字段;model.generationConfig=3000 7000 7000 7000ms 7009, 7003, 7004
F0 对照组:model.generationConfig=3000,端点 12 秒后醒 3000 3000ms 3020, 3003, 3004
F model.generationConfig=0,端点 12 秒后醒 不超时 (无) 12010, 12004 → server-completed,模型文本正常输出
  • A / F0 ——「For runtime models, increase model.generationConfig.streamIdleTimeoutMs」✅
  • B —— 这正是本 PR 要修的问题:显式值 8000 压过 QWEN_STREAM_IDLE_TIMEOUT_MS=2000,所以旧信息里唯一的那条建议对这类用户完全无效。✅
  • C ——「or whenever no explicit value is active」✅
  • D ——「provider configuration takes precedence」✅
  • E ——「so model.generationConfig is ignored for those models」即使 provider 条目根本没写这个字段也成立:最终生效的是环境变量而不是 settings 里的值。此时 CLI 还会打印自己的启动告警 —— Warning: model.generationConfig.streamIdleTimeoutMs is ignored for provider model "provider-test-model" from modelProviders.openai. Move this field to modelProviders.openai[].generationConfig …
  • F ——「Set the active value to 0 to disable it」✅,而 F0 作为对照证明同一个端点在未禁用时确实会在 3 秒触发。

2. 内置 Qwen OAuth 模型

端到端跑不通(见观察 1),因此改为直接探测真实构建出的 core:先把 settings 的值播种进去,再按 CLI 的方式选中该模型:

runtime model  | resolver streamIdleTimeoutMs              3000
runtime model  | source        {"kind":"settings","settingsPath":"model.generationConfig.streamIdleTimeoutMs"}
qwen-oauth     | seeded from settings                      3000
qwen-oauth     | after selecting the built-in model        undefined      ← 被清掉
qwen-oauth     | source after selection                    {"kind":"modelProviders","authType":"qwen-oauth","modelId":"coder-model",...}
provider model | after selecting the provider model        5000

QWEN_OAUTH_MODELS 没有携带 generationConfig,而 applyResolvedModelDefaults 会覆盖 MODEL_GENERATION_CONFIG_FIELDS 中的每一个字段,所以 settings 里的值变成 undefined,环境变量确实是唯一的旋钮。这条论断准确。

3. 真实 TUI 的前后对比

相同配置(model.generationConfig.streamIdleTimeoutMs = 3000)、相同端点,通过 pty 驱动交互式会话。

修改前 —— 合并基线 a770aef 唯一的建议是环境变量,而该配置恰恰会覆盖它。

修改后 —— 本 PR: 用户被指向对自己真正生效的那个位置。

(截图见英文版,两张图对应同一次运行。)

4. 行为没有变化

同一套 7 组矩阵在「修改前」bundle 上重跑,得到完全一致的实报超时值和逐次服务端耗时(A 3010/3003/3003、B 8009/8002/8003、C 3006/3002/3001、D 3008/3002/3002、E 7008/7003/7001、F0 3012/3003/3004、F 12002/12001)。这印证了 PR 描述中「超时选择与重试行为均未改变」的说法 —— 变的只有文案。

5. 新增测试确有辨别力

packages/core/src/models/content-generator-config.ts 里的 applyResolvedModelConfigregistryValue !== undefined 变异成真值判断:

测试文件 变异后的结果
本 PR 的 content-generator-config.test.ts 1 条失败 / 18 条通过 —— 恰好是 should preserve a zero stream idle timeout from the registry
合并基线的 content-generator-config.test.ts 18 条全通过 —— 这个回归会被悄悄放行

反向检查同样成立:基线 pipeline.ts + 本 PR 的 pipeline.test.ts → 2 条失败,说明新文案被精确匹配与 toContain 两类断言同时钉住。

6. 已跑的检查

检查项 结果
PR 给出的命令(pipeline.test.ts + content-generator-config.test.ts--maxWorkers=1 189 条通过
更大范围:src/models + src/core/openaiContentGenerator 25 个文件、1120 条通过
三个改动文件的 ESLint + Prettier 干净
npm run build && npm run bundle(两臂各一次) 干净
合入当前 main10c5b3e 无冲突;自 a770aef 起相关文件无漂移

非阻断性观察

  1. 关于 Qwen OAuth 的那一句(144 字符)指向的是 CLI 自 2026-04-15 起就已拒绝的路径。 validateAuthMethodAuthType.QWEN_OAUTH 无条件返回错误(packages/cli/src/config/auth.ts:295),而交互式(gemini.tsx:579)与 headless(validateNonInterActiveAuth.ts:40)两条启动路径都会调用它。我用伪造的、未过期的缓存凭据跑的那一臂根本没发出请求 —— 直接以「Qwen OAuth free tier was discontinued on 2026-04-15」退出(exit 1)。对于任何不经过这道闸门就抵达 core 生成器的调用方,这句话在技术上仍然成立(机制见 §2),所以不算正确性问题;只是就 CLI 而言,如果将来需要压缩信息长度,它是当下覆盖面最小的一句。
  2. stream lifetime: 3001.0384169999998ms —— performance.now() - streamStartedAt 被原样拼进文案(pipeline.ts:499;这是既有问题,两臂都有,上面两张截图里都能看到)。既然本 PR 正是在打磨这条字符串,加一个 Math.round(...) 是很划算的可读性改进。
  3. 「Set the active value to 0 to disable it」只讲了一半。 streamMaxLifetimeMs(默认 900000)仍然会给流封顶,这一点 docs/users/configuration/settings.md 已经说明,但错误信息里没有。这条我没有实测(需要跑满 15 分钟),仅作为文案与文档一致性的提醒。
  4. 小问题:PR 描述写的是「181/181」,在与 main 合并之后同样两个文件现在是 189/189

关于观察 1 的补充背景:对于 provider 与 settings 冲突这一情形,CLI 本来就会打印一条点名了确切修法的启动告警(见 E 组引用)。这条超时信息从 149 字符增长到了 529 字符;在我的实测中真正物有所值的部分是 provider / runtime 的区分,以及「显式值压过环境变量」这一纠正 —— B 组和 D 组恰恰证明了这两点在改动前确实是错的。

@wenshao
wenshao added this pull request to the merge queue Aug 27, 2026
Merged via the queue into QwenLM:main with commit ff1b691 Aug 27, 2026
57 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.3.

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.

3 participants