Skip to content

feat(models): enforce per-account upstream cloud catalogs - #232

Merged
dwgx merged 7 commits into
dwgx:masterfrom
andya1lan:feat/cloud-catalog-filter
Jul 30, 2026
Merged

feat(models): enforce per-account upstream cloud catalogs#232
dwgx merged 7 commits into
dwgx:masterfrom
andya1lan:feat/cloud-catalog-filter

Conversation

@andya1lan

@andya1lan andya1lan commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

改了什么 / What changed

  • Synchronize and store GetCascadeModelConfigs per active account, expose the active-account union through Cascade model listings, and route only through accounts whose own catalog contains the requested model.
  • Invalidate accepted and pending catalog state across account lifecycle changes, and quarantine anomalously small snapshots behind one delayed confirmation that ordinary sync triggers cannot bypass.
  • Keep drought protection aligned with catalog-approved free models, and show a plain-language Dashboard warning when only the drought premium restriction fails open.
  • Do not apply Cascade account allowlists to DEVIN_CONNECT or special-agent routing, start Connect synchronization without waiting for Cascade confirmation, and preserve WINDSURFAPI_IGNORE_CLOUD_FILTER=1 as a compatibility escape hatch.

为什么 / Why

The static model table was exposed independently by API, Dashboard, and routing paths. Cascade deployments with a restricted upstream catalog could therefore advertise models that their accounts could not use.

The initial filtering implementation also kept one process-wide snapshot from the first active account. In a mixed account pool, that could hide usable models or apply one account's catalog to unrelated accounts.

This change treats the active pool listing as the union of its account catalogs, while keeping routing decisions isolated to the selected account.

Relates to #231.

行为 / Behavior

  • Cascade model listings use the union of all active account catalogs.
  • Cascade account selection checks the candidate account's own catalog before entitlement and tier overrides.
  • Disabled and removed accounts stop contributing models to pool listings, and both accepted and pending catalog state are invalidated before a later reactivation.
  • Dynamically discovered models remain visible when at least one active account exposes them.
  • Cascade account allowlists do not narrow DEVIN_CONNECT or special-agent routing. Existing Connect discovery still starts from the shared model registry and its own selector catalog, as described under Out of scope.
  • A quarantined, malformed, or failed Cascade response does not delay the independent DEVIN_CONNECT selector-catalog synchronization.
  • Missing, empty, or failed catalog responses remain fail-open for startup and compatibility.
  • A non-empty snapshot containing fewer than half as many unique model UIDs as the account's last accepted snapshot, or the static catalog when no account snapshot exists yet, is quarantined. The current fail-open or last-known-good view remains active, a warning is logged, and one confirmation fetch runs 30 seconds later. Ordinary sync triggers are coalesced while that timer is pending, so they cannot confirm the candidate early. The pending candidate is applied during that confirmation only when the delayed round returns the exact same UID set; a different response remains unaccepted and does not start another polling loop.
  • On first synchronization, that safety check deliberately leaves discovery fail-open for up to 30 seconds. Models outside the candidate catalog may therefore remain temporarily visible until an exact delayed confirmation arrives; this is the availability trade-off for not trusting one potentially partial upstream response.
  • A malformed response clears the restrictive snapshot and remains retryable; a valid empty array is synchronized as an explicit no-filter snapshot.
  • On the Cascade transport, drought mode restricts the pool to catalog-approved free-tier models when available; if that intersection is empty, only the drought restriction fails open and the account catalog remains authoritative.
  • When that Cascade drought fail-open path is active, the Dashboard plainly states that no free model is available, paid models are not being blocked by low-quota protection, and account-unsupported models remain hidden and blocked.
  • Setting WINDSURFAPI_IGNORE_CLOUD_FILTER=1 restores the complete static catalog.

Out of scope

This PR does not add regular time-based catalog refresh for an unchanged active account. Normal synchronization remains event-driven: it runs at startup and after existing account lifecycle events such as account creation, reactivation, or API key replacement. The only timer added here is a single 30-second confirmation retry after an anomalously small non-empty response; it does not continue polling after the candidate is accepted or replaced. General refresh scheduling, adaptive backoff, request-rate policy, and persisted last-known-good state remain out of scope.

DEVIN_CONNECT uses a different selector namespace, but its discovery paths still start from the shared MODELS registry before applying the Connect selector snapshot/live catalog. This PR does not make /v1/models or Dashboard model views account-entitlement-aware, does not isolate that shared registry, does not move the drought gate into the Connect request path, and does not silently rewrite paid-model requests to swe-1-6-slow. With Connect enabled, the displayed model set therefore remains broader than the account-entitled selector set, and a confirmed Cascade catalog can still change that display when it contributes a previously unknown row to the shared registry. Connect-specific discovery, registry isolation, and enforcement are left for a separate follow-up.

测试 / Testing

Automated verification:

  • Rebased onto upstream master v3.9.4 before verification.
  • Local npm test: 3096/3096 passed.
  • npm run test:release: all 233 test files completed with exit code 0.
  • Targeted catalog regression suites pass: 23/23 tests across test/cloud-catalog-filter.test.js and test/cloud-catalog-backend-boundary.test.js.
  • Regression coverage verifies that ordinary sync triggers cannot bypass the delayed confirmation, account deactivation clears pending candidates, restrictive Cascade allowlists do not narrow DEVIN_CONNECT /v1/models or Dashboard views, Connect mode does not trigger the Cascade-only drought fail-open warning, and Connect selector-catalog synchronization is not delayed.
  • The earlier proxy-ordering result was specific to the local proxy/network environment. A local-only isolated fixture passes that file 9/9; this PR does not include any proxy test change or claim that the failure reproduces on master.
  • On the current GitHub CI run, Node syntax and three test shards passed. Test shard 2/4 reported one timing-boundary mismatch in the unchanged test/rate-limit.test.js: expected 1632000, received 1631999. The exact file passes locally 13/13 and the exact shard command passes locally; this branch does not modify that test or its rate-limit implementation. The failed shard is pending a CI rerun.
  • npm run secret-scan: passed.
  • git diff --check: passed.

Manual smoke testing with a live restricted account confirmed the expected transition: the first below-threshold snapshot emitted a warning and preserved the fail-open view, then an exact confirmation about 30 seconds later applied the restricted listing. Two live accounts governed by the same policy produced identical sorted model-ID sets, and enabling both produced the same deduplicated pool union. Toggling Connect mode confirmed the existing broader Connect discovery view described above; its displayed count can change after Cascade confirmation because both paths still share the mutable MODELS registry. Entitlement-aware Connect discovery and registry isolation remain in the follow-up.

Not run with heterogeneous live account catalogs: unique-model union, per-account routing, and removal of one account's unique models. These paths are covered by automated mixed-pool regression tests.

Checklist

  • 代码风格和现有文件一致 / Code style matches existing files
  • 没有引入 npm 依赖 / No new npm dependencies (project is zero-dep)
  • 涉及 LS binary 协议改动时 在 PR 描述里注明字段号来源 - 未改动 / If touching LS protocol, document field-number source in the PR description — not applicable; no LS protocol fields changed
  • 涉及 dashboard UI 用 App.confirm / App.prompt 不用浏览器原生 alert/confirm - 未添加弹窗 / Uses App.confirm / App.prompt, not native dialogs (if dashboard) — passive warning only; no dialog added
  • Open and link the feature issue required by CONTRIBUTING.md ([Feature] Filter model catalogs per upstream account #231)

Copilot AI review requested due to automatic review settings July 28, 2026 06:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR implements per-upstream-account cloud model catalogs by syncing GetCascadeModelConfigs for each active account, exposing the union of active catalogs in pool-wide listings (/v1/models + Dashboard), and enforcing per-account catalog checks during routing.

Changes:

  • Added per-account cloud catalog storage + filtering logic in src/models.js, including a pool-union view and a compatibility opt-out via WINDSURFAPI_IGNORE_CLOUD_FILTER=1.
  • Updated account lifecycle + routing gates in src/auth.js to sync catalogs per active account, invalidate stale catalogs on key/account changes, and enforce catalog checks before entitlements/tier overrides.
  • Updated Dashboard endpoints and added a dedicated test suite to ensure union listings + per-account routing behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/cloud-catalog-filter.test.js Adds coverage for union listings, per-account routing enforcement, fail-open behavior, and lifecycle invalidation.
src/models.js Introduces per-account cloud catalog filtering, updates tier/model listing behavior, and stores catalog snapshots per account.
src/dashboard/api.js Ensures Dashboard model/tier-access endpoints expose the filtered union instead of the full static catalog.
src/auth.js Implements per-account catalog synchronization, catalog invalidation on lifecycle changes, and per-account routing checks.
README.md Documents WINDSURFAPI_IGNORE_CLOUD_FILTER and clarifies listing vs routing behavior (CN).
README.en.md Documents WINDSURFAPI_IGNORE_CLOUD_FILTER and clarifies listing vs routing behavior (EN).
.env.example Adds example documentation for WINDSURFAPI_IGNORE_CLOUD_FILTER.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/models.js Outdated
Comment thread src/auth.js Outdated
@dwgx

dwgx commented Jul 28, 2026

Copy link
Copy Markdown
Owner

评审结论:方向正确,但有 2 个阻塞问题 + 1 条声明不实,暂不合并

感谢这个 PR。它修的问题(#231)是真的:静态模型表被 /v1/models、Dashboard、路由三条路径各自独立暴露,受限上游目录的部署会宣传自己账号用不了的模型。"池级用并集、路由校验所选账号自己的目录"这个分层设计是对的,也修掉了初版"全进程复用第一个活跃账号快照"的问题。

以下结论都经过实测,不是读描述得出的。

已验证做对的部分

  • 并集/隔离语义正确:A 只有 sonnet、B 只有 gpt-5.2 时,池级列出 2 个,而 isModelAllowedByCloudCatalog('gpt-5.2', env, 'A') = false。
  • 池级 fail-open 分层正确:池里存在尚未同步的账号 C 时,池级整体回落全量(实测 130),同时账号维度判定不受影响。
  • 15 个无 modelUid 的模型会被判 false,但全部已 deprecated,无用户可感知影响。
  • 没犯"漏改一条协议路由"的老陷阱:connect 路径走独立的 selector 命名空间门禁(isConnectSelectorAllowedForAccount),不受这个 MODELS 命名空间过滤影响。
  • 全量测试:我在本 PR 分支上实跑 npm run test:release3012 pass / 0 fail

阻塞问题

1)(blocker)干旱模式会被打成全站中断

isModelBlockedByDrought(src/auth.js:201)的白名单是 getTierModels('free'),而这个函数现在受 catalog 过滤。实测:某账号 catalog 只含 sonnet 时,free 白名单变成 []

catalog 只含 sonnet 后 free 模型数 = 0  []

一旦干旱模式激活(credits 低于阈值),空白名单意味着 !freeModels.has(k)每个模型都为真 → 所有模型全被拦。这个调用点是池级(不传 accountId),PR 没有为它做例外。

建议:池级调用点(auth.js:201auth.js:229freeTierModels)排除在过滤之外,或对干旱白名单单独 fail-open。

2)(major)上游"部分响应"没有守卫

getCascadeModelConfigs 只在抛错时 fail-open。返回一个截断但非空的 configs 时,PR 会把它当权威目录。实测单账号池 catalog 只回 sonnet:

getAvailableModelsForAccount 数量 = 6   (原 130)
isModelAllowedForAccount(gpt-5.2)  = false

上游少回一批模型 == 给用户断供,而唯一的出路是 WINDSURFAPI_IGNORE_CLOUD_FILTER=1 —— 那时用户已经在报障了。建议加一个合理性门槛:新目录相比上次显著缩小(或显著小于静态表)则拒绝采纳并告警。

声明不实,需要你确认

PR 描述写:

The remaining failure also reproduces on master: test/account-add-proxy-ordering.test.js:320 expects 400 but receives 200

这条在 master 上不复现。 我在干净 master 上实跑该文件:9 tests / 9 pass / 0 fail;在你的分支上也是 0 fail。请确认是不是本地环境残留(比如 .env / 代理设置)导致的,并更正描述 —— 本仓库合并前要逐条核实诊断链,一条不实的测试声明会让整份验证结果失去可信度。

结论

修掉上面第 1 条(必须)和第 2 条(强烈建议),并更正测试声明后,我会重新评审。设计本身我认可,不需要重做。

@dwgx

dwgx commented Jul 28, 2026

Copy link
Copy Markdown
Owner

复审 b8de001:修的是另一个场景,两条阻塞问题都还在

感谢跟进。b8de001 本身是对的 —— configs 不是数组时 fail-open,实测 mergeCloudModels(null, {accountId})listModels() = 130(全量),cloud-catalog-filter.test.js 14/14 通过。这条可以保留。

但它处理的是"响应不是数组",而我提的两条说的是"响应合法数组、只是内容不全"。在你更新后的 head 上重跑,两条都仍然复现:

=== blocker 1:干旱 free 白名单 ===
无 catalog 时 free = 1
catalog 只含 sonnet 后 free = 0  []
→ 仍未修

=== major 2:部分但非空响应 ===
pro 账号可用模型数 = 6   (静态约 130)
gpt-5.2 可路由 = false
→ 仍未修

为什么 b8de001 接不住这两条

两处都走的是 Array.isArray(configs) === true 的正常路径:

  • [{modelUid:'claude-sonnet-4-6'}]合法数组、非空,所以 nextCloudCatalogUids.size > 0,catalog 被当权威采纳。fail-open 分支不触发。
  • 于是 getTierModels('free') 过滤后为空 → isModelBlockedByDrought!freeModels.has(k)每个模型为真 → 干旱一激活就是全站中断。
  • 同理 pro 账号从 130 塌到 6。

也就是说:现在的 fail-open 条件是"上游没给出可解析的东西",而缺的是"上游给的东西不合理"。

建议的修法

1)干旱白名单(必须)

src/auth.js:201getTierModels('free'):229freeTierModels 都是池级语义,不该被账号 catalog 过滤。最小改法是让这两个调用点拿未过滤的基线,或对干旱白名单单独 fail-open —— 空白名单绝不能被解释成"什么都不许用"。

2)合理性门槛(强烈建议)

给采纳加一道 sanity check:新目录相比该账号上次的目录、或相比静态表显著缩小时,拒绝采纳并 log.warn。上游少回一批模型就等于给用户断供,而现在唯一的出路是 WINDSURFAPI_IGNORE_CLOUD_FILTER=1 —— 用户报障之后才会想到它。

3)测试声明(仍需更正)

上一条评审里提到的 test/account-add-proxy-ordering.test.js 那句"在 master 上也复现",PR 描述里还没改。我在干净 master 上是 9/9 全过。麻烦确认下本地环境并更正描述。

设计我依然认可,不用重做 —— 补上第 1 条就可以进下一轮评审。

@dwgx dwgx left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Request changes — 设计认可,还剩 2 个问题 + 1 条声明待更正

前面两条 comment 里已给出实测细节(评审复审 b8de001),这里是正式结论,方便你对着清单收尾。

设计层面我认可,不需要重做。 "发现用并集 + 路由用账号维度"这个分层是对的,b8de001 的非数组 fail-open 也是对的,请保留。connect 路径走独立 selector 命名空间门禁、没被这个 MODELS 命名空间过滤影响 —— 这点你处理对了,本仓库历史上"修复只覆盖部分协议路由"已经犯过两次。

待改清单

  • (blocker) 干旱白名单不能被账号 catalog 过滤。 src/auth.js:201getTierModels('free'):229freeTierModels 都是池级语义。实测某账号 catalog 只含 sonnet 时白名单变成 [],而空白名单会让 isModelBlockedByDrought每个模型返回 true —— 干旱一激活即全站中断。空白名单绝不能被解释成"什么都不许用"。
  • (major) 给采纳加一道合理性门槛。 目前只在响应不可解析时 fail-open;响应是合法数组但内容不全时会被当权威目录(实测 pro 账号 130 → 6)。建议:新目录相比该账号上次目录、或相比静态表显著缩小时,拒绝采纳并 log.warn。上游少回一批模型等于给用户断供,而 WINDSURFAPI_IGNORE_CLOUD_FILTER=1 是用户报障之后才会想到的东西。
  • 更正测试声明。 描述里写 test/account-add-proxy-ordering.test.js:320 那条失败"also reproduces on master"。我在干净 master 上实跑是 9 tests / 9 pass / 0 fail,你分支上也是 0 fail。请确认是否本地环境残留(.env / 代理设置)并更正描述。

第三条单独说一句,不是针对那条测试本身:本仓库合并前会逐条核实提交者的诊断链,所以测试结论的准确性比它是否通过更重要 —— 一条不实的声明会让整份验证结果都需要重新核对。你其余的诊断我复核后都成立,这条更正掉就没有信任成本了。

改完 ping 我,我重新跑全量 + 对抗验证。

@andya1lan

Copy link
Copy Markdown
Contributor Author

感谢作者答复,b8de001 是修复 copilot 自动审阅提出的小问题。

您提出的问题清单修改如下:

  1. drought 模式改为先计算账号池 catalog 实际允许的 free-tier 模型。如果至少存在一个可用免费模型,干旱模式仍只允许这些免费模型;如果交集为空,干旱限制单独 fail-open,继续以账号 catalog 为准。这样账号 catalog 只包含 Sonnet 等付费模型时,不会因为免费白名单为空而把所有模型都拦截。
  2. 账号 catalog 的缩小检查改为只和该账号的 last-known-good 比较,不再使用本地静态模型数量作为首次同步基线。首次非空快照直接采纳;后续快照若小于上次已接受数量的 50%,本轮只进入 quarantine,不覆盖 last-known-good,也不立即重试。只有后续独立同步轮次返回完全相同的 model UID 集合才采纳。这样确认之间具有真实时间间隔,首次同步也不会让合法的小型 allowlist 多发一次请求。协议没有完整性标记,因此跨轮持续返回且内容完全相同的部分响应仍无法与真实 allowlist 区分,这是当前可验证信息下的边界。
  3. 已撤回 “the remaining failure also reproduces on master” 这条不准确的笼统声明。之前的本地结果受到 DNS 设置影响;调整测试夹具后已稳定通过。
  4. Dashboard 的两个界面都新增了明确的 restrictionFailOpen 告警。当 catalog 与 free-tier 模型的交集为空时,界面不再静默显示普通 drought 文案,而是提示用户:低配额保护不会阻止付费模型;账号本身不支持的模型仍会被隐藏和拦截。Snipaste_2026-07-29_00-19-22@2x

对抗场景复测:首次同步即返回 Sonnet-only catalog 时,一次请求后直接采纳 1 个模型;已有 130 个模型的 last-known-good 突然缩小到 Sonnet-only 时,模型数为 130 → 本轮响应后仍 130 → 后续同步轮次返回相同快照后 1。catalog 与 free-tier 的交集为空时,Dashboard 会明确显示干旱限制处于 fail-open,Sonnet 仍由账号 catalog 正常控制。

补充同步范围:本 PR 没有新增定时 catalog refresh。

当前同步仍由事件触发:进程启动时执行,并在账号新增、重新激活或 API key 更换等现有生命周期事件后执行;长期保持 active 且凭证不变的账号不会在运行过程中定时重新拉取上游目录。因此,无账号事件时的上游 catalog 变更检测,以及相应的调度、退避和请求频率策略,明确属于本 PR 的 out of scope,适合后续单独处理。

验证结果:本地全部自动化测试通过,包括 npm testnpm run test:releasenpm run secret-scangit diff --check

@dwgx

dwgx commented Jul 28, 2026

Copy link
Copy Markdown
Owner

答复质量很好,但代码还没推上来

先说要紧的:PR head 仍是 b8de001,你描述的四项修改在分支上一行都没有。我核实了:

$ gh pr view 232 --json headRefOid   → b8de0010
$ git show pull/232/head:src/auth.js → isModelBlockedByDrought 仍是
    const freeModels = new Set(getTierModels('free'));
    return !freeModels.has(modelKey);

也就是空白名单拦一切的问题原样还在。是漏 push 了,还是推到了别的分支?推上来我就重新审。


对四条答复本身的评价

方案层面我基本认可,尤其第 1、3、4 条:

  • 第 1 条(干旱):"先算 catalog ∩ free-tier,非空则照旧限制、空则单独 fail-open" —— 这个思路对,而且比我建议的"池级调用点直接不过滤"更精确,保住了干旱本来的语义。
  • 第 3 条:撤回那条声明并说明是 DNS 影响,这就够了。你其余的诊断我复核后都成立,这条更正掉就没有信任成本了。
  • 第 4 条:主动给 Dashboard 加 restrictionFailOpen 告警,这不在我要求范围内,是加分项。fail-open 是"安全但可能违反预期"的状态,让运维方看见它是对的。
  • out-of-scope 的划法:同意。"无账号事件时的上游 catalog 变更检测"确实是独立议题,不该塞进这个 PR。

第 2 条有一处内部矛盾,需要你重新想一下

你的缩小检查依赖"后续独立同步轮次返回完全相同的 UID 集合才采纳"。但你在同一条答复的补充说明里写:

当前同步仍由事件触发……长期保持 active 且凭证不变的账号不会在运行过程中定时重新拉取上游目录。

这两句放在一起会得到:一个长期 active、凭证不变的账号永远不会有"后续轮次"。 于是上游真的缩减了目录(比如运维方在 Devin 侧调整了 allowlist)时,新目录被 quarantine 后永久卡在旧的 last-known-good 上 —— 直到进程重启或账号事件发生。

这个方向的错误比原来那条轻(宁可多暴露模型,也不要少),所以不是 blocker。但它意味着第 2 条的保护在最常见的稳态部署下永远不会解除。两个出路:

  1. 承认这一点,把 quarantine 明确定义为"仅在有第二次同步机会时生效",并在日志里说清楚 —— 配合你划的 out-of-scope,这是自洽的;
  2. 或者放弃"需两轮确认",改成单轮 + 一个更宽的阈值 + log.warn,让运维方自己判断。

我倾向 1,因为它诚实且不引入新机制。但请在代码注释里写明这个限制,否则下一个读者会以为 quarantine 会自动解除。

另外:分支需要 rebase

你的分支基于 9586593,master 现在是 v3.9.3(期间发了 3 个版本,src/handlers/messages.jsgemini.jsresponses.jsresponse-store.js 都有改动)。你改的是 auth.js / models.js,直接冲突的概率不大,但请 rebase 到最新 master 再推,让 CI 跑在真实的合并结果上。

推上来后我会跑全量 + 对抗验证,并按台账惯例记录。

@dwgx

dwgx commented Jul 28, 2026

Copy link
Copy Markdown
Owner

先说清楚:你的方案我认可,不用重做。 干旱那条你想的办法(先算 catalog 和 free-tier 的交集,非空就照旧限制、空了才单独 fail-open)比我原来建议的更好,保住了干旱本来的用途。Dashboard 那个 restrictionFailOpen 提示也不在我要求范围内,是你自己想到的,挺好。

不过有个情况得先确认一下:代码好像没推上来。

$ gh pr view 232 --json headRefOid   → b8de0010
$ git show pull/232/head:src/auth.js
    export function isModelBlockedByDrought(modelKey) {
      ...
      const freeModels = new Set(getTierModels('free'));   ← 还是原来这行
      return !freeModels.has(modelKey);
    }

是推到别的分支了,还是本地还没 push?你说的四项改动我在分支上都没看到,所以没法审。方便的话推一下,或者告诉我在哪个分支,我去看。


剩下要做的,就这几条

  1. 干旱白名单 —— 按你自己说的那个思路改就行。

  2. 缩小检查 —— 你的做法是"缩到一半以下先 quarantine、要等下一轮同步返回相同快照才采纳"。这里有个小问题:你在同一条回复里也说了同步只由账号事件触发(新增/重新激活/换 key),那一个长期 active、凭证没变的账号就不会有下一轮,被 quarantine 的目录会一直卡在旧的上,直到进程重启。

    这个方向比原来那条安全(宁可多暴露模型,也不要少),所以不算阻塞。你只要在注释里写明这个限制就行 —— 让下一个读代码的人知道 quarantine 不会自动解除。不用为它加定时刷新,那确实是你说的 out of scope。

  3. PR 描述 —— 你说撤回了那条 "also reproduces on master",但描述第 36 行还在,可能是改了没保存。顺手删掉就好,这条不用再解释了,你说是本地 DNS 的问题我信。

这三条就是全部,我不会再往上加。除非你的新代码本身带出新问题 —— 那种情况我会明确说是新代码引起的,不会当成一直都有的要求。

另外分支需要 rebase 一下:master 现在到 v3.9.4 了(这几天连着修了几个流处理的 bug)。你动的是 auth.js / models.js,大概不会冲突,但让 CI 跑在真实的合并结果上比较稳。

推上来我就跑全量测试和对抗验证,通过就合并,台账也会补上。

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Not ready to approve

The new pool-wide filtering path repeatedly rebuilds the active-account union per model key, which can add avoidable per-request overhead and should be optimized before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (1)

src/models.js:718

  • filterModelKeysByCloudCatalog() currently calls isModelAllowedByCloudCatalog() once per key. When accountId is omitted and there are active accounts, isModelAllowedByCloudCatalog() rebuilds the active-account union set on every call via applicableCloudCatalogUids(), making pool listings O(models × union) unnecessarily expensive (notably listModels(), MODEL_TIER_ACCESS getters, and dashboard endpoints).
export function filterModelKeysByCloudCatalog(
  keys = Object.keys(MODELS),
  env = process.env,
  accountId = null,
) {
  const input = Array.from(keys || []);
  return input.filter((key) => isModelAllowedByCloudCatalog(key, env, accountId));
}
  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@andya1lan

andya1lan commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

昨晚测试中发现其他问题 #234 ,b8de001 是修复 Copilot 第一次提出的问题。
现已将实现推送到本 PR 分支,并 rebase 到 upstream master v3.9.4,Copilot 第二个评论中提出的性能问题也已修复。

PR 正文也已按最新行为和验证结果更正。

本次修复更新如下:

  1. Cascade 干旱模式现在先计算账号池 catalog 实际允许的 free-tier 模型。交集非空时仍只允许这些免费模型;交集为空时只让低配额保护 fail-open,账号 catalog 继续负责隐藏和拦截账号不支持的模型。Dashboard 的账号管理界面会显示这个状态(如上截图),DEVIN_CONNECT 模式不会显示 Cascade 专用告警。
  2. Catalog 采纳增加了合理性门槛:非空快照少于该账号上次已采纳目录唯一 UID 数量的一半时保留 last-known-good;首次同步没有历史快照时使用静态目录数量作为基线。异常小快照只安排一次 30 秒延迟确认,普通同步触发不能提前确认;只有延迟轮次返回完全相同的 UID 集合才会采纳,响应不同则保持未采纳且不会自行继续轮询。代码注释已经明确说明这不是常规定时刷新。首次触发该门槛时,发现结果最多保持 30 秒 fail-open,候选 catalog 之外的模型可能暂时可见,这是避免把一次可能残缺的响应直接当成权威目录的可用性取舍。
  3. 已删除 “the remaining failure also reproduces on master” 这条不准确声明。之前的结果来自本地代理网络设置;调整本地测试环境后该文件 9/9 通过。本 PR 不包含代理测试修改。
  4. Cascade 账号 allowlist 不会收窄 DEVIN_CONNECT 或 special-agent 的路由,Connect selector 同步也不会等待 Cascade 的 30 秒确认。不过进一步实测确认,Connect 的发现路径仍从共享 MODELS registry 出发,再结合自己的 snapshot/live selector catalog;因此确认后的 Cascade catalog 如果向共享 registry 新增模型行,Connect 展示数量仍可能变化。这是 master 现有 Connect discovery 设计的延续,Connect 的 entitlement-aware 模型发现、registry 隔离和 backend-aware 干旱门禁留作独立后续,不在本 PR 范围内。

验证结果:

  • 本地 npm test 为 3096/3096 通过;
  • npm run test:release 的 233 个测试文件全部完成且退出码为 0;
  • 三组 catalog/drought 定向回归为 33/33 通过;
  • npm run secret-scangit diff --check 均通过。
  • 两个同策略 live 账号的排序后模型 ID 集合完全一致,同时启用后的 pool union 也无差异;
  • 异构 live catalog 未实测,但并集、账号路由和账号移除路径已有自动化覆盖。
  • 当前 GitHub CI 的 Node syntax 和 test shards 已通过。

请作者基于新的 PR head 复测。

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Not ready to approve

listModels({ env }) introduces env injection for filtering, but still reads a related flag from process.env, which can produce inconsistent behavior when callers supply an effective environment.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (1)

src/models.js:772

  • listModels() now accepts opts.env and uses it for cloud-catalog filtering, but includeDisabledSpecialAgent still reads from process.env. When listModels({ env: effectiveEnv }) is used (e.g. in handleModels()), this makes env injection partial and can produce inconsistent behavior vs the provided env overrides.
  const env = opts.env ?? process.env;
  const specialAgentEnabled = opts.specialAgentEnabled ?? isSpecialAgentCatalogEnabled();
  const includeDisabledSpecialAgent = opts.includeDisabledSpecialAgent
    ?? process.env.WINDSURFAPI_SHOW_DISABLED_SPECIAL_AGENT_MODELS === '1';
  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@andya1lan

Copy link
Copy Markdown
Contributor Author

Copilot 最新的评论不在本 PR 修改范围内,考虑为过度防御,或者后续再单独处理。

@dwgx
dwgx merged commit d7a175d into dwgx:master Jul 30, 2026
5 checks passed
dwgx added a commit that referenced this pull request Jul 30, 2026
#232 按账号隔离上游模型目录:方案取舍是最值钱的部分(并集发现 + 账号维度路由),
两轮修复精准命中,并主动做了 Dashboard fail-open 告警与 out-of-scope 划分。
未到 S 是因为初版漏了两个 fail-open 场景且一度有条不实的测试声明。

#233 四行 CI 改动,价值全在诊断 —— 它替我们发现了自己连漏四个版本的
macOS x64 构建缺口(终态是 cancelled 而非排队,而我们四次都判断成不用管)。
dwgx added a commit that referenced this pull request Jul 30, 2026
合并 #232(按账号隔离上游模型目录)与 #233(CI x64 runner),外加检索端点的
5 条契约修复 —— 其中 2 条是 v3.9.4 自己引入的,含一条把 PII 写进 URL。

两条教训记入交接文档:
- 读过某条约束不等于会遵守它。那条 PII 缺陷里,我两小时前才引用过
  caller-key.js 的 PII 注释,然后亲手违反了它。
- 注释解释的是设计意图、不是当前事实。macOS x64 连续四个版本终态是 cancelled
  而非排队,我每次照 workflow 注释解释就放过了,是外部贡献者发现的。

#234 已验证成立,进未修清单。测试 3072 → 3108,全量绿。
@dwgx

dwgx commented Jul 30, 2026

Copy link
Copy Markdown
Owner

已合并 ✅ — v3.9.5

复测通过,已用 Merge PR #232: 合入 master 并随 v3.9.5 发布。

验证记录(在 head f0a0074d 上实跑):

结果
干旱空白名单不再拦一切 ✅ catalog 只含 sonnet 时 isModelBlockedByDrought 返回 false
缩小门槛保住 last-known-good ✅ 40 条目录突然缩到 1 条 → 仍是 130,不塌到 1
全量 test:release ✅ 3096 / 0 fail
rebase ✅ base = 0e7a6713(v3.9.4)
合并后全量 ✅ 3108 / 0 fail

对抗验证(找新代码激活了什么):重点查了新增的 quarantine 定时器 —— unref() 了、可取消、removeAccountinvalidateModelCatalogForAccount 会清 attempt key 并取消定时器,生命周期闭合。未发现新引入缺陷。另确认 connect 路径走独立 selector 命名空间门禁(isConnectSelectorAllowedForAccount),不受这层 MODELS 命名空间过滤影响 —— 没踩本仓库"修复只覆盖部分协议路由"的老陷阱(这个陷阱最近刚犯过两次)。

台账:contributors.json 已补,weight A

给 A 而不是 S 的理由我说明白:方案取舍(并集发现 + 账号维度路由)是这个 PR 最值钱的部分,你在 #231 里把三种替代方案逐一论证否决,这个论证本身就是贡献;两轮修复都精准命中;Dashboard 的 restrictionFailOpen 告警和 out-of-scope 划分都是超出要求的。扣分在初版漏了那两个 fail-open 场景(干旱那条是全站中断级别的),以及描述里一度有条"该失败在 master 上也复现"的不准确声明。

干旱那条你给的办法比我建议的好 —— 我原本想的是"池级调用点直接不过滤",你先算 catalog ∩ free-tier、交集非空才限制,保住了干旱本来的用途而不是绕过它。这个我在合并 commit 里写明了。

#234 我已验证成立,打了 bug 标签接受。你报的那条最硬的实测我复现了:FREE_REACHABLE_SELECTORS = ['swe-1-6-slow'] 而 drought 的 freeTierModels = ['gemini-2.5-flash'] —— 生产默认走 DEVIN_CONNECT,所以干旱在 connect 部署上确实用错了白名单。这条已进未修清单,下一轮处理。

感谢,首次贡献质量很高。

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants