@dushayulll/dsh-model-catalog-refresh
A host-only DeepSeek Harness (dsh) plugin
that refreshes every provider you have already configured under
llm-pi-ai.providers from a live model directory — once, shortly after DSH
starts. No provider is hard-coded in the plugin configuration: it discovers
them from your own settings.
Append-only: it only adds new model ids to your existing list — it never
removes, reorders, or rewrites any user-configured entry. The original
configuration is backed up and can be restored on uninstall.
Features
- Auto-discovery — reads whichever providers exist in
llm-pi-ai.providers;
add a provider to settings and it is picked up on the next DSH start.
- Append-only ("只增加不减少") — your existing
models entries stay
exactly as-is (fields, order); only new live model ids are appended.
- Data sources —
models.dev catalog for known providers (no auth,
includes context/output capacity), or live GET {baseURL}/models for
OpenAI-compatible routes.
- Built-in fallback for known providers — when models.dev is unreachable
and the provider is known to this plugin (currently
opencode-go), it uses
the provider's stable OpenAI-compatible /models endpoint and auto-fills
api/baseURL on the next write, so no manual route editing is required.
- Validation-safe appends — models outside dsh's built-in catalog require
route-level
api/baseURL. When you have not declared them, the plugin
infers them from models.dev provider metadata (npm client → wire protocol,
provider api → base URL), so the write passes dsh-llm-pi-ai validation.
- Tunable reasoning efforts — appended models get a
reasoningEfforts map
derived from models.dev reasoning_options (explicit effort lists win;
otherwise low/medium/high defaults), so the thinking level is selectable
in DSH instead of silently pinned off.
- Input modality preservation —
text/image input modalities from the
source are kept on appended models (unsupported ones like video are safely
dropped), so multimodal models declare their inputs without failing dsh's
validation.
- One-shot, not a poller — a single refresh shortly after startup, then quiet.
- Reversible — original config is backed up before the first write; restore
happens on uninstall (best-effort) or via a standalone
restore.mjs script
(guaranteed).
Problem
dsh's llm-pi-ai.providers.* model list is static: dsh reads whatever the
llm-pi-ai settings section says and never asks a provider which models it
serves. So a model newly released on a configured platform (e.g. Muse Spark
1.2 Contributor on opencode go) never appears in dsh until someone writes it
into settings.yaml. This plugin automates that write.
How it works
After DSH starts, the plugin waits startupDelayMs (default 5s) and then runs
one refresh:
- It reads
llm-pi-ai.providers from the settings seam — all providers you
already configured.
- For each provider it picks a data source automatically:
- models.dev knows the provider → fetch
https://models.dev/api.json and use that provider's models map (no
auth needed, includes context/output capacity);
- otherwise the provider has an OpenAI-compatible
baseURL →
GET {baseURL}/models live, using the route's apiKeyEnv when set;
- otherwise the provider is a known provider with a built-in fallback
(currently
opencode-go) → use its stable OpenAI-compatible /models
endpoint and auto-fill api / baseURL on write;
- otherwise → skip that route and leave it untouched.
- Append-only merge: your existing
models entries stay exactly as-is
(fields, order); only new model ids from the live source are appended to the
end (duplicate ids inside the source are collapsed to the first).
- Enrich appended models so dsh accepts and serves them:
reasoningEfforts is derived from models.dev reasoning_options
(explicit effort lists win; otherwise low/medium/high defaults), making
the thinking level selectable in DSH;
input modalities (text/image) are preserved from the source's
modalities.input onto each appended model; unsupported values such as
video are dropped so the write passes dsh validation;
- when you have not declared route-level
api/baseURL, they are inferred
from the models.dev provider entry (npm client → wire protocol,
provider api → base URL) — required for models outside dsh's built-in
catalog.
- The merged list is persisted through the
llm-pi-ai settings namespace
(ctx.settings.update('llm-pi-ai', …)).
Because dsh-llm-pi-ai re-reads profiles once per operation, the new models
are selectable on the next request without a restart. Adding a new provider
to settings is picked up the next time DSH starts.
Not a poller: it is a one-shot startup refresh, not a periodic timer.
Backup & Restore
The plugin backs up your original models list before the first write, so you
can always get back to your pre-plugin state.
What is backed up
For each provider the plugin updates, the backup (default
$DSH_HOME/model-catalog-refresh-backup.json) records:
hadExplicit: whether you originally had a models list (vs. using the
built-in catalog);
original: your original models list;
lastWritten: the complete list the plugin last wrote (to detect hand-edits).
Automatic restore (best-effort)
When restoreOnDispose: true (default), the plugin attempts to restore your
original config on disposal (uninstall / host shutdown). It only restores
providers whose current list matches lastWritten — if you hand-edited after
the plugin wrote, the auto-restore is skipped to avoid clobbering your work.
Note: disposal also fires on normal DSH shutdown; since the plugin re-refreshes
at every boot, the served catalog stays fresh while DSH runs and reverts to
the original when DSH stops — you won't notice the cycle.
Guaranteed restore via script
pnpm restore # or: node scripts/restore.mjs
node scripts/restore.mjs --force # restore even if you hand-edited
The script reads the backup file and applies the restore directly to
settings.yaml (using yaml for round-trip fidelity). It is idempotent —
running it a second time does nothing. The backup file is deleted only after all
providers are restored; if some were skipped, it is preserved so you can retry
with --force.
Configuration
All fields optional; no provider list in this JSON:
| Field | Default | Description |
|---|
modelsUrl | https://models.dev/api.json | models.dev-style directory endpoint |
startupDelayMs | 5000 | Delay after DSH startup before the one-shot refresh |
timeoutMs | 30000 | Network timeout |
skipUnavailable | true | Skip models.dev entries marked available: false |
maxModels | 2000 | Safety cap per provider — only freshly appended entries beyond the cap are shed; your existing entries are never truncated, even if the list already exceeds the cap |
refreshOpenAiCompatible | true | Live-refresh hand-declared OpenAI-compatible routes |
backupPath | $DSH_HOME/model-catalog-refresh-backup.json | Backup file path |
restoreOnDispose | true | Auto-restore original config on disposal |
proxyUrl | empty (auto-detect) | Proxy for catalog fetches. Node's fetch ignores the Windows system proxy — behind a local/corporate proxy set e.g. http://127.0.0.1:7890. Empty → auto-detect from HTTPS_PROXY/HTTP_PROXY env; "direct" forces direct connections |
Install
[!NOTE]
Requires an existing DeepSeek Harness installation.
npm
dsh plugin --profile web add @dushayulll/dsh-model-catalog-refresh
Build from source
git clone https://github.com/dushayulll/dsh-model-catalog-refresh.git
cd dsh-model-catalog-refresh
pnpm install
pnpm build
dsh plugin --profile web add .
Run pnpm build again after changing the source. The local plugin install
remains linked to this checkout.
Runtime injection (no restart)
For development or testing without a full profile install:
dev_inject_plugin → <absolute path of this repo>
Validate
Check the composed profile, restart DSH, and verify the catalog was refreshed:
dsh --profile web --dump-config
dsh web
After DSH starts, settings.yaml should contain the appended models for each
configured provider.
Compatibility
- DSH / dsh-core:
0.1.5-rc.x (peer deps @deepseek-ai/cordis ^4.0.2, @deepseek-ai/dsh-settings ^0.1.5-rc.1, @deepseek-ai/dsh-timeout ^0.1.5-rc.1)
- Node.js:
^22.19.0 || >=24
- Type: ESM, strict TypeScript
Adapted to dsh 0.1.5-rc.1: the settings seam validates every write against
the llm-pi-ai schema plus the adapter's serviceability check, and resolves
an absent models key to an empty array. The plugin reads whether you
explicitly declared a models list from the raw settings document
(settings.describe().user), so backup/restore correctly distinguishes
"declared list" from "inherited built-in catalog".
Development
pnpm install
pnpm build # tsc → lib/
pnpm test # vitest
pnpm verify # typecheck + test + verify:pack
Verification
After the next DSH start, settings.yaml contains the appended models for
each configured provider, e.g.:
llm-pi-ai:
providers:
opencode-go:
apiKeyEnv: OPENCODE_GO_API_KEY
models:
- id: deepseek-v4-flash # ← original, untouched
name: DeepSeek V4 Flash
contextWindow: 1000000
maxTokens: 384000
- id: muse-spark-1.2-contributor # ← appended automatically
name: Muse Spark 1.2 Contributor
contextWindow: 1048576
maxTokens: 131072
input: [text, image] # ← input modalities (unsupported ones dropped)
reasoningEfforts: # ← thinking level made tunable
off: null
minimal: minimal
low: low
medium: medium
high: high
xhigh: xhigh
You can also dry-run the restore flow against a temporary settings.yaml and
backup file to confirm it behaves as expected.
Uninstall
dev_uninject_plugin (match package name @dushayulll/dsh-model-catalog-refresh).
- If auto-restore didn't run (e.g. force-killed), run:
pnpm restore
Security
- No credentials are stored: the plugin only reads
apiKeyEnv names and
resolves them through dsh's credentials seam or the environment at request
time.
- The
files whitelist in package.json plus verify:pack ensure no source,
test, or local data is shipped in the published package.
License
MIT