nnbw-liu
deepseek-ai-dsh-llm-local
基于本地的ollma的模型推理
- Stars
- 1
- Language
- TypeScript
- Created
- Aug 15, 2026
- Updated
- Aug 15, 2026
Introduction
@deepseek-ai/dsh-llm-local
English | 中文
OpenAI-compatible local model adapter for the harness LLM seam: direct fetch + SSE (framed by eventsource-parser) against any server that speaks the OpenAI chat-completions dialect — Ollama (/v1), the llama.cpp server (/v1), Microsoft Foundry Local, LM Studio, vLLM, and OpenAI-compatible gateways, local or remote. No DeepSeek-specific wire fields are sent, so the same adapter serves every endpoint; reasoning-capable servers that emit reasoning_content still stream it as harness reasoning blocks.
The package owns the provider routes ollama, llamacpp, and foundry-local as shipped presets, plus every route you declare by hand. The plugin is mounted dormant in the product: it shows up on the Models page, but zero routes serve requests until a llm-local: settings section (written by the web Models page or by hand) supplies provider profiles.
About this repository — this is a standalone source snapshot of the
@deepseek-ai/dsh-llm-localplugin, extracted from the DeepSeek Harness monorepo (packages/llm/llm-local). It is a Cordis plugin for the harness and is built and tested inside that monorepo; thepackage.jsondependencies andtsconfig.jsonreferences therefore resolve against the harness workspace. The canonical upstream is https://github.com/deepseek-ai/deepseek-harness.
Installation
dsh-llm-local is a plugin for DeepSeek Harness. It is wired into the harness's base bundle and mounts dormant: the ollama, llamacpp, and foundry-local routes appear on the Models page but serve no requests until a llm-local: settings section supplies profiles.
As an end user — install the harness, then configure the plugin (no separate install of this repository is needed):
npx @deepseek-ai/dsh web
Open the web UI at http://127.0.0.1:3080, go to Settings → Models, and edit one of the local provider rows, or write a llm-local: section in $DSH_HOME/settings.yaml (see Configuration).
The
npxcommand fetches the published harness build, so that build must include this package (and its base-bundle entry) for the plugin to be present. Until that release lands, build from source instead.
As a developer — build inside the harness monorepo. This package uses workspace:^ dependencies and monorepo tsconfig.json references, so it is not built standalone:
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
# place this repository's src/ and tests/ under packages/llm/llm-local/
pnpm install
pnpm run build
pnpm dsh web
Then configure the plugin exactly as an end user would.
Quick start
- Restart the dsh server (the web GUI process). The plugin is loaded at boot; a running server from before the plugin existed will silently ignore
llm-local:settings — there is no error, the section just does nothing until the restart. - Configure the provider either in the web UI (Settings → Models → the provider row → Edit → Base URL + Fetch available models + Apply) or in
$DSH_HOME/settings.yaml(see below). - In a new conversation, switch the model: open the model picker (
/model) and choose the local provider + model id. The default provider of new sessions is stilldeepseek-officialuntil you pick another one.
Where settings.yaml lives
The settings document is settings.yaml inside the harness home:
- Default home:
~/.dsh— on Windows that isC:\Users\<you>\.dsh, so the file isC:\Users\<you>\.dsh\settings.yaml. - If
DSH_HOMEis set, the file is$DSH_HOME/settings.yaml. - The path shown by the web UI's settings page, or set in a composition
cordis.yml(settings-file.config.path), wins over both.
The file is hot-reloaded: an external edit is picked up within ~100 ms and the very next model request uses the new values — no restart needed for edits after the plugin is loaded once.
Configuration
The plugin registers the llm-local settings namespace. The providers dict keys ARE the provider routes: ollama, llamacpp, and foundry-local are shipped presets (endpoint and display name inherited), any other key is a hand-declared endpoint and must name a baseURL.
Minimal working example (Ollama)
llm-local:
providers:
ollama:
models:
- id: qwen3:8b
contextWindow: 32768
That is everything: the preset endpoint http://127.0.0.1:11434/v1 is inherited, no API key is needed, and the route registers the moment the file is saved. Request with provider: ollama, model: qwen3:8b.
Full example with every field
llm-local:
providers:
# Shipped preset: endpoint defaults to http://127.0.0.1:11434/v1.
ollama:
models: # optional; discovery can fill these
- id: qwen3:8b
contextWindow: 32768
# defaultContextWindow: 128000 # fallback capacity for unlisted models
# maxTokens: 32768 # default per-request output cap
# usageInStream: true # stream_options.include_usage
# vision: false # image input for vision models
# streamIdleTimeoutMs: 300000 # per-read idle budget
# Shipped preset for the llama.cpp server.
llamacpp:
baseURL: http://127.0.0.1:8080/v1
models:
- id: qwen2.5-coder-14b-instruct-q4_K_M
contextWindow: 32768
# Shipped preset for Microsoft Foundry Local.
foundry-local:
baseURL: http://127.0.0.1:53415/v1 # confirm the port in `foundry local` output
models: []
# Hand-declared route: any OpenAI-compatible endpoint.
my-gateway:
displayName: My Gateway
baseURL: http://127.0.0.1:1234/v1
apiKeyEnv: MY_GATEWAY_KEY # optional; local servers usually need none
vision: true # optional; image input for vision models
usageInStream: true # optional; stream_options.include_usage
maxTokens: 32768 # optional default per-request output cap
defaultContextWindow: 128000 # optional fallback context capacity
streamIdleTimeoutMs: 300000 # optional per-read idle budget
retryPolicy: # optional; normal defaults when omitted
mode: normal
maxRetries: 2
models:
- id: gpt-oss-120b
contextWindow: 131072
maxTokens: 32768
Per-provider fields:
baseURL— the OpenAI-compatible root (/chat/completionsand/modelsare appended). Presets default tohttp://127.0.0.1:11434/v1(Ollama),http://127.0.0.1:8080/v1(llama.cpp),http://127.0.0.1:53415/v1(Foundry Local — confirm the exact port infoundry localoutput). Required for hand-declared routes.apiKeyEnv— credential reference resolved per request throughctx.credentials, then the environment. Omitted means unauthenticated — the local default; Ollama, llama.cpp, and Foundry Local need no key. Once named, a miss fails loud withMISSING_CREDENTIAL.models— advisory catalog exposed to selectors and discovery; requests accept unlisted ids too. Each entry:id, optionalname/description/contextWindow/maxTokens/vision.models: []advertises none.vision(profile or per-model) — declares image input. Image blocks serialize asimage_urldata-URL parts through the durable attachment service; a text-only model rejects them withUNSUPPORTED_CONTENT.usageInStream— defaulttrue(stream_options.include_usage). Disable for an older llama.cpp build that rejects the field.maxTokens,defaultContextWindow,streamIdleTimeoutMs,retryPolicy— same semantics as the DeepSeek adapter.
Configuring from the Models page
The web Models settings page knows this family: each provider row (the three presets are always listed, dormant or not) opens an editor with API key (optional — leave blank for unauthenticated local access), Base URL (pre-filled from the preset), and a model catalog with a Fetch available models button that interrogates GET {baseURL}/models — for a preset route it works even before you type a URL. "Add provider" offers the three dormant presets. Hand-declared routes still need settings.yaml (there is no per-route protocol selector for local routes because they all speak the same dialect).
Dynamic configuration (settings + credentials)
Connection facts are not frozen at load. resolveAdapterOptions is the one explicit resolve step, and the adapter re-reads facts through a thunk once per operation: base URL, catalog, usage reporting, vision, output cap, and idle budget all take effect on the next request, while an in-flight stream keeps the facts it started with.
ctx.settings— the plugin registers thellm-localnamespace with this sameConfigschema and its composition entry as thebase, so allm-local:section in the user settings document overrides any field without a restart.ctx.credentials— the key resolves per stream call from the same snapshot that supplies the endpoint. Configuration carries onlyapiKeyEnv, never a literal key; a route without one is asked unauthenticated.
The one registration-captured fact is the retry policy: when a route's resolved value changes, the plugin re-registers the route set in place (same adapter instance, one synchronous section). Route-set changes (adding/removing a route) re-register the same way. Every route change logs a line (llm-local: serving ollama (http://127.0.0.1:11434/v1), ...), and a dormant mount logs llm-local: dormant; ... — the first thing to check when a section appears to do nothing.
Troubleshooting: my settings.yaml does nothing
- Did the server restart since the plugin was added? A
llm-local:section in the document is silently preserved but inert until the plugin registers its namespace at boot. Restart the dsh server, then look for thellm-local:log line. - Is the file in the right place? The section must be at the top level of
$DSH_HOME/settings.yaml(default~/.dsh/settings.yaml), not inside another key, and not in acordis.yml. - Is the YAML valid and indented like the examples?
providersis a dict, so the provider key is indented under it and the profile fields under the provider key. - Did the route register? The Models page row for the provider flips from a dormant "add" state to a configured row; the server log prints
llm-local: serving <route> (<baseURL>). - Is the session using the provider? Configuring a provider only makes it available; a conversation still uses whatever provider/model it was created with. Open the model picker (
/model) in a new conversation and choose the local route. - Check the server log for errors. A schema-invalid section logs
settings: keeping last good "llm-local" after invalid stored sectionwith the reason; a route that fails to resolve keeps the previous routes and logsllm-local: keeping the previously registered routes after a refused update.
App attribution
Every request carries the shared attribution header from dsh-llm's attributionHeaders() (the mandatory User-Agent baseline). Request identity rides the wire as x-deepseek-harness-user-id (the stable anonymous id) and, when the request has a session, x-deepseek-harness-session-id. No provider-specific headers are added — local endpoints need none.
Wire-format notes
- Streaming only;
stream_options.include_usagesent unlessusageInStream: false.usagemay arrive attached to the finish chunk or as a trailing usage-only chunk — the translator defers both to[DONE], sousagealways precedesfinish. - Assistant history never replays
reasoning_content(OpenAI's dialect has no CoT passback; local servers ignore or reject it). Incomingreasoning_contentdeltas still become harness reasoning blocks. - Cache accounting:
cacheReadTokens←prompt_tokens_details.cached_tokens(llama.cpp) orprompt_cache_hit_tokens(DeepSeek-style spellings); the count is subtracted frominputTokensto keep disjoint totals. - The first thinking-mode chunk often carries
reasoning_content: ""— handled (no spurious reasoning block).
Errors
Non-2xx responses throw LlmError with stable codes: AUTH (401/403), QUOTA (provider details identify exhausted quota), RATE_LIMIT (429), CONTEXT_WINDOW_EXCEEDED (a 400 whose details identify context overflow), INVALID_REQUEST (other 400s), SERVER (5xx), HTTP_<status> otherwise. The serializable failure retains the HTTP status plus a valid positive Retry-After delay and x-request-id when present. Pre-response transport failures (DNS, refused connection, TLS, proxy) throw TRANSPORT naming the configured endpoint; caller aborts throw ABORTED; protocol violations throw STREAM_CLOSED (no [DONE]) or MALFORMED_RESPONSE (bad JSON payload). Unknown wire finish_reasons become finish {kind: 'error'} chunks, and a completed stream whose stop finish opened no content blocks becomes EMPTY_RESPONSE (retried by default policy).
Known Limitations and Deferred Work
- A settings
modelslist replaces the composition list wholesale — settings-layer merging is per-field, and arrays are one field. tool_choiceis not mapped — not part of the core vocabulary (shared with the DeepSeek and pi-ai adapters).- Requests use raw
fetch, not@cordisjs/plugin-http— no shared proxy/interception configuration. - No reasoning-effort selectors — local OpenAI-compatible servers have no standard
reasoning_effortdialect; reasoning models stream CoT but the harness cannot pick a thinking level for them. - Serialization flattens user and tool-result content to text unless the model declares vision; empty tool output crosses the wire as the literal
(no output).