dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Real-time model-list refresh for DeepSeek Harness llm-pi-ai provider routes: query the live endpoint, merge the installed pi-ai catalog metadata onto new ids, append-only writes through the official settings seam.
English | 中文
A companion plugin that keeps DSH llm-pi-ai provider routes' model catalogs current with what their
endpoints actually serve. It patches nothing: it works through the official settings, credentials,
llm, and tools seams.
The shipped @deepseek-ai/dsh-llm-pi-ai adapter answers "which models can this provider serve?" from
pi-ai's installed snapshot for every route the snapshot knows, and never contacts the endpoint:
// dsh-llm-pi-ai/lib/index.js discoverModels()
const installed = catalogModels(request.provider)
if (installed.size > 0) return [...installed.values()].map(...) // snapshot, no network
That is a deliberate trade — the snapshot carries contextWindow, maxTokens, and modality facts no
listing endpoint discloses — but it means a model released after the pinned pi-ai version can never
appear, and a hand-written models list stays frozen at whatever was typed.
The drift has been reported repeatedly:
This plugin implements what those reports ask for together: always query the live endpoint, fall back to the catalog on failure, merge catalog metadata onto live ids, and actually persist the result into the route.
One refresh — scheduled, on startup, or via the refresh_model_catalog tool:
llm.listConfigurableProviders() to learn each route's owning settings namespace and path.
Nothing hard-codes llm-pi-ai: the settings namespace is the profile's loader entry id
(include:llm-pi-ai in a stock profile), so a plugin that hard-codes the plugin name silently
does nothing.GET {baseURL}/models with bearer auth for the OpenAI dialects,
GET {root}/v1/models?limit=1000 with x-api-key and anthropic-version for Anthropic Messages.
Both reply dialects normalize into one candidate shape.name,
contextWindow, maxTokens, and input; an endpoint-reported capacity is the fallback.settings.mutate(), re-reading the revision first and confirming the value
landed afterwards.models value is not a list this plugin can fully
account for — a string, or an entry with no id — the whole route is skipped and reported, instead
of being overwritten with a guess.id, name, contextWindow, maxTokens, and input are
ever written. reasoningEfforts and compat are deliberately never inferred from a listing:
leaving them unset inherits the installed catalog entry's capability, which is always at least as
correct as anything derivable from a bare list of ids.# 1) build
cd dsh-plugins/dsh-llm-pi-ai-live
npm install && npm run verify && npm run build
# 2) add to the target profile (local path uses link:)
dsh plugin --profile <profile> add link:$PWD
# 3) restart DSH
Alternatively merge the insert block from cordis.patch.yml into
<DSH_PROFILE_DIR>/cordis.patch.yml.
Start with dryRun: true for the first pass. A dry run computes and reports without writing, and
the report goes to the log:
[live-catalog] mounted (first pass in 0ms, periodic refresh off, dry run)
[live-catalog] openrouter: dry run — would add 464 model(s): openai/gpt-6.1-sol-pro, ...
[live-catalog] scheduled: 1 updated, 464 added, 0 failed, 0 skipped in 932ms
That output is from a real boot against OpenRouter's live endpoint. Turn dryRun off once it looks
right.
Every field is optional; defaults are documented in cordis.patch.yml.
| Field | Default | Meaning |
|---|---|---|
enabled | true | Master switch; false loads the plugin but schedules nothing |
startupDelayMs | 10000 | Delay after mount before the first pass (ms); 0 runs it immediately, and the startup pass always runs |
intervalMs | 21600000 | Period between passes (ms, six hours); 0 disables the periodic pass |
timeoutMs | 30000 | Per-request network timeout |
settingsNamespaces | [] | Namespaces to serve; empty auto-detects the pi-ai namespace |
include | [] | Routes to refresh; empty means every configured route |
exclude | [] | Routes to leave alone, e.g. ['openrouter'] |
enrichFromCatalog | true | Fill new entries from the installed pi-ai catalog |
maxModels | 2000 | Cap on stored model entries per route |
dryRun | false | Compute and report without writing |
toolEnabled | true | Register the refresh_model_catalog tool |
The gap, measured. A route pi-ai's catalog does not describe has to declare everything itself,
and reasoningEfforts is the one field with no sensible default. Against a real DSH boot:
| Third-party model | resolveModelInfo().reasoning |
|---|---|
declares no reasoningEfforts | null — no thinking level is offered at all |
declares reasoningEfforts | {efforts: [off, high, max]} ✓ |
declares reasoningEfforts: false | null (correctly declared non-reasoning) |
Writing the map by hand works, but every new model needs it written again. This automates that without guessing:
reasoning:
enabled: true
rules:
- provider: 'acme-gateway' # glob against the route key
model: 'glm-*' # glob against the model id
efforts:
off: null # null is allowed only for `off`
high: high
max: ultra # rename a level for this gateway
- provider: 'local-vllm'
model: '*'
efforts: false # declare them non-reasoning
Only a genuine gap is filled, on two rules:
reasoningEfforts — including false, which is a deliberate
statement that the model does not reason — is never touched.Nothing is inferred from a model's name. The levels and their wire spellings are the operator's statement about their own gateway, and a wrong guess changes the request shape. A rule with a mistyped level is refused and named by index in the log, rather than failing silently.
refresh_model_catalog — optional provider narrows the pass to one route. Lets the agent pick up a
newly released model on demand, with no config edit or restart.
ctx.llm.registerModelDiscovery(ns)
throws DUPLICATE_DISCOVERY for a namespace that already has one, and llm-pi-ai owns its own, so
no external plugin can replace the built-in short-circuit. This plugin takes the other road: it
writes the live result into the route's models.baseURL nor a catalog endpoint (pure-OAuth Bedrock/Vertex) is reported as NO_ENDPOINT.models. See the guarantee above.models list. Measuring OpenRouter's 464 live models
wrote about 88 KB into cordis.patch.yml. That is the inherent cost of pinning the live
directory into configuration; maxModels (default 2000) is the guard rail, and exclude
can drop a route entirely.Several community plugins already work this area, and the differences are worth knowing before choosing. The sharpest one:
ctx.llm.discoverModels('llm-pi-ai', …). That service
short-circuits to the installed snapshot for any route the pi-ai catalog knows — the exact root
cause #3816 and #5306 describe. A "live sync" built on it receives the snapshot. This plugin makes
its own HTTP request instead.opencode-go whose provider record carries no
baseUrl.models value abandons that route rather than
overwriting it.reasoningEfforts or compat. Some peers probe or infer thinking levels;
leaving them unset inherits the catalog capability, and a wrong guess changes the request shape.Related work: mpetruc/dsh-model-sync, ddddd-ren/dsh-relay-toolkit (broader — settings UI,
connectivity probing, capability backfill), Retr67/dsh-opencode-models, fan56/dsh-model-sync,
zpis666/dsh-opencode-go-sync, dsh-model-catalog-refresh, dsh-model-catalog-sync. This plugin's
trade-off is narrow and verifiable: no UI panel, with the effort spent on merge semantics and
verification depth.
npm run typecheck # source and tests, via two tsconfigs
npm test # vitest (unit + real-catalog integration)
npm run build # src → lib
npm run verify # typecheck + test
npm run test:e2e # boot a real DSH and verify end to end
Two layers of tests:
test/{listing,merge,routes,sync,index}.test.ts — pure logic and orchestration over fake seams.test/integration.test.ts — exercises the #3816 scenario (an opencode-go route configured with
nothing but a credential) against the real installed catalog, and skips itself when
@earendil-works/pi-ai is not resolvable.To run the real-catalog integration test, link the profile's packages in:
PROF=$DSH_PROFILE_DIR/node_modules
mkdir -p node_modules/@earendil-works
ln -sfn $PROF/@earendil-works/pi-ai node_modules/@earendil-works/pi-ai
npm run test:e2e:
DSH_HOME and scratch profile under this package's .e2e/;~/.dsh/profiles/desktop) and links
this package in by name — the loader imports a bare specifier, and an absolute directory path
cannot be resolved by ESM, so name: must be the package name;/models endpoint on 127.0.0.1 and writes a patch mounting a real llm-pi-ai
route;dsh --profile scratch;Point it elsewhere with DSH_PROFILE_DIR=... npm run test:e2e. No server other than the local fake
endpoint is started.
This step is not optional. It caught two defects no unit test could: a missing timer in
inject that stopped the plugin activating in a real profile at all, and a startupDelayMs: 0 guard
that silently skipped the startup pass. Both are fixed and pinned by regression tests.
Per the official publish.md and the app-boot README:
@deepseek-ai/dsh and @deepseek-ai/dsh-* entry in
peerDependencies against the runtime version and refuses a mismatch. This plugin declares
>=0.1.7-rc.2 <0.2.0.optional: true: the version claim still feeds the
admission check, while pnpm is never sent looking for a package it cannot fetch.@deepseek-ai/dsh* (cordis, the timer plugin, pi-ai) are not admission-checked and
are optional too.@deepseek-ai/schemastery stays a plain dependencies entry: it is a stateless schema utility,
which the official guide puts under dependencies, and shipping our own copy guarantees the plugin
loads in any profile.How pi-ai is found. The plugin does not depend on pi-ai at build time and resolves it at runtime
in two steps: the ordinary bare import first, then a filesystem search, which matters because
dsh plugin add produces pnpm's non-hoisted layout. The usual fallback cannot help — pi-ai exports
no ./package.json and declares its subpaths under the import condition alone — so the plugin
carries a minimal exports wildcard resolver (pi-ai declares "./providers/*", not a literal key).
When both steps fail it is not an error: new models simply carry only what the endpoint disclosed.
inject: ['settings', 'llm', 'timer'] are hard dependencies; timer comes from
@deepseek-ai/cordis-plugin-timer, which dsh-base already includes. credentials and tools are
read optionally through ctx.get: without them the plugin still refreshes, it simply cannot resolve
an apiKeyEnv reference and has no manual tool.
MIT