Files
deepseek-harness/packages/client/ui-model/README.md
Yichen Jiang bb43ff4f37 feat(ui): make a session that cannot send refuse to accept one
A default naming a route the Models page has since removed left the
composer saying 选择模型 while the input still accepted a message, which
then failed inside the adapter mid-turn.

`session.prompt` now refuses with `model-unavailable` before opening a
turn. That is the enforcement boundary: the method stays callable no
matter what a client disables. `session.models` reports the same fact as
`routable`, and ui-model pushes a block through the new
`ctx.conversation.blocks` registry so the bar renders the disabled
textarea it already renders without a workspace, carrying the blocker's
own reason. The push direction is forced — ui-model already depends on
ui-conversation, so ui-conversation cannot read it back.

The gate is `routable`, not "matches no advertised group": catalog
membership is advisory, so a route serving a model it stopped advertising
is missing from the groups yet perfectly usable, and `null` before the
first load never blocks so a slow Host cannot lock a working composer.

The scaffold gains a route-only adapter for fixture-less keyless
scenarios. Registering zero providers is a test artifact — every product
composition mounts one — and the goldens that froze the seat's fallback
label now show the model those scenarios actually route to.
2026-08-07 15:26:42 +08:00

4.0 KiB

@deepseek-ai/dsh-client-ui-model

English | 中文

Model selection plugin, browser half: TWO entries over ONE per-session directory owned by ModelService (ctx.models). For ordinary sessions, the /model popupSelect contribution (registered through ctx.command) and the composer's named conversation.input.model seat both load the session's advisory directory through session.models and submit through session.selectModel via the same ModelDirectory instance. The compact composer trigger opens a two-level Model/Effort menu: models stay provider-grouped, while the selected exact model supplies its adapter-owned effort names, descriptions, and default. /model applies the selected model's default effort, and the composer can then choose any advertised effort.

The Host-reported provider/model/reasoning target is the single selection fact, but it is echoed only when the exact route remains in the advertised groups; removing that catalog row leaves the routable target intact while the trigger prompts Select model, no stale row is synthesized, and no Effort row is shown until the user picks an advertised model. Directory loads and selections share a generation counter so an older response never overwrites a newer one; a connection reset drops every resident projection and repulls the Host-restored target before display. Provider-local metadata failures list inline while usable groups stay selectable, and selection failures retain the prior target and directory.

When the Host reports that no adapter serves the session's route (session.models.routable), this plugin raises a composer block through ctx.conversation.blocks and the input goes inert with this plugin's own copy; recovering clears it without a reload. It follows routable and nothing else: a null — before the first load, or after one failed — never blocks, or a slow Host would lock a working composer, and catalog membership never blocks either, because a route serving a model it stopped advertising is missing from the groups yet perfectly usable. The trigger's own Select model fallback still covers that case, which is display, not a gate.

Directories are per-session, resolved lazily through ctx.models.directoryFor(sessionId), and disposed with the session scope. Addressed subagent sessions expose neither entry, and their directory rejects loads, selections, and reconnect refreshes, because ordinary Agent-bound model RPCs would activate persisted child history outside the direct-parent continuation seam.

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 available to ordinary sessions, both entries submit the provider/model/reasoning target that the Host snapshots at the next prompt-assembly boundary, so the following request uses the chosen route and effort while a running step keeps its assembled target; the selection becomes durable only when the existing request header records a request that consumes it, and menu interaction adds no prompt content.

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 or addressed-subagent selection — both entries require an existing ordinary session's Agent; there is no draft-phase model choice to fold into session creation, and subagent continuation deliberately exposes no independent model-retargeting contract.
  • Directory names are presentation-only — selection and persistence use provider/model/effort ids; a provider whose catalog or exact-model metadata lookup fails lists as an unselectable failure row until reload.
  • No arbitrary effort input — the composer offers only the exact model's adapter-advertised levels; an adapter without reasoning metadata leaves the Effort row absent.