# Agent Note: Web 对话输入区的会话模型选择

Status: implemented

[English](2026-07-24-web-session-model-selector.md) | 中文

## 问题

Web 对话原本通过 Host 固定的提供方与模型路由显示并发送消息，既不呈现该路由，也不允许用户更改。TUI 已经具备会话级的路由目标，但如果照搬其呈现方式，或在浏览器中硬编码 DeepSeek 模型，就会让模型发现逻辑和步骤边界语义分散到不同前门中。响应运行期间发生的切换还需要一个原子边界：提示词变量与请求路由不能观测到不同的目标。

## 决策

Web Host 为每个新建或恢复的 agent（智能体）复用 `installAgentLlmTarget`。如果会话已经使用过模型，提供方／模型／推理（reasoning）目标从最新的 `request/header` 开始；否则采用 Host 默认路由。`session.selectModel` 会更改会话级可变目标，提示词组装则将该目标与请求路由一并捕获，因此运行中步骤发生的切换会应用于下一个组装步骤。下一个实际采用的目标通过现有的完整 `request/header` 快照持久化；尚未进入请求的选择则仅保存在当前进程中。

会话 RPC 领域公开 `session.models` 模型目录与 `session.selectModel`。该目录从 LLM（大语言模型）注册表动态构建，并按提供方分组；每个已列出模型的精确元数据还会加入由适配器持有的推理强度 ID、名称、说明和可选默认值。各提供方的目录与精确元数据会按提供方并发加载，且彼此独立失败，因此成功加载的分组仍可与可重试的失败记录一同使用。模型是否位于目录仅供参考：`session.models.current` 独立返回，即使不在任何分组中也仍然可以路由，但提供方停止公布该模型后，Host 不会合成未列出行。两个前门有意对这一状态给出不同回答：TUI 仍把未列出的当前模型渲染为独立一行并标记为当前，Web 则显示未设置状态的触发器标签并要求选择替代模型。Web 正是编辑目录所在的前门，因此用户刚刚删除的目标应当读作一个有待作出的决定，而不是一项可以保留的选择；TUI 只在已存在的模型中挑选，没有这类编辑需要调和。这一代价真实存在且已被接受——显示未设置标签的 Web composer 仍会发送到实际路由的目标——这一分歧是有意为之，而不是一处遗漏的迁移。精确解析决定路由与显式推理强度是否可用。选择操作通过 `resolveCallConfig` 拒绝不支持的推理强度 ID，并在更新目标前具体化适配器配置的默认值。

浏览器中的 `ModelService` 为每个实时会话持有一个 `ModelDirectory`。其快照包含当前完整目标、分组目录、提供方失败记录、操作错误，以及 `idle`、`loading`、`ready`、`selecting`、`error` 状态。挂载时会预先填充触发器标签，此后每次打开菜单都会刷新目录。目录与选择调用共用操作代次，防止较早响应覆盖较新结果；连接重置会先丢弃当前进程中的投影，再恢复 Host 目标。失败时保留先前的当前目标和可用分组。

`@deepseek-ai/dsh-client-ui-conversation` 将会话作用域的单实例 slot `conversation.input.model` 声明为其输入栏 entry 的子 slot。InputBar 在尾部控件区将该 seat 渲染于 pending 指示器与主按钮之前；该 seat 接收输入栏的 `locked` owner prop 与会话作用域。`@deepseek-ai/dsh-client-ui-model` 占用该 seat，并在同一目录上提供 `/model`。其紧凑型触发器显示目录中精确模型的名称与生效的推理强度标签。当前目标不在分组中时，触发器改为显示 `Select model`，模型列表不标记任何活动行，Effort 行也保持隐藏；选择一个已列出的模型，会通过现有选择路径替换完整目标。除此情形外，向上展开的菜单会首先提供 Model 与 Effort；Model 可深入提供方分组，Effort 可深入适配器排序的级别。仅当适配器没有配置模型默认值时，才显示提供方默认值行。

生产环境的浏览器名册由 `apps/cli/config/base.cordis.yml` 与 `apps/cli/config/web.cordis.yml` 共同组装；模型功能对应其中一行 `dshClient` 配置项，而不是 Web boot 代码中硬编码的包。其包 manifest（元数据清单）将加载顺序置于运行时与命令功能之后；Cordis 服务注入则等待 conversation slot 可用，再注册 composer 占用方。

## 考虑过的替代方案

**分别使用提供方与模型下拉框。** 模型列表依赖提供方，每次更改都需要经过两阶段交互。单个分组菜单仍以提供方组织模型，同时不会增加触发器或各行的显示长度。

**在 Web 客户端中硬编码当前 DeepSeek 目录。** 该目录会与已注册适配器发生偏离，也会排除部署自有的提供方。LLM 注册表继续作为提供方与模型元数据的真源，也负责呈现部分查询失败。

**将 `High`／`Max` 保留为客户端本地 UI 状态。** 静态 DeepSeek 标签无法覆盖 `off`、pi-ai 的提供方词汇、适配器默认值与校验，也不能参与恢复或下一次提供方请求。精确模型元数据拥有可选词汇，会话目标则拥有已选择的 ID。

**将选择设为全局默认值。** 全局变更会意外改道其他已打开的对话。目标仅属于一个实时会话；对于没有已记录请求的会话，Host 配置仍是默认值。

**agent 运行期间拒绝更改。** 共享原子目标已经将当前组装步骤与下一次选择分离。保持选择器可用，可以让用户为下一个步骤预先选择模型，而不会改变正在执行的请求。

**将每次点击作为新的会话事件持久化。** 只有提示词组装采用某项选择后，该选择才对模型可见。持久化尚未使用的 UI 意图，会增加一个无法重建模型请求的持久事件；现有 `request/header` 会记录首次实际使用该路由的请求。

## 影响

任何由 Host 支撑的 Web 对话（包括空白会话）都可以在动态发现的提供方分组和适配器持有的推理级别之间切换，而无需显示重复的 `provider/model` 标签。当前实际采用的目标会在恢复和重连后保留；目录名称仅用于呈现，而选择和持久化使用提供方／模型／推理强度 ID。某个提供方的目录或精确元数据不可用时，只有相应分组会降级。路由变更可能降低提供方侧的缓存复用率，但选择器不会添加任何提示词内容，也不会干扰正在执行的步骤。没有推理元数据的模型不显示 Effort 行。

## 测试

Host 测试固定分组发现、目录与精确元数据失败隔离、已记录推理强度恢复且不注入陈旧行、建议性的未列出模型选择、不支持的推理强度拒绝、默认值具体化，以及切换仅影响下一次组装。客户端测试固定共享目录、重连恢复与完整目标提交。组件测试固定动态推理强度标签、说明、提供方默认值展示、推理强度提交，以及已删除模型行的 `Select model` 回退。无密钥 built-app fixture（测试前置数据）加载生产模型插件，选择 OpenAI 的 GPT-5 及其 Max 推理强度，发起一个轮次，并验证下一条生成的响应会报告两个 ID；DeepSeek 配置 fixture 会删除活动目录行，在选择替代模型之前固定该回退。
