Skip to content
dsh.fish
Bundle

@fhxgs/dsh-model-hub

DeepSeek Harness plugin: provider sign-in, model catalog, and selection routing over a loopback-only /model-hub channel

Source
yhyfhgs
stars
2 stars
License
MIT
Updated
Updated 11 days ago

Readme

<div align="center">

# @fhxgs/dsh-model-hub

**Unified provider authentication, model catalog, and selection routing for DeepSeek Harness.**

[English](README.md) | [简体中文](README.zh.md)

[![npm version](https://img.shields.io/npm/v/%40fhxgs%2Fdsh-model-hub?color=cb3837&logo=npm&logoColor=white)](https://www.npmjs.com/package/@fhxgs/dsh-model-hub)
[![license MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org)
[![pnpm](https://img.shields.io/badge/pnpm-required-F69220?logo=pnpm&logoColor=white)](https://pnpm.io)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
[![DSH plugin](https://img.shields.io/badge/DSH-plugin-6E56CF)](#compatibility)

</div>

---

`@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 plugin` runs pnpm under the hood (`spawnSync('pnpm')`), so **pnpm must be available in your `PATH`**. If needed, enable it via corepack:
> ```sh
> corepack enable pnpm
> ```

Add the plugin to your target profile:

```sh
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:

```sh
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

1. Open the DSH Web UI on the **same machine running DSH** (all endpoints require loopback access).
2. Go to **Settings → Model Hub → Providers**. Select your provider and complete the sign-in flow.
3. Click **Activate** on the provider card to enable the route.
4. Switch to the **Catalog** tab to select which models you want visible in your picker.
5. 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:

```yaml
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-model` namespace. The plugin reads this value but only modifies it when explicitly confirmed by the user.
- **Built-in provider configs**: Stored separately in `model-hub-providers` to 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 |
|---|---|---|
| ![Composer picker bubble with the reasoning-effort slider](https://raw.githubusercontent.com/yhyfhgs/dsh-model-hub/main/docs/screenshots/composer-picker.png) | ![Model Hub settings, Providers panel](https://raw.githubusercontent.com/yhyfhgs/dsh-model-hub/main/docs/screenshots/settings-providers.png) | ![Model Hub settings, Catalog panel](https://raw.githubusercontent.com/yhyfhgs/dsh-model-hub/main/docs/screenshots/settings-catalog.png) |

## Architecture Overview

Both Host and Client components reside in a single npm package:
- **Host (Node.js ESM)**: Manages the `/model-hub` RPC 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 `/model` modal.

```
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 `./client` export is Loader-only.** `lib/client.js` is not a module anything imports. Its body is a single `window.__ModuleLoader__.load({ ... })` call, and the harness shell serves the file to the browser rather than resolving it — `import()` from Node and `require()` 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 why `publint` reports 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 in `scripts/verify-manifest.mjs`, and the exemption is matched against the report's exact wording, so a different finding still fails the gate.

## Development & Testing

```sh
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
```

- `prepack` runs `verify` before `npm pack` or `npm publish` to prevent broken or dirty artifacts; `prepublishOnly` additionally rebuilds from a cleaned `lib/` 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.md`](https://github.com/yhyfhgs/dsh-model-hub/blob/main/scripts/gate-p1.md) and [`scripts/gate-p3.md`](https://github.com/yhyfhgs/dsh-model-hub/blob/main/scripts/gate-p3.md).
- Release procedures are defined in [`scripts/release-checklist.md`](https://github.com/yhyfhgs/dsh-model-hub/blob/main/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

1. **Loopback Only**: All `/model-hub` endpoints return HTTP 403 when accessed outside loopback. Non-loopback pages display a read-only badge; model changes must be performed on the host machine.
2. **Native UI Disabled**: The official models settings and onboarding steps are disabled while this plugin is active.
3. **No Background Push Stream**: UI updates are driven by forwarded host events and polling during active login attempts.
4. **Zero-Message Session Selections**: Model selections on sessions with zero sent messages do not persist across restarts (matching official DSH behavior).
5. **Send Blocking Scope**: Composer send-blocking is enforced on web UI interactions; headless or SDK clients are validated on the host directly.
6. **Codex Token Cost**: OpenAI Codex token costs report as zero because plan-based subscription pricing differs from standard API token rates.
7. **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 code `LOOPBACK_PORT_IN_USE` rather than waiting, and there is no device-code fallback for this route. The receiver binds both loopback families where the machine has both — if `::1` cannot be bound the sign-in continues on `127.0.0.1` alone, recorded as `LOOPBACK_IPV6_UNAVAILABLE`.
8. **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.
9. **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.
10. **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/header` event 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.
11. **Core-Switched Sessions**: Core's own `session.selectModel` remains 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](./LICENSE) © 2026 FHGS

Install

dsh plugin --profile web add github:yhyfhgs/dsh-model-hub

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source