The remaining P1 from the #939 review, plus the P2 it shares a mechanism with. Nothing carried a version, so two tabs editing one namespace silently overwrote each other — reproduced as tab B's `reasoning` lost to tab A's older draft. The seam's per-namespace write queue orders writes; it cannot tell a fresh writer from one replaying a snapshot a predecessor superseded. Each namespace now carries a monotonic `revision` over its RAW section. A write may send `expectedRevision`, checked at the FRONT of the queue (not at call time, which would race the very predecessor it guards against); a mismatch rejects with `SettingsConflictError` → `settings-conflict` on the wire, carrying both revisions. The editor captures the revision it opened at and, on conflict, asks the user to reopen rather than replaying its snapshot. The same counter fixes the missing broadcast. `settings/updated` is gated on the resolved value — correct for consumers, wrong for configuration surfaces: storing an override equal to the composition base leaves the resolved value alone while changing what the document says (the field is now overridden, not inherited) and moving every open editor's revision. `settings/document-updated (ns, revision)` fires on any raw-section change, in-process or external, and `host/settings-changed` now rides it. That event also closes the stale model picker: editing a provider's `models` changes no route, so `llm/adapters-updated` never fired and an open picker kept serving the old catalog. A change to an exposed provider namespace now emits `host/models-changed` too — that namespace holds the catalog. Docs: both sides of the five touched README pairs, a type-equiv block for `SettingsPathOp`, and an Agent Note recording what the plane exposes and who may overwrite what. The deferred wire-redaction gaps (secrets behind union/intersection/transform, `.default(...)` in the served envelope, schema text in rejection messages, `new Function` rehydration, pi-ai's `headers`) are recorded as TODO(settings-wire-redaction) and in Known Limitations rather than half-fixed.
3.6 KiB
@deepseek-ai/dsh-client-ui-models
English | 中文
模型设置分区插件:提供方配置页。它把三个协议领域汇聚为一个界面——llm.providers(可配置提供方目录,含每条路由的存活/休眠状态)、settings.describe(序列化 schema、分层脱敏值、secret 槽位)与 credentials.describe(不含值的 configured/source/writable 徽标)——并渲染提供方行,一次只展开一张编辑卡片。
行是已配置的提供方(其 profile 在所属 namespace 中解析得出);密钥未在任何地方配置的整分节提供方(DeepSeek 的首次运行姿态)会渲染为其展开的设置卡片而非一行,「新增」流程则是一张承载休眠目录提供方选择框的卡片——裸挂载的 llm-pi-ai 在任何路由存在之前就能提供其完整的已安装 catalog。编辑器是每个适配器家族各一张的手写卡片:主字段是单独一个 API 密钥输入框——页面从不询问环境变量名;键入的密钥经 credentials.set 以只写方式存入 profile 的引用之下,profile 没有引用时便派生 <ROUTE>_API_KEY,pi-ai profile 会把这次派生记录为 apiKeyEnv,因此 settings.yaml 从不携带密钥值。收起的「自定义设置」折叠区承载精选的额外字段——两个家族都有 baseURL(deepseek 的占位符显示公共端点),另加 reasoningEffort(deepseek)或 reasoning(pi-ai);其余每个 profile 字段仍归 settings.yaml 所有。只有当某行仅由用户层承载时它才可删除(删除会还原组合 base)。
每一次编辑都以 settings.mutate 的路径 op 落到已存分节上——每个变更字段一条 set、每个清空字段一条 unset、删除整行则是单独一条 unset。页面自始至终只持有脱敏后的 descriptor,因此它点名自己看得见的字段,而不是重建分节:一个它从未收到过的已存字面机密不会被任何 op 提及,也就得以留存。每次写入都携带该卡片打开时的 revision,因此来自另一个标签页或对 settings.yaml 的外部编辑所产生的并发写入会以 settings-conflict 被拒绝,卡片会请用户重新打开,而不是把自己过期的快照重放上去。
模型体验
无。该分区渲染浏览器配置 UI;这里没有任何内容进入模型请求。
KV Cache 影响
无;该包(package)既不组装也不发送提供方请求。
已知限制与暂缓事项
- 卡片上可编辑的只有 API 密钥与精选折叠区字段:手写编辑器用 schema 通用的字段覆盖面换来了设计稿上的布局(Agent Note(agent 决策记录));进阶字段(
models、重试策略、超时……)在settings.yaml中编辑,折叠区会指向它。不带这些约定字段的 profile schema 只渲染该提示,两套精选布局则以llm-deepseek/llm-pi-ai这两个 namespace 的名字为键。 - 删除一行会把它已存储的密钥留在
.env里:删除取消设置的是 settings profile,却刻意不清除那条派生凭据;重新添加该提供方时会发现密钥已配置。显式的密钥移除控件暂缓。 - 页面上没有逐提供方的模型列表:模型由选择器呈现;本页只展示路由状态。逐行的模型预览暂缓,待有消费方需要时再实现。
- 未声明的存活路由无处渲染:未附带可配置提供方声明即注册的路由没有 settings 地址;它在各选择器中仍然可见,但不会出现在本页的行里。