Fills in what the official pi-ai adapter leaves out on the Models page: gateway reachability probing with latency, one-click pull of the gateway model catalog with checkbox write-back into settings, DeepSeek balance and provider quota shown in place, the protocol and endpoint each provider is actually wired to, and a model dropdown that lists only what works right now — a dead key, an empty balance or an exhausted quota is hidden until you fix it, and comes back on its own.
Install
# from npm (prebuilt)
dsh plugin --profile web add @dsh-plugins/dsh-llm-hub
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:webkubor/dsh-llm-hub
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
On DSH's Models page, the official adapters leave half the job undone. This plugin finishes it:
| Official adapter | dsh-llm-hub | |
|---|---|---|
| Which DeepSeek models exist | invisible | one click, live list |
| How much credit is left | invisible | balance row on the card |
| Is the gateway up, how fast | button does nothing | measured latency & status |
| How many models it serves | invisible | 71 measured (11 hand-typed) |
| Why it can't be probed | no hint | says "no baseURL" |
Install
dsh plugin --profile web add @dsh-plugins/dsh-llm-hub
Add @dsh-plugins/dsh-llm-hub to dsh.profile.bundles in ~/.dsh/profiles/web/package.json, then run ~/.dsh/restart.sh.
Open Settings → Models — a new row appears under the provider cards.
A boot-graph change requires a restart; hot reload won't pick it up. cordis.patch.yml is inserted automatically by the bundle mechanism.
What it fills in
DSH already ships every mechanism involved; what was missing is that the official adapter never used them:
| Capability | Official mechanism | State on the direct route |
|---|---|---|
| Model discovery | llm service's registerModelDiscovery(ns, discover) + the Models page's "Fetch available models" |
@deepseek-ai/dsh-llm-deepseek never registers one (discover has zero occurrences in both 0.1.2-rc.1 and 0.1.5-rc.2) |
| Provider-card extension | settings.models.provider-card (keyed by settingsNs) |
No registrant → the area renders nothing |
| Account balance | DeepSeek GET /user/balance |
Not exposed by the adapter |
The discovery registry permits exactly one registration per settings
namespace (a second throws DUPLICATE_DISCOVERY), and the llm-deepseek
namespace is unoccupied — so this plugin takes it. DSH's own
slot-contract.d.ts states plainly that those two slots exist for plugins
distributed outside the repository.
Usage
Model discovery — Settings → Models → the DeepSeek card → "Fetch
available models". It live-fetches https://api.deepseek.com/models and
offers every advertised model for adoption.
Balance — a balance row appears under the same DeepSeek card (fetched on mount, with a manual refresh).
pi-ai bypass card — under any pi-ai provider card (modelgo / minimax /
zai-coding-cn …): a standing row with the configured model count and key state,
a Probe gateway button reporting reachability, latency and the remote
catalog size, and — modelgo only — Fetch catalog with one-click id copy.
Providers without a baseURL (zai-coding-cn) show an honest "cannot probe"
hint instead.
Behaviour
Connection facts
baseURL and apiKey resolve in the same order the adapter itself uses, and
the llm-deepseek settings section is re-read lazily on every call — at
plugin-apply time the section may not be registered yet (a startup race), and
the adapter re-resolves its own connection facts per request:
| Resolution order | |
|---|---|
| baseURL | request.baseURL → the section's baseURL → $DEEPSEEK_BASE_URL → https://api.deepseek.com |
| apiKey | request.apiKey (a one-shot key typed into the form) → the credential named by the section's apiKeyEnv → that environment variable |
Balance route
GET /api/dsh-llm-hub/balance →
{ ok, isAvailable, balances: [{ currency, total, granted, toppedUp }] }
Amounts are kept as the strings DeepSeek returns (the upstream sends
strings; parsing them would invite floating-point drift). Only GET/HEAD are
accepted (otherwise 405), and cross-origin reads are refused (Sec-Fetch-Site
other than same-origin/none → 403) — a balance is account information and
should not be readable by a cross-site page even when the server is bound to
loopback.
UI mount point
settings.models.provider-card with key = 'llm-deepseek'. The owner prop
keyConfigured decides whether a request is made at all: with no key
configured the card shows a hint instead of fetching.
Model dropdown availability (only what works)
The composer's model dropdown lists every configured provider regardless of whether it can actually be called: an expired key, an empty balance, or a gateway returning 401 all still show up and only fail once you pick them. This plugin hides providers that are confirmed unusable:
Criteria are evidence-only (fail-open — anything inconclusive is kept; hiding a working model is worse than showing a broken one):
| Signal | Hides when |
|---|---|
| Credential | the variable named by apiKeyEnv resolves nowhere (neither the credential store nor the environment) |
| Probe | the gateway answers the catalog endpoint with 401/403/402 |
| Balance | DeepSeek /user/balance reports is_available=false or a zero balance; a MiniMax/Zhipu quota is provably exhausted |
| Runtime | a real request failed with INVALID_CREDENTIAL / QUOTA_EXCEEDED (via agent/request-error) |
Recovery is automatic: after you fix the key or top up, settings/document-updated invalidates
the cache and the next read re-probes; a successful real request also clears the runtime mark at once.
The "Re-check all" button at the bottom of Settings → Models forces a full re-probe.
Hidden providers do not disappear: their cards stay in Settings → Models; the card just grows a red "hidden from dropdown" chip in its action row (the reason lives in its tooltip). The footer carries the global count and the "Re-check all" action. There is deliberately no separate panel restating each provider — the cards already show their own state, so that would only be duplication.
How it works, and the trade-off
Filtering happens in the host half, on ctx.llm.listProviders() — the only seam that covers every
consumer at once (composer dropdown, /model popup, subagent picker, ACP), and it is subtractive and
reverted when the plugin unloads. The settings page reads the configurable-provider directory
(listConfigurableProviders) instead, so it is unaffected.
Wrapping ctx.modelDirectories in the client half was tried and fails: it is a cordis
inject-tracking proxy whose methods come back as traceable proxies, so a plugin fiber without the
remote.session inject throws cannot get property "remote.session" without inject — measured on
2026-09-16, it crashes the shipped model seat right out of the composer. The contract marks
conversation.input.model as a single slot with replaceRisk: shadows-shipped-ui; taking it over
means re-implementing the whole menu and tracking upstream forever, which is not worth it.
One more counter-intuitive trap: judging must be driven by the settings section, never by
listProviders() — the latter is the filter's own output, so treating it as the target means a
hidden provider never enters the next probe round: hidden forever. Likewise, a cordis service method
cannot be verified with !== (every access through a traceable proxy yields a new object); check
the property descriptor instead.
External harness subagents (they appear only if installed)
Registers the external agent CLIs already installed on this machine as DSH subagent providers, so a session can hand a self-contained task to one of them — each billed against its own subscription:
| Tool | Requires | Actually runs |
|---|---|---|
subagent_codex |
codex |
codex exec --skip-git-repo-check <task> |
subagent_claude_code |
claude |
claude -p <task> |
subagent_antigravity |
agy |
agy -p <task> --dangerously-skip-permissions |
What isn't installed doesn't show up. A provider is registered only when its
executable actually resolves, and dsh-tool-subagent logs a single info line for
a missing provider while deferring the tool row until that provider appears. On a
machine without codex, subagent_codex never enters the tool catalog and the host
still starts normally — no switch, no configuration.
Detection goes through ctx.subprocess.resolveExecutable (the kernel's own
resolver, sharing the PATH view the child process will actually get) rather than
scanning PATH by hand: a login-shell alias fools command -v (agy is commonly
aliased with --dangerously-skip-permissions), and a launchd-started host never
reads .zshrc at all.
Pinned edge cases
agymust carry--dangerously-skip-permissions: in headless print mode it auto-denies thecommandpermission, so any task touching files or commands exits 0 with no output — no error, the subagent just "succeeds" having done nothing.- Exit 0 with empty output always fails, never folds to
completed(otherwise the parent agent proceeds on an empty answer). codexcarries--skip-git-repo-check: the parent cwd is not necessarily a git repository, and without the flag codex refuses to run.- Only an 8 KiB stderr tail is kept: agy's glog bridge emits 300+ lines when its log directory is not writable.
- The child does not inherit parent context and advertises no start-time capabilities (persona, tool filter, depth cap, structured output cannot be enforced inside another runtime), so the kernel rejects requests needing them up front instead of silently ignoring them.
Coexisting with the official bundles and preset rows
- The official
@deepseek-ai/dsh-subagent-codex/-claude-coderegister providers under the same names (codex/claude-code). First one wins: this plugin logs one info line, skips, and keeps registering the rest. Remove those bundles from the profile to let this plugin provide all three. - ⚠️ If you hand-added rows like
tool-subagent-codexin~/.dsh/.agent-presets/*/agent.cordis.yml, delete them — the sametoolNamecannot be registered twice.
Why kernel symbols are imported dynamically
@deepseek-ai/dsh-subagent / -session are supplied by the host (resolved through
.dsh-module-fallback inside a profile) and cannot be resolved from the repository.
A top-level static import would fail both ways: MODULE_NOT_FOUND when running the
repo's tests, and — should a host version ever drop one of those exports — a plugin
that fails to load entirely, taking balance and model discovery down with it. Lazy
loading confines that risk to this feature. Detection and registration never touch a
kernel import at all: the empty capability advertisement is inlined.
Known limitation
Discovery candidates cannot carry inputModalities. The llm service keeps
only id/name/contextWindow/maxTokens. So a deepseek-flash added
through the button lands as a text-only entry, while it actually accepts
image input. Patch it by hand afterwards:
llm-deepseek:
models:
- id: deepseek-flash
inputModalities: [ text, image ]
This is a limit of the harness's discovery contract itself (the official pi-ai route has it too); no plugin can fix it.
Development
npm run check # syntax of both halves
npm test # regression tests (node:test, zero dependencies)
npm run deploy # sync into the web profile
Tests live in test/ and use only node:test + node:assert; CI runs them. Each case pins a
bug that was actually hit — the availability chain (credential → probe → balance → runtime)
is long, and degrading any link produces no compile error: it just silently hides a working model
or leaves a broken one in the dropdown.
Releasing
Pushing a v* tag triggers .github/workflows/publish.yml: it re-runs syntax + regression
tests + the publish-artifact check before publishing (rather than trusting that some earlier
CI run passed), then npm publish, then creates the GitHub Release from the matching CHANGELOG
section when one is missing. Requires an NPM_TOKEN repository secret.
A missed or failed publish can be retried without re-pushing the tag:
gh workflow run publish.yml -f tag=v0.7.0
Idempotency comes from two checks — the tag must match package.json's version, and an
already-published version is skipped. Release existence is probed separately, so "npm succeeded
but no Release was created" is recoverable by re-running.
- Host half
lib/index.js: ESM (the cordis loader reads it as ESM). - Client half
lib/client.js: source is the artifact, a classic script (no top-levelimport/export) registered throughwindow.__ModuleLoader__.load({ id, factory }). Itsidmust exactly equal package.json'sname, or DSH refuses the registration. React arrives viafactory(require)and is never bundled into the artifact. At the current size no build step is warranted; if it ever splits into several files, add esbuild (format: 'iife', with React and friends marked external).
License
MIT
Links
More in this category
V1ki/dsh-plugin-subscriptions★ 373
Use ChatGPT (Codex), Claude, and Grok subscriptions as DeepSeek Harness LLM providers, with Settings login, model catalogs, usage, plus image_generate, video_generate, and x_search tools.
Mars-Sea/dsh-commandcode-provider★ 289
Unofficial Command Code LLM provider: registers a `commandcode` route with a live model catalog and reasoning-effort support.
corrinehu/dsh-workbuddy-connect★ 140
Brings the models in the WorkBuddy desktop app straight into DeepSeek Harness — zero configuration in the DSH chat.
volcengine/ark-cli#ark-plan-api★ 133
Registers Ark Agent Plan, Coding Plan and postpaid model routes in the native DSH model picker.
franksong2702/dsh-codex-connect★ 114
Connect ChatGPT OAuth and OpenAI Codex models to DeepSeek Harness, with opt-in search and image tools.
Stormycry-cryp/dsh-AuthInOne★ 104
Adds account login, API and custom Provider setup, model switching, image fallback for text-only models, and token/cost attribution to DeepSeek Harness 47f.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.