Install
Inside DeepSeek Harness, with dsh-market
dsh plugin --profile web add dshmarket
Or from the command line
dsh plugin --profile web add github:Ztyss/dsh-llm-provider
Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.
README
A dsh (DeepSeek Harness) plugin that takes over
the official pi-ai adapter (llm-pi-ai), the native DeepSeek adapter (llm-deepseek), the model
selector (ui-model-selection) and the Models settings page (ui-settings-models), providing a
self-maintained pi-ai bridge, model selector, quota lookups and provider management UI.
中文 · English
This repository is forked from imchangchang/dsh-llm-provider and maintained independently since. Current version v0.2.3 (installs from main).
Install
dsh plugin --profile web add github:Ztyss/dsh-llm-provider # private repo; git needs credentials (gh auth login)
dsh web # restart required: the plugin tree is assembled at process start
The build output (lib/) ships with the repository, so it works right after install. After
changing source, run npm run build and commit lib/ together with src/ (there is deliberately
no prepare script: pnpm runs it in a temp dir on git installs where tsdown is missing, which
fails the whole install).
Features
pi-ai bridge: enable the latest version
The "Enable latest pi-ai" toggle in the settings page (Provider → pi-ai bridge) runs the
official pi-ai adapter on the newest @earendil-works/pi-ai downloaded from npm, replacing the
DSH-bundled copy — new pi-ai versions and model support arrive without waiting for a dsh release.
- ON = a standing intent: pull
dist-tags.latestof@earendil-works/pi-aifrom the npm registry — sha512 verification → extract → install the dependency closure → probe against the bridge copy's own import requirements — into the safe zone$DSH_HOME/llm-provider-bridge/pi-ai/<version>/(the plugin package can be recursively deleted at any time, so hundreds of MB never live inside it). After a restart the bridge link points at the downloaded copy, replacing the DSH-bundled one; a failed probe falls back automatically, so the switch always has a safety net. - OFF = fall back to the DSH-bundled copy: downloaded files are kept, and flipping back ON costs nothing.
- The toggle preference also lives in the safe zone:
$DSH_HOME/llm-provider-bridge/vendor-status.json— the preference used to be written inside the package asvendor/status.json, and the package manager rebuilds the plugin directory from scratch on every reinstall/upgrade, resetting the switch to OFF. Moved out, a reinstall loses nothing (an in-package legacy file is merged into the safe zone on first read, then removed). - Only the newest downloaded version is kept:
loadBridgecleans older ones after the switch has completed (the running process never steps on a directory being deleted). - The network is touched only while the switch is ON, in two places: the flip itself, and
one check on every startup — no local copy ready, the download starts right away (the UI
enters its downloading state, no second flip needed); a copy ready, npm is still checked and a
newer version downloads automatically (
updateDecisionskips "latest ≤ newest local"). The 60-second floor is crash-loop damping, not a throttle on intent. OFF never touches the network. - The version row tells the truth: current x.y.z (DSH-bundled / npm latest / vendor) — which
copy is in use at a glance. The toggle row only speaks when there is something in progress or
to do: downloading / x.y.z downloaded (restart to apply) / x.y.z updated (restart to apply) /
x.y.z downloaded (restart to fall back to official) / x.y.z downloaded (cannot be enabled) /
x.y.z downloaded (not in use). Whether a restart is pending is derived live by
piAiNeedsRestart(preference + newest ready safe version + the copy currently loaded) — the enable-in-place state (nothing to download) says so too; a pending version that this very check actually downloaded (lastCheck.installedis it — typically the startup auto-update after a restart) says "updated", while a copy that was already on disk says "downloaded"; verification failures live in the detail rows. While downloading, poll every 2s.
Model services settings page
- Bilingual (zh/en): dictionary +
tfinterpolation through dsh's own locale mechanism, following language switches live. - Inline provider editing: display name, endpoint, protocol and credential name are edited right on the card — no edit mode, no separate form. With no changes the card shows no editing affordances at all; as soon as a draft exists, "Save changes / Cancel" appear. Only changed fields are written, clearing a field removes the key. The route id stays read-only.
- The model list is the checklist: expanding "Models (N)" shows checkboxes directly, decoupled
from any edit button. Existing entries are read-only (capability badges + formatted context,
same columns as the display list); configured rows get an ✕ to delete them in one click; only
custom ids not in the catalog need context window / max output and vision / video. Saving goes
through the dedicated route
POST /provider/set-models(server-side validation, native routes rejected), with the declared entry as the base — hand-writtenreasoningEfforts/compatsurvive; "Follow catalog (restore)" deletes the key and returns to the full catalog. - pi-ai bridge card, now a toggle: the "Enable latest pi-ai" switch plus a status line (current version + source / already latest / downloaded, restart pending / failed verification with the reason, persistent); after flipping ON — and during each startup check — it polls every 2s until the download settles.
- Delete confirmation modal: cost list + "Export config (YAML)" backup (secrets excluded).
- Three-state capability badges: supported / explicitly unsupported / unknown are rendered separately; models missing from the catalog get capabilities from the route declaration and adapter self-report (modlens-style synthetic providers).
Console cookie storage
Providers that need a console session (StepFun's Step Plan points, for one) keep their cookies in
$DSH_HOME/llm-provider-bridge/.cookies.yaml — one file, dot-prefixed to match the
.credentials.yaml habit; same schema as credentials (version + flat key table), keys are
credential ref names (e.g. STEPFUN_CONSOLE_COOKIE), so a new provider or cookie is just another
key. Parsing tolerates hand edits, and a bad row never kills a lookup.
- Legacy single-slot session files migrate automatically:
stepfun-console-session.jsonis merged into the new file on first read, then removed. - Seed fingerprint dedup (write convergence): an entry records
seedSha, the fingerprint of the seed credential — later quota lookups for the same credential reuse the stored value instead of writing the static credential back. Previously one lookup meant two writes plus a pointless renewal RPC; now writes happen only on two real changes: repasting a cookie, and renewal rotation. - Fragment seed guard: a seed must contain an
Oasis-Tokensection — a fragment with onlyOasis-Webidcan no longer overwrite a rotated good jar. - First write on a fresh machine: the bridge directory is created when missing.
Model selector
- Full takeover: once the official model selector is disabled, the selection seat (current
model state), the
/modelcommand and the model-catalog state machine are all provided by this plugin. - The
/modelcommand: filter by provider, search models, candidates grouped by provider with quota/balance shown per group. - The
availablecontract: implements the officially requiredavailable(session)for/command contributions — sessions addressed as subagents get no model selection; the host calls it unguarded, so one throw would take the whole/candidate batch down — the implementation never throws and swallows every exception, preferring to show one extra menu entry.
Stability & safety design
- Pre-load integrity check and candidate chain:
src/pi-ai-source.tsverifies the manifest, the entry file and the four subpaths the official bundle actually imports; when broken it prints an executable restore recipe (npm packover the host directory — the plugin never downloads for you).piAiCandidates()= safe zone (when ON, newest → oldest) → legacy vendor tiers → bundled dependency → DSH-bundled, probed in order, first pass wins. - The bridge workspace lives outside the package, in
$DSH_HOME/llm-provider-bridge/: on Node ≥24.15rmSyncfollows junctions and empties their targets, and in this layout there is no link left to follow. Invariant: the installed package contains zero links (enforced bytest/host-safety.mjs). - The npm cache never sticks around: during a download the npm cache lives in the OS temp dir
and is deleted when the install finishes (success or failure); on startup a sweep removes any
legacy
.npm-cacheleft in the safe zone. - Conditional disable of the official entries (fail-open):
cordis.patch.ymluses!!jsexpressions — the official entries are disabled only while the host's pi-ai is intact; if it is missing or broken they stay enabled and DSH boots normally. Generated bysrc/patch-condition.ts, kept in sync withscripts/sync-patch-condition.mjs --check.
Tests
npm test # build + an 18-step offline chain (no dsh needed; the last step is the patch-condition drift check)
node test/host-safety.mjs # junction-safety regression: run under BOTH node 24.14 and the DSH runtime (≥24.15)
Comments
Comments live in GitHub Discussions. Sign in with GitHub to post or react.