Files
deepseek-harness/packages/client/ui-models
Yichen Jiang dd36db5434 test(ui-models): cover reading a stored capacity back out of a row
Every capacity assertion typed into the field first, so the path that reads a
stored count — the one an already-configured route takes every time it is
opened — was never exercised, and the coverage gate said so. A row now opens
on stored counts and is expected to spell them `1M` and `256K`, the same
vocabulary the field accepts.

`capacityText` takes the row it is rendering rather than looking it up again
by index, which retires the impossible empty-row branch that lookup needed.
The list editor's pre-flight model check is unreachable from the card that
disables submit on the same failure, and says so where it stands.
2026-08-06 15:18:18 +08:00
..

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

English | 中文

Models settings plugin: the provider configuration page and official-DeepSeek conditional onboarding step. It joins three wire domains into one shared snapshot — llm.providers (the configurable-provider directory with each route's live/dormant state), settings.describe (serialized schemas, layered redacted values, secret slots), and credentials.describe (value-free configured/source/writable badges) — and renders provider rows with one editor card at a time, without presenting route liveness as provider status.

Rows are the configured providers (their profile resolves in the owning namespace); a whole-section provider whose key is not configured anywhere (the first-run DeepSeek posture) renders as its open setup card instead of a row, and the add flow is a card carrying the dormant-directory provider select — a bare-mounted llm-pi-ai offers its whole installed catalog before any route exists. The pi-ai card additionally edits that route's model list and can ask the provider what it serves. The editor is a hand-written card per adapter family: the primary field is a single API key input — the page never asks for an environment-variable name; a typed key stores write-only through credentials.set under the profile's reference, deriving <ROUTE>_API_KEY when the profile has none, and the pi-ai profile records that derivation as apiKeyEnv, so settings.yaml never carries a key value. The collapsed 自定义设置 fold carries the curated extras — baseURL for both families (the deepseek placeholder shows the public endpoint), reasoningEffort (deepseek) or reasoning (pi-ai), and the direct DeepSeek adapter's advisory model catalog. Each DeepSeek row edits id, optional display name, and optional contextWindow; existing fields outside that curated set survive edits, while every other profile field stays owned by settings.yaml. A row is deletable only when the user layer alone carries it (removal restores the composition base), and a localized confirmation dialog must complete before the page submits that destructive unset.

The DeepSeek step projects deepseek-official readiness from that same joined snapshot after earlier onboarding pages complete. It recognizes the official adapter through its llm-deepseek configurable-provider declaration, so an undeclared live route with the same provider id is not treated as repairable configuration. A configured literal apiKey secret sidecar or configured credential reference completes the step without rendering, including a read-only launch-environment credential. Only a mounted, active adapter with a missing writable reference shows the page that opens Settings on Models, whose existing setup card exclusively owns key input and credentials.set; the step never holds a secret. An absent adapter, inactive route, failed join, read-only deployment, or unusable settings or credential capability completes the step without rendering so onboarding cannot block the product; Models remains the diagnostic surface.

Every edit lands as settings.mutate path ops against the stored section — a set per changed field, an unset per cleared one, and a single unset for a deleted provider row. The page only ever holds the REDACTED descriptor, so it names the fields it can see rather than rebuilding a section: a stored literal secret it never received is mentioned by no op and survives. DeepSeek's models is one replace-by-value array: the editor shows inherited effective rows until the first model edit materializes the complete array in the user layer, while reset unsets that override. A row carries the model id and display name; its context window and output cap sit behind the row's own disclosure, the same shape the pi-ai provider form uses. Either capacity is typed as a count with an optional decimal K or M suffix (256K, 1M; 1M is 1000K) and stored as the plain count, spelled back in the shortest form that round-trips. Empty ids, duplicate ids, empty explicit names, and unreadable, non-positive, or fractional capacities fail before any write. Each write carries the revision the card opened at, so a concurrent write from another tab or an external settings.yaml edit is refused as settings-conflict and the card asks the user to reopen instead of replaying its stale snapshot. The page refetches on the pushed invalidations (settings/changed, credentials/changed, models/changed, and connection/reset) once it has loaded, so an external settings.yaml edit, a second tab, or a settings-born route converges without polling.

Model list and endpoint interrogation

A pi-ai profile's models list is edited on the card: one row per model showing its id and display name, with the context window and output cap behind a per-row disclosure and two label-free actions — expand and delete — on the right. An empty list means "serve this route's built-in catalog", so a row is only ever added deliberately; clearing a capacity drops it rather than storing a value the schema would reject, and the adapter's route-level fallbacks size whatever configuration leaves out — an empty capacity shows those fallbacks' magnitude as its placeholder, a hint rather than a mirror, since the field counts K as 1000 and a deployment may override them. A capacity that is not a positive integer is simply not stored.

Fetch available models asks llm.discoverModels about the endpoint the form currently shows, including a base URL edited but not yet saved and a key typed but not yet stored, so adding a provider is one pass instead of save-then-return. The reply opens a picker rather than being written: candidates already configured start unchecked, so adopting a selection never overwrites a capacity the user corrected. A provider that cannot be interrogated is a detour, not a dead end — the adapter's own message appears beside the rows, which stay editable by hand.

Add a custom provider declares a route pi-ai does not ship. It is its own card rather than the editor with extra fields, because the route id is being chosen here and the settings address does not exist until it is: one settings.mutate sets the whole profile at providers.<route>, and the key travels separately through credentials.set under the same <ROUTE>_API_KEY derivation an existing provider uses. What a hand-declared route cannot default gates the create button — a unique Provider ID, an endpoint, a protocol, and at least one uniquely-identified model — so the failure names the field while the user is still looking at it. Capacities do not gate it: the adapter's fallbacks size a model the endpoint described by id alone, which is what most listings return. The protocol choices are read out of the namespace's own schema rather than a wire field or a constant, so they cannot drift from the ones the adapter accepts.

Model Experience

None, as the section renders a browser configuration UI; nothing here reaches a model request.

KV Cache effect

None; this package neither assembles nor sends a provider request.

Known Limitations and Deferred Work

  • Only the API key and curated fold fields are editable on the card — the hand-written editor traded schema-generic field coverage for the mockup layout (Agent Note). DeepSeek exposes baseURL, reasoningEffort, and model id/name/contextWindow/maxTokens; pi-ai exposes baseURL and reasoning. Retry policy, timeouts, DeepSeek model descriptions, and other advanced fields remain in settings.yaml; existing model fields the editor does not show are preserved. A profile schema without the conventional fields renders the hint alone, and the two curated layouts key on the llm-deepseek/llm-pi-ai namespaces by name.
  • Deleting a row leaves its stored key in .env — removal unsets the settings profile but deliberately does not unset the derived credential; re-adding the provider finds the key already configured. An explicit key-removal control is deferred.
  • Only pi-ai routes can be hand-declared — the custom-provider card writes into llm-pi-ai, the one namespace whose profiles describe a whole provider. A llm-deepseek route is a composition fact, not something this page can create.
  • Interrogation covers OpenAI-compatible endpoints — the adapter reads only that listing shape, so a gateway speaking another protocol reports that it cannot be asked and its models are entered by hand.
  • Undeclared live routes render nowhere — a route registered without a configurable-provider declaration has no settings address; it stays visible in pickers but not on this page's rows.