@fhxgs/dsh-model-hub
Unified provider authentication, model catalog, and selection routing for DeepSeek Harness.
@fhxgs/dsh-model-hub delivers a single, full-featured model management layer for DeepSeek Harness (DSH). It packages both a Node.js Host plugin and a lazy-loaded browser Client. Everything this plugin owns — provider lifecycle, sign-in attempts, the model catalog, and session selection — travels over its own secure, loopback-only /model-hub RPC channel. Everything the harness already owns keeps using the official /api: settings reads and writes, credential description and storage, endpoint discovery, and the host model list behind the global default. The plugin joins the harness rather than tunnelling around it.
Installing this plugin replaces the default fragmented model settings with a unified experience: one settings section, one composer picker, and one /model command.
Key Features
| Feature | Description |
|---|---|
| Provider Sign-in | Complete OAuth / device-code authentication flow directly in the browser with real-time attempt tracking, prompt dialogs, and a 30-minute host timeout. Credentials pass straight into secure storage and are never exposed in logs or state snapshots. |
| Provider Lifecycle | Clean management actions (activate, deactivate, logout, useRecord) guarded by impact confirmation dialogs. No generic settings overrides. |
| Provider Customization | Edit API keys (stored safely via credentials.set), base URLs, protocols, display names, and per-route model mappings. Includes endpoint discovery and custom provider creation. |
| Rich Model Catalog | Extends the native llm.models schema to retain inputModalities, contextWindow, and defaultMaxTokens. Features revision-keyed caching, an 8-request concurrency gate, and per-model error isolation. |
| Curation Policy | Flexible visibility rules (all or explicit include allowlists) applied synchronously across both settings and the composer picker. |
| Session Selection & Routing | In-memory session model selection with { prepend: true } hook priority. Subagents follow a 3-tier routing strategy evaluated at agent/created. Blocks unroutable message sends upfront. |
| Thinking Effort Control | Dynamic slider automatically calibrated to each model's supported reasoning levels (e.g., xhigh/max for gpt-5.6-sol, hidden for non-reasoning models). |
| Fast Mode | Metadata-driven toggle for models declaring accelerated service tiers (e.g., gpt-5.6-sol priority tier). |
| Built-in Providers | Out-of-the-box support for qwen-code (RFC 8628 device-code login via chat.qwen.ai) and codex (OpenAI Codex: authorization code + PKCE loopback login with 7 GPT-5.x models). |
| Bilingual Interface | Native Simplified Chinese and English UI following the harness shell's language preferences. |
| Loopback Security | Every /model-hub endpoint is bound strictly to authority: 'loopback' with strict Zod payload validation and sanitized error responses. |
Installation
[!NOTE]
dsh pluginruns pnpm under the hood (spawnSync('pnpm')), so pnpm must be available in yourPATH. If needed, enable it via corepack:corepack enable pnpm
Add the plugin to your target profile:
dsh plugin --profile web add @fhxgs/dsh-model-hub
Restart the profile after installation (profile composition changes do not hot-reload).
The plugin automatically registers its patch configuration (cordis.patch.yml), enabling @deepseek-ai/dsh-authorization and disabling the default ui-settings-models and ui-model-selection components. No manual configuration editing is required.
To uninstall:
dsh plugin --profile web remove @fhxgs/dsh-model-hub
Uninstalling cleanly restores the official default model settings and picker.
Peer Dependency Warnings
dsh plugin add runs pnpm, and pnpm resolves peer dependencies against the profile directory alone. The harness installs none of its own packages there: the Host half's @deepseek-ai/* imports and the browser half's platform modules are both answered at runtime by the running shell's module table. Those eighteen peers are therefore declared optional — their ranges say which harness this build was written against, not what a package manager should go and fetch — so a cold profile install no longer reports them missing.
Two peers stay required, because they are the two an environment could genuinely be without and the warning is worth keeping as a signal:
Issues with peer dependencies found
✕ missing peer @earendil-works/pi-ai
✕ missing peer react
That warning is expected on a cold install and the plugin loads normally — the host runtime supplies @earendil-works/pi-ai, and the web shell seeds react into the frozen module table before any plugin factory runs. A warning naming anything else is worth reading.
Quick Start
- Open the DSH Web UI on the same machine running DSH (all endpoints require loopback access).
- Go to Settings → Model Hub → Providers. Select your provider and complete the sign-in flow.
- Click Activate on the provider card to enable the route.
- Switch to the Catalog tab to select which models you want visible in your picker.
- In any chat session, pick your model and reasoning effort from the composer bubble or type
/model.
Configuration
The plugin manages its configuration under the model-hub settings namespace:
model-hub:
picker:
mode: include # 'all' | 'include' (default: all models visible)
include: # Active when mode is 'include'
- { provider: kimi-coding, model: k3 }
fastMode: # Routes whose accelerated service tier is switched on
- { provider: codex, model: gpt-5.6-sol }
preferredEffort: high # Optional default reasoning effort level
subagent: inherit # 'inherit' | { provider, model, reasoningEffort? }
- Default behavior: An empty configuration resolves to
{ picker: { include: [], fastMode: [] }, subagent: 'inherit' }. - Global default model: Stored in the native
agent-default-modelnamespace. The plugin reads this value but only modifies it when explicitly confirmed by the user. - Built-in provider configs: Stored separately in
model-hub-providersto prevent catalog cache invalidation during policy updates.
Screenshots
Captured from a cold-installed isolated profile on DSH 0.1.1-rc.2, dark theme.
| Composer picker (Simple) | Providers | Catalog |
|---|---|---|
![]() |
![]() |
![]() |
Architecture Overview
Both Host and Client components reside in a single npm package:
- Host (Node.js ESM): Manages the
/model-hubRPC channel, settings schemas, built-in provider adapters, and session selection hooks. - Client (Lazy CJS): Loads on-demand in the browser, providing the settings UI, composer picker, and
/modelmodal.
src/
├── index.ts # Host entrypoint: RPC channel, settings namespace, lifecycle setup
├── rpc/ # Single-layer wire envelope, router, and 7 standard error codes
├── auth/ # Authorization attempt bridge and two-axis auth.state projection
├── provider/ # Adapter bindings and lifecycle operations (activate/deactivate/logout/useRecord)
├── provider/native/ # Self-served providers (OAuth flows for qwen-code and codex)
├── catalog/ # Super-set model catalog, curation policy filter, and LRU cache
├── selection/ # Session model selection, effort resolver, and subagent routing rules
├── settings/ # Settings schema definitions
└── client/ # Browser UI components (Settings cards, Composer picker, Attempt modal)
Build outputs are published to lib/index.js, lib/invariant.js, lib/client.js, lib/types/**, and cordis.patch.yml.
[!IMPORTANT] The
./clientexport is Loader-only.lib/client.jsis not a module anything imports. Its body is a singlewindow.__ModuleLoader__.load({ ... })call, and the harness shell serves the file to the browser rather than resolving it —import()from Node andrequire()from a bundler both throw, whatever extension the file carries. The export is declared so the shell can address the file by name; it is not a public API surface, and nothing outside the DSH browser runtime can consume it. This is also whypublintreports exactly one finding against this package (it suggests renaming the file to.cjs), and why that finding is documented and declined rather than silenced: the rename would quiet the warning without making the export usable. The reasoning is recorded in full inscripts/verify-manifest.mjs, and the exemption is matched against the report's exact wording, so a different finding still fails the gate.
Development & Testing
corepack pnpm install
corepack pnpm run build # Generates lib/types (tsc) and runtime bundles (tsdown)
corepack pnpm run verify # Five gates: bundle purity, patch keys, sourcemap link, published paths, manifest
corepack pnpm run test # Runs vitest test suite
corepack pnpm run smoke:p0 # Cold-installs tarball into an isolated test profile
prepackrunsverifybeforenpm packornpm publishto prevent broken or dirty artifacts;prepublishOnlyadditionally rebuilds from a cleanedlib/and runs the full test suite, so a publish cannot ship a stale bundle.- Packaging smoke test (
smoke:p0) uses an isolated temporary directory (DSH_HOME=$(mktemp -d)) and never touches your local~/.dsh. - Test requirements and validation gates are documented in
scripts/gate-p1.mdandscripts/gate-p3.md. - Release procedures are defined in
scripts/release-checklist.md.
Those three documents live in
scripts/, which is deliberately kept out of the published tarball, so the links above are absolute: they resolve from the npm page as well as from a checkout.
Compatibility
| Requirement | Supported Range |
|---|---|
| Node.js | ^22.19 || >=24 |
| DeepSeek Harness | 0.1.1-rc.2 |
@deepseek-ai/* peer dependencies |
^0.1.1-rc.2 |
@deepseek-ai/cordis |
^4.0.1 |
@earendil-works/pi-ai |
~0.82.1 (peer dependency supplied by host runtime) |
| React | ^18.2.0 (peer dependency) |
Known Limitations
- Loopback Only: All
/model-hubendpoints return HTTP 403 when accessed outside loopback. Non-loopback pages display a read-only badge; model changes must be performed on the host machine. - Native UI Disabled: The official models settings and onboarding steps are disabled while this plugin is active.
- No Background Push Stream: UI updates are driven by forwarded host events and polling during active login attempts.
- Zero-Message Session Selections: Model selections on sessions with zero sent messages do not persist across restarts (matching official DSH behavior).
- Send Blocking Scope: Composer send-blocking is enforced on web UI interactions; headless or SDK clients are validated on the host directly.
- Codex Token Cost: OpenAI Codex token costs report as zero because plan-based subscription pricing differs from standard API token rates.
- Fixed OAuth Callback Port: The built-in Codex route receives its browser redirect on the single port its client registered (
http://localhost:1455/auth/callback), so only one such sign-in can run on a machine at a time; a port already held ends the attempt immediately with the codeLOOPBACK_PORT_IN_USErather than waiting, and there is no device-code fallback for this route. The receiver binds both loopback families where the machine has both — if::1cannot be bound the sign-in continues on127.0.0.1alone, recorded asLOOPBACK_IPV6_UNAVAILABLE. - Grant-Nominated API Host: An OAuth grant may report the API host its account is routed to, and only a route whose issuer actually sends one consumes it (Qwen Code does; Codex does not, and a stored host on that route is ignored). A nominated host must be HTTPS with no userinfo, query, or fragment, and may not be a host another built-in route reaches; anything else is dropped and the route falls back to its vendor default. Plain HTTP is refused outright, loopback included.
- Split Selection Ownership: Choosing a model here and Core deciding whether to accept a message are two separate owners. Core admits every message on its own chain (
picked▸ request header ▸ global default), which this plugin deliberately never writes. Two consequences follow. Switching to a model that takes images does not make the image already in the composer sendable — Core still judges it against the model its own chain names, and the refusal quotes that model rather than the one just chosen. And where the global default names a provider no adapter serves, Core refuses every message of a session that has not yet completed a request — text as well as images — whatever is selected here; repoint or clear the global default to lift it. - Selection Persistence Boundary: The mechanism behind Zero-Message Session Selections above. A session's choice lives in an in-memory reference, while the durable record is the
request/headerevent that only a completed request writes. Until that first request, nothing reading the durable record sees the choice — Core's own admission chain included — and a restart drops the session back to the global default. - Core-Switched Sessions: Core's own
session.selectModelremains reachable from ACP and SDK clients even with the official Models UI disabled, and it writes a tier of Core's chain that outranks the request header and has no clearing path while that session's agent lives. A session switched that way admits messages against Core's choice from then on, and selections made here no longer converge with it, however many requests the session completes.
License
MIT © 2026 FHGS



No comments yet. Be the first to write one.