dsh-llm-pi-ai-live
Manifest validReal-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.
dsh-llm-pi-ai-live
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.
Why it exists
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:
- #3816 — opencode-go serves 28 models live, the snapshot knew 16; glm-5.3, qwen3.8-max and others never showed up
- #5306 — "model list is stale and incomplete — never refreshed from provider endpoints"
- #5691 — "model catalogs are static: OpenRouter shows 333 of 431 models"
- #4685 — "fetching models from OpenRouter doesn't work"
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.
What it does
One refresh — scheduled, on startup, or via the refresh_model_catalog tool:
- Plan — read the live configurable-provider directory through
llm.listConfigurableProviders()to learn each route's owning settings namespace and path. Nothing hard-codesllm-pi-ai: the settings namespace is the profile's loader entry id (include:llm-pi-aiin a stock profile), so a plugin that hard-codes the plugin name silently does nothing. - Interrogate —
GET {baseURL}/modelswith bearer auth for the OpenAI dialects,GET {root}/v1/models?limit=1000withx-api-keyandanthropic-versionfor Anthropic Messages. Both reply dialects normalize into one candidate shape. - Merge — live ids decide membership; the installed pi-ai catalog supplies
name,contextWindow,maxTokens, andinput; an endpoint-reported capacity is the fallback. - Persist — through
settings.mutate(), re-reading the revision first and confirming the value landed afterwards.
Guarantees
- Append-only. Existing entries pass through by reference, in order, with their fields untouched. New ids are appended. A model the endpoint stopped advertising is never removed.
- Refuse rather than guess. If the stored
modelsvalue is not a list this plugin can fully account for — a string, or an entry with noid— the whole route is skipped and reported, instead of being overwritten with a guess. - Failures never touch the configuration. A 5xx, a timeout, a non-JSON body, an unreadable protocol, or a rejected write all leave the route exactly as it was and appear in the report.
- No synthesized risky fields. Only
id,name,contextWindow,maxTokens, andinputare ever written.reasoningEffortsandcompatare 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.
Install
# 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.
Configuration
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 |
Declaring reasoning effort for third-party models
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:
- An entry that already declares
reasoningEfforts— includingfalse, which is a deliberate statement that the model does not reason — is never touched. - A model the installed catalog knows is never touched either: leaving the field absent inherits the catalog entry's capability, so nothing is missing.
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.
Tool
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.
What it does not do
- It does not replace the "fetch available models" action.
ctx.llm.registerModelDiscovery(ns)throwsDUPLICATE_DISCOVERYfor a namespace that already has one, andllm-pi-aiowns 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'smodels. - It does not touch a route with no interrogable endpoint. A provider with neither a configured
baseURLnor a catalog endpoint (pure-OAuth Bedrock/Vertex) is reported asNO_ENDPOINT. - It does not probe protocols. When a catalog route speaks several (OpenRouter ships both an Anthropic and an OpenAI dialect), the listing request uses the plurality dialect; the route's own models keep their individual protocols.
- It does not fill fields beyond
models. See the guarantee above. - A large gateway produces a long
modelslist. Measuring OpenRouter's 464 live models wrote about 88 KB intocordis.patch.yml. That is the inherent cost of pinning the live directory into configuration;maxModels(default 2000) is the guard rail, andexcludecan drop a route entirely.
How it differs from the other plugins in this space
Several community plugins already work this area, and the differences are worth knowing before choosing. The sharpest one:
- This plugin does not go through
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. - Protocol-generic, not tied to one gateway: both the OpenAI dialects and Anthropic Messages listings are read.
- Endpoint recovery from the catalog's models, which is what makes the common
"credential-only route" case work for providers like
opencode-gowhose provider record carries nobaseUrl. - Append-only with a refusal path: an unreadable
modelsvalue abandons that route rather than overwriting it. - Never synthesizes
reasoningEffortsorcompat. 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.
Development
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 (anopencode-goroute configured with nothing but a credential) against the real installed catalog, and skips itself when@earendil-works/pi-aiis 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
End-to-end verification
npm run test:e2e:
- builds a throwaway
DSH_HOMEandscratchprofile under this package's.e2e/; - links the packages of the profile you point it at (default
~/.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, soname:must be the package name; - serves a fake
/modelsendpoint on127.0.0.1and writes a patch mounting a realllm-pi-airoute; - boots the real
dsh --profile scratch; - asserts the profile patch gained the endpoint's new model while the hand-written entry survived.
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.
Dependencies and resolution
Per the official publish.md and the app-boot README:
- Before importing a plugin, DSH checks every
@deepseek-ai/dshand@deepseek-ai/dsh-*entry inpeerDependenciesagainst the runtime version and refuses a mismatch. This plugin declares>=0.1.7-rc.2 <0.2.0. - Those packages are distributed inside the DSH installation and have no matching version on the
public registry, so every peer is marked
optional: true: the version claim still feeds the admission check, while pnpm is never sent looking for a package it cannot fetch. - Peers outside
@deepseek-ai/dsh*(cordis, the timer plugin, pi-ai) are not admission-checked and are optional too. @deepseek-ai/schemasterystays a plaindependenciesentry: it is a stateless schema utility, which the official guide puts underdependencies, 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.
Environment
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.
License
MIT
Comments
Loading…
From the same category
by zhu1090093659
DeepSeek Harness (DSH) Web Plugin Aggregation Ecosystem · Everything is a plugin, distributed via the Creative Workshop
★ 8.3k
↓ 172/wk
Apache-2.0
TypeScript
Oct 1, 2026
dsh plugin --profile web add dsh-webA collection of independent Web UI plugins and skins, including task boards, Git graphs, mobile access, and live token stats.
★ 7.4k
↓ 172/wk
Apache-2.0
TypeScript
dsh plugin --profile web add dsh-webby dsh-market
The plugin market inside DeepSeek Harness — browse, search, one-click install · DSH 可视化插件市场
★ 5.2k
↓ 150.1k/wk
MIT
TypeScript
Oct 1, 2026
dsh plugin --profile web add dshmarketby crafter-station
A public gallery of animated pets for Codex, Claude Code, DeepSeek Harness, Hermes, OpenCode, Gemini CLI, and more.
★ 4.2k
MIT
TypeScript
Sep 28, 2026
by superdesigndev
OpenRouter for agent tools. Join community here: https://discord.gg/6mQYYfFMAn
★ 4k
NOASSERTION
Python
Oct 1, 2026
dsh plugin --profile web add treg-dshby xiaobright
Two-phase DeepSeek Harness preset: Minimal-aligned bootstrap, then full Standard tools (Project2 98/99)
★ 3.8k
JavaScript
Sep 10, 2026