DSH / Atlas
2026-08-07implementedfeature

the default model follows the picker

默认模型跟随选择器

A session model picker and a deployment default are two layers of the same preference. If the picker affects only its addressed session, the next blank session can select a different model with no user-facing way to align the default. If the default lives inside a Host gateway, direct Agent entry points cannot share it without depending on Host or duplicating state. Reasoning effort makes the persistence shape signif

English

Problem

A session model picker and a deployment default are two layers of the same preference. If the picker affects only its addressed session, the next blank session can select a different model with no user-facing way to align the default. If the default lives inside a Host gateway, direct Agent entry points cannot share it without depending on Host or duplicating state.

Reasoning effort makes the persistence shape significant: a model selection without an effort must clear a stored effort, or the next Agent may apply an effort that its selected model does not accept.

Decision

AgentDefaultModelConfig provides ctx.agentDefaultModel and registers {provider, model, reasoningEffort?} as the agent-default-model Settings section. Its {provider, model} composition entry is the base layer and settings.yaml supplies the user layer. The service is entry-point-neutral, so direct creation and ApiProxy-backed creation share one default (headless direct core entry point).

reasoningEffort belongs to the Settings section but not to the plugin config. Settings layers merge by field, so a configured effort would survive a user selection that omits it. saveSelection() instead writes the complete user section; absence therefore clears a stored effort. A deployment-wide effort default belongs to the adapter profile, which resolves it per model.

session.selectModel applies an accepted ModelSelection to its session and calls saveDefaultModelSelection() for the shared Agent default. A storage failure is logged without undoing the session selection. A deployment with no settings provider retains the composition entry and keeps the accepted selection only in that session.

ApiProxyDefaults carries defaultModelSelection() and saveDefaultModelSelection() closures, so createApiProxy has no dependency on the Settings seam. ApiProxyService wires them to ctx.agentDefaultModel.currentSelection() and ctx.agentDefaultModel.saveSelection().

selectionFor(agent) resolves its tiers on every read: a process-local session selection, otherwise the session's latest logged request/header, otherwise the live Agent default. A session with a logged request remains bound to that durable selection. A blank session observes the current default even when it was created before the preference was saved; this matches the New Session surface, which may reuse a blank session.

The stored selection does not require catalog membership. A provider route may serve a model omitted from its advisory catalog. session.models therefore reports the stored selection independently of advertised groups and separately reports whether an adapter serves its provider.

Consequences

host.describe reports the live Agent default. A successful model switch stores an agent-default-model: section in settings.yaml. The gateway does not expose that namespace through its Settings-page allowlist; the model picker is its editor.

A session that cannot send

session.prompt refuses with model-unavailable before opening a turn when no adapter serves the session's selected provider. This method is the enforcement boundary; a disabled composer is only a client affordance.

session.models reports routable. The ui-model-selection plugin projects an unroutable selection through ctx.conversation.blocks, and the composer becomes inert while leaving the model seat available. An unknown client-side routability state, including an initial or failed catalog load, does not block input.

Routability is distinct from catalog membership. A live provider route can serve an unadvertised model, so absence from catalog groups does not imply that the session is unusable.

Alternatives considered

AlternativeContract mismatch
Fall back to the composition entry when the stored provider is unavailableThe product silently switches away from the user's selection.
Validate the stored selection against catalog membershipCatalogs are advisory and may omit requestable models.
Save with a merge patchAn omitted reasoningEffort cannot clear the stored field.
Save only selections made in blank sessionsAn informed mid-conversation selection never becomes the deployment default.
Add a separate “set as default” gestureThe session picker and future-session preference can diverge despite representing the same user choice.

中文

问题

会话模型选择器与部署默认值是同一项偏好的两个层次。如果选择器只影响其所在会话,下一个空白会话可能选择不同模型,用户却没有途径使默认值与选择器一致。如果默认值位于 Host 网关内部,直接创建 Agent 的入口只有依赖 Host 或复制状态才能共享它。

推理强度使持久化形态变得重要:不含强度的模型选择必须清除已存强度,否则下一个 Agent 可能会采用所选模型不接受的强度。

决定

AgentDefaultModelConfig 提供 ctx.agentDefaultModel,并把 {provider, model, reasoningEffort?} 注册为 agent-default-model Settings 分节。其 {provider, model} 组合条目是 base 层,settings.yaml 提供用户层。该服务不偏向特定入口,因此直接创建与 ApiProxy 支撑的创建共享同一个默认值(headless 直接 core 入口)。

reasoningEffort 属于 Settings 分节,但不属于插件配置。Settings 层按字段合并,因此已配置的强度会在用户选择省略它时继续存在。saveSelection() 写入完整的用户分节;因此,缺少该字段会清除已存强度。部署级强度默认值属于适配器 profile,并由它按模型解析。

session.selectModel 把被接受的 ModelSelection 应用于所在会话,并调用 saveDefaultModelSelection() 保存共享的 Agent 默认值。存储失败只记日志,不撤销会话选择。没有 Settings 提供方的部署保留组合条目,被接受的选择只停留在该会话中。

ApiProxyDefaults 携带 defaultModelSelection()saveDefaultModelSelection() 闭包,因此 createApiProxy 不依赖 Settings seam。ApiProxyService 将它们分别接到 ctx.agentDefaultModel.currentSelection()ctx.agentDefaultModel.saveSelection()

selectionFor(agent) 每次读取时都解析各层:先取进程内的会话选择,其次取会话最新记录的 request/header,最后取当前 Agent 默认值。已有请求日志的会话持续绑定到日志中持久化的选择。空白会话即使创建于偏好保存之前,也会观察到当前默认值;这与 New Session 界面可能复用空白会话的行为一致。

已存选择不要求属于目录。某条提供方路由可能服务其仅供参考的目录未列出的模型。因此,session.models 会在已公布分组之外单独报告已存选择,并另行报告适配器是否服务其提供方。

影响

host.describe 报告当前 Agent 默认值。模型切换成功后,settings.yaml 中会存有一个 agent-default-model: 分节。网关不通过 Settings 页 allowlist 暴露该 namespace;模型选择器是它的编辑器。

无法发送消息的会话

当没有适配器服务会话所选提供方时,session.prompt 会在开启轮次前以 model-unavailable 拒绝。这一方法是强制执行边界;禁用 composer 只是客户端提供的便利。

session.models 报告 routable。ui-model-selection 插件通过 ctx.conversation.blocks 投影不可路由的选择,composer 随之变为不可操作,同时保留模型 seat 可用。客户端不知道是否可路由时不会阻断输入,包括目录首次加载或加载失败的情况。

可路由性与目录成员关系不同。仍在服务的提供方路由可以处理未公布的模型,因此不在目录分组中并不代表会话不可用。

考虑过的替代方案

替代方案约定不匹配之处
已存提供方不可用时回落到组合条目产品会静默切离用户选择。
根据目录成员关系校验已存选择目录仅供参考,可能省略仍可请求的模型。
使用合并 patch 保存省略的 reasoningEffort 无法清除已存字段。
只保存空白会话中的选择对话期间知情作出的选择不会成为部署默认值。
增加单独的「设为默认」手势会话选择器与未来会话偏好虽然代表同一用户选择,却仍可能分歧。