DSH / Atlas
2026-08-12implementedbug-fix

First-run readiness reads every provider, and the setup card closes

First-run readiness reads every provider, and the setup card closes

The first-run step and the Models page both asked one question — is `deepseek-official`'s credential stored? — of a join that describes every provider. Two defects followed from that single reading. A user who configured some other provider (a pi-ai gateway, a self-hosted route) and never wanted the official DeepSeek endpoint was taken over by the full-screen credential prompt on every blank session, with a working m

English

Problem

The first-run step and the Models page both asked one question — is deepseek-official's credential stored? — of a join that describes every provider. Two defects followed from that single reading.

A user who configured some other provider (a pi-ai gateway, a self-hosted route) and never wanted the official DeepSeek endpoint was taken over by the full-screen credential prompt on every blank session, with a working model already selected in the composer behind it. Nothing they could do short of storing a DeepSeek key would end it, because the step's readiness projection never looked at the row they had configured.

On the Models page the same reading opened the DeepSeek setup card over them on every visit, and that card could not be closed: it was rendered from row data with no local state a Cancel could flip, so its Cancel button did nothing visible. Worse, it shared the row-editor/add/declare close handler, which unconditionally clears all three of those states — so cancelling the card that owned none of them discarded the add card's draft while staying open itself.

Decision

One predicate answers what both surfaces actually need. providerUsable(row) is true when the route is registered with the adapter registry (entry.active) and whatever credential its resolved profile names is stored; a profile naming no reference authenticates through the provider's own path, as does a live route with no settings address, so neither owes this page a key.

onboardingReadiness (renamed from deepSeekReadiness, which no longer describes what it reads) returns provider-ready as soon as any joined row is usable. Only a user with none of those reaches the official DeepSeek lookup, which is unchanged: it is the one route the prompt can offer a key field for. The gate subsumes two diagnostics the old projection carried — settings-unavailable and credential-ref-unavailable — because both described an active route the new gate now calls usable; the outcome for the user was already identical (the step completed without rendering).

needsSetup(row, anyUsable) takes the same fact, so the setup card is the first-run posture alone. With another provider reachable, DeepSeek is an ordinary row carrying the missing-key dot, one Edit click from the same card.

Each card kind now owns its own close handler. closeSetup records the provider in a component-local dismissedSetup set and touches nothing else; closeEditor keeps clearing the three states its cards own. Both route the post-save reload through one announceSaved helper. Dismissal is viewing state, like the open editor and the add card: a reload restores the first-run posture for a user still in it.

Alternatives considered

  • Deriving readiness from the model catalog (llm.models) instead of the join. It answers "can the user talk to something" most directly, but it costs a per-provider listing round trip on a surface that already holds the join, and a provider whose listing fails transiently would re-open onboarding.
  • Requiring row.configured in providerUsable. It reads as the stricter check, and would exclude exactly the routes a deployment mounts through cordis.yml without a configurable-provider declaration — live routes serving models that this page cannot configure. Registration, not configurability, is what makes a provider usable.
  • Only adding the dismissal, leaving the card auto-opening. It fixes the Cancel button and nothing else: a user with a working provider would still be handed the DeepSeek form on every visit to Models, which is the same misreading in a quieter form.
  • Persisting the dismissal to settings. A durable "do not ask about DeepSeek" flag is a second fact about first-run state that can disagree with the join. The credential itself already ends the posture permanently, and every other card on this page is session-local.

Consequences

Onboarding now ends for reasons the DeepSeek route knows nothing about, so the step's name is the last thing tying it to that adapter; a future step that offers more than one route to configure would replace the prompt, not the readiness projection. The narrowed diagnostic union means an unresolvable llm-deepseek settings address is reported as provider-ready rather than as its own reason — the user-visible behavior is unchanged, and the Models page remains the diagnostic surface.

Testing

Package tests pin providerUsable over the four join states and onboardingReadiness over both the new gate and every surviving diagnostic; the section tests cover the first-run posture, the plain-row posture, and the cancel that collapses the setup card while the add card keeps its draft. The onboarding-usable-provider web e2e lane replays the whole scenario through the real wire: cancel with both cards open, configure minimax-cn instead, reload, and find no takeover — with one aria golden of the dismissed state.

中文

Problem

首次使用引导步骤与 Models 页都只向一个描述全部提供方的联接快照提出了同一个问题——deepseek-official 的凭据存了吗?两个缺陷由这一次读取而来。

配置了别的提供方(某个 pi-ai 网关、某条自建路由)、根本不打算用 DeepSeek 官方端点的用户,会在每一个空白会话上被全屏凭据提示接管,而其背后输入框里早已选好了一个可用模型。除了存入一把 DeepSeek 密钥,他们做什么都结束不了它——因为该步骤的就绪投影从不看他们已经配好的那一行。

在 Models 页上,同一次读取每次进入都会把 DeepSeek 设置卡片展开在他们面前,而这张卡片关不掉:它由行数据渲染而来,没有任何本地状态可供「取消」翻转,因此那颗取消按钮不产生任何可见效果。更糟的是,它与行内编辑卡/新增卡/自定义声明卡共用同一个关闭回调,而该回调会无条件清空那三个状态——于是取消一张它们一个都不拥有的卡片,反而丢弃了新增卡里的草稿,自己却仍然开着。

Decision

一个谓词回答两处界面真正需要的事实。providerUsable(row) 在路由已注册进适配器注册表(entry.active)、且其解析后 profile 所指名的凭据已存储时为真;不指名任何引用的 profile 走提供方自己的认证路径,没有 settings 地址的存活路由亦然,因此二者都不欠这个页面一把密钥。

onboardingReadiness(原名 deepSeekReadiness,该名称已不再描述它读取的内容)只要联接中有任意一行可用,就返回 provider-ready。只有二者皆无的用户才会走到官方 DeepSeek 查找,那部分保持不变:它是这条提示唯一能为其提供密钥输入框的路由。这道门槛吸收了旧投影携带的两个诊断——settings-unavailablecredential-ref-unavailable——因为二者描述的都是新门槛现在判为可用的活跃路由;对用户而言结果本就一致(该步骤不渲染直接完成)。

needsSetup(row, anyUsable) 接受同一个事实,因此设置卡片仅代表首次运行姿态。当另有可触达的提供方时,DeepSeek 就是一行带缺失密钥点的普通行,距离同一张卡片只有一次「编辑」点击。

现在每一类卡片各自拥有自己的关闭回调。closeSetup 把该提供方记入组件本地的 dismissedSetup 集合,别的一概不碰;closeEditor 继续清空它那些卡片所拥有的三个状态。两者都经由同一个 announceSaved 助手完成保存后的重载。关闭状态属于查看态,与展开的编辑卡和新增卡一样:对仍处于首次运行姿态的用户,重载会恢复该姿态。

Alternatives considered

  • 从模型目录(llm.models)而非联接推导就绪状态。 它最直接地回答「用户有没有能对话的东西」,但会在一个已经持有联接的界面上多花每提供方一次列举往返,而且某个提供方列举的瞬时失败会让引导重新弹出。
  • providerUsable 中要求 row.configured 它读起来更严格,却会恰好排除部署通过 cordis.yml 挂载、没有可配置提供方声明的那些路由——它们是正在提供模型、只是这个页面配置不了的存活路由。使一个提供方可用的是注册,不是可配置性。
  • 只加关闭状态,保留卡片自动展开。 那只修好取消按钮,别的什么都没修:已有可用提供方的用户每次进入 Models 仍会被塞一张 DeepSeek 表单,那是同一个误读的安静版本。
  • 把关闭状态持久化到 settings。 一个「别再问 DeepSeek」的持久标志,是关于首次运行状态的第二个事实,可能与联接互相矛盾。凭据本身已经永久结束该姿态,而这个页面上其他每一张卡片都是会话内的。

Consequences

引导现在会因为 DeepSeek 路由一无所知的理由而结束,因此该步骤的名字是最后一处把它和那个适配器绑在一起的东西;未来若有一个步骤能提供不止一条可配置路由,替换掉的会是提示本身,而非就绪投影。收窄后的诊断联合意味着无法解析的 llm-deepseek settings 地址会被报为 provider-ready 而非它自己的理由——用户可见行为不变,Models 页仍是诊断界面。

Testing

包内测试针对四种联接状态钉住 providerUsable,并针对新门槛与每一个存留的诊断钉住 onboardingReadiness;分区测试覆盖首次运行姿态、普通行姿态,以及在新增卡保住草稿的同时折叠设置卡片的那次取消。onboarding-usable-provider web e2e 泳道通过真实协议重放整个场景:两张卡片都开着时取消、改配 minimax-cn、重载,然后不再出现接管——并附一份关闭后状态的 aria golden。