Skip to content

feat(providers): load model recommendations before editing - #9980

Merged
wenshao merged 11 commits into
QwenLM:mainfrom
qqqys:feat/provider-model-discovery-v2
Aug 26, 2026
Merged

feat(providers): load model recommendations before editing#9980
wenshao merged 11 commits into
QwenLM:mainfrom
qqqys:feat/provider-model-discovery-v2

Conversation

@qqqys

@qqqys qqqys commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This replaces #9389 with a bounded, snapshot-before-editing design. Token Plan and Coding Plan opt in to one authenticated OpenAI-compatible model-list request when the setup wizard reaches Model IDs. The wizard shows a cancellable loading state and does not mount the editor until the request has either produced a usable catalog or fallen back, so a late network result can never overwrite text under the cursor.

Successful standard { data: [{ id }] } responses are filtered for chat-capable model IDs and merged with the built-in recommendations. Known IDs keep all curated capability metadata and stable built-in order; newly served IDs are appended without invented metadata and start unchecked; built-in IDs absent from the catalog are no longer recommended; user-entered custom IDs remain in the free-form input. Edits stay local to that visit and are committed only on Enter, so Esc discards unsubmitted changes and re-entry creates a fresh snapshot.

Every missing-key, timeout, cancellation, redirect, non-2xx, malformed, empty, or all-non-chat result falls back to the complete built-in list with an inline notice. The request has a five-second whole-transfer budget, a one-megabyte response cap, same-host redirect enforcement, and no logging or persistent cache. All other providers retain the existing behavior.

Why it's needed

The ModelStudio plan catalogs change independently of Qwen Code releases. Static recommendations can therefore continue offering retired IDs or omit models an account can already use, and selecting a stale recommendation persists a broken entry whose failure appears only on the first request. Loading a bounded account snapshot at the one point where recommendations are edited closes that drift without adding a background refresh or a second source of persistent state.

Reviewer Test Plan

How to verify

Open /auth, choose Alibaba ModelStudio and then Token Plan or Coding Plan, enter a valid key, and continue to Model IDs. Expect a loading-only state first, followed by Recommended models · from the provider; the editor must not appear before the catalog settles. With an account catalog that omits a built-in ID and adds a new ID, expect the omitted ID to disappear, the new ID to be shown unchecked, existing custom IDs to remain in the input, and Enter to persist only the custom IDs plus checked known recommendations. Press Esc during loading or editing and re-enter to confirm a fresh snapshot with no unsubmitted edit retained. With a syntactically valid bad key or an offline endpoint, expect Recommended models · provider list unavailable, showing built-ins and the complete static list.

Automated verification passed: the focused core tests cover 84 cases, including 12 discovery cases; the rendered setup-step tests cover 10 cases; full build, full workspace typecheck, focused ESLint, pre-commit Prettier, and pre-commit ESLint all pass.

Evidence (Before & After)

The global qwen binary is not available in this checkout environment, so the TUI evidence below comes from the real rendered component frames asserted by the focused tests rather than a live authenticated account.

Before:

Recommended models
◉ MiniMax-M3
◉ MiniMax-M2.7

After a successful snapshot:

Recommended models · from the provider
◉ MiniMax-M3
○ MiniMax-M4
○ custom-model

After fallback:

Recommended models · provider list unavailable, showing built-ins
◉ MiniMax-M3
◉ MiniMax-M2.7

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS, Node.js 22, mocked provider responses plus rendered Ink component tests. No authenticated live provider key was stored or used.

Risk & Scope

  • Main risk or tradeoff: entering Model IDs now makes one authenticated request for the two opted-in presets. It is bounded, cancellable, same-host only, never logged, and always degrades to the built-in list.
  • Not validated / out of scope: an authenticated live Token Plan catalog was not probed in this environment; manual refresh, persistent caching, the broader /model integration from feat: Add dynamic multi-model support for OpenAI-compatible APIs #1206, and static catalog curation remain separate work. Name-based non-chat filtering is necessarily heuristic, and any filtered ID remains available through free-form input.
  • Breaking changes / migration notes: none. The provider capability is optional and no settings format changes.

Linked Issues

Closes #9368

中文说明

这个 PR 做了什么

这个 PR 用“编辑前先确定快照”的受限方案替代 #9389。Token Plan 和 Coding Plan 选择加入:配置向导进入 Model IDs 时,只发起一次带认证的 OpenAI 兼容模型列表请求。向导先显示可取消的加载状态,在请求得到可用目录或完成回退前不挂载编辑器,因此再晚到达的网络结果也不可能覆盖光标下的文本。

成功的标准 { data: [{ id }] } 响应会先过滤出可聊天模型 ID,再与内置推荐合并。已知 ID 保留全部人工维护的能力元数据和稳定的内置顺序;服务端新增 ID 追加展示,不虚构元数据且默认不勾选;目录中缺失的内置 ID 不再推荐;用户手输的自定义 ID继续保留在自由输入框中。编辑只在本次访问的局部状态中发生,按 Enter 才提交,所以 Esc 会丢弃未提交修改,重新进入会创建新的快照。

缺少 key、超时、取消、重定向、非 2xx、响应格式错误、空目录或过滤后全是非聊天模型时,全部回退到完整内置列表并显示行内提示。请求采用 5 秒全传输预算、1 MiB 响应上限和同 host 重定向限制,不记录日志,也不做持久缓存。其他 provider 维持现有行为。

为什么需要

ModelStudio 套餐目录的变化不依赖 Qwen Code 发版。因此静态推荐可能继续提供已退役 ID,或漏掉账号已经可用的模型;用户选中陈旧推荐后,会把坏条目持久化,直到第一次请求才看到错误。在唯一需要编辑推荐的步骤加载一次有界账号快照,可以消除这类漂移,同时不引入后台刷新或第二套持久状态。

Reviewer 测试计划

如何验证

打开 /auth,选择 Alibaba ModelStudio,再选择 Token Plan 或 Coding Plan,输入有效 key 并进入 Model IDs。应先只看到加载状态,随后看到 Recommended models · from the provider;目录确定前编辑器不能出现。使用一个会缺少某个内置 ID、同时包含新 ID 的账号目录时,缺失的 ID 应消失,新 ID 应显示为未勾选,现有自定义 ID 应继续留在输入框中,按 Enter 后只持久化自定义 ID 与已勾选的已知推荐。加载或编辑期间按 Esc,再重新进入,应得到新快照且不保留未提交编辑。使用格式合法但错误的 key 或离线端点时,应看到 Recommended models · provider list unavailable, showing built-ins 和完整静态列表。

自动验证已通过:core 聚焦测试覆盖 84 个用例,其中 12 个是发现逻辑用例;渲染后的配置步骤测试覆盖 10 个用例;全量 build、全 workspace typecheck、聚焦 ESLint、pre-commit Prettier 和 pre-commit ESLint 均通过。

证据(Before & After)

当前检出环境中没有全局 qwen 可执行文件,因此下面的 TUI 证据来自聚焦测试所断言的真实组件渲染帧,而不是带真实账号认证的在线截图。

之前:

Recommended models
◉ MiniMax-M3
◉ MiniMax-M2.7

成功获得快照后:

Recommended models · from the provider
◉ MiniMax-M3
○ MiniMax-M4
○ custom-model

回退后:

Recommended models · provider list unavailable, showing built-ins
◉ MiniMax-M3
◉ MiniMax-M2.7

测试平台

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS、Node.js 22、模拟 provider 响应与 Ink 组件渲染测试。未存储或使用带认证的在线 provider key。

风险与范围

  • 主要风险或取舍:两个选择加入的 preset 在进入 Model IDs 时会新增一次带认证请求。该请求有界、可取消、只允许同 host、不记录日志,并且始终能降级到内置列表。
  • 未验证或超出范围:当前环境未探测带认证的 Token Plan 在线目录;手动刷新、持久缓存、feat: Add dynamic multi-model support for OpenAI-compatible APIs #1206 的更广泛 /model 集成以及静态目录维护仍是独立工作。基于名称的非聊天过滤必然是启发式,被过滤的 ID 仍可通过自由输入框使用。
  • 破坏性变更或迁移说明:无。provider 能力是可选字段,设置格式不变。

关联 Issue

Closes #9368

@qqqys

qqqys commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

E2E / behavioral verification report

Exact verified head: 08c1c85fe82216ce58902b8a51eeaca1e6447aa9.

Baseline on upstream/main (8be07151f69641520e9cf692c8190bf094d25588): the setup-step tests passed 6/6 and the provider configuration plus two Alibaba preset suites passed 72/72. The existing Model IDs editor mounted immediately with static recommendations.

Final-head automated verification:

  • Core discovery/configuration/preset tests: 84/84 passed, including successful standard payload parsing, metadata merge, stable order, deduplication, non-chat filtering, malformed/empty/non-2xx/redirect/network fallback, missing credentials, and cancellation wiring.
  • Rendered setup-step tests: 10/10 passed, covering the loading-only state, successful immutable snapshot, absent built-in removal, unknown-model unchecked state, custom-ID preservation, submit-only commit, static fallback, and abort on unmount.
  • npm run build: passed across all workspaces.
  • npm run typecheck: passed across all workspaces and integration tests.
  • Focused ESLint plus the pre-commit Prettier/ESLint gate: passed.
  • git diff --check: passed; two consecutive full diff self-audits were clean.

Manual limitation: this checkout has no global qwen executable and no operator-supplied ModelStudio key, so no authenticated live TUI session or Token Plan endpoint probe was performed. No credential was stored. The user-visible frames in the PR description are the actual Ink render output asserted by the focused test.

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 25, 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 — this is a much tighter take than #9389, and it shows.

  • Template: complete, including the bilingual summary ✓
  • Problem: real and evidenced. Linked issue feat(providers): dynamically fetch model list for ModelStudio Token Plan / Coding Plan presets #9368 (P2, roadmap/model-inference) documents the drift observed on CLI 0.21.12 — retired IDs still recommended, the current qwen3.8-max missing from the list, and stale selections getting persisted into settings. Observed problem, not a theoretical one.
  • Direction: aligned. Fetching the account's own catalog at the one point where recommendations are edited is what the issue asks for, and the area is active elsewhere — claude-code shipped /v1/models discovery for its model picker and later moved it behind an opt-in flag, a useful signal that the default-on behavior deserves attention. One caveat: this lands in the auth wizard and changes model-selection recommendations, which is maintainer-territory surface. Flagging it for maintainer awareness — the review continues, but I will defer the final call rather than auto-approve.
  • Size: 237 production lines (wizard step 119, discovery module 111, presets/types/export 7) vs 283 test lines — well under every threshold, and more than half the diff is tests.
  • Approach: the scope feels right — a genuine reduction from feat(providers): recommend the live model list in the setup wizard #9389 (6,640 → 503 additions). Dropping the 24h persistent cache the issue sketched in favor of one stateless snapshot per wizard visit is the simpler design: no cache-invalidation state, and the request only fires when the Model IDs step is entered. Gating the editor behind the settled snapshot directly answers the "late response overwrites typing" race. The non-chat filter is necessarily a name-based heuristic (acknowledged in the description); acceptable since any filtered ID stays reachable through free-form input.
  • Risk: no matches against the revert-history high-risk paths. The security-sensitive bit — sending the API key to {baseUrl}/models — stays same-host: it reuses the existing bounded fetch helper, which never follows cross-host redirects, caps the response at 1 MiB, budgets five seconds for the whole transfer, and the exchange is never logged or cached.

Moving on to code review. 🔍

中文说明

感谢贡献——相比 #9389,这一版收敛了很多,能看出来下了功夫。

  • **模板:**完整,包含中文翻译 ✓
  • **问题:**真实且有证据。关联 issue feat(providers): dynamically fetch model list for ModelStudio Token Plan / Coding Plan presets #9368(P2、roadmap/model-inference)记录了在 CLI 0.21.12 上观察到的漂移——已退役的模型 ID 仍在推荐列表、当前的 qwen3.8-max 缺失、过期的选择会被持久化到 settings。是已观测到的问题,不是理论性问题。
  • **方向:**对齐。在唯一会编辑推荐模型的入口处拉取账户自己的目录,正是 issue 想要的;这个方向在生态里也有先例——claude-code 的模型选择器上线过 /v1/models 发现,后来又改为开关控制,这提醒我们"默认开启"的行为值得留意。一个注意点:本改动落在认证向导里、并改变模型选择推荐,属于维护者重点关注的表面。在此提请维护者知悉——审查继续,但最终决定会交给维护者,不会自动批准。
  • **规模:**生产代码 237 行(向导步骤 119、发现模块 111、预设/类型/导出 7),测试 283 行——远低于所有阈值,且 diff 一半以上是测试。
  • **方案:**范围合理——是对 feat(providers): recommend the live model list in the setup wizard #9389 的实质性收敛(6,640 → 503 行新增)。相比 issue 草案中的 24 小时持久缓存,每次进入向导只做一次无状态快照是更简单的设计:没有缓存失效状态,且请求只在进入 Model IDs 步骤时发起。编辑器等快照落定后再挂载,直接化解了"迟到的响应覆盖正在输入内容"的竞态。非聊天模型过滤只能是基于名称的启发式(描述中已说明);可以接受,因为被过滤的 ID 仍可通过自由输入框使用。
  • **风险:**未命中 revert 历史高风险路径清单。安全敏感点——把 API key 发送到 {baseUrl}/models——保持同 host:复用现有的受限 fetch 工具,不跟随跨 host 重定向、响应上限 1 MiB、全传输 5 秒预算,且全程不落日志、不做缓存。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 08c1c85fe82216ce58902b8a51eeaca1e6447aa9 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Before reading the diff, my independent take on this problem was: one small core module wrapping the existing bounded-fetch helper with a hard timeout and byte cap, an opt-in flag on the two presets, and a wizard step that refuses to mount the editor until the request settles — because the one unforgivable failure mode is a late response rewriting text under the cursor. The PR lands essentially on that same shape, which is a good sign it is the minimal design.

What I verified against the code:

  • Reuse, not new plumbing. discoverProviderModels delegates to the existing fetchWithPolicy (whole-transfer 5s budget, 1 MiB cap enforced while streaming, same-host-only redirects, caller abort wiring) instead of adding another fetch wrapper. The cross-host-redirect result kind is treated as a fallback, so the bearer key never follows a redirect off the configured host.
  • The overwrite race is actually closed. The editor component is not rendered until the snapshot settles; syncChangesToFlow={false} keeps edits local until Enter; the effect aborts on unmount with an active guard, and I checked the dialog shell: Esc is handled at the wizard level, so "Esc to go back" works during loading too, and there is no Enter path that can advance the step before the editor exists.
  • Non-opted-in providers are untouched. The new props default to the old behavior, and the added built-in filter in the preselection logic is a no-op when the option list equals the built-in list — so the other providers render exactly as before.
  • Failure is always graceful. Missing key or baseUrl, timeout, non-2xx, cross-host redirect, malformed body, empty catalog, all-filtered catalog — every path returns null and the wizard falls back to the full built-in list with an inline notice. Nothing is logged or cached.

No blockers found. Two non-blocking notes:

  • The three new user-facing strings (Loading models from provider…, · from the provider, · provider list unavailable, showing built-ins) are not added to any of the nine locale files. t() falls back to the English key, so nothing breaks, but non-English users will see a mixed-language label until translations land.
  • Accounts that authenticate purely through the env-var key without typing a key into the wizard will always land on the fallback notice, since discovery needs the entered key. That matches the stated "missing key falls back" design; just worth knowing.

The new flow has one moving part worth a picture:

sequenceDiagram
    participant P1 as User
    participant P2 as Auth wizard
    participant P3 as Discovering step
    participant P4 as discoverProviderModels
    participant P5 as Provider models endpoint
    P1->>P2: enter Model IDs step
    P2->>P3: mount (preset opted in)
    P3->>P3: show loading, editor not mounted
    P3->>P4: baseUrl, key, static models, abort signal
    P4->>P5: GET baseUrl/models (5s budget, 1 MiB cap)
    alt catalog ok
        P5-->>P4: data with model ids
        P4-->>P3: merged list (known specs kept, new ids appended)
        P3->>P2: mount editor with provider snapshot
    else any failure
        P5-->>P4: timeout, non-2xx, malformed or empty
        P4-->>P3: null
        P3->>P2: mount editor with built-ins plus notice
    end
    P1->>P2: Esc during load aborts the request
Loading
Files changed (10 of 10 shown)
File What changed
packages/core/src/providers/model-discovery.ts New module: bounded GET of baseUrl/models, strict parse of the standard listing, non-chat name filtering, merge that keeps curated specs for known ids
packages/core/src/providers/types.ts Adds the optional supportsModelDiscovery flag to ProviderConfig
packages/core/src/providers/index.ts Exports discoverProviderModels from the core package
packages/core/src/providers/presets/alibaba-token-plan.ts Opts the Token Plan preset into discovery
packages/core/src/providers/presets/alibaba-coding-plan.ts Opts the Coding Plan preset into discovery
packages/cli/src/ui/auth/ProviderSetupSteps.tsx Loading-gate wrapper step, snapshot-then-mount editor, edits commit only on Enter, source label on the recommendations heading
packages/cli/src/ui/auth/ProviderSetupSteps.test.tsx Render tests: no editor before settle, merge and no-promote behavior, fallback notice, abort on unmount
packages/core/src/providers/tests/model-discovery.test.ts Merge ordering, malformed and empty listings, all-non-chat fallback, error paths, no request without key and endpoint, abort passthrough
packages/core/src/providers/tests/presets/alibaba-token-plan.test.ts Asserts the new flag on the preset
packages/core/src/providers/tests/presets/alibaba-coding-plan.test.ts Asserts the new flag on the preset

Testing evidence

This is an unattended CI run — I do not execute PR code, so the evidence below is the PR's own CI on the reviewed commit, read through the API. The unit suite was still running at review time; the table region below is updated in place by the finalize job once CI settles. Security-related checks (secret scan, dependency CVE audit) and both Desktop Shell jobs are already green. The author's message cites focused-test counts (84 core cases, 10 render cases) — that is the author's claim; the CI result below is the evidence.

Final CI results for 08c1c85 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
route ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Sandboxed verification would settle the remaining gap: @qwen-code /verify — the render tests mock discoverProviderModels and the core tests mock fetchWithPolicy, so no layer exercises a real catalog response, and the author states a live authenticated Token Plan catalog was not probed (author tested on macOS only). Whether the loading → merge → fallback sequence holds against an actual endpoint is the central behavioural claim and is currently unverified. The author lacks write access, so this would be a sponsored run: a maintainer's @qwen-code /verify comment approves the head it was written against, and that run carries a pre-execution risk screen plus a full workspace wipe — read the resulting report with the same skepticism as the fork's own CI logs.

中文说明

代码审查:实现与独立评估的最小设计一致。发现模块复用现有的受限 fetch 工具(5 秒全传输预算、1 MiB 流式上限、仅同 host 重定向、调用方可中断),跨 host 重定向按回退处理,bearer key 不会离开配置的 host。"迟到响应覆盖输入"的竞态确实被关闭:编辑器在快照落定前不挂载,编辑仅在 Enter 时提交,卸载时中止请求;已核对向导外壳——Esc 在外壳层处理,加载中也能返回,且不存在能在编辑器挂载前推进步骤的 Enter 路径。未选择加入的 provider 完全保持原行为。所有失败路径(缺 key、超时、非 2xx、格式错误、空目录、全为非聊天模型)都回退到完整内置列表并行内提示,不落日志、不做缓存。无阻塞问题。两个非阻塞提醒:三个新界面字符串未加入九个语言文件(t() 回退英文,不坏,但非英文用户会看到混合语言标签);纯环境变量 key 的账户会始终看到回退提示,符合"缺 key 即回退"的设计。

测试证据:本次为无人值守 CI 运行,不执行 PR 代码,以上证据来自 API 读取的 PR 自身 CI。审查时单元测试套件仍在运行,下方表格区域会在 CI 落定后由 finalize 任务原地更新;安全扫描与两个 Desktop Shell 任务已通过。作者描述的测试数量是作者自述,以下方 CI 结果为准。

沙箱验证建议:渲染测试与核心测试分别 mock 了发现函数与 fetch 层,没有任何一层跑过真实目录响应,作者也说明未用真实认证的 Token Plan 目录探测过(且仅在 macOS 上测试)。@qwen-code /verify 可以补齐这一空缺;作者无写权限,需要维护者以 sponsored run 方式触发(附带执行前风险筛查与工作区清理,报告应像对待 fork 自身 CI 日志一样保持怀疑)。

Qwen Code · qwen3.8-max

Reviewed at 08c1c85fe82216ce58902b8a51eeaca1e6447aa9 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean static review and a genuinely well-reduced design, but this sits on the auth wizard and model-selection surface, so it needs a maintainer's sign-off rather than an auto-approval.

Stepping back: this is the second attempt by the same author, and the iteration went the right way — 6,640 additions became 503, the persistent cache the issue sketched was dropped for a stateless snapshot, and what remains is exactly the minimal design I would have proposed: one bounded request per wizard visit, reuse of the existing fetch policy helper, and an editor that physically cannot mount before the snapshot settles. The race everyone worries about with this kind of feature is closed at the structure level, not just by a flag. Every line in the diff serves the stated goal; no drive-bys.

What I cannot honestly sign off on, and why I'm not approving:

  • The surface is maintainer territory. The change sends the user's API key to {baseUrl}/models from the auth wizard and changes which models get recommended. It is built safely (same-host only, bounded, never logged), but whether discovery should be default-on for these two presets — versus opt-in — is a product call. Worth noting claude-code shipped the equivalent feature automatically and then moved it behind a flag.
  • The central behavioral claim is still unverified end-to-end. Both test layers mock their boundary (fetchWithPolicy in the core tests, discoverProviderModels in the render tests), no live authenticated catalog was probed, and the unit suite was still running on the reviewed head at review time. @qwen-code /verify as a sponsored run is the lane named in my review comment for closing that gap.

So: no approval, no request-changes — the code itself has nothing I would block on.

⏸️ Deferring to @wenshao and the /packages/core/ code owners (@tanzhenxin @yiliang114 @LaZzyMan @doudouOUC) — needs a human call on (1) whether default-on provider discovery is the right product direction for the Token Plan / Coding Plan presets, and (2) whether to run @qwen-code /verify (sponsored) before merge to settle the live-endpoint behavior. The two non-blocking notes (missing locale entries, env-key accounts always seeing the fallback) are recorded in the review comment.

中文说明

信心度:3/5 —— 静态审查干净,设计也收敛得相当好,但改动落在认证向导与模型选择这一敏感面上,需要维护者拍板,而不是自动批准。

整体看:这是同一作者的第二版,迭代方向正确——6,640 行新增收敛到 503 行,issue 草案里的持久缓存被换成无状态快照,剩下的正是我会提出的最小设计:每次进入向导只发一次受限请求、复用现有 fetch 策略工具、编辑器在快照落定前物理上不可能挂载。这类功能最担心的竞态在结构层面就被关死了,而不是靠某个标志位。diff 中每一行都服务于既定目标,没有夹带改动。

我无法签字认可、因此不予批准的原因:

  • **该表面属于维护者决策范围。**改动会在认证向导中把用户的 API key 发送到 {baseUrl}/models,并改变推荐模型。实现本身是安全的(仅同 host、有边界、不落日志),但这两个预设是否应默认开启发现功能——还是改为可选开启——是产品决策。值得一提的是,claude-code 的同类功能先默认开启、后又改为开关控制。
  • **核心行为声明尚未端到端验证。**两层测试各自 mock 了边界(核心测试 mock fetchWithPolicy、渲染测试 mock discoverProviderModels),没有用真实认证的目录探测过,审查时单元测试套件仍在所审 commit 上运行。评审评论中已指明以 @qwen-code /verify sponsored run 作为补齐该空缺的路径。

因此:不批准、也不要求修改——代码本身没有可阻塞的问题。

⏸️ 转交 @wenshao/packages/core/ 代码负责人(@tanzhenxin @yiliang114 @LaZzyMan @doudouOUC)——需要人工决定:(1) Token Plan / Coding Plan 预设默认开启 provider 模型发现是否是正确的产品方向;(2) 合并前是否以 @qwen-code /verify(sponsored run)验证真实端点行为。两个非阻塞提醒(语言文件缺条目、纯环境变量 key 的账户始终看到回退提示)已记录在评审评论中。

Qwen Code · qwen3.8-max

Reviewed at 08c1c85fe82216ce58902b8a51eeaca1e6447aa9 · re-run with @qwen-code /triage

@qqqys qqqys added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 25, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

Comment on lines +879 to +881
if (provider.supportsModelDiscovery) {
return <DiscoveringModelIdsStep config={provider} flow={flow} />;
}

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.

[Critical] This gate defers mounting the model editor behind a real network fetch, which breaks two existing Token Plan tests in AuthDialog.test.tsx (a file this PR does not touch) and turns them into real-network tests against the production endpoint.

AuthDialog renders the Step 3/3 · Model IDs header from flow state independently of the step body, so the tests' pressEnterAndWaitFor passes while DiscoveringModelIdsStep is still on the loading screen. AuthDialog.test.tsx has no mock of discoverProviderModels (only ProviderSetupSteps.test.tsx mocks the core module), so a real fetchWithPolicy request goes to the production Token Plan /models endpoint with the fake bearer key. While loading, no keypress handler consumes Enter (ModelIdsStep is not mounted yet; the ProviderSetupSteps handler is only active for advancedConfig/review), so the submit test's \r is dropped.

Witness — A/B run at this commit, same environment both arms, CI= unset:

BASE: ✓ should submit Token Plan through the shared subscription handler 425ms
BASE: ✓ should pre-fill the Model IDs step with previously saved custom model IDs 440ms
PR:   × should submit Token Plan… → expected "spy" to be called at least once (vi.waitFor timeout, 5427ms)
PR:   × should pre-fill… → frame stuck on "Loading models from provider… │ Esc to go back" (deterministic, 448ms)

Both tests are skipped when process.env.CI === 'true' (itWhenTuiInputReliable), which is why CI stays green while local/non-CI runs of the auth suite fail deterministically — and no CI surface exercises the discovery path at all.

Suggested fix: mock discoverProviderModels in AuthDialog.test.tsx the same way ProviderSetupSteps.test.tsx does — vi.mock('@qwen-code/qwen-code-core', ...) with importOriginal, the stub resolving null — and have the two tests wait for the editor to mount (e.g. the Enter model IDs directly text) before asserting or sending Enter.

中文说明

这个 gate 把模型编辑器的挂载推迟到一次真实网络请求之后,破坏了 AuthDialog.test.tsx(本 PR 未改动的文件)中两个既有的 Token Plan 测试,并使它们变成对生产端点发起真实网络请求的测试。

AuthDialog 从 flow state 渲染 Step 3/3 · Model IDs 标题时独立于步骤内容,因此测试的 pressEnterAndWaitForDiscoveringModelIdsStep 仍停留在加载界面时就已经通过。AuthDialog.test.tsx 没有 mock discoverProviderModels(只有 ProviderSetupSteps.test.tsx mock 了 core 模块),于是会用伪造的 bearer key 向生产环境的 Token Plan /models 端点发起真实的 fetchWithPolicy 请求。加载期间没有任何按键处理器消费 Enter(ModelIdsStep 尚未挂载;ProviderSetupSteps 的处理器只在 advancedConfig/review 阶段激活),因此提交测试的 \r 被丢弃。

见证——在本提交上进行的 A/B 运行,两臂使用相同环境且未设置 CI=:两个测试在 merge base 上通过,在本 PR 上确定性失败(一个因 vi.waitFor 超时,一个在断言时帧仍停留在 "Loading models from provider… │ Esc to go back")。

这两个测试在 process.env.CI === 'true' 时被跳过(itWhenTuiInputReliable),所以 CI 保持绿色,而本地/非 CI 环境运行 auth 测试套件会确定性失败——并且没有任何 CI 环节执行到发现(discovery)路径。

修复建议:在 AuthDialog.test.tsx 中用与 ProviderSetupSteps.test.tsx 相同的方式 mock discoverProviderModels——带 importOriginalvi.mock('@qwen-code/qwen-code-core', ...),stub 解析为 null——并让这两个测试在断言或发送 Enter 之前先等待编辑器挂载(例如等待 Enter model IDs directly 文本出现)。

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

/(^|[-_.])(asr|audio|embed|embedding|image|moderation|rerank|speech|tts|video)([-_.]|$)/i;
const NON_CHAT_MODEL_FAMILY =
/(cosyvoice|sambert|paraformer|whisper|stable-diffusion|(^|[-_.])flux([-_.]|$))/i;
const MEDIA_MODEL_PREFIX = /^wan(?:\d|x[-_.]|[-_.])/i;

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.

[Suggestion] The non-chat filter misses current-generation media-model naming. MEDIA_MODEL_PREFIX matches wan+digit and wanx+separator, but not wanx immediately followed by a digit, and the part/family blocklists have no t2i/t2v tokens or bge/sensevoice families. Probe-confirmed by driving the real discoverProviderModels with a mocked transport: wanx2.1-t2i-turbo, wanx2.1-t2v-turbo, bge-m3, sensevoice-v1 all pass the filter and land in "Recommended models · from the provider", while legacy wanx-v1 and wan2.7-image are caught — same family, one generation caught, the next slips through. A user who checks one persists a non-chat model as a chat model, and later chat-completion calls against it fail.

Witness:

PR:  surviving ids = ["qwen-chat-control","wanx2.1-t2i-turbo","wanx2.1-t2v-turbo","bge-m3","sensevoice-v1"]
FIX: /^wan(?:\d|x[-_.\d]|[-_.])/i plus (bge|sensevoice) family tokens → surviving ids = ["qwen-chat-control"]

This stays a Suggestion because discovered IDs are never auto-checked (initial selections are the intersection with built-ins only), so impact requires manually selecting a misclassified entry. The blocklist-over-unbounded-space design never converges, though, so also consider failing closed (only IDs intersecting built-in specs count as recommendations) or dropping the filter entirely — noise is cheaper than misclassification when nothing is auto-selected.

Suggested change
const MEDIA_MODEL_PREFIX = /^wan(?:\d|x[-_.]|[-_.])/i;
const MEDIA_MODEL_PREFIX = /^wan(?:\d|x[-_.\d]|[-_.])/i;
中文说明

非聊天模型过滤器漏掉了新一代媒体模型命名。MEDIA_MODEL_PREFIX 能匹配 wan+数字和 wanx+分隔符,但匹配不到紧跟数字的 wanx;part/family 黑名单也缺少 t2i/t2v 词元以及 bge/sensevoice 系列。通过用 mock 传输层驱动真实的 discoverProviderModels 进行探针验证:wanx2.1-t2i-turbowanx2.1-t2v-turbobge-m3sensevoice-v1 全部通过过滤器并进入 "Recommended models · from the provider",而旧版 wanx-v1wan2.7-image 被过滤——同一系列,抓住了上一代,漏掉了下一代。用户若勾选其中一项,会把非聊天模型作为聊天模型持久化,之后对它的聊天补全调用会失败。

由于发现到的 ID 从不被自动勾选(初始勾选仅取与内置列表的交集),影响需要用户手动误选,因此严重级别维持 Suggestion。但针对无界输入空间的手工黑名单永远不会收敛,也可以考虑收紧策略(只把与内置规格相交的 ID 作为推荐)或干脆去掉过滤——在没有任何自动勾选的前提下,列表里多一些噪音比误分类代价更低。

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

Comment on lines +64 to +66
const knownModels = staticModels.filter((model) =>
discoveredIds.has(model.id),
);

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.

[Suggestion] A previously selected built-in model that the account's /models listing does not serve becomes invisible everywhere in the models step and is silently removed from settings on submit. mergeModelSpecs filters it out of the snapshot, getCustomModelIdsText excludes it from the free-form input precisely because it is a built-in id, and no option row exists for it — the selection has no representation anywhere. Realistic triggers: entitlement-scoped listings (the static lists themselves describe "Pro subscribers only" models), the China-vs-Intl region catalogs this wizard switches between, and a transiently partial or paginated listing (readModelIds reads only data and ignores pagination markers).

Your own test demonstrates the mechanism: with modelIds: 'custom-model, MiniMax-M3, MiniMax-M2.7' and a snapshot omitting MiniMax-M2.7, submit produces ['xcustom-model', 'MiniMax-M3'] — the prior selection is gone. "Built-in IDs absent from the catalog are no longer recommended" covers de-recommendation, but not silently dropping a previously selected id from settings with no trace. Consider preserving previously selected built-ins the snapshot does not serve (seed them into the custom-ids input or keep them as visible rows), or at least make the drop visible before submit.

中文说明

账号 /models 列表中未提供的、此前已被选中的内置模型,会在模型步骤中完全不可见,并在提交时被从设置中静默移除。mergeModelSpecs 把它从快照中过滤掉;getCustomModelIdsText 恰恰因为它是内置 ID 而将其排除在自由输入框之外;同时也不存在对应的选项行——该选择在任何地方都没有表示。现实触发场景:按权益范围返回的列表(静态列表本身就描述了 "Pro 订阅专属" 模型)、本向导可切换的中国区/国际区目录、以及瞬时不完整或分页的列表(readModelIds 只读取 data,忽略分页标记)。

你们自己的测试演示了这个机制:当 modelIds: 'custom-model, MiniMax-M3, MiniMax-M2.7' 且快照缺少 MiniMax-M2.7 时,提交结果为 ['xcustom-model', 'MiniMax-M3']——之前的选择丢失了。"目录中缺失的内置 ID 不再推荐"覆盖的是取消推荐,但不覆盖在毫无提示的情况下把一个已被选中的 ID 从设置中移除。建议保留快照未提供但此前已选中的内置模型(把它们放进自定义输入框,或保持为可见选项行),至少应在提交前让这种移除对用户可见。

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

Comment on lines +44 to +46
const trimmedId = id.trim();
if (
trimmedId &&

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.

[Suggestion] readModelIds accepts any non-empty trimmed string — no validation for characters the downstream model-id pipeline treats as structural (comma) or for control sequences. This diff is the first path where model IDs travel from a network response into settings.json and the TUI. A served id "a, b" survives the first submit as one entry (the override array is used as-is), but on the next wizard run start() rebuilds state via [...defaultIds, ...customIds].join(', ') and normalizeModelIds splits on commas — one served id silently becomes two bogus models persisted to settings. Control/ANSI bytes in a served id render raw through formatModelOptionLabel<Text> and persist into every UI that lists configured models. The trigger is attenuated (both presets pin baseUrl to fixed https option lists, so it takes a buggy or compromised listing), but this ingestion point is the cheap place to guard.

Suggested change
const trimmedId = id.trim();
if (
trimmedId &&
const trimmedId = id.trim();
if (
trimmedId &&
!/[\u0000-\u001f\u007f,]/.test(trimmedId) &&
中文说明

readModelIds 接受任何非空的去空白字符串——没有校验下游模型 ID 管线视为结构化字符的逗号,也没有校验控制字符。这个 diff 是模型 ID 第一次从网络响应进入 settings.json 和 TUI 的路径。服务端返回的 ID "a, b" 在第一次提交时会作为一个条目存活(override 数组被原样使用),但下一次运行向导时 start() 通过 [...defaultIds, ...customIds].join(', ') 重建状态,而 normalizeModelIds 按逗号拆分——一个服务端 ID 会静默变成两个被持久化到设置的假模型。服务端 ID 中的控制/ANSI 字节会原样通过 formatModelOptionLabel<Text> 渲染,并持久化到每个列出已配置模型的界面。触发条件有所削弱(两个 preset 都把 baseUrl 固定为 https 选项,需要列表本身有缺陷或被攻陷),但这个入口正是成本最低的设防点。

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

Comment on lines +69 to +71
expect.objectContaining({
timeoutMs: 5000,
maxBytes: 1024 * 1024,

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.

[Suggestion] The bounded-request contract is pinned for 2 of its 3 parameters — this assertion checks timeoutMs: 5000 and maxBytes: 1024 * 1024, but maxRedirects: 2 from the same options literal is never asserted, so any change to it ships green. fetchWithPolicy iterates for (let hop = 0; hop <= options.maxRedirects; hop++), so a future maxRedirects: 2 → 0 edit makes a provider whose /models endpoint issues even one benign same-host redirect fail with EMAXREDIRECTS, silently degrading every such account to the static built-in list while the suite stays green.

Witness — mutation-probed in a scratch tree:

PR baseline: 12 passed (12)
mutation maxRedirects: 2 → 0: 12 passed (12)   ← mutation survives
mutation + fix below: 1 failed | 11 passed     ← fix discriminates
Suggested change
expect.objectContaining({
timeoutMs: 5000,
maxBytes: 1024 * 1024,
expect.objectContaining({
timeoutMs: 5000,
maxBytes: 1024 * 1024,
maxRedirects: 2,
中文说明

有界请求契约的三个参数中只有两个被固定——此断言检查了 timeoutMs: 5000maxBytes: 1024 * 1024,但同一个选项字面量中的 maxRedirects: 2 从未被断言,因此对它的任何修改都会在测试全绿的情况下上线。fetchWithPolicyfor (let hop = 0; hop <= options.maxRedirects; hop++) 迭代,因此未来把 maxRedirects 改为 0 会让任何 /models 端点即便只发一次良性同域重定向的 provider 以 EMAXREDIRECTS 失败,并把这类账号静默降级到静态内置列表,而测试套件依然全绿。

见证——在临时树中进行的变异测试:基线 12 个测试全部通过;变异 maxRedirects: 2 → 0 后仍然 12 个全部通过(变异存活);加上下方的修复后 1 个失败、11 个通过(修复可以区分变异)。

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

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 6 finishedview run. See this round's report below.

中文说明

AutoFix 第 6 轮已完成 —— 查看运行。本轮报告见下方。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下:

Autofix round 1 — all five findings addressed

[rc:3849825673] Critical — model-discovery gate breaks two Token Plan tests in AuthDialog.test.tsxFixed

Reproduced on the pre-round head with CI= unset, matching the reviewer's witness exactly:

  • should submit Token Plan through the shared subscription handlerexpected "spy" to be called at least once (vi.waitFor timeout)
  • should pre-fill the Model IDs step with previously saved custom model IDs → frame stuck on "Loading models from provider…"

Fix follows the reviewer's suggested shape: AuthDialog.test.tsx now mocks discoverProviderModels via vi.mock('@qwen-code/qwen-code-core', …) with importOriginal (same pattern as ProviderSetupSteps.test.tsx), the stub resolves null (built-in fallback), and both tests wait for the editor to mount (Enter model IDs directly) before sending Enter / asserting. The real network request to the production /models endpoint no longer happens in this suite. Both tests now pass with CI= unset.

[rc:3849825677] Suggestion — non-chat filter misses current-generation media models → Fixed

Applied the reviewer's suggested regex and tokens: MEDIA_MODEL_PREFIX is now /^wan(?:\d|x[-_.\d]|[-_.])/i (catches wanx2.1-…), t2i/t2v were added to NON_CHAT_MODEL_PART, and sensevoice plus the anchored (^|[-_.])bge([-_.]|$) were added to NON_CHAT_MODEL_FAMILY (anchored like flux to avoid substring false positives). The falls back when all returned ids are non-chat models test now feeds wanx2.1-t2i-turbo, wanx2.1-t2v-turbo, bge-m3, and sensevoice-v1. I kept the blocklist design rather than the fail-closed/drop-the-filter alternatives: changing recommendation semantics is a product call, while closing the named generation gap is not.

[rc:3849825691] Suggestion — selected built-in absent from the snapshot is silently dropped → Fixed

Implemented the reviewer's first option (seed into the custom-ids input). getCustomModelIdsText now excludes only ids that are represented by a pre-checked recommendation row instead of every built-in id, so a previously selected built-in that the account's listing does not serve appears in the free-form input and survives submit. Providers the snapshot does serve, and the non-discovery/fallback paths, behave exactly as before (verified by the unchanged neighboring tests). The snapshot test was updated to pin the new contract: no option row for MiniMax-M2.7, but it is visible in the input and included on submit (['xcustom-model', 'MiniMax-M2.7', 'MiniMax-M3']). This is an intentional behavior change demanded by the finding, not a weakened assertion — the updated test pins strictly more than the old one.

[rc:3849825694] Suggestion — readModelIds accepts comma/control bytes → Fixed

Added UNSAFE_MODEL_ID_CHARS = /[,\u0000-\u001f\u007f]/ and skip any served id matching it, with an eslint-disable-next-line no-control-regex comment following the codebase convention (control bytes are exactly what the regex rejects). New tests pin that "a, b", an ESC-sequence id, and a DEL id are skipped while valid ids survive, and that an all-unsafe listing falls back to built-ins.

[rc:3849825698] Suggestion — maxRedirects: 2 never asserted → Fixed

Added maxRedirects: 2 to the fetchWithPolicy objectContaining assertion, exactly as suggested.

Mutation probes (each guard has its own witness)

Probe Result
Revert filter regex/tokens + remove unsafe-char guard 3 tests fail (falls back when all returned ids are non-chat models, both unsafe-id tests); restored → 14/14
Product maxRedirects: 2 → 0 merge test fails on the new assertion; restored → 14/14
Revert getCustomModelIdsText to built-in exclusion updated snapshot test fails; restored → 10/10
Critical: the two Token Plan tests on the pre-round head both fail (witness above); with the fix both pass

Note on one unrelated local failure

Running the full AuthDialog.test.tsx with CI= unset shows one additional failure: drives API key provider steps from endpoint options metadata. I verified it is not caused by this round or this PR: it fails identically with the pre-round head (without my test change) and after swapping packages/cli + packages/core to origin/main sources and rebuilding (same assertion: expected frame to match /›\s*(?:\d+\.\s*)?MiniMax API Key/). It is an environment-specific TUI-input timing failure of the kind the itWhenTuiInputReliable guard exists for (the test is skipped when CI=true), and it exercises only third-party provider navigation, which this PR does not touch. In CI=true mode the suite is fully green.

Conflict notes

None — --conflict false; no merge performed.

Verification

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0, no findings)
  • prettier --check on the five touched files — passed
  • vitest run src/providers in packages/core — 16 files, 168 passed
  • vitest run src/ui/auth/ProviderSetupSteps.test.tsx in packages/cli (CI= unset) — 10 passed
  • vitest run src/ui/auth/AuthDialog.test.tsx in packages/cli with CI=true — 7 passed | 18 skipped (green)
  • vitest run src/ui/auth/AuthDialog.test.tsx in packages/cli with CI= unset — 24 passed | 1 failed (the pre-existing MiniMax endpoint navigation failure reproduced identically on origin/main, see note above); the two Token Plan tests from the Critical finding both pass
  • No settings source changed → npm run generate:settings-schema not required
中文说明

Autofix 第 1 轮 — 五项发现全部处理

[rc:3849825673] Critical — 模型发现 gate 破坏 AuthDialog.test.tsx 中两个 Token Plan 测试 → 已修复

在轮次前的 HEAD 上、未设置 CI= 的环境下复现,与评审见证完全一致:

  • should submit Token Plan through the shared subscription handlerexpected "spy" to be called at least once(vi.waitFor 超时)
  • should pre-fill the Model IDs step with previously saved custom model IDs → 帧停留在 "Loading models from provider…"

修复采用评审建议的形态:AuthDialog.test.tsx 现在通过带 importOriginalvi.mock('@qwen-code/qwen-code-core', …) mock discoverProviderModels(与 ProviderSetupSteps.test.tsx 相同的模式),stub 解析为 null(回退到内置列表),两个测试在发送 Enter / 断言之前先等待编辑器挂载(Enter model IDs directly)。该测试套件不再向生产 /models 端点发起真实网络请求。两个测试在未设置 CI= 的环境下均已通过。

[rc:3849825677] Suggestion — 非聊天过滤器漏掉新一代媒体模型 → 已修复

采用评审建议的正则与词元:MEDIA_MODEL_PREFIX 改为 /^wan(?:\d|x[-_.\d]|[-_.])/i(可捕获 wanx2.1-…),NON_CHAT_MODEL_PART 增加 t2i/t2vNON_CHAT_MODEL_FAMILY 增加 sensevoice 以及带锚定的 (^|[-_.])bge([-_.]|$)(与 flux 一样加锚定,避免子串误伤)。falls back when all returned ids are non-chat models 测试现在会喂入 wanx2.1-t2i-turbowanx2.1-t2v-turbobge-m3sensevoice-v1。保留黑名单设计、未采用收紧策略/去掉过滤器的替代方案:改变推荐语义属于产品决策,而补齐已点名的代际漏洞不属于。

[rc:3849825691] Suggestion — 快照未提供的已选内置模型被静默移除 → 已修复

实现了评审的第一选项(放进自定义输入框)。getCustomModelIdsText 现在只排除已有预勾选推荐行表示的 ID,而不是排除所有内置 ID,因此账号列表未提供的、此前已选的内置模型会出现在自由输入框中并在提交时保留。快照已提供的模型、以及非发现/回退路径的行为与之前完全一致(由相邻未改动的测试验证)。快照测试已更新以固定新契约:MiniMax-M2.7 没有选项行,但在输入框中可见且包含在提交结果中(['xcustom-model', 'MiniMax-M2.7', 'MiniMax-M3'])。这是发现所要求的有意的行为变更,而不是弱化断言——更新后的测试比旧测试固定了更多内容。

[rc:3849825694] Suggestion — readModelIds 接受逗号/控制字节 → 已修复

新增 UNSAFE_MODEL_ID_CHARS = /[,\u0000-\u001f\u007f]/,跳过任何匹配的服务端 ID,并按代码库惯例添加 eslint-disable-next-line no-control-regex 注释(控制字节正是该正则要拒绝的对象)。新测试固定:"a, b"、含 ESC 序列的 ID、含 DEL 的 ID 被跳过而合法 ID 保留;全部 ID 均不安全时回退到内置列表。

[rc:3849825698] Suggestion — maxRedirects: 2 从未被断言 → 已修复

完全按建议在 fetchWithPolicyobjectContaining 断言中增加 maxRedirects: 2

变异探针(每个守卫都有自己的见证)

探针 结果
回退过滤器正则/词元 + 移除不安全字符守卫 3 个测试失败(falls back when all returned ids are non-chat models、两个不安全 ID 测试);恢复后 14/14
产品代码 maxRedirects: 2 → 0 merge 测试在新断言上失败;恢复后 14/14
getCustomModelIdsText 回退为按内置排除 更新后的快照测试失败;恢复后 10/10
Critical:两个 Token Plan 测试在轮次前 HEAD 上 均失败(见上方见证);修复后均通过

关于一个无关的本地失败

在未设置 CI= 的环境下运行完整的 AuthDialog.test.tsx 会出现一个额外失败:drives API key provider steps from endpoint options metadata。我已确认它不是本轮或本 PR 引入的:在轮次前 HEAD(不含我的测试改动)上它以完全相同的方式失败;把 packages/cli + packages/core 换成 origin/main 的源码并重新构建后同样失败(相同断言:expected frame to match /›\s*(?:\d+\.\s*)?MiniMax API Key/)。这是一个环境相关的 TUI 输入时序失败,正是 itWhenTuiInputReliable 守卫存在的意义(CI=true 时该测试被跳过),且它只涉及本 PR 未改动的第三方 provider 导航。在 CI=true 模式下该套件全绿。

冲突说明

无 — --conflict false;未执行任何合并。

验证

  • npm run build — 通过(exit 0)
  • npm run typecheck — 通过(exit 0)
  • npm run lint — 通过(exit 0,无发现)
  • 对五个改动文件运行 prettier --check — 通过
  • packages/core 中运行 vitest run src/providers — 16 个文件,168 个通过
  • packages/cli 中运行 vitest run src/ui/auth/ProviderSetupSteps.test.tsx(未设置 CI=)— 10 个通过
  • packages/cli 中以 CI=true 运行 vitest run src/ui/auth/AuthDialog.test.tsx — 7 个通过 | 18 个跳过(全绿)
  • packages/cli 中以未设置 CI= 运行 vitest run src/ui/auth/AuthDialog.test.tsx — 24 个通过 | 1 个失败(即上文说明的、在 origin/main 上同样复现的既有 MiniMax 端点导航失败);Critical 发现中的两个 Token Plan 测试均已通过
  • 未改动任何 settings 源 → 无需运行 npm run generate:settings-schema

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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.

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/ui/auth/ProviderSetupSteps.tsx:334 — [probe] stale modelIdsError banner never cleared on discovery-path edits
  • packages/cli/src/ui/auth/ProviderSetupSteps.tsx:460 — [probe] no test pins the absence of the discovery source suffix on the non-discovery path
  • packages/core/src/providers/model-discovery.ts:113 — [review] 401 (wrong key) indistinguishable from outage in the generic fallback message
中文说明

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

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。

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

Comment on lines +12 to +13
const NON_CHAT_MODEL_PART =
/(^|[-_.])(asr|audio|embed|embedding|image|moderation|rerank|speech|t2i|t2v|tts|video)([-_.]|$)/i;

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.

[Suggestion] These three filter regexes curate recommendations with a hand-maintained enumeration of non-chat model families, but the entrance space — ids served by the account's /models — is unbounded, so the list has no last corner and both error directions fail silently. This supersedes the round-1 finding on this filter: that round named a specific media-model naming gap, the autofix patched it by extending this very list, and the next corners are already demonstrable. Executing the regexes at this commit: qwen-vl-ocr-latest (a single-task OCR-extraction family) passes all three filters and is returned as a recommended chat model, while qwen2-audio-instruct (an audio-input chat model) is dropped by the audio token and becomes invisible in the recommendations. The documented heuristic rationale answers the drop direction (filtered ids remain free-form enterable), but not the wrong-endorsement direction, where a non-chat model receives positive endorsement and, if selected, becomes the chat model with no uncertainty signal. Any preset that later sets supportsModelDiscovery silently inherits this taxonomy.

Witness — probe at this commit against the real discoverProviderModels:

input  {data:[{id:'qwen-vl-ocr-latest'}]}  → returned [{ id: 'qwen-vl-ocr-latest' }]  (endorsed)
input  {data:[{id:'qwen2-audio-instruct'}]} → returned null                            (dropped)

Suggested direction (spans the merge/render sites, so no one-click block): endorse only the built-in specs as "recommended", render served-but-unclassified ids without endorsement (e.g. a separate heading, or unfiltered) instead of dropping them, and delete the family regexes — then no future family can produce a wrong endorsement or hide a model.

中文说明

这三个过滤正则用人工维护的非聊天模型家族枚举来筛选推荐,但入口空间——账号 /models 返回的 id——是无界的,因此枚举永远补不完,且两个错误方向都会静默失败。本条取代第一轮针对同一过滤器的发现:那一轮指出了具体的媒体模型命名缺口,autofix 通过扩展这个列表来修补,而新的缺口已经可以演示。在本提交上执行这些正则:qwen-vl-ocr-latest(单任务 OCR 抽取家族)通过全部三个过滤,被作为推荐聊天模型返回;而 qwen2-audio-instruct(音频输入聊天模型)被 audio 词条丢弃,在推荐中不可见。文档中“启发式过滤、被过滤 ID 仍可自由输入”的说明只回应了丢弃方向,未回应错误背书方向——非聊天模型获得正面背书,一旦被勾选就会成为聊天模型且没有任何不确定性提示。未来任何设置 supportsModelDiscovery 的 preset 都会静默继承这套分类法。

见证——在本提交上对真实 discoverProviderModels 运行探针:输入 {data:[{id:'qwen-vl-ocr-latest'}]} 返回 [{ id: 'qwen-vl-ocr-latest' }](被背书);输入 {data:[{id:'qwen2-audio-instruct'}]} 返回 null(被丢弃)。

建议方向(跨合并/渲染多处,故不提供一键应用块):只把内置规格背书为“推荐”,对服务端返回但未分类的 id 不做背书地展示(例如单独标题或不外过滤)而不是丢弃,并删除家族正则——这样未来任何新家族都不会产生错误背书或隐藏模型。

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

const trimmedId = id.trim();
if (
trimmedId &&
!UNSAFE_MODEL_ID_CHARS.test(trimmedId) &&

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.

[Suggestion] The validation guard checks characters but not length: the 1 MiB response budget can become a single ~1 MiB model id (no commas/control bytes) that passes every check here, merges into the snapshot, and is rendered raw — formatModelOptionLabel applies padEnd with no truncation, wrapped to terminal width for up to 8 visible options and re-diffed on every keystroke. If selected, the string is also written to settings.json and sent as the model field on subsequent chat requests; normalizeModelIds and submitModelIds apply no length limit either. The trigger is a compromised or misconfigured endpoint behind one of the two fixed preset URLs — inside this feature's own threat model, which the character filter was added for.

Witness — probe at this commit against the real readModelIds/discoverProviderModels: serving 'a'.repeat(1_000_000) as a single id returned it unchanged (1 id, length 1,000,000).

Suggested change
!UNSAFE_MODEL_ID_CHARS.test(trimmedId) &&
trimmedId.length <= 256 &&
!UNSAFE_MODEL_ID_CHARS.test(trimmedId) &&

An id longer than that is never a real model name; over-length ids can alternatively fail the whole listing to the built-in fallback.

中文说明

该校验守卫检查了字符但没有检查长度:1 MiB 的响应预算可以变成一个约 1 MiB 的单个模型 id(不含逗号/控制字节),它能通过这里的所有检查、并入快照并被原样渲染——formatModelOptionLabel 只做 padEnd 不做截断,按终端宽度换行后最多 8 个可见选项、每次按键都重新 diff 重绘,实际上会冻结配置向导。若被选中,该字符串还会写入 settings.json 并作为后续聊天请求的 model 字段发送;normalizeModelIdssubmitModelIds 也没有长度限制。触发条件是固定 preset URL 背后被入侵或配置错误的端点——这属于本功能自身的威胁模型,字符过滤器正是为此而加的。

见证——在本提交上对真实 readModelIds/discoverProviderModels 运行探针:提供 'a'.repeat(1_000_000) 作为单个 id,原样返回(1 个 id,长度 1,000,000)。

建议应用上面的代码块,在守卫中加入长度上限(例如 <= 256);超过该长度的 id 不可能是真实模型名,也可以选择让整个列表失败并回退到内置列表。

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

// The wizard joins ids with commas and renders them raw, so a served id with
// a comma or control bytes would split into bogus models or poison the TUI.
// eslint-disable-next-line no-control-regex -- control bytes are exactly what this rejects
const UNSAFE_MODEL_ID_CHARS = /[,\u0000-\u001f\u007f]/;

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.

[Suggestion] The TUI-poisoning filter added in the previous round rejects commas and control bytes but misses invisible Unicode format/zero-width characters (U+200B, U+200C/D, U+202A–E, U+2060–2069, U+00AD, interior U+FEFF — trim() only strips edge ones), so a served id can render visually identical to a legitimate one. The endpoint can serve both qwen3.7-plus and qwen3.7\u200b-plus; both pass all four regexes and the trim, so the wizard renders two recommendation rows whose ID columns are pixel-identical, and if the lookalike is selected, the invisible-character id lands in settings.json (modelProviders, model.name) where it never matches the real id while remaining visually indistinguishable. The filter's own comment states its purpose is stopping served ids from poisoning the TUI; invisible characters are exactly that channel.

Witness — probe at this commit against the real function:

served {data:[{id:'qwen3.7-plus'},{id:'qwen3.7\u200b-plus'}]} → returned BOTH ids (no dedupe)
served {id:'\u200bqwen-lookalike'}                             → returned unchanged (trim() keeps U+200B)
Suggested change
const UNSAFE_MODEL_ID_CHARS = /[,\u0000-\u001f\u007f]/;
const UNSAFE_MODEL_ID_CHARS = /[,\u0000-\u001f\u007f\u00ad\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/;
中文说明

上一轮加入的 TUI 污染过滤器拒绝了逗号和控制字节,但漏掉了不可见的 Unicode 格式/零宽字符(U+200B、U+200C/D、U+202A–E、U+2060–2069、U+00AD、内部的 U+FEFF——trim() 只去除首尾的),因此服务端返回的 id 可以渲染得与合法 id 完全相同。端点可以同时提供 qwen3.7-plusqwen3.7\u200b-plus;两者都通过全部四个正则和 trim,向导会渲染出 ID 列逐像素相同的两行推荐,若选中了那个“孪生” id,带不可见字符的 id 会落入 settings.json(modelProvidersmodel.name),永远无法匹配真实 id,却又无法从视觉上区分。过滤器自身的注释写明其目的是阻止服务端 id 污染 TUI;不可见字符正是这条通道。

见证——在本提交上对真实函数运行探针:提供 {data:[{id:'qwen3.7-plus'},{id:'qwen3.7\u200b-plus'}]} 返回两个 id(未去重);提供 {id:'\u200bqwen-lookalike'} 原样返回(trim() 保留 U+200B)。

建议应用上面的代码块,把拒绝正则扩展到零宽/格式字符。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下:

Review round 2 — address summary (PR #9980)

All three inline [Suggestion] findings were reproduced on the pre-round commit with a probe against the built code, then resolved in one commit (fix(providers): stop curating discovered model ids (#9980)).

Feedback points

[rc:3851459630] Hand-maintained family taxonomy — RESOLVED

Reproduced at the pre-round commit: qwen-vl-ocr-latest passed all three filters and was returned as a recommended chat model, while qwen2-audio-instruct (an audio-input chat model) was dropped by the audio token — exactly as the finding described.

Following the suggested direction:

  • Deleted all three family regexes (NON_CHAT_MODEL_PART, NON_CHAT_MODEL_FAMILY, MEDIA_MODEL_PREFIX) — subtractive; round 1's enumeration-extension approach had already proven unbounded. Discovery now returns every structurally safe served id, so no future family can be silently dropped.
  • The wizard endorses only the built-in specs as "Recommended models". Served-but-unclassified ids are no longer merged under that heading: they render under a separate, unendorsed heading "Other models from the provider", initially unchecked (selecting one is an explicit opt-in). When no built-in spec matches the (optionally searched) listing, the "Recommended models" heading is hidden rather than floating empty.
  • Navigation, search, toggling, and submit semantics are unchanged: the two groups share one flat focus/search/window space.

Witnesses: the rewritten merge test now returns qwen2-audio-instruct and qwen-vl-ocr-latest uncurated (a mutation probe re-adding the audio filter makes this test fail); three CLI tests pin the heading split, its order between the two groups, the hidden endorsement when only provider-only models exist, and toggle+submit of an unendorsed model (each heading branch fails its test when mutated).

[rc:3851459636] No length bound on model ids — RESOLVED

Reproduced: serving 'a'.repeat(1_000_000) as a single id returned it unchanged (1 id, length 1,000,000), inside the 1 MiB response budget.

Added MAX_MODEL_ID_LENGTH = 256; an id longer than the limit is skipped alongside the other unsafe ids (per the finding's primary suggestion; a real model name is never longer). Witness: a boundary test (257 rejected, 256 kept) fails when the guard is removed (mutation probe).

[rc:3851459653] Invisible Unicode format characters bypass the TUI-poisoning filter — RESOLVED

Reproduced: qwen3.7-plus and qwen3.7\u200b-plus both passed every check and were returned side by side (pixel-identical rows, no dedupe), and a leading U+200B survived trim().

Extended UNSAFE_MODEL_ID_CHARS to also reject U+00AD, U+200B–U+200F, U+202A–U+202E, U+2060–U+2069, and U+FEFF — the invisible/format character classes the finding named. The lookalike id is now rejected outright, which also removes the duplicate-row variant. Witness: a dedicated test (ZWSP lookalike, leading ZWSP, RTO, soft hyphen, interior FEFF, LRI) fails against the pre-round regex (mutation probe).

Deleted test — content evidence

falls back when all returned ids are non-chat models was removed: it pinned the curation behavior that rc:3851459630 proved wrong (the finding's probe shows an audio-input chat model hidden from recommendations). The corrected behavior is pinned by the rewritten merge test, which expects qwen2-audio-instruct and wan2.7-t2v-plus among the returned ids; a mutation probe re-adding the audio family filter makes that test fail, so the coverage survives in a named test.

Reviewer-deferred probes — no action this round

The review body lists three probes "Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round" (stale modelIdsError banner, missing non-discovery suffix-absence test, 401-vs-outage fallback message). They remain recorded and untouched, per the reviewer's own deferral.

Pre-existing local test failure — not introduced by this round

AuthDialog > drives API key provider steps from endpoint options metadata fails consistently on this runner (5 consecutive runs). It was reproduced identically with every file of this round restored to its pre-round content, so it pre-exists this round. The test mocks discoverProviderModels entirely and fails at provider-list navigation, before any model step mounts — it cannot interact with this round's changes. It was left alone as out of scope; the PR reports no failed CI checks at the pre-round commit.

Verification

  • npm run build — passed (rerun after each mutation restore and after the final source state; 0 errors)
  • npm run typecheck — passed
  • npm run lint — initially 1 error (eslint-disable-next-line displaced by prettier's line wrap of the regex); fixed by moving the directive to the regex line; rerun passed clean
  • npx vitest run src/providers/__tests__/model-discovery.test.ts (core, from packages/core) — 15/15 passed
  • npx vitest run src/ui/auth/ProviderSetupSteps.test.tsx (cli, from packages/cli) — 12/12 passed
  • npx vitest run src/providers (core, whole providers directory) — 16 files, 169/169 passed
  • npx vitest run src/ui/auth (cli, whole auth directory) — 61/62 passed; the 1 failure is the pre-existing AuthDialog failure documented above
  • Reproduction probe (node script stubbing fetch, driving the real built discoverProviderModels): pre-round it reproduced all three findings; post-round it shows qwen2-audio-instruct returned, the 1 MiB id falling back to built-ins, and both invisible-character attacks rejected
  • Mutation probes (each new guard temporarily removed/negated, focused tests rerun, then restored): length guard → length test failed; regex revert → invisible-char test failed; re-added audio filter → merge test failed; disabled heading injection → 2 CLI tests failed; neutralized endorsement-heading guard → provider-only test failed. All restored to green afterwards
  • Not run: integration tests (the changed behavior is fully exercised by the unit tests above, not only through the bundled CLI) and npm run generate:settings-schema (no settings source changed)
中文说明

审查第 2 轮——处理摘要(PR #9980

三条行内 [Suggestion] 发现均已在本轮前的提交上通过针对构建产物的探针复现,并在一个提交中解决(fix(providers): stop curating discovered model ids (#9980))。

反馈点

[rc:3851459630] 手工维护的模型家族分类法——已解决

在本轮前的提交上复现:qwen-vl-ocr-latest 通过了全部三个过滤器,被作为推荐聊天模型返回;而 qwen2-audio-instruct(音频输入的聊天模型)被 audio 词条丢弃——与发现描述完全一致。

按建议方向处理:

  • 删除了全部三个家族正则(NON_CHAT_MODEL_PARTNON_CHAT_MODEL_FAMILYMEDIA_MODEL_PREFIX)——这是删减式修复;第一轮"扩充枚举"的做法已被证明补不完。Discovery 现在返回每一个结构上安全的已下发 id,未来任何新家族都不会再被静默丢弃。
  • 向导只把内置规格背书为"Recommended models"。已下发但未分类的 id 不再并入该标题下:它们渲染在一个单独的、不作背书的标题 "Other models from the provider" 之下,初始全部未勾选(勾选即显式选择)。当(可选搜索后的)列表中没有内置规格匹配时,"Recommended models" 标题会被隐藏,而不是悬空显示。
  • 导航、搜索、勾选、提交语义不变:两个分组共享同一个扁平的焦点/搜索/窗口空间。

见证:重写后的合并测试现在原样返回 qwen2-audio-instructqwen-vl-ocr-latest(重新加回 audio 过滤器的变异探针会使该测试失败);三个 CLI 测试钉住了标题拆分、两个分组之间的标题顺序、仅存在 provider 独有模型时背书标题被隐藏、以及未背书模型的勾选+提交(每个标题分支在被变异时都会使其测试失败)。

[rc:3851459636] 模型 id 没有长度上限——已解决

复现:下发单个 'a'.repeat(1_000_000) 作为 id 时原样返回(1 个 id,长度 1,000,000),且在 1 MiB 响应预算之内。

新增 MAX_MODEL_ID_LENGTH = 256;超过该长度的 id 与其他不安全 id 一样被跳过(采用发现的主建议;真实模型名不可能更长)。见证:一个边界测试(257 被拒、256 保留)在移除该守卫时失败(变异探针)。

[rc:3851459653] 不可见 Unicode 格式字符绕过了 TUI 污染过滤器——已解决

复现:qwen3.7-plusqwen3.7\u200b-plus 同时通过全部检查并被并列返回(两行逐像素相同、未去重),且行首的 U+200B 在 trim() 后保留。

扩展 UNSAFE_MODEL_ID_CHARS,同时拒绝 U+00AD、U+200B–U+200F、U+202A–U+202E、U+2060–U+2069 与 U+FEFF——即发现所指名的不可见/格式字符类别。孪生 id 现在会被直接拒绝,重复行的变体也随之消除。见证:一个专门测试(ZWSP 孪生、行首 ZWSP、RTO、软连字符、内部 FEFF、LRI)在本轮前的正则下失败(变异探针)。

被删除的测试——内容证据

falls back when all returned ids are non-chat models 被移除:它钉住的正是 rc:3851459630 证明为错误的筛选行为(发现的探针显示音频输入的聊天模型会从推荐中消失)。修正后的行为由重写后的合并测试钉住:该测试期望返回的 id 中包含 qwen2-audio-instructwan2.7-t2v-plus;重新加回 audio 家族过滤器的变异探针会使该测试失败,因此覆盖在指名测试中存续。

审查者延后的探针——本轮不处理

审查正文列出了三条"在收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改"的探针(过期的 modelIdsError 横幅、缺失的非 discovery 路径后缀缺席测试、401 与宕机无法区分的回退文案)。遵照审查者自身的延后决定,它们保持已记录、未改动状态。

本地既有测试失败——非本轮引入

AuthDialog > drives API key provider steps from endpoint options metadata 在本 runner 上持续失败(连续 5 次)。把本轮所有文件恢复到本轮前的内容后,该失败原样复现,因此它先于本轮存在。该测试完整 mock 了 discoverProviderModels,且失败发生在任何模型步骤挂载之前的供应商列表导航阶段——不可能与本轮改动发生交互。因其超出范围而未处理;本轮前的提交在 PR 上没有失败的 CI 检查。

验证

  • npm run build——通过(每次变异恢复后及最终源码状态后均重新运行;0 错误)
  • npm run typecheck——通过
  • npm run lint——最初 1 个错误(prettier 对正则换行后 eslint-disable-next-line 错位);将指令移到正则所在行后重跑通过
  • npx vitest run src/providers/__tests__/model-discovery.test.ts(core,在 packages/core 下)——15/15 通过
  • npx vitest run src/ui/auth/ProviderSetupSteps.test.tsx(cli,在 packages/cli 下)——12/12 通过
  • npx vitest run src/providers(core,整个 providers 目录)——16 个文件,169/169 通过
  • npx vitest run src/ui/auth(cli,整个 auth 目录)——61/62 通过;唯一失败是上文记录的既有 AuthDialog 失败
  • 复现探针(node 脚本桩掉 fetch,驱动真实构建出的 discoverProviderModels):本轮前复现了全部三条发现;本轮后显示 qwen2-audio-instruct 被返回、1 MiB id 回退到内置列表、两种不可见字符攻击均被拒绝
  • 变异探针(将每个新守卫临时移除/取反、重跑聚焦测试、再恢复):长度守卫→长度测试失败;正则回退→不可见字符测试失败;重新加回 audio 过滤器→合并测试失败;禁用标题注入→2 个 CLI 测试失败;中和背书标题守卫→provider 独有模型测试失败。全部恢复后回到绿色
  • 未运行:集成测试(上述改动行为完全由以上单元测试覆盖,并非只经由打包后的 CLI 执行);npm run generate:settings-schema(未改动任何 settings 源)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

@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.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Convergence: round 3 posted 2 inline comment(s), 2 of them reported for the first time; the previous round posted 3 (3 new). Findings keep coming back to the same files: packages/core/src/providers/model-discovery.ts (findings in round 2; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

收敛情况:第 3 轮发布了 2 条行内评论,其中 2 条是首次提出;上一轮发布了 3 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/core/src/providers/model-discovery.ts(第 2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment on lines +16 to +18
const UNSAFE_MODEL_ID_CHARS =
// eslint-disable-next-line no-control-regex -- control bytes are exactly what this rejects
/[,\u0000-\u001f\u007f\u00ad\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/;

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.

[Critical] This character class hand-enumerates Unicode danger ranges over an unbounded untrusted surface (the account's /models response), so whole categories of the characters its own comment targets pass validation, are rendered raw in the Model IDs step, and are persisted into provider settings. A probe driving the real discoverProviderModels (fetch mocked as in the suite), serving one id per claimed class, returned every one of them to the UI at this commit: U+2028/U+2029 line/paragraph separators, the C1 controls U+009B (single-byte CSI), U+0085 (NEL), U+0090 (DCS), U+009C (ST), plus U+061C and U+180E — with U+034F, variation selectors and homoglyph letters passing as well. The repo's own sanitizers treat exactly these as dangerous: terminalSafe.ts collapses [\x00-\x1f\x7f-\x9f] and suggestionGenerator.ts rejects the same range for terminal-rendered text. This is the same surface R2-3 patched one range-set at a time, so a compromised or MITM'd catalog endpoint — the threat the neighbouring test's control-byte fixture guards against — can still poison the TUI (on a terminal honouring 8-bit C1 controls a served U+009B id executes as a CSI sequence) and persist the poisoned id to settings. Close the class instead of enumerating it: with /[,\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u patched in, the same probe reports all eight primary entrances rejected while good-model still passes (probe flipped). Note U+034F/U+FE0F (Mn) and homoglyphs still pass property escapes — treat this as the floor, not the full remedy.

Suggested change
const UNSAFE_MODEL_ID_CHARS =
// eslint-disable-next-line no-control-regex -- control bytes are exactly what this rejects
/[,\u0000-\u001f\u007f\u00ad\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/;
const UNSAFE_MODEL_ID_CHARS = /[,\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
中文说明

这个字符类在无界且不可信的表面(账号 /models 响应)上手工枚举 Unicode 危险区间,因此其注释所要拦截的整类字符都能通过校验,被原样渲染到 Model IDs 步骤,并持久化进 provider 设置。在该提交上驱动真实 discoverProviderModels(fetch 按测试套件方式 mock)、逐类投喂单个 id 的探针显示,它们全部被返回给 UI:U+2028/U+2029 行/段分隔符、C1 控制字符 U+009B(单字节 CSI)、U+0085(NEL)、U+0090(DCS)、U+009C(ST),以及 U+061C、U+180E——此外 U+034F、变体选择符与同形字母同样通过。仓库自己的净化器恰恰把这些视为危险:terminalSafe.ts 收敛 [\x00-\x1f\x7f-\x9f],suggestionGenerator.ts 对终端渲染文本拒绝相同区间。这与 R2-3 逐区间修补的是同一个表面,因此被攻破或中间人的目录端点(邻近测试中控制字节夹具所防御的威胁)依然能毒害 TUI(在支持 8 位 C1 控制符的终端上,服务端投喂的 U+009B id 会作为 CSI 序列执行),并把被污染的 id 持久化到设置。应关闭整个类别而不是继续枚举:打上 /[,\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u 后,同一探针报告八个主要入口全部被拒绝,而 good-model 仍通过(探针翻转)。注意 U+034F/U+FE0F(Mn)与同形字母仍能通过属性转义——请把它当作底线而非完整补救。

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

Comment on lines +156 to +157
it.each([
response({}, 401),

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.

[Suggestion] These two fixtures cannot detect deletion of the guard they exist to pin: with if (result.kind !== 'response' || result.status < 200 || result.status >= 300) return null; removed, the 401 body {} still fails readModelIds' shape check and the cross-host-redirect fixture has no body field (so result.body.toString throws into the catch) — both paths still resolve to null. Probe at this commit: the guard-deletion mutant leaves the whole file green (15/15 tests pass), while changing the 401 fixture to carry a valid listing body kills it (expected [ { id: 'model-a' } ] to be null), and the improved fixture stays green against the real code (15/15). As it stands, a future refactor trusting readModelIds validation alone ships green, and an error response whose body happens to be listing-shaped (a gateway/proxy 4xx or 5xx JSON containing data: [{id: ...}]) would be rendered in the wizard as the provider's live catalog instead of falling back to built-ins. Give the failure fixture a valid listing body so the guard becomes the discriminating factor.

Suggested change
it.each([
response({}, 401),
it.each([
response({ data: [{ id: 'model-a' }] }, 401),
中文说明

这两个夹具无法检测到它们所要钉住的守卫被删除:移除 if (result.kind !== 'response' || result.status < 200 || result.status >= 300) return null; 后,401 夹具的空响应体 {} 仍会被 readModelIds 的形状检查拒绝,而 cross-host-redirect 夹具没有 body 字段(result.body.toString 会抛进 catch)——两条路径仍解析为 null。在本提交上的探针:删除守卫的突变体让整个文件保持绿色(15/15 通过);把 401 夹具改为携带合法列表响应体后突变体被杀死(expected [ { id: 'model-a' } ] to be null),改进后的夹具对真实代码仍保持绿色(15/15)。照现状发展,未来某个只信任 readModelIds 校验的重构会一路绿灯地合入,而一个响应体恰好是列表形状的错误响应(网关/代理返回的、包含 data: [{id: ...}] 的 4xx/5xx JSON)将在向导中被渲染为 provider 的实时目录,而不是回退到内置列表。请给失败夹具一个合法的列表响应体,让守卫成为判别因素。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round summary — PR #9980

Commit: f61eea41cffix(providers): reject full unsafe unicode classes in discovered model ids (#9980)

Feedback points and dispositions

[rc:3854486497] Critical — hand-enumerated Unicode danger ranges in UNSAFE_MODEL_ID_CHARS — RESOLVED

Reproduced before changing anything: a probe of the exact regex at this commit (/[,\u0000-\u001f\u007f\u00ad\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/) confirmed every claimed entrance passed validation — U+2028, U+2029, C1 controls U+009B / U+0085 / U+0090 / U+009C, U+061C, U+180E (and U+034F / U+FE0F as the noted floor).

Fix (exactly the suggested change): replaced the enumeration with the property-escape class /[,\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u. Verified the new class rejects all eight primary entrances while every character the old class rejected (comma, C0, DEL, soft hyphen, ZWSP/bidi/joiner ranges, BOM) is still rejected and good-model still passes. The now-inert eslint-disable-next-line no-control-regex comment was removed with it (the pattern no longer contains literal control bytes). As the finding states, U+034F/variation selectors (Mn) and homoglyphs still pass — this change is the floor, not an extended remedy; no additional categories were added beyond the suggestion.

Witness: added the eight named entrances to the tests — U+2028, U+2029, U+061C, U+180E to the existing invisible/formatting-characters test, and a new skips ids with C1 control bytes test for U+009B / U+0085 / U+0090 / U+009C.

Mutation probe: with the regex temporarily reverted to the old enumeration, skips ids with invisible or formatting characters and skips ids with C1 control bytes FAIL (2 failed | 14 passed); with the fix, 16/16 pass.

[rc:3854486518] Suggestion — 401 fixture cannot kill a guard-deletion mutant — RESOLVED

Reproduced before changing anything: temporarily deleting the status guard (if (result.kind !== 'response' || result.status < 200 || result.status >= 300) return null;) left the whole file green (15/15), because the {} 401 body fails the shape check anyway and the cross-host-redirect fixture has no body.

Fix (exactly the suggested change): the 401 fixture now carries a valid listing body — response({ data: [{ id: 'model-a' }] }, 401) — so the guard is the discriminating factor: an error response whose body happens to be listing-shaped must fall back to built-ins instead of being rendered as the live catalog.

Mutation probe: with the guard deleted and the improved fixture in place, falls back for an unsuccessful response FAILS (1 failed | 15 passed); with the guard restored, 16/16 pass.

[rv:5020702217] CHANGES_REQUESTED review body — gap disclosure noted

The state was carried by the Critical above, now resolved in code. The disclosed gap (Integration Tests (CLI, No Sandbox) not run) concerns behavior that is fully exercised by the mocked-fetch unit suite; discovery requires a live catalog endpoint, so it is not behavior only reachable through the bundled CLI or integration harness, and no local integration run was performed.

[ic:5411555630] review-pipeline fallback notice — no action

This is a transient "review did not complete" notification suggesting a retry; it contains no findings and requires no code change.

Files changed

  • packages/core/src/providers/model-discovery.ts — property-escape regex, removed inert eslint-disable (net −2 lines)
  • packages/core/src/providers/__tests__/model-discovery.test.ts — 8 new witness ids, improved 401 fixture (+23 lines)

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx vitest run src/providers/__tests__/model-discovery.test.ts (packages/core, touched) — 16/16 passed
  • npx vitest run src/providers/ (packages/core providers suite) — 170/170 passed (16 files)
  • npx vitest run src/ui/auth/ProviderSetupSteps.test.tsx (packages/cli consumer of the touched export) — 12/12 passed
  • npx vitest run src/ui/auth/AuthDialog.test.tsx (packages/cli consumer) — with CI=true (the gate environment): 7 passed | 18 skipped, file green. On this runner CI is unset, which un-skips the repo's TUI-input tests by design; one pre-existing navigation test (drives API key provider steps from endpoint options metadata) fails there identically with and without this round's diff (verified by restoring both files to their pre-round state and re-running twice). It is skipped under CI=true via itWhenTuiInputReliable and is unrelated to this change — the test mocks discoverProviderModels entirely.
  • Mutation probe A (regex witness): old regex restored → 2 tests fail; fix restored → 16/16 green
  • Mutation probe B (guard witness): guard deleted → 401 test fails; guard restored → 16/16 green
  • npx prettier --check on both touched files — passed
  • npm run generate:settings-schema — not required (no settings source changed)
  • Integration tests — not run (touched behavior fully covered by mocked-fetch unit tests, not bundled-CLI-only behavior)
中文说明

Autofix 审查轮次总结 — PR #9980

提交:f61eea41cffix(providers): reject full unsafe unicode classes in discovered model ids (#9980)

反馈点及处理

[rc:3854486497] Critical — UNSAFE_MODEL_ID_CHARS 手工枚举 Unicode 危险区间 — 已解决

改动前先复现:对该提交上原始正则(/[,\u0000-\u001f\u007f\u00ad\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/)的探针确认所有声称的入口都能通过校验 —— U+2028、U+2029、C1 控制字符 U+009B / U+0085 / U+0090 / U+009C、U+061C、U+180E(以及作为底线的 U+034F / U+FE0F)。

修复(与建议完全一致):将枚举替换为属性转义类别 /[,\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u。已验证新类别拒绝全部八个主要入口,同时旧类别拒绝的每个字符(逗号、C0、DEL、软连字符、零宽/双向/连接符区间、BOM)仍被拒绝,且 good-model 仍通过。随之移除了已失效的 eslint-disable-next-line no-control-regex 注释(新模式不再包含字面控制字符)。正如该发现所述,U+034F/变体选择符(Mn)与同形字符仍会通过 —— 本次改动是底线而非扩展补救;未超出建议范围添加额外类别。

见证测试:将八个被点名的入口加入测试 —— U+2028、U+2029、U+061C、U+180E 加入既有的不可见/格式化字符测试,并新增 skips ids with C1 control bytes 测试覆盖 U+009B / U+0085 / U+0090 / U+009C。

突变探针:将正则临时还原为旧枚举后,skips ids with invisible or formatting charactersskips ids with C1 control bytes 失败(2 失败 | 14 通过);恢复修复后 16/16 通过。

[rc:3854486518] Suggestion — 401 夹具无法杀死删守卫突变体 — 已解决

改动前先复现:临时删除状态守卫(if (result.kind !== 'response' || result.status < 200 || result.status >= 300) return null;)后整个文件仍为绿色(15/15),因为 {} 的 401 响应体本来就会被形状检查拒绝,而 cross-host-redirect 夹具没有响应体。

修复(与建议完全一致):401 夹具现在携带合法列表响应体 —— response({ data: [{ id: 'model-a' }] }, 401) —— 使守卫成为判别因素:响应体恰好为列表形状的错误响应必须回退到内置列表,而不是被渲染为实时目录。

突变探针:删除守卫并使用改进后的夹具后,falls back for an unsuccessful response 失败(1 失败 | 15 通过);恢复守卫后 16/16 通过。

[rv:5020702217] CHANGES_REQUESTED 审查正文 — 已记录缺口披露

该状态由上述 Critical 承载,现已在代码中解决。所披露的缺口(Integration Tests (CLI, No Sandbox) 未运行)涉及的行为已由 mock fetch 的单元测试完整覆盖;发现流程需要一个真实存活的目录端点,因此该行为并非只能通过打包 CLI 或集成测试框架触达,未进行本地集成测试。

[ic:5411555630] 审查流水线回退通知 — 无需处理

这是一条建议重试的临时性"审查未完成"通知,不包含任何发现,无需代码改动。

变更文件

  • packages/core/src/providers/model-discovery.ts — 属性转义正则,移除失效的 eslint-disable(净 −2 行)
  • packages/core/src/providers/__tests__/model-discovery.test.ts — 8 个新见证 id,改进 401 夹具(+23 行)

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx vitest run src/providers/__tests__/model-discovery.test.ts(packages/core,触及包)— 16/16 通过
  • npx vitest run src/providers/(packages/core providers 套件)— 170/170 通过(16 个文件)
  • npx vitest run src/ui/auth/ProviderSetupSteps.test.tsx(packages/cli,触及导出的消费方)— 12/12 通过
  • npx vitest run src/ui/auth/AuthDialog.test.tsx(packages/cli 消费方)— 在 CI=true(门禁环境)下:7 通过 | 18 跳过,文件绿色。本运行器上 CI 未设置,按仓库设计会取消跳过 TUI 输入测试;其中一个既有的导航测试(drives API key provider steps from endpoint options metadata)在本机无论是否应用本轮 diff 都同样失败(已通过把两个文件还原到本轮前状态并复跑两次验证)。该测试在 CI=true 下经 itWhenTuiInputReliable 被跳过,且与本改动无关 —— 该测试完全 mock 了 discoverProviderModels
  • 突变探针 A(正则见证):还原旧正则 → 2 个测试失败;恢复修复 → 16/16 绿色
  • 突变探针 B(守卫见证):删除守卫 → 401 测试失败;恢复守卫 → 16/16 绿色
  • npx prettier --check 两个触及文件 — 通过
  • npm run generate:settings-schema — 不需要(未改动 settings 源)
  • 集成测试 — 未运行(触及行为已由 mock fetch 的单元测试完整覆盖,并非仅限打包 CLI 的行为)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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. Suggestions are inline.

Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:

  • packages/core/src/providers/__tests__/model-discovery.test.ts:84 — [probe] fail-closed reject-whole decision unpinned — no fixture mixes valid and malformed entries
  • packages/core/src/providers/model-discovery.ts:44 — [probe] per-id trim() has no surviving fixture — deletion leaves the suite green
  • packages/core/src/providers/__tests__/model-discovery.test.ts:83 — [probe] it.each spreads the single-element array row — raw-array rejection path uncovered

Convergence: round 4 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 2 (2 new). Findings keep coming back to the same files: packages/core/src/providers/model-discovery.ts (findings in round 3; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

已审查。 建议见行内评论。

收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 4 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/core/src/providers/model-discovery.ts(第 3 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

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

// The wizard joins ids with commas and renders them raw, so a served id with
// a comma, control byte, or invisible format character would split into bogus
// models or poison the TUI.
const UNSAFE_MODEL_ID_CHARS = /[,\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;

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.

[Suggestion] The property-class rewrite drops exactly one code point the old hand-enumerated ranges rejected: U+2065, which the old \u2060-\u2069 range swept up incidentally. U+2065 is unassigned (category Cn) and matches none of Cc/Cf/Zl/Zp, so a served model id containing it now passes validation, is merged into the wizard's comma-joined Model IDs list, rendered raw, and persisted to settings. A probe sweeping U+0000–U+10FFFF at this commit confirms it is the only dropped point — the change is otherwise a strict net hardening, rejecting 183 more code points (all C1 controls, the remaining Cf incl. U+061C/U+180E, Zl U+2028, Zp U+2029) — so this is the one monotonicity delta in a commit whose purpose was to widen the filter:

oldRe.test('\u2065')=true, newRe.test('\u2065')=false
OLD-rejects-and-NEW-accepts = exactly 1 code point (U+2065)
NEW additionally rejects 183 code points OLD accepted

Impact is bounded (the character has no control, bidi, or format semantics and renders visibly in standard terminals), so this is a note, not a blocker: either state in the comment that unassigned code points are deliberately out of scope, or close the last corner by rejecting all of category C:

Suggested change
const UNSAFE_MODEL_ID_CHARS = /[,\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
const UNSAFE_MODEL_ID_CHARS = /[,\p{C}\p{Zl}\p{Zp}]/u;
中文说明

属性类重写恰好漏掉了一个旧手工枚举区间会拒绝的码位:U+2065——它是旧 \u2060-\u2069 区间顺带扫进来的。U+2065 是未分配码位(类别 Cn),不匹配 Cc/Cf/Zl/Zp 中的任何一类,因此包含它的模型 id 现在能通过校验,被合并进向导的逗号分隔 Model IDs 列表、原样渲染并持久化到设置。在本提交上对 U+0000–U+10FFFF 全量扫描的探针确认它是唯一被漏掉的码位——此改动在其他方面是严格的净强化,多拒绝了 183 个码位(全部 C1 控制字符、其余 Cf 含 U+061C/U+180E、Zl U+2028、Zp U+2029)——所以这是「以扩大过滤器为目的的提交」中唯一的单调性回退。影响有限(该字符没有控制、双向或格式化语义,在标准终端上可见地渲染),因此这是一条提示而非阻断:要么在注释里说明有意不覆盖未分配码位,要么通过拒绝整个 C 类关掉最后这个角。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round summary — PR #9980 (round 4)

Commit: 34d352a6aafix(providers): close the unassigned-code-point gap in model id validation (#9980) (2 files, +22/−3).

No base conflict was reported (--conflict false); no merge performed.

Feedback points and dispositions

1. [rc:3856380340] [Suggestion] Property-class rewrite drops U+2065 — RESOLVED (implemented)

The finding is real and was reproduced before changing anything: /[,\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u accepts U+2065 (category Cn), which the previous hand-enumerated \u2060-\u2069 range rejected. A full U+0000–U+10FFFF sweep confirmed the suggested \p{C} form is a strict hardening: 0 code points newly accepted, ~952k newly rejected (all Cn unassigned, Co private-use, Cs lone surrogates — the old class also never rejected lone surrogates). Implemented option (b) from the finding, which is also the smaller change:

  • UNSAFE_MODEL_ID_CHARS/[,\p{C}\p{Zl}\p{Zp}]/u (fewer classes than before), comment updated to match.
  • New regression test skips ids with unassigned or private-use code points with U+2065 and U+E000 fixtures, so reverting to Cc/Cf fails the suite (mutation-verified, see probes).

2. Deferred probe: fail-closed reject-whole decision unpinned — RESOLVED (implemented)

Added the missing mixed fixture { data: [{ id: 'model-a' }, null] } to the rejects a non-standard or empty listing table: a valid entry followed by a malformed one must reject the whole listing (fail closed), not keep the valid prefix. Mutation probe confirms it is a real witness (below).

3. Deferred probe: per-id trim() has no surviving fixture — RESOLVED (implemented)

Added { id: ' padded-model ' } to the ordering/dedup test, expecting padded-model in the merged output. Deleting .trim() now fails that test (mutation-verified).

4. Deferred probe: "it.each spreads the single-element array row — raw-array rejection path uncovered" — DECLINED (refuted with evidence)

This claim is incorrect for the vitest in this repo (3.2.7). taskFn.each in @vitest/runner spreads a row into arguments only when every row is an array (arrayOnlyCases = cases.every(Array.isArray)); otherwise each row is passed verbatim as the single argument. This table mixes array and object rows, so the original [{ id: 'model-a' }] row already delivers the raw array [{ id: 'model-a' }] as the response body — the raw-array rejection path is covered. Evidence:

  • Source: node_modules/@vitest/runner/dist/chunk-hooks.js, taskFn.each (arrayOnlyCases branch).
  • Scratch capture test over the identical table shape showed row 0 received as [[…]] verbatim (no spread) in mixed tables.
  • With a temporary mutation accepting raw arrays (Array.isArray(value) → readModelIds({ data: value })), exactly that original row fails and nothing else — i.e. the existing fixture pins the path the probe claimed was unpinned.
  • Likely source of the probe's misreading: the %j title renders the row as {"id":"model-a"} because formatTitle formats the row's items, dropping one bracket level, which makes a raw-array row look like a bare object.

No code change made for this item.

5. Failed checks: Test (macos-latest) / Test (windows-latest) — DIAGNOSED, no evidence-backed fix available in this PR's footprint

Mutation probes (witness verification for this round's commit)

# Temporary mutation Expected witness Result
1 Regex reverted to \p{Cc}\p{Cf} new unassigned/private-use test fails FAILED as expected (U+2065 + U+E000 leaked through); restored → green
2 .trim() removed from readModelIds ordering/dedup test fails on ' padded-model ' FAILED as expected; restored → green
3 Reject-whole return nullcontinue (skip-and-keep) mixed row {data:[{id:'model-a'},null]} fails only that row FAILED, all other rows stayed green; restored → green
4 Raw arrays accepted (Array.isArray(value) → readModelIds({data: value})) original raw-array row fails FAILED as expected — confirms pre-existing coverage (see item 4); restored → green

Verification

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0); npx prettier --check on both changed files — passed
  • npx vitest run src/providers/ (packages/core, touched) — 16 files, 172 passed
  • npx vitest run src/ui/auth/ (packages/cli, touched, CI=true as in CI) — 3 files, 44 passed | 18 skipped (the skips are isUnreliableTuiInputEnvironment-gated TUI-input tests, skipped on every CI platform and win32; the gate comes from Feat/openrouter auth #3576, not this PR)
  • Local note: without CI=true, one pre-existing gated test (drives API key provider steps from endpoint options metadata) fails locally on input-timing flakiness; it is skipped in CI by that gate and is not touched by this round.
  • Integration tests after npm run bundle: not run — the touched behavior (model-id validation + wizard rendering) is fully covered by the unit suites above; nothing here is only exercisable through the bundled CLI.
中文说明

Autofix 评审轮次总结 — PR #9980(第 4 轮)

提交:34d352a6aafix(providers): close the unassigned-code-point gap in model id validation (#9980)(2 个文件,+22/−3)。

未报告基线冲突(--conflict false);未执行合并。

反馈点与处置

1. [rc:3856380340] [Suggestion] 属性类重写漏掉 U+2065 — 已解决(已实现)

该发现属实,且在改动前已复现:/[,\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u 会接受 U+2065(类别 Cn),而旧的手工区间 \u2060-\u2069 会拒绝它。对 U+0000–U+10FFFF 的全量扫描确认,建议的 \p{C} 形式是严格强化:没有新接受任何码位,新拒绝约 95.2 万个码位(全部为 Cn 未分配、Co 私有区、Cs 孤立代理项——旧类别同样从不拒绝孤立代理项)。实现了发现中的选项 (b),这也是更小的改动:

  • UNSAFE_MODEL_ID_CHARS/[,\p{C}\p{Zl}\p{Zp}]/u(类别数比之前更少),注释同步更新。
  • 新增回归测试 skips ids with unassigned or private-use code points,含 U+2065 与 U+E000 夹具;若回退到 Cc/Cf,测试套件会失败(已用变异探针验证,见下表)。

2. 延后探针:fail-closed 整体拒绝判定未被固定 — 已解决(已实现)

rejects a non-standard or empty listing 表中补上缺失的混合夹具 { data: [{ id: 'model-a' }, null] }:合法条目后跟一个畸形条目时,必须整体拒绝(fail closed),而不是保留合法前缀。变异探针确认它是有效见证(见下表)。

3. 延后探针:逐条 trim() 没有存活的夹具 — 已解决(已实现)

在排序/去重测试中加入 { id: ' padded-model ' },期望合并输出中出现 padded-model。现在删除 .trim() 会使该测试失败(已变异验证)。

4. 延后探针:「it.each 展开了单元素数组行——原始数组拒绝路径未被覆盖」— 已拒绝(有证据驳回)

该说法对本仓库所用的 vitest(3.2.7)不成立。@vitest/runner 中的 taskFn.each 仅在所有行都是数组时才把行展开为参数列表(arrayOnlyCases = cases.every(Array.isArray));否则每行原样作为单个参数传入。本表混合了数组行和对象行,因此原有的 [{ id: 'model-a' }] 行本来就传入原始数组 [{ id: 'model-a' }] 作为响应体——原始数组拒绝路径已被覆盖。证据:

  • 源码:node_modules/@vitest/runner/dist/chunk-hooks.jstaskFn.eacharrayOnlyCases 分支)。
  • 用相同表结构写的捕获测试显示:混合表中第 0 行按 [[…]] 原样接收(未展开)。
  • 加入「接受原始数组」的临时变异(Array.isArray(value) → readModelIds({ data: value }))后,恰好只有该原始数组行失败,其余行全绿——即现有夹具确实固定了探针声称未覆盖的路径。
  • 探针误读的可能原因:%j 标题把该行渲染为 {"id":"model-a"},因为 formatTitle 格式化的是行的各个元素,脱掉了一层方括号,使原始数组行看起来像裸对象。

此项未做任何代码改动。

5. 失败检查:Test (macos-latest) / Test (windows-latest) — 已诊断,本 PR 足迹内没有证据支持的修复可做

变异探针(本轮提交的见证验证)

# 临时变异 预期见证 结果
1 正则回退为 \p{Cc}\p{Cf} 新的 unassigned/private-use 测试失败 按预期失败(U+2065 与 U+E000 漏过);恢复后全绿
2 readModelIds 删除 .trim() 排序/去重测试在 ' padded-model ' 上失败 按预期失败;恢复后全绿
3 整体拒绝 return nullcontinue(跳过保留) 混合行 {data:[{id:'model-a'},null]} 失败 仅该行失败,其余行保持绿色;恢复后全绿
4 接受原始数组(Array.isArray(value) → readModelIds({data: value}) 原始数组行失败 按预期失败——确认既有覆盖(见第 4 项);恢复后全绿

验证

  • npm run build — 通过(exit 0)
  • npm run typecheck — 通过(exit 0)
  • npm run lint — 通过(exit 0);对两个改动文件执行 npx prettier --check — 通过
  • npx vitest run src/providers/(packages/core,触及包)— 16 个文件,172 通过
  • npx vitest run src/ui/auth/(packages/cli,触及包,按 CI 环境设 CI=true)— 3 个文件,44 通过 | 18 跳过(跳过项为 isUnreliableTuiInputEnvironment 门控的 TUI 输入测试,在所有 CI 平台与 win32 上均跳过;该门控来自 Feat/openrouter auth #3576,非本 PR 引入)
  • 本地说明:不设 CI=true 时,一个既有门控测试(drives API key provider steps from endpoint options metadata)因输入时序抖动在本地失败;它在 CI 中由该门控跳过,且本轮未触及。
  • npm run bundle 后的集成测试:未运行——触及的行为(model-id 校验 + 向导渲染)已被上述单元测试完全覆盖;此处没有任何只能通过打包 CLI 才能验证的内容。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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.

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:

  • packages/core/src/providers/model-discovery.ts:107 — [review] all discovery failure modes collapse to a bare null with zero diagnostics

Convergence: round 5 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 1 (1 new). The rate of new findings is not falling. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 5 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 1 条(其中 1 条首次提出)。新发现的产出速度没有下降。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment on lines +138 to +140
it('skips ids with unassigned or private-use code points', async () => {
fetchWithPolicyMock.mockResolvedValue(
response({

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.

[Suggestion] R5-1: The regex was widened to the full \p{C} supercategory this round, but the surrogate subcategory (\p{Cs}) has no fixture pinning it — no test id contains a lone surrogate. A mutation that drops surrogates (for example spelling the class out as \p{Cc}\p{Cf}\p{Co}\p{Cn}) survives the whole suite green, while a served catalog can deliver such an id: {"data":[{"id":"\ud800x"}]} is valid JSON, and JSON.parse yields the lone surrogate. If that rejection guarantee ever regresses, the id merges into the snapshot and is comma-joined and rendered raw in the wizard — the exact surface this regex exists to close. Measured at this commit: under that mutant all 18 shipped tests still pass and only a probe fixture fails (expected [ { id: '…x' }, { id: 'good-model' } ] to deeply equal [ { id: 'good-model' } ]), while the same probe passes against the real code. Add a lone-surrogate id to this test's data array (keeping the same toEqual([{ id: 'good-model' }]) expectation) so the \p{Cs} branch stays pinned:

          { id: 'surrogate\ud800point' },
中文说明

本轮把正则放宽到了完整的 \p{C} 超类,但代理子类别(\p{Cs})没有任何夹具钉住——没有测试 id 包含孤立代理项。一个丢掉代理项的突变(例如把类别写成 \p{Cc}\p{Cf}\p{Co}\p{Cn})能让整个测试套件保持绿色,而服务端目录可以投喂这样的 id:{"data":[{"id":"\ud800x"}]} 是合法 JSON,JSON.parse 会解析出孤立代理项。如果这条拒绝保证将来回退,该 id 会并入快照,被逗号拼接并原样渲染到向导中——正是这个正则存在所要封堵的表面。在本提交上实测:应用该突变体后 18 个既有测试全部通过,只有探针夹具失败(expected [ { id: '…x' }, { id: 'good-model' } ] to deeply equal [ { id: 'good-model' } ]),而同一探针对真实代码保持绿色。请在本测试的 data 数组中加入一个孤立代理项 id(保持同样的 toEqual([{ id: 'good-model' }]) 期望),以钉住 \p{Cs} 分支:

          { id: 'surrogate\ud800point' },

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

The revived macOS and Windows test lanes check out the PR head, which
predates b331509 (the lane repairs) — both lanes failed on those
pre-existing platform test failures. Bring main in so the lanes test a
tree that contains the fixes.
)

The stdin-driven walk pressed down once from DeepSeek expecting MiniMax,
but the Grok (xAI) preset landed between them on main. The suite skips
under CI=true, so only local runs saw the stale adjacency.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下:

🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round summary — PR #9980 (round 5)

Commits this round (all additive, no history rewrite):

  • f7427bc0a8test(providers): pin the lone-surrogate model id rejection (#9980)
  • 479c270448 — merge of origin/main (base sync; clean, no conflicts)
  • 1b40f11525test(cli): step past Grok in the MiniMax endpoint auth walk (#9980)

Feedback points and dispositions

[rc:3857629635] Suggestion — the \p{Cs} branch has no fixture → Fixed

Reproduced the claim's premise first, then applied the suggested one-line fixture: added { id: 'surrogate\ud800point' } to the data array of the (renamed) skips ids with unassigned, private-use, or surrogate code points test, keeping the toEqual([{ id: 'good-model' }]) expectation.

Mutation probe (this round's witness): temporarily respelled the class as \p{Cc}\p{Cf}\p{Co}\p{Cn} (dropping the surrogate subcategory) — exactly one test failed, the new fixture (expected [ { id: 'surrogate…point' }, …(1) ] to deeply equal [ { id: 'good-model' } ]), the other 17 stayed green; restored the regex and the file returned to 18/18. Matches the reviewer's own measurement of the same mutant.

Failed checks: Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) → Diagnosed; repaired by syncing the base

Evidence chain:

  • The branch diverged from main at 8be07151f6 (2026-08-25 02:38 UTC). git merge-base --is-ancestor shows it is missing both b331509de4 (fix: repair the Windows and macOS test lane failures #9728, landed 11:49 UTC — the commit that repaired 72 pre-existing Windows/macOS platform test failures) and 519e824dc7 (fix(ci): give the macOS and Windows lanes a trigger again #9370, landed 11:53 UTC — the commit that revived these lane triggers).
  • The revived lanes ran for this PR via the new platform-sensitivity classifier and check out refs/pull/9980/head — i.e., this branch's pre-lane-repair tree — so those pre-existing platform failures fired on both lanes. The ubuntu lane passed because that failure class is platform-specific.
  • The failing test names could not be recovered (no CI-log access or GitHub credentials on this runner); the diagnosis rests on branch ancestry plus the lane-repair commits' own census of the failure class.

Fix: merged origin/main into the branch (clean merge, no conflicts, PR footprint unchanged — git diff origin/main...HEAD still lists only this PR's 11 files). The next lane run tests a tree that contains #9728's repairs plus this PR's platform-neutral additions (mocked fetch + unicode regex in core, ink rendering in cli — ubuntu is green on the identical code).

Latent defect found during local verification: AuthDialog stdin walk to MiniMax → Fixed

drives API key provider steps from endpoint options metadata presses down once from DeepSeek expecting MiniMax, but main's Grok (xAI) preset now sits between them. This is pre-existing on origin/main (the test body and the grok.ts preset are byte-identical there); CI never sees it because stdin-driven TUI tests skip under CI=true. Reproduced at this commit before changing anything (3 consecutive failures); fixed minimally by routing the walk through Grok with the same self-verifying moveDownAndWaitForSelection helper; both touched cli auth files now pass 37/37 locally.

[rv:5024438818] Review body — partial-review disclosure and convergence deferral → No action (nothing was requested)

The deferred item (model-discovery.ts:107 — all discovery failure modes collapse to a bare null with zero diagnostics) was recorded by the reviewer under the convergence posture as not requested this round; left untouched. The disclosed gap (Integration Tests (CLI, No Sandbox) skipped in CI and not run locally) required no code change; the discovery behavior is exercised by the unit suites above, not by the integration harness.

Verification

Commands actually run this round (results):

  • Mutation probe, Cs-less mutant on UNSAFE_MODEL_ID_CHARS — new fixture failed as predicted; restored regex — vitest run model-discovery.test.ts 18/18 passed
  • vitest run packages/core focused (model-discovery + both alibaba preset tests) — 24 passed
  • vitest run packages/cli focused (AuthDialog.test.tsx + ProviderSetupSteps.test.tsx) — 37 passed (after the Grok walk fix; failed 3× before it)
  • npm install — workspace re-sync after the base merge; only lockfile formatting churn (libc fields), restored, committed lockfile unchanged
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • Full vitest run packages/core — 603 files passed / 1 skipped, 21700 tests passed / 10 skipped, exit 0
  • Full vitest run packages/cli — 890 files passed, 24755 tests passed / 72 skipped, exit 0
  • Settings schema: no settings source changed, generate:settings-schema not applicable; integration tests: touched behavior is unit-covered, not exercised through the bundled CLI/integration harness, so none run

Environment caveat, stated for honesty: the agent sandbox exports QWEN_HOME, QWEN_CODE, and SANDBOX, which leak into HOME-sensitive tests (storage/logger/installationManager/token-storage and the editor.test.ts sandbox-mode cases). Those failures reproduce with the leaked env and vanish under the lane-faithful env used for the two full-suite runs above (fresh HOME, leaked vars unset — mirroring the CI lanes, which set HOME to a fresh runner-temp dir and none of those variables). The lane-faithful full suites are the ones reported.

中文说明

🤖 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下。

Autofix 评审轮次总结 — PR #9980(第 5 轮)

本轮提交(均为增量提交,未改写历史):

  • f7427bc0a8test(providers): pin the lone-surrogate model id rejection (#9980)
  • 479c270448 — 合并 origin/main(同步基线;干净合并,无冲突)
  • 1b40f11525test(cli): step past Grok in the MiniMax endpoint auth walk (#9980)

反馈点与处置

[rc:3857629635] Suggestion — \p{Cs} 分支没有夹具钉住 → 已修复

先验证了该结论的前提,然后按建议加入一行夹具:在(已更名的)skips ids with unassigned, private-use, or surrogate code points 测试的 data 数组中加入 { id: 'surrogate\ud800point' },保持 toEqual([{ id: 'good-model' }]) 期望不变。

突变探针(本轮的见证测试):把类别临时改写为 \p{Cc}\p{Cf}\p{Co}\p{Cn}(丢掉代理子类别)——恰好只有一个测试失败,即新夹具(expected [ { id: 'surrogate…point' }, …(1) ] to deeply equal [ { id: 'good-model' } ]),其余 17 个保持绿色;恢复正则后回到 18/18。与评审者对同一突变体的实测结果一致。

失败检查:Test (macos-latest, Node 22.x) 与 Test (windows-latest, Node 22.x) → 已诊断;通过同步基线修复

证据链:

  • 分支在 8be07151f6(2026-08-25 02:38 UTC)从 main 分出。git merge-base --is-ancestor 显示分支缺少 b331509de4fix: repair the Windows and macOS test lane failures #9728,11:49 UTC 合入——修复了 72 个既有的 Windows/macOS 平台测试失败)和 519e824dc7fix(ci): give the macOS and Windows lanes a trigger again #9370,11:53 UTC 合入——恢复了这两个车道的触发器)这两个提交。
  • 恢复后的车道经由新的平台敏感度分类器在本 PR 上运行,检出的是 refs/pull/9980/head——即本分支修复车道之前的树——因此那些既有的平台失败在两条车道上同时触发。ubuntu 车道通过,是因为该失败类是平台特有的。
  • 失败的具体测试名无法取得(本运行器无 CI 日志访问权限,也没有 GitHub 凭据);诊断依据是分支祖先关系,以及车道修复提交自身对失败类的清点记录。

修复方式:将 origin/main 合并进分支(干净合并,无冲突,PR 足迹不变——git diff origin/main...HEAD 仍然只列出本 PR 的 11 个文件)。下一次车道运行测试的树将包含 #9728 的修复,以及本 PR 平台无关的新增内容(core 中被 mock 的 fetch + unicode 正则,cli 中的 ink 渲染——相同代码在 ubuntu 上是绿色的)。

本地验证中发现的潜在缺陷:AuthDialog 中走到 MiniMax 的 stdin 导航 → 已修复

drives API key provider steps from endpoint options metadata 从 DeepSeek 按一次下键就期望到达 MiniMax,但 main 上的 Grok (xAI) 预设现在排在两者之间。这是 origin/main 上的既有问题(该测试体与 grok.ts 预设在 main 上逐字节相同);CI 看不到它,因为 stdin 驱动的 TUI 测试在 CI=true 时跳过。在改动前已在本提交上复现(连续 3 次失败);以最小改动修复——让导航用同一个自验证的 moveDownAndWaitForSelection 辅助函数途经 Grok;两个被触及的 cli auth 文件现在本地 37/37 通过。

[rv:5024438818] 评审正文 — 部分评审披露与收敛姿态下的延后项 → 不做处理(本轮未提出要求)

延后项(model-discovery.ts:107 — 所有 discovery 失败模式都塌缩为裸 null,零诊断信息)由评审者按收敛姿态记录为本轮不要求修改;保持不动。披露的缺口(Integration Tests (CLI, No Sandbox) 在 CI 中被跳过、本地也未运行)不需要代码改动;discovery 行为由上面的单元测试套件覆盖,而非集成测试框架。

验证

本轮实际执行的命令(结果):

  • 突变探针:对 UNSAFE_MODEL_ID_CHARS 应用去掉 Cs 的突变体——新夹具按预期失败;恢复正则——vitest run model-discovery.test.ts 18/18 通过
  • vitest run packages/core 聚焦(model-discovery + 两个 alibaba 预设测试)——24 通过
  • vitest run packages/cli 聚焦(AuthDialog.test.tsx + ProviderSetupSteps.test.tsx)——37 通过(Grok 导航修复之后;修复前连续失败 3 次)
  • npm install —— 基线合并后的工作区重新同步;仅产生锁文件格式抖动(libc 字段),已还原,提交的锁文件未变
  • npm run build —— 通过
  • npm run typecheck —— 通过
  • npm run lint —— 通过
  • 完整 vitest run packages/core —— 603 个文件通过 / 1 跳过,21700 个测试通过 / 10 跳过,退出码 0
  • 完整 vitest run packages/cli —— 890 个文件通过,24755 个测试通过 / 72 跳过,退出码 0
  • 设置 schema:未改动任何设置源,不适用 generate:settings-schema;集成测试:被触及的行为由单元测试覆盖,不经由打包 CLI/集成测试框架执行,因此未运行

环境变量说明(如实陈述):agent 沙箱导出了 QWEN_HOMEQWEN_CODESANDBOX,它们会泄漏进依赖 HOME 的测试(storage/logger/installationManager/token-storage 以及 editor.test.ts 的沙箱模式用例)。这些失败在带泄漏环境时可复现,在上面两次完整套件运行所用的车道忠实环境下消失(全新 HOME、取消设置泄漏变量——与 CI 车道一致:车道把 HOME 指向全新的 runner 临时目录,且不设置上述任何变量)。报告采用的是车道忠实环境下的完整套件结果。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:

  • packages/core/src/providers/__tests__/model-discovery.test.ts:45 — [review] PR description still asserts a non-chat filter the code no longer has (undisclosed deviation from issue 9368 item 4)
  • packages/cli/src/ui/auth/ProviderSetupSteps.tsx:507 — [probe] showOtherModelsHeading visibleIndex===0 branch untested — guard-deletion mutant survives
  • packages/cli/src/ui/auth/ProviderSetupSteps.tsx:341 — [probe] syncChangesToFlow=true default asserted only in the negative — default-flip mutant survives
  • packages/core/src/providers/model-discovery.ts:107 — [review] all discovery failure modes collapse to a bare null with zero diagnostics
中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。

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

Comment on lines +642 to +643
recommendationSource={snapshot.source}
syncChangesToFlow={false}

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.

[Critical] On the discovery path ModelIdsStep is rendered with syncChangesToFlow={false}, so after an empty submit sets modelIdsError ("Model IDs cannot be empty.") the error is never cleared while the user types or toggles corrections: handleCustomModelIdsChange/toggleRecommendationAtIndex call the no-op syncModelIds, and only flow.changeModelIds clears the error (useProviderSetupFlow.ts:312-315). The stale banner persists until the next successful Enter, while on every non-discovery provider the first keystroke clears it. (The related "Esc discards edits" behavior was verified as documented design — the PR description states edits are committed only on Enter — so this comment targets only the error clearing.)

Witness (probe driving the real hook + component at this commit):

DISCOVERY ARM      {"errorAfterTyping":"Model IDs cannot be empty.","bannerVisibleAfterTyping":true}
NON-DISCOVERY ARM  {"errorAfterTyping":null,"bannerVisibleAfterTyping":false}
flip syncChangesToFlow={true} -> discovery arm {"errorAfterTyping":null,"bannerVisibleAfterTyping":false}

Fix (spans two files, so no one-click suggestion): add a clearModelIdsError action to useProviderSetupFlow and call it from handleCustomModelIdsChange/toggleRecommendationAtIndex on the discovery path — or have syncModelIds call flow.changeModelIds(flow.state.modelIds) (value unchanged, error cleared) when syncChangesToFlow is false — so the error clears on edit without re-enabling the suppressed sync.

中文说明

发现(discovery)路径以 syncChangesToFlow={false} 渲染 ModelIdsStep,因此空提交触发 modelIdsError("Model IDs cannot be empty.")之后,用户输入或切换纠正时该错误永远不会被清除:handleCustomModelIdsChange/toggleRecommendationAtIndex 调用的是空操作 syncModelIds,而只有 flow.changeModelIds 会清除该错误(useProviderSetupFlow.ts:312-315)。过期的错误横幅会一直保留到下一次成功回车,而在所有非发现路径的 provider 上,第一次按键就会清除它。见证:在本提交上驱动真实 hook + 组件的探针——发现路径输入后错误仍在、横幅可见;非发现路径输入后错误清空、横幅消失;把 syncChangesToFlow 翻转为 true 后发现路径同样翻转。(相关的"Esc 丢弃编辑"行为已核实为文档声明的设计——PR 描述写明编辑仅在回车时提交——因此本评论只针对错误清除。)

修复(跨两个文件,故不提供一键 suggestion):在 useProviderSetupFlow 中新增 clearModelIdsError 动作,并在发现路径的 handleCustomModelIdsChange/toggleRecommendationAtIndex 中调用;或让 syncModelIdssyncChangesToFlow 为 false 时调用 flow.changeModelIds(flow.state.modelIds)(值不变、错误被清除),从而在不重新启用被抑制同步的情况下实现编辑即清除错误。

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

Comment on lines 300 to 306
const [selectedRecommendationKeys, setSelectedRecommendationKeys] = useState(
() => getRecommendedSelections(selectedModelIds, modelOptions),
() =>
getRecommendedSelections(selectedModelIds, modelOptions, builtInModelIds),
);
const [customModelIdsText, setCustomModelIdsText] = useState(() =>
getCustomModelIdsText(selectedModelIds, selectedRecommendationKeys),
);

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.

[Critical] With a partial catalog, unserved built-in defaults are demoted into the free-form input (getCustomModelIdsText), and mergeModelIds puts input ids first — so a no-edit Enter submits a list starting with an unserved model, which buildInstallPlan (models[0]modelSelection) and applyProviderInstallPlan (providers/install.ts) write as model.name on first-time setup. planOffersCurrentModel only guards re-runs, and re-running the wizard does not self-heal because the unserved id stays in modelProviders — a manual /model switch is required.

Failure scenario: first-time Coding Plan setup on a non-Pro account whose /models does not serve the Pro-only default qwen3.6-plus (the preset's own description tags it "Currently available to Pro subscribers only", and linked issue 9368 documents live endpoints serving partial catalogs): pressing Enter without editing makes model.name = qwen3.6-plus — the CLI's active model is one the account's own catalog does not list, and the first chat request targets it. This contradicts the PR's stated purpose ("selecting a stale recommendation persists a broken entry") — here the stale entry becomes the active model with zero user selection.

Witness (probes at this commit):

PARTIAL-CATALOG  submit order ["qwen3.6-plus","qwen3.5-plus",…] -> model.name writes [["model.name","qwen3.6-plus"]]
FULL-CATALOG     model.name writes [["model.name","qwen3.5-plus"]]
UI probe (catalog serves only MiniMax-M3, no edits): SUBMITTED {"modelIds":["MiniMax-M2.7","MiniMax-M3"]}  <- unserved first
recommendations-first mutant flips the order to ["MiniMax-M3","MiniMax-M2.7"]

Fix (spans merge ordering + tests, so no one-click suggestion): order checked served recommendations ahead of demoted/free-form ids on submit (e.g. recommendations-first in mergeModelIds, or reorder the no-edit submit so previously-default-but-unserved ids follow the served ones), so models[0] stays a model the account's catalog serves; add a test asserting modelSelection/model.name for a partial-catalog no-edit submit.

中文说明

当目录不完整时,未被提供的内置默认模型会被降级进自由输入框(getCustomModelIdsText),而 mergeModelIds 把输入框的 id 排在最前——于是不做任何编辑直接回车提交的列表会以一个未被提供的模型开头,buildInstallPlanmodels[0]modelSelection)和 applyProviderInstallPlanproviders/install.ts)在首次配置时会把它写成 model.nameplanOffersCurrentModel 只保护重复运行的场景,且重新运行向导无法自愈——未被提供的 id 仍留在 modelProviders 中,必须手动 /model 切换。

失败场景:非 Pro 账号首次配置 Coding Plan,其 /models 不提供 Pro 专属的默认模型 qwen3.6-plus(preset 自己的描述标注 "Currently available to Pro subscribers only",关联 issue 9368 也记录了线上端点只提供部分目录):不编辑直接回车会让 model.name = qwen3.6-plus——CLI 的激活模型不在账号自己的目录里,第一次聊天请求就会打到它。这与 PR 自述的目的("选中陈旧推荐会持久化一个坏条目")相矛盾——此处陈旧条目在用户零选择的情况下直接成为激活模型。

见证(本提交上的探针):部分目录下提交顺序 ["qwen3.6-plus","qwen3.5-plus",…]model.name 写入 qwen3.6-plus;完整目录时写入 qwen3.5-plus。UI 探针(目录只提供 MiniMax-M3、不做编辑):提交结果为 ["MiniMax-M2.7","MiniMax-M3"](未提供的排在最前);把合并顺序改为推荐优先的突变体将顺序翻转为 ["MiniMax-M3","MiniMax-M2.7"]

修复(跨合并顺序与测试,故不提供一键 suggestion):提交时让已勾选的被提供服务推荐排在被降级/自由输入的 id 之前(例如在 mergeModelIds 中推荐优先,或重排无编辑提交使"曾是默认但未被提供"的 id 排在被提供模型之后),保证 models[0] 始终是账号目录中存在的模型;并新增测试断言部分目录下无编辑提交的 modelSelection/model.name

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

…dels (QwenLM#9980)

Two Critical review findings on the discovery wizard. On the discovery
path edits never cleared the empty-submit error banner because they only
call the no-op sync; add a clearModelIdsError flow action for that
branch. And mergeModelIds put free-form ids first, so a partial-catalog
no-edit submit led with an unserved demoted default, which
buildInstallPlan writes as model.name on first-time setup; checked
recommendations lead now.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 6/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 6/100 轮)。改动内容与我反驳保留之处如下:

Autofix round summary — PR #9980 (Critical-only mode)

Both Critical inline findings from the automated review (rv:5025619269) are fixed. Each was reproduced on the pre-fix tree with a focused failing test first, then fixed minimally; the committed tests fail before the fix and pass after it. No conflict resolution was needed (--conflict false; the branch already contains all of origin/main). The 4 items under Deferred non-Critical feedback were left untouched per Critical-only mode (audit record, not work).

Findings and dispositions

[rc:3858676058] Critical — partial-catalog no-edit submit leads with an unserved model → Fixed

Reproduced before fixing: with the built-in defaults MiniMax-M3, MiniMax-M2.7 pre-selected and a catalog serving only MiniMax-M3, a no-edit Enter submitted ['MiniMax-M2.7', 'MiniMax-M3'] — the demoted unserved default first. buildInstallPlan maps models[0] to modelSelection, and applyProviderInstallPlan writes it as model.name on first-time setup, so the unserved id would become the active model with zero user selection. Root cause: mergeModelIds in ProviderSetupSteps.tsx placed free-form input ids ahead of checked recommendations, and with a partial catalog the unserved built-in defaults are demoted into exactly that free-form input. Fix: checked recommendations now lead the merged submit order (they are catalog-served, while the free-form text can hold demoted unserved defaults); the display of the input box and the Esc-discards/Enter-commits design are unchanged. Three pre-existing tests pinned the buggy input-first ordering and their expectations were updated to the fixed order (['MiniMax-M3', 'MiniMax-M2.7', 'custom-model'], ['MiniMax-M3', 'xcustom-model', 'MiniMax-M2.7'], ['MiniMax-M3', 'MiniMax-M4', 'custom-model']) — stated here because their expected values changed. New regression test submits served recommendations ahead of unserved defaults on a partial catalog pins the no-edit partial-catalog submit order; the downstream models[0] → modelSelection and modelSelection → model.name links stay pinned by the existing core tests (provider-config.test.ts, install.test.ts).

[rc:3858676053] Critical — stale empty-submit error banner on the discovery path → Fixed

Reproduced before fixing by driving the real useProviderSetupFlow hook plus component: on the discovery path (syncChangesToFlow={false}) an empty submit set Model IDs cannot be empty. and the banner survived typing because handleCustomModelIdsChange/toggleRecommendationAtIndex only call the no-op syncModelIds, while only flow.changeModelIds cleared the error. Fix: a new clearModelIdsError action in useProviderSetupFlow, called from the syncChangesToFlow={false} branch of syncModelIds (the single choke point for both typing and recommendation toggles), matching how changeModelIds clears the error on the synced path. The suppressed sync stays suppressed — edits still commit only on Enter. New regression test clears the model-ids error on edit after an empty discovery submit.

Failed check: Test (windows-latest, Node 22.x) → diagnosed; no evidence-backed fix exists inside this PR's footprint

  • The failing run (32908170834) tested head 1b40f11525, which already contains all of origin/main (git log HEAD..origin/main is empty), including b331509de4 (fix: repair the Windows and macOS test lane failures #9728, the commit that repaired the pre-existing Windows/macOS platform test failures) and 519e824dc7 (fix(ci): give the macOS and Windows lanes a trigger again #9370). The round-5 base sync already addressed the last known cause of this lane failing.
  • On that same run the Ubuntu and macOS test lanes passed on identical code, and this runner has no GitHub credentials, so the Windows job log cannot be retrieved here; checks.json carries only the FAILURE conclusion.
  • Every test this PR touches that could be timing/platform-sensitive is an itWhenTuiInputReliable test, which is skipped whenever process.platform === 'win32' || process.env.CI === 'true' — i.e. skipped on every CI lane including Windows, so the PR's test changes cannot be the failing case there. The rest of the diff is platform-neutral (React render logic, string merging, mocked fetch; no fs/path/child_process/locale dependencies).
  • Local evidence on this Linux runner: focused auth suite green with CI=true (46 passed | 18 skipped under the pre-existing gate), the ungated AuthDialog TUI walk green 25/25, core providers green (172 passed), build/typecheck/lint green, and the full workspace npm run test:ci result listed below. The repo history shows recurring Windows-lane infrastructure repairs (e.g. 3dd1b6dd6c), and the Windows lane is not runnable on this Linux runner, so per the surrogate-verification rule this is recorded rather than guessed at; the workflow's independent CI after the push remains the final gate.

Verification

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0)
  • npx prettier --check on the 3 touched files — clean
  • CI=true npx vitest run src/ui/auth/ (packages/cli, touched) — 3 files, 46 passed | 18 skipped (pre-existing isUnreliableTuiInputEnvironment gate)
  • env -u CI npx vitest run src/ui/auth/AuthDialog.test.tsx (packages/cli, ungated TUI walk) — 25 passed
  • CI=true npx vitest run src/providers/ (packages/core, PR footprint) — 16 files, 172 passed
  • Mutation probe A (reverted mergeModelIds to input-first): 4 tests failed — the 3 updated ordering tests plus the new partial-catalog test; fix restored, green again
  • Mutation probe B (removed the clearModelIdsError else-branch): the new error-clearing test failed; fix restored, green again
  • CI=true npm run test:ci (full workspace suite, as the CI Test job runs it) — first run under this agent runner's ambient environment showed 32 CLI test failures in 9 files (config/settings/commands/Footer/serve-live); every one of them is environment-caused, not code: the same files pass 584/584 under the CI job's env block (env -u QWEN_HOME -u SANDBOX -u QWEN_CODE_CUSTOM_SANDBOX_IMAGE -u OPENAI_*, empty API keys, fresh HOME), and the CI Ubuntu lane already ran this exact head green. This runner hosts a Qwen Code agent, so its ambient QWEN_HOME, SANDBOX, QWEN_CODE_CUSTOM_SANDBOX_IMAGE, and OPENAI_* variables poison those env-sensitive suites. The definitive clean-environment full-suite result is listed next
  • env -u QWEN_HOME -u SANDBOX -u QWEN_CODE_CUSTOM_SANDBOX_IMAGE -u OPENAI_MODEL -u OPENAI_BASE_URL CI=true NO_COLOR=true HOME=<fresh> USERPROFILE=<fresh> OPENAI_API_KEY= DASHSCOPE_API_KEY= QWEN_API_KEY= GEMINI_API_KEY= QWEN_DEFAULT_AUTH_TYPE= npm run test:ci — every workspace fully green except the CLI workspace, where 4 files (src/commands/update.test.ts, src/commands/review/script-lint-isolation.test.ts, src/serve/workspace-registration-store.test.ts, src/serve/server-default-bridge-wiring.test.ts) lost 5 tests to 15s timeouts under the all-workspaces-parallel load on this shared runner; all 4 files pass in isolation under this same env block (45 passed | 1 Windows-only skip), none of them imports anything this diff touches, and the CI Ubuntu lane ran this exact head green
  • Integration tests after npm run bundle — not run: both fixes are exercised by the unit suites above; the integration harness does not drive the auth wizard UI
  • npm run generate:settings-schema — not needed: no settings source changed
中文说明

Autofix 轮次总结 — PR #9980(仅处理 Critical 模式)

自动审查(rv:5025619269)提出的两条 Critical 行内发现均已修复。每一条都先在修复前的代码树上用聚焦的失败测试复现,再做最小修复;本轮提交的测试在修复前失败、修复后通过。无需冲突处理(--conflict false;分支已包含 origin/main 的全部内容)。Deferred non-Critical feedback 下的 4 条按仅处理 Critical 模式保持原样(仅为审计记录,非本轮工作)。

发现与处置

[rc:3858676058] Critical — 部分目录下无编辑提交以未提供服务模型开头 → 已修复

修复前已复现:内置默认 MiniMax-M3, MiniMax-M2.7 处于预选状态、而目录只提供 MiniMax-M3 时,不做任何编辑直接回车会提交 ['MiniMax-M2.7', 'MiniMax-M3']——被降级的未提供服务默认模型排在最前。buildInstallPlanmodels[0] 映射为 modelSelectionapplyProviderInstallPlan 在首次配置时将其写为 model.name,因此未经任何用户选择,未提供服务的 id 就会成为激活模型。根因:ProviderSetupSteps.tsx 中的 mergeModelIds 把自由输入框的 id 排在已勾选推荐之前,而在部分目录下,未被提供的内置默认模型恰好被降级进了这个自由输入框。修复:合并后的提交顺序改为已勾选推荐在前(它们是目录实际提供的,而自由输入框可能包含被降级的未提供服务默认模型);输入框的展示、Esc 丢弃/回车提交的设计均不变。有 3 个既有测试固化了有缺陷的"输入优先"顺序,其期望值已更新为修复后的顺序(['MiniMax-M3', 'MiniMax-M2.7', 'custom-model']['MiniMax-M3', 'xcustom-model', 'MiniMax-M2.7']['MiniMax-M3', 'MiniMax-M4', 'custom-model'])——因其期望值发生变化,特此说明。新增回归测试 submits served recommendations ahead of unserved defaults on a partial catalog 固化部分目录无编辑提交的顺序;下游 models[0] → modelSelectionmodelSelection → model.name 两个环节仍由既有 core 测试(provider-config.test.tsinstall.test.ts)固化。

[rc:3858676053] Critical — 发现路径上空提交错误横幅长期不消失 → 已修复

修复前已复现:用真实的 useProviderSetupFlow hook 加组件驱动——发现路径(syncChangesToFlow={false})空提交会设置 Model IDs cannot be empty.,之后用户输入时横幅依然存在,因为 handleCustomModelIdsChange/toggleRecommendationAtIndex 只调用空操作的 syncModelIds,而只有 flow.changeModelIds 会清除错误。修复:在 useProviderSetupFlow 中新增 clearModelIdsError 动作,在 syncModelIdssyncChangesToFlow={false} 分支中调用(输入与推荐切换两条路径的唯一汇聚点),与同步路径上 changeModelIds 清除错误的行为对齐。被抑制的同步保持抑制——编辑仍然只在回车时提交。新增回归测试 clears the model-ids error on edit after an empty discovery submit

失败检查:Test (windows-latest, Node 22.x) → 已诊断;本 PR 足迹内没有证据支持的修复可做

  • 失败的那次运行(32908170834)测试的 head 是 1b40f11525,它已包含 origin/main 的全部内容(git log HEAD..origin.main 为空),包括 b331509de4fix: repair the Windows and macOS test lane failures #9728,修复既有 Windows/macOS 平台测试失败的提交)和 519e824dc7fix(ci): give the macOS and Windows lanes a trigger again #9370)。第 5 轮的基线同步已经解决了该车道失败的上一个已知原因。
  • 同一次运行中 Ubuntu 与 macOS 测试车道在完全相同的代码上通过;本 runner 没有 GitHub 凭据,无法拉取 Windows 任务日志,checks.json 只携带 FAILURE 结论。
  • 本 PR 触及的所有可能对时序/平台敏感的测试都是 itWhenTuiInputReliable 测试,它们在 process.platform === 'win32' || process.env.CI === 'true' 时跳过——即在包括 Windows 在内的所有 CI 车道上都跳过,因此本 PR 的测试改动不可能是那里的失败项。diff 的其余部分是平台无关的(React 渲染逻辑、字符串合并、mock 的 fetch;无 fs/路径/子进程/区域设置依赖)。
  • 本 Linux runner 上的本地证据:CI=true 下聚焦 auth 套件绿色(46 通过 | 18 跳过,跳过来自既有门控),未门控的 AuthDialog TUI 走查 25/25 绿色,core providers 绿色(172 通过),build/typecheck/lint 绿色,完整工作区 npm run test:ci 结果见下。仓库历史显示 Windows 车道有反复的基础设施修复(如 3dd1b6dd6c),且 Windows 车道无法在本 Linux runner 上运行,因此按替代验证规则此处只做记录、不做猜测;推送后工作流的独立 CI 仍是最终验证门。

验证

  • npm run build — 通过(exit 0)
  • npm run typecheck — 通过(exit 0)
  • npm run lint — 通过(exit 0)
  • 对 3 个触及文件运行 npx prettier --check — 干净
  • CI=true npx vitest run src/ui/auth/(packages/cli,触及包)— 3 个文件,46 通过 | 18 跳过(既有 isUnreliableTuiInputEnvironment 门控)
  • env -u CI npx vitest run src/ui/auth/AuthDialog.test.tsx(packages/cli,未门控 TUI 走查)— 25 通过
  • CI=true npx vitest run src/providers/(packages/core,PR 足迹)— 16 个文件,172 通过
  • 突变探针 A(把 mergeModelIds 还原为输入优先):4 个测试失败——3 个更新过顺序的测试加新的部分目录测试;恢复修复后重新绿色
  • 突变探针 B(移除 clearModelIdsError else 分支):新的错误清除测试失败;恢复修复后重新绿色
  • CI=true npm run test:ci(完整工作区套件,与 CI Test 任务相同命令)— 首次在本 agent runner 的环境变量下运行时,9 个文件(config/settings/commands/Footer/serve-live)出现 32 个 CLI 测试失败;全部为环境所致,非代码问题:同样的文件在 CI 任务的环境块(env -u QWEN_HOME -u SANDBOX -u QWEN_CODE_CUSTOM_SANDBOX_IMAGE -u OPENAI_*、API key 置空、全新 HOME)下 584/584 通过,且 CI Ubuntu 车道已在完全相同的 head 上绿色通过。本 runner 自身承载 Qwen Code agent,其环境中的 QWEN_HOMESANDBOXQWEN_CODE_CUSTOM_SANDBOX_IMAGEOPENAI_* 变量污染了这些对环境敏感的套件。干净环境下的最终完整套件结果见下一条
  • env -u QWEN_HOME -u SANDBOX -u QWEN_CODE_CUSTOM_SANDBOX_IMAGE -u OPENAI_MODEL -u OPENAI_BASE_URL CI=true NO_COLOR=true HOME=<全新> USERPROFILE=<全新> OPENAI_API_KEY= DASHSCOPE_API_KEY= QWEN_API_KEY= GEMINI_API_KEY= QWEN_DEFAULT_AUTH_TYPE= npm run test:ci — 除 CLI 工作区外全部工作区完全绿色;CLI 工作区有 4 个文件(src/commands/update.test.ts、src/commands/review/script-lint-isolation.test.ts、src/serve/workspace-registration-store.test.ts、src/serve/server-default-bridge-wiring.test.ts)在本共享 runner 的全工作区并行负载下因 15 秒超时丢了 5 个测试;这 4 个文件在相同环境块下单独运行全部通过(45 通过 | 1 个仅 Windows 的跳过),均未导入本 diff 触及的任何内容,且 CI Ubuntu 车道已在完全相同的 head 上绿色通过
  • npm run bundle 后的集成测试 — 未运行:两个修复均由上述单元套件验证;集成测试框架不驱动 auth 向导 UI
  • npm run generate:settings-schema — 无需:未改动任何 settings 源

Deferred non-Critical feedback

Critical-only mode is active: 5 change-producing rounds are complete. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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 blocking issues. LGTM! ✅

Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:

  • packages/core/src/providers/types.ts:70 — [review] supportsModelDiscovery does not state the OpenAI-compatible catalog contract (Bearer GET {baseUrl}/models)
中文说明

无阻断问题。LGTM!✅

收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

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

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

Copy link
Copy Markdown
Collaborator

Released in v0.22.2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(providers): dynamically fetch model list for ModelStudio Token Plan / Coding Plan presets

4 participants