dsh-plugin-local-llm
Run the agent on models served by your own machine. The plugin registers local inference engines as ordinary Harness provider routes, so a local model is picked in the composer's model selector exactly like a hosted one, and each engine is declared as a configurable third-party provider.
Engines it knows
| Provider id | Default endpoint | Catalog dialect |
|---|---|---|
lmstudio |
http://127.0.0.1:1234/v1 |
LM Studio (/api/v0/models enrichment: load state, quantization, vision) |
ollama |
http://127.0.0.1:11434/v1 |
Ollama (/api/tags plus per-model sizes and quantization) |
strata |
http://127.0.0.1:8080/v1 |
Strata (packs named by quantization) |
lemonade |
http://127.0.0.1:13305/v1 |
Lemonade Server (labels: chat, vision, tool-calling, reasoning) |
local-openai |
http://127.0.0.1:8081/v1 |
llama.cpp server, vLLM, LocalAI, TGI, anything OpenAI-compatible — off by default so two engines never fight over one port |
All five speak OpenAI-compatible /v1/chat/completions; what differs is how a
model list is described. Strata's listing is not a documented contract, so
providers.strata.models declares the packs you serve (names and context
windows), and that declaration fills whatever the server leaves out.
Install
Build a package from this checkout and install that, rather than the directory:
npm pack --pack-destination dist # -> dist/dsh-plugin-local-llm-<version>.tgz
Then in the Harness: Settings → Plugins → Install bundle, and point it at the
.tgz. A bundle is a patch layer over the Loader entry list, so if you prefer
declaring it, add one row to ~/.dsh/profiles/<profile>/cordis.patch.yml:
- insert:
- id: local-llm
name: 'dsh-plugin-local-llm'
The row id local-llm is what its config, settings namespace and /local-llm/api
routes are keyed by; name is the package specifier, whose version comes from
what you installed. Installing a package is the supported layout — only then
can the plugin resolve its own dependencies, which is what gives it a settings
form (see below).
Use it
local_models scan # what is up, and what it can serve
local_models use provider=strata model=iq3_s
local_models ping provider=lemonade model=Qwen3-0.6B-GGUF
local_models settings # can a settings surface edit these engines?
use writes the profile's default model, so new sessions start on that route.
Any registered route is accepted — including one owned by another adapter.
Settings, and why the install layout matters
providers.<id> in this row's config is the single source of truth for an
engine: endpoint, the baseURLEnv names allowed to override it, apiKey,
enabled, and a hand-declared models catalog. The plugin declares each engine
in the configurable-provider directory with settingsNs: local-llm and
settingsPath: ["providers", <id>], which is what the third-party provider list
reads.
An auto-generated editing form additionally needs the plugin's Config
schema, and that needs @deepseek-ai/schemastery importable from the installed
package. Two install layouts behave differently:
- Installed as a package (tarball or registry) — the copy lives at
~/.dsh/profiles/<profile>/node_modules/dsh-plugin-local-llm, where its declared dependencies resolve. This is the layout you want:npm pack --pack-destination ..\pkg-out, then install the.tgz. - Linked from a source checkout (
install_bundlepointed at the directory) — Node resolves bare imports from the checkout, where no@deepseek-ai/*packages exist. The plugin still activates becauseindex.jstreats the Harness modules as optional and falls back tovendor/llm-contract.js, butConfigstays undefined, so no generated fields appear and endpoint edits go through the profile patch (~/.dsh/profiles/<profile>/cordis.patch.yml).
Both were observed in one profile: linked, the engines were claimed, scanned,
pinged and listed in the provider directory while Config reported absent;
packed, the same sources resolved the dependency.
Provider ids are contested territory
Provider ids are global, and both the adapter registry and the configurable-provider directory are all-or-nothing registrations. So this plugin claims an id only when nothing else owns it and reports the rest:
served by local-llm: lmstudio, ollama, lemonade | left to another owner: strata@http://127.0.0.1:8080/v1
If you declared strata yourself (for instance as a pi-ai provider), that owner
keeps serving it — which is what keeps an existing brain working — and this
plugin still scans, pings and selects that endpoint. To hand the route to this
engine, delete the other provider declaration and reload the profile.
Development notes
node --check index.js; ESM, no build step.node test/dry-run.mjsruns 27 offline checks. It stages a throwaway tree first: the runtime sources are copied totest/shim/pkgand the stand-ins intest/shims/are placed attest/shim/node_modules/@deepseek-ai/{dsh-llm,schemastery}, so the copy's bare imports resolve there and nothing in the package or the workspace gains anode_modules(test/shim/is gitignored). That staging matters: a stand-in inside the checkout's own module chain would satisfy a linked install and silently replace the real Harness module.test/shims/schemastery.jsmirrors the real v3 surface — lowercase factories, nesting instead of chaining — so calling a builder method that does not exist fails offline rather than at activation.- Five fake servers, one per catalog dialect, back the suite and can be run by
hand:
PORT=1234 ENGINE=lmstudio node test/fake-openai-server.mjs, withENGINE=lmstudio|ollama|strata|lemonade|openai. They also answer/v1/chat/completions: a prompt containingTOOLTESTstreams a split tool call,HALFTOKENends with finish_reasonlength. - Host code hot-reloads on a plugin off/on toggle.
package.jsonandcordis.patch.ymlchanges need a reinstall. - There is no Client half: no composer-dock line and no chip. Local models appear only in the model selector and in Settings.
Configuration reference
- insert:
- id: local-llm
name: 'dsh-plugin-local-llm'
config:
probeTimeoutMs: 2500 # per listing request
scanTtlMs: 15000 # reuse a scan for this long
maxTokens: 0 # 0 = leave the cap to the server
apiKey: 'dsh-local' # default bearer
providers:
lemonade:
baseURL: 'http://192.168.1.20:13305/v1'
apiKey: 'set when that server exports LEMONADE_API_KEY'
local-openai:
enabled: true
baseURL: 'http://127.0.0.1:8080/v1'
MIT licensed; see LICENSE.
No comments yet. Be the first to write one.