Settle the local gates for the model-selector round

- ui-model README pair (short-form Model Experience + audited registry
  entry + Known Limitations), recorded in the pairing manifest
- llm-deepseek README pair rebased onto master's version with this PR's
  real deltas (friendly names, 256k, selector consumers) on both sides;
  runtime/ui-conversation READMEs revert to master (their sections
  documented the removed Session state machine and control slot)
- config catalog regenerated for the ui-model roster row
- ModelDirectory.store JSDoc; ui-model files join the TODO(gui) coverage
  deferral lane beside their ui-command/ui-slash siblings
This commit is contained in:
imccyu
2026-07-27 12:11:54 +08:00
parent ae0b0cc49b
commit b4476d24fe
14 changed files with 70 additions and 18 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-15-llm-model-catalog-and-acp-selection.md: e97eb9abec35cca5608f3a10d6b2363a2d936510
2026-07-15-llm-model-catalog-and-acp-selection.zh.md: d3c410d6cb4585f2bd67b376bbd00ed25565c48c
2026-07-15-llm-model-catalog-and-acp-selection.md: 9edc723b0dfafeaf395eb9325373835138ddbc41
2026-07-15-llm-model-catalog-and-acp-selection.zh.md: 677d9e2a200d488fa9fc27fc2a922dc8f1f871d1

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-23-client-plugin-loading-model.md: 4894670d16c083093f970b0058fd88f79c150673
2026-07-23-client-plugin-loading-model.zh.md: 94f4e8891bcc065873b932c4a7e07741d6521b50
2026-07-23-client-plugin-loading-model.md: 4028f50bf5cf8a05063df3bf5e2b4b7f45a3c02e
2026-07-23-client-plugin-loading-model.zh.md: 7dc96b47c950af07adb71ba7fe131d53e5c0e51d

View File

@@ -487,7 +487,7 @@ Source: [`packages/hooks/hooks-codex/src/index.ts:42`](../packages/hooks/hooks-c
## `@deepseek-ai/dsh-host-apiproxy`
Requires: `agents` · `sessions` · `tools` · `userInteraction` · `workspace`
Requires: `agents` · `llm` · `sessions` · `tools` · `userInteraction` · `workspace`
```ts config-catalog
/** Gateway plugin config: host-level agent routing and Workspace creation root. */
@@ -2050,6 +2050,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-client-ui-command` ([`packages/client/ui-command/src/index.ts`](../packages/client/ui-command/src/index.ts))
- `@deepseek-ai/dsh-client-ui-conversation` ([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts))
- `@deepseek-ai/dsh-client-ui-layout` ([`packages/client/ui-layout/src/index.ts`](../packages/client/ui-layout/src/index.ts))
- `@deepseek-ai/dsh-client-ui-model` ([`packages/client/ui-model/src/index.ts`](../packages/client/ui-model/src/index.ts))
- `@deepseek-ai/dsh-client-ui-models` ([`packages/client/ui-models/src/index.ts`](../packages/client/ui-models/src/index.ts))
- `@deepseek-ai/dsh-client-ui-question` — requires `tools` · `userInteraction` ([`packages/client/ui-question/src/index.ts`](../packages/client/ui-question/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings` ([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts))

View File

@@ -22,17 +22,13 @@ SlotsService gives the renderer separate bare observables for `useSessions` and
`SessionManager` retains the latest validated `session/title` control snapshot independently of list and session-instance arrival. Newer event seqs replace older snapshots, title timestamps contribute to list recency, and a subscription baseline discards any retained title beyond its `lastSeq` before the optional folded title arrives. Explicit session removal also clears the retained title. The client-facing `SessionSummary.title` is therefore only the actual durable title; `displayTitle` is always present and falls back through the cwd basename and session id. A cold persisted session keeps that fallback until opening or resuming it causes the host to fold and project its log-backed title.
## Session model selection
Each resident `Session` owns a `modelSelection` snapshot containing the current provider/model target, provider-grouped directory, provider-local failures, and the `idle`/`loading`/`ready`/`selecting`/`error` state. History establishes or refreshes the current target, opening a selector refreshes the directory, and selection failures preserve the last target and usable groups. Directory and selection operations share a monotonically increasing generation so an older response cannot overwrite a newer selection. A reconnect rebuild restores the target reported by the Host without replacing unchanged selection substructure.
## Model Experience
None, as the session object layer selects the provider/model route used by a later Host request but adds no model-visible content.
None, as the client runtime hosts browser-side services and the session object layer; nothing here reaches a model request.
#### KV Cache effect
Changing the target can change or invalidate provider-side cache reuse; this package does not alter the prompt prefix itself.
None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work

View File

@@ -14,8 +14,6 @@ Tool rows are slots too — the standalone tool ring (`ToolViewRegistry`/`ctx.to
Per-session UI state (selection, ordinary composer draft, active view) lives in the declared chat store (`stores.ts` `createChatStore`): apply constructs one handle and passes it to the conversation, chat-view, and details registrations, so the session slots share one instance per session (selection written by the chat view, read by details) and the framework owns instance lifecycle and draft persistence. The frontend Session Intent comes from the Session list projection; after publication, any retained prompt comes from that Session's conversation snapshot. Components are pure — the framework standard kit (`useSession`/`sessionId` when session-scoped, plus global `useSessions`/`useWorkspaces`) and the store faces (`useStore`/`actions`) arrive automatically from the registration declaration; inject factories contribute plain data and callbacks for runtime Session actions, send/stop, tabs, details, and paging.
The resident composer declares the session-scoped single slot `'conversation.composer.control'` and renders its occupant immediately before the send/stop button. Feature packages own the control and its state; ui-conversation supplies only the placement and standard slot shares. The new-session empty-state composer deliberately has no corresponding control slot.
`src/client/` is organized for the future package split: `contract/` is the sole inter-domain shared face (`slots.ts` slot declarations + composed slot props including the tool-row contract, `views.ts` shared primitives, `tool-call-model.ts`); the `skeleton/`, `chat/`, and `toolviews/` (sample registrants) domain directories import contract files and never each other; `apply.ts` is the only assembly point allowed to import all three domains. The `/client` export surface is the contract only — `apply`/`inject`, the two service classes, and the `contract/` type families; implementation components (skeleton, chat rows) and the store factory stay internal and reach the page exclusively through apply's slot registrations (tests take them via the `./src/*` subpath).
## Model Experience

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: fa972e01579a5608b968e13010bd5224f38bdfbc
README.zh.md: b1a4980dc74e4cb0a4d6607dabae58d37bb18792

View File

@@ -0,0 +1,21 @@
# @deepseek-ai/dsh-client-ui-model
English | [中文](README.zh.md)
Model selection plugin, browser half: TWO entries over ONE per-session directory owned by `ModelService` (`ctx.models`). The `/model` popupSelect contribution (registered through `ctx.command`) and the composer's named `conversation.input.model` seat (a compact trigger + upward provider-grouped menu, figma 313:14108's ToggleButton chrome) both load the session's advisory directory through `session.models` and submit through `session.selectModel` via the same `ModelDirectory` instance — the host-reported current target is the single fact both surfaces echo, so a switch made in either entry is what the other shows next. Directory loads and selections share a generation counter (an older response never overwrites a newer one); provider-local catalog failures list inline while usable groups stay selectable; whole-request and selection failures surface on each entry's own retry face (the popup shell's error/retry, the seat menu's inline error) without forking the state. Directories are per-session, resolved lazily through `ctx.models.directoryFor(sessionId)`, and disposed with the session scope.
The `/client` export surface is the plugin body (`apply`/`inject`), `ModelService`, `ModelDirectory` with its state shape, and the seat's injected face type.
## Model Experience
Indirectly, through the `session.selectModel` RPC both entries submit: the host snapshots the selected provider/model pair at the next prompt-assembly boundary, so the following request routes (and stamps its prompt variables) with the chosen target while a running step keeps its assembled one — the directory, both menus, and every selection interaction stay client-side and never enter the session log.
#### KV Cache effect
Switching the route can reduce or invalidate provider-side cache reuse for subsequent requests; the prompt prefix itself is untouched.
## Known Limitations and Deferred Work
- **No create-time selection** — both entries address an existing session's agent; there is no draft-phase model choice to fold into session creation (the seed order at the host's `targetFor` documents where such a tier would go).
- **Directory names are presentation-only** — selection and persistence use provider/model ids; a provider whose catalog lookup fails lists as an unselectable failure row until reload.
- **The seat shows no effort level** — the figma mock's `High` text has no wire concept behind it yet; the trigger renders the model name alone.

View File

@@ -0,0 +1,21 @@
# @deepseek-ai/dsh-client-ui-model
[English](README.md) | 中文
模型选择插件(浏览器半侧):**两个入口共用一份 per-session 目录**,由 `ModelService``ctx.models`)持有。`/model` popupSelect contribution`ctx.command` 注册)与 composer 的具名 `conversation.input.model` 坑位(紧凑触发器 + 向上展开的按提供方分组菜单,视觉取 figma 313:14108 的 ToggleButton都通过同一个 `ModelDirectory` 实例经 `session.models` 加载会话的建议目录、经 `session.selectModel` 提交——host 报告的 current target 是两个界面共同回显的唯一事实在任一入口切换另一入口下次打开显示的就是新值。目录加载与选择共享一个代次计数器旧响应永不覆盖新结果提供方级目录失败内联列出可用分组保持可选整体失败与选择失败落各入口自己的重试面popup 壳的 error/retry、坑位菜单的内联错误状态不分叉。目录按会话惰性解析`ctx.models.directoryFor(sessionId)`),随会话 scope 一并释放。
`/client` 导出面为插件本体(`apply`/`inject`)、`ModelService``ModelDirectory` 及其状态形状、坑位注入面类型。
## Model Experience
间接影响,经两个入口共同提交的 `session.selectModel` RPChost 在下一次提示词组装边界快照所选提供方/模型对,因此后续请求按所选目标路由(并盖入提示词变量),运行中的步骤保持其已组装目标——目录、两个菜单及全部选择交互都留在 client 侧,永不进入 session log。
#### KV Cache effect
切换路由可能降低或作废提供方侧后续请求的缓存复用;提示词前缀本身不受影响。
## Known Limitations and Deferred Work
- **无创建期选择**——两个入口都寻址既有会话的 agent没有 Draft 期模型选择折入会话创建的通道host `targetFor` 处的种子序注释记录了该层未来的落点)。
- **目录名仅供呈现**——选择与持久化使用提供方/模型 id目录查询失败的提供方以不可选失败行列出重新加载前保持原样。
- **坑位不显示 effort 档位**——figma 设计稿中的 `High` 文本尚无对应 wire 概念;触发器只渲染模型名。

View File

@@ -27,6 +27,7 @@ export interface ModelDirectoryState {
/** One session's shared directory controller; disposed with the session scope. */
export class ModelDirectory {
/** The shared snapshot both entries render from (uSES-safe store). */
readonly store: SnapshotStore<ModelDirectoryState> = createSnapshotStore<ModelDirectoryState>({
current: null, groups: [], failures: [], status: 'idle', error: null,
})

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: e191f3fcd265a6ca9cec3a8dae5f730ce27accf1
README.zh.md: 268096e5f1a145e8d5cf6469524d36fe48984617
README.md: f7d44be3ee9e0d44cf338dcc0dc7210ef16309dd
README.zh.md: 557aca834d9842749b5b3dd97f98f21c8046a51c

View File

@@ -1,5 +1,7 @@
# @deepseek-ai/dsh-llm-deepseek
English | [中文](README.zh.md)
DeepSeek chat-completions adapter for the harness LLM seam: hand-rolled `fetch` + SSE translation from the official wire format (source of truth: the API docs — guides/thinking_mode, guides/tool_calls, api/create-chat-completion) into the `StreamChunk` protocol.
A second, library-backed implementation of the same seam exists in `@deepseek-ai/dsh-llm-pi-ai`. This package always owns the `deepseek` provider route; mounting a pi-ai profile with `provider: deepseek` in the same context throws `LlmError('DUPLICATE_ADAPTER')` by design.
@@ -49,7 +51,7 @@ Every request carries the shared attribution header from dsh-llm's `attributionH
## Errors
Non-2xx responses throw `LlmError` with stable codes: `AUTH` (401/403), `QUOTA` (a response whose provider details identify exhausted quota, balance, or credits), `RATE_LIMIT` (other 429s), `CONTEXT_WINDOW_EXCEEDED` (a 400 whose provider code, type, or message identifies context overflow), `INVALID_REQUEST` (other 400s), `SERVER` (5xx), `HTTP_<status>` otherwise. Its serializable `failure` retains the HTTP status plus a valid positive `Retry-After` seconds/date delay and `x-request-id` / `x-deepseek-request-id` when present. A pre-response transport failure (DNS, refused connection, TLS, proxy) throws `TRANSPORT` naming the configured endpoint and chaining the original rejection as `cause`; caller aborts throw `ABORTED`, and the loop's cancellation signal remains authoritative. Protocol violations throw `STREAM_CLOSED` (no `[DONE]`) or `MALFORMED_RESPONSE` (bad JSON payload). Unknown wire `finish_reason`s (e.g. `content_filter`, `insufficient_system_resource`) become `finish {kind: 'error', failure}` chunks.
Non-2xx responses throw `LlmError` with stable codes: `AUTH` (401/403), `QUOTA` (a response whose provider details identify exhausted quota, balance, or credits), `RATE_LIMIT` (other 429s), `CONTEXT_WINDOW_EXCEEDED` (a 400 whose provider code, type, or message identifies context overflow), `INVALID_REQUEST` (other 400s), `SERVER` (5xx), `HTTP_<status>` otherwise. Its serializable `failure` retains the HTTP status plus a valid positive `Retry-After` seconds/date delay and `x-request-id` / `x-deepseek-request-id` when present. A pre-response transport failure (DNS, refused connection, TLS, proxy) throws `TRANSPORT` naming the configured endpoint and chaining the original rejection as `cause`; caller aborts throw `ABORTED`, and the loop's cancellation signal remains authoritative. Protocol violations throw `STREAM_CLOSED` (no `[DONE]`) or `MALFORMED_RESPONSE` (bad JSON payload). Unknown wire `finish_reason`s (e.g. `content_filter`, `insufficient_system_resource`) become `finish {kind: 'error', failure}` chunks, and a completed stream whose `stop` (or absent) finish opened no content blocks becomes a `finish {kind: 'error'}` with code `EMPTY_RESPONSE` (retried by default policy).
## Testing

View File

@@ -22,13 +22,13 @@ harness LLM seam 的 DeepSeek chat-completions 适配器:手写 `fetch` + SSE
defaultContextWindow: 256000 # optional positive-integer fallback for models without an exact value
models: # optional; defaults to V4 Flash and V4 Pro
- id: deepseek-v4-flash
name: DeepSeek V4 Flash
name: DeepSeek-V4-Flash
- id: private-reasoner
description: Company-hosted reasoning model
contextWindow: 64000
```
该插件注册唯一提供方路由 `deepseek`。请求使用 `provider: deepseek` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models`公布 `deepseek-v4-flash` `deepseek-v4-pro`,两者的上下文窗口均为 128,000 token显式列表会替换这些默认值`models: []` 则不公布任何模型。Catalog 配置项通过 `ctx.llm.listModels('deepseek')` 公开给 UI selector 与部署自省,但仍只提供建议:未列出模型 id 仍原样传递。省略配置项 name 默认为其 id。
该插件注册唯一提供方路由 `deepseek`。请求使用 `provider: deepseek` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models``DeepSeek-V4-Flash` 名义公布 `deepseek-v4-flash`、以 `DeepSeek-V4-Pro` 名义公布 `deepseek-v4-pro`,两者的上下文窗口均为 256,000 token显式列表会替换这些默认值`models: []` 则不公布任何模型。Catalog 配置项通过 `ctx.llm.listModels('deepseek')` 公开给 ACP 编辑器与 Web selector 等客户端,但仍只提供建议:未列出模型 id 仍原样传递。省略配置项 name 默认为其 id。
`contextWindow` 对每个已配置模型都可选,不会通过建议 catalog 公开。`ctx.llm.resolveModelContext('deepseek', model)` 先返回精确模型值,再对不含容量的配置项或未列出原样传递 id 返回 `defaultContextWindow`。两者都不存在时返回 `undefined`,不会使路由失效。因此,压力敏感插件可以获得部署拥有的容量,不会将模型 selector 视为权威。为 `deepseek` 注册另一个适配器会抛出 `LlmError('DUPLICATE_ADAPTER')`

View File

@@ -57,6 +57,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-slash': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-command': { kind: 'indirect', reason: 'The dispatch paths trigger the host command.execute RPC; each command handler\'s host package owns any model-visible effect.' },
'packages/client/ui-model': { kind: 'indirect', reason: 'Selection routes session.selectModel; the host snapshots the target at the next prompt-assembly boundary and owns the model-visible effect.' },
'packages/client/ui-question': { kind: 'indirect', reason: 'The package mounts dsh-tool-ask-user; that tool owns the model-visible schema and answer rendering.' },
'packages/client/ui-trajectory': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-workspace': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },

View File

@@ -133,6 +133,11 @@ export default defineConfig({
'packages/client/ui-command/src/client/directory.ts',
'packages/client/ui-command/src/client/service.ts',
'packages/client/ui-command/src/client/PopupSelectView.tsx',
'packages/client/ui-model/src/index.ts',
'packages/client/ui-model/src/client/ModelSelect.tsx',
'packages/client/ui-model/src/client/directory.ts',
'packages/client/ui-model/src/client/index.ts',
'packages/client/ui-model/src/client/service.ts',
'packages/client/ui-slash/src/client/controller.ts',
'packages/client/ui-slash/src/client/service.ts',
'packages/client/ui-slash/src/core/menu.ts',