DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Ai Model Hub — DeepSeek Harness 插件(DSH Plugin)
← Plugins
A

dsh-ai-model-hub

Ai Model Hub

用于本地 AI 模型的 DeepSeek Harness 插件和库:提供能力目录、路由器、运行时管理器和真实引擎适配器。代理指定一项能力;中心选择、启动并调用合适的模型。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:Mhmd7-7/dsh-ai-model-hub#77f9bf21792e0b50f01a3808d1531b3c5e92ab41
README兼容性版本

兼容性与来源证明

Ai Model Hub 以 dsh-ai-model-hub 发布,当前版本为 0.7.0。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
github
Registry 更新时间
2026/10/13

版本

0.7.0stable
2026/10/13
0.6.0stable
2026/10/13
0.4.0stable
2026/10/12

相关插件

正在加载相关插件…

最新版
0.7.0
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
web
许可证
MIT
发布源
github
GitHub
★ 0
周下载
0
最近提交
2026/10/13
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录

相关插件

继续浏览 models-usage 分类下经过校验的插件。

Whale Widgetdsh-whale-widgetDSH Web 界面右下角的 DeepSeek 余额小鲸鱼挂件:余额/今日已用/峰谷定价、自定义泡泡点击序列(文本/余额/今日/峰谷/图片/随机语句与并列加权选择)、逐行样式与字体、悬浮快捷编辑、音效与每轮消耗、自定义角色/动图/音效、吸附与翻转自定义Usage@linxin666/dsh-usage用于 dsh Web GUI 的使用统计插件:检测各提供商的余额和编程套餐配额,并实时记录令牌使用情况;侧边栏 Settings 行上方显示紧凑的价格概览卡片Client Ui Model Capabilities@linxin666/dsh-client-ui-model-capabilities为自定义 pi-ai 提供商声明每个模型的能力(图像输入和推理力度),可直接在 Models 设置卡片中编辑;写入官方 llm-pi-ai 设置命名空间,不更改 DSH 源代码。Cost Meterdsh-cost-meterDeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 170+ 模型 ID 价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(13 家,含 OpenDesign 工作区查询、含小米 MiMo Token Plan 控制台 Cookie 查询、SCNet Token Plan 本地 Credits 计量与火山方舟 Volcano Ark Coding Plan AK/SK 签名查询)、自定义 Provider

README

dsh-ai-model-hub

A modular local AI model ecosystem for DeepSeek Harness (DSH).

DSH is the agent — it reasons, plans, calls tools, and orchestrates workflows. It is not the model runtime. The specialised models (text, image, 3D, audio, video) are external and local, reached through a capability interface. DSH never learns how to launch Stable Diffusion, which Python file starts a mesh generator, or how a given engine manages its process. It only asks:

What capabilities are available?

Everything below that question is this project's job.

┌──────────────────────────────────────────────────────────────────────┐
│  DeepSeek Harness                        agent reasoning, planning,  │
│  ─────────────────                       tool invocation, workflows  │
│                                                                      │
│      tools: invoke_model · list_models · list_capabilities ·         │
│             get_model_status · start_model · stop_model · …          │
└───────────────────────────────┬──────────────────────────────────────┘
                                │  dsh-ai-model-hub DSH plugin
                                │  (the ONLY DSH-aware code)
┌───────────────────────────────▼──────────────────────────────────────┐
│  ModelHub facade                        invokeModel({ capability })  │
│  ┌────────────────┬──────────────────┬───────────────────────────┐   │
│  │ Capability     │ Model Router     │ Runtime Manager           │   │
│  │ Catalog        │ deterministic    │ lifecycle · health ·      │   │
│  │ registry +     │ capability-first │ idle timeout · resources  │   │
│  │ validation     │ selection        │                           │   │
│  └────────────────┴──────────────────┴───────────────────────────┘   │
│  ┌──────────────────────────────┬────────────────────────────────┐   │
│  │ Model Adapters               │ Artifact Store                 │   │
│  │ openai_compatible · http_json│ durable, typed, addressable    │   │
│  │ comfyui · three_d ·          │ text/image/audio/video/3D/…    │   │
│  │ mock (tests only)            │                                │   │
│  └──────────────────────────────┴────────────────────────────────┘   │
└───────────────────────────────┬──────────────────────────────────────┘
                                │  spawn (argv, never a shell string)
┌───────────────────────────────▼──────────────────────────────────────┐
│  Local engines: Ollama · llama.cpp · A1111/Forge · ComfyUI ·         │
│  TRELLIS · Hunyuan3D · Stable Fast 3D · TripoSR · …                  │
└──────────────────────────────────────────────────────────────────────┘

Status

Live as a DSH profile plugin. The package installs as one unit — dsh plugin add mounts it, so the hub is composed on every boot — and it serves real local engines only: Ollama for text, ComfyUI / A1111 / Forge for images, llama.cpp for a second text engine, and any local image-to-3D Gradio app for meshes. There are no mock models anywhere in the shipped catalogs and no mock fallback at runtime: a wrong model name or a stopped engine is a loud error (or a cold start), never fixture output.

The whole architecture — catalog → routing → runtime → adapter → artifact → chained workflow — is covered by the test suite, which uses an in-process mock adapter as a test double. That adapter is not a model: nothing ships it in a catalog, and no deployment can reach it by accident.

PhaseScopeStatus
1Full architecture, DSH plugin, tests✅ done
2Real local text model (Ollama / llama.cpp / vLLM / LM Studio)✅ done — openai_compatible adapter ships
3Real local image model (A1111 / ComfyUI)✅ done — http_json and comfyui adapters ship
4Real local 3D model✅ done — three_d adapter ships; see docs/three-d.md
5Runtime lifecycle + resource-aware routing✅ done — the machine is probed and routed against
6Multi-model workflows✅ done

Phase 2 needs no new code: the openai_compatible adapter is implemented, and config/examples/real-models.example.json contains ready-to-copy Ollama and llama.cpp entries. Phase 3 is implemented by two adapters, because the two engine families are genuinely different:

  • http_json — engines that answer one request with a finished image: AUTOMATIC1111, Forge, and stable-diffusion.cpp's sd-server. It POSTs the familiar /sdapi/v1/txt2img body and decodes the base64 images that come back.
  • comfyui — ComfyUI takes an API-format node graph, not a prompt endpoint. Each explicitly configured workflow declares capabilities, exact input bindings and a typed output selector. The adapter treats graph internals as opaque, edits only declared inputs, queues on /prompt, polls /history, and retrieves the declared output. No checkpoint or saved-workflow enumeration creates providers.

Phase 4 is three_d: one adapter that speaks the shape the local 3D ecosystem actually has — a Gradio queue API or a JSON HTTP route, one or two calls, a mesh file at the end — so TRELLIS, Hunyuan3D, Stable Fast 3D, and TripoSR are catalog entries rather than code. It writes a typed model_3d artifact (GLB, GLTF, OBJ, STL, or PLY, sniffed from the bytes), and a discoverer verifies a declared engine against the API surface it actually exposes before publishing anything.

Phase 5's remaining gap is closed: ModelHub now probes RAM, VRAM, free VRAM, and disk, re-measures on demand, and routes against measured headroom — so a model that would not fit beside what is already loaded is refused with the shortfall in the message instead of crashing inside the engine.

All adapters are registered by default. See docs/adding-a-model.md, docs/three-d.md, and the protocol templates in config/workflows/.


Install

One command, on a machine that already has Node ≥ 22.6 and DeepSeek Harness:

# into the profile behind `dsh web` — the normal case
dsh plugin --profile web add github:Mhmd7-7/dsh-ai-model-hub

This package is the plugin: it declares dsh.bundle.patch, so dsh plugin add installs it and appends dsh-ai-model-hub to dsh.profile.bundles. The profile composes that bundle on every boot, which is what makes the plugin live, not merely installed. No profile file is edited by hand and no cordis.patch.yml has to be written.

Then restart DeepSeek Harness and ask:

List the available AI model capabilities.
What you wantCommand
Another profiledsh plugin --profile hubtest add github:Mhmd7-7/dsh-ai-model-hub
A pinned releasedsh plugin --profile web add github:Mhmd7-7/dsh-ai-model-hub#v0.7.0
A published copydsh plugin --profile web add dsh-ai-model-hub
A checkout you are editingnode --no-deprecation scripts/install-plugin.mjs --profile web

The last one links the working tree instead of copying it, so the plugin loads your sources; run npm run build there first if you edited anything under src/ or dsh-plugin/.

install.ps1 and install.sh remain one-line wrappers for the first row, and keep their flags (-Profile, -Ref, -Repository, -SkipDoctor):

irm https://raw.githubusercontent.com/Mhmd7-7/dsh-ai-model-hub/main/install.ps1 | iex
curl -fsSL https://raw.githubusercontent.com/Mhmd7-7/dsh-ai-model-hub/main/install.sh | sh

To remove an installation:

dsh plugin --profile web remove dsh-ai-model-hub

Which DeepSeek Harness versions this runs on

The plugin tracks the DSH 0.x line rather than a single release:

Supported@deepseek-ai/dsh* >=0.1.5-rc.2 <0.3.0
Developed and tested against0.1.5-rc.2 (this package's own tree) and 0.2.0-rc.2 (a current host)

Two independent things decide whether that holds, and they fail in different places — which is worth knowing, because only one of them is visible in the plugin's own logs.

The manifest gate runs first, before any plugin code. DSH reads peerDependencies and refuses the row when a @deepseek-ai/dsh* range does not match the harness. For a 0.x version, caret pins the minor: ^0.1.5-rc.2 means >=0.1.5-rc.2 <0.2.0, so it silently refuses every 0.2.x host. That is why the peers here are written as explicit ranges and why tests/peer-range.test.ts exists — it asserts against the harness's own evaluatePluginCompatibility when that package is installed, and against a written-out range matcher when it is not. The old caret form is demonstrated failing in the same file, so it cannot come back unnoticed.

Then the runtime surface is detected, not assumed. dsh-plugin/compat.ts is the only module that touches a host member. It reads the context once, reports what it found, and every registration goes through a helper that contains a missing or differently-shaped member to one diagnostic line:

If the host has no…What is lostWhat is not lost
toolseverythingnothing — the load fails, by name, on purpose
systemPromptthe capability snapshot in the promptevery tool; the agent can still call list_capabilities
webServerthe Local models and Priorities pagesevery tool
effectstopping the hub's processes on plugin unloadevery tool; the effect body is not run at all, because nothing could call its disposer
skillsthe bundled skillevery tool
sandboxPolicyartifacts in the calling session's workspaceevery tool; artifacts fall back to the hub's own store
getnothingthe injected-property path still resolves every optional service
loggerhost-formatted diagnosticsthe diagnostics — they go to the console instead

tools is the one hard requirement. Without a tool registry there is nothing for this plugin to do, and a hub the agent cannot reach is worse than an honest failure: it produces a settings page and the false impression that the capability is available.

Two things are deliberately not wrapped. defineTool, because it is a pure function from a closed input to a closed output whose signature has not moved across the whole 0.1.5-rc → 0.2.0-rc line. And the import type lines, which vanish at compile time — a host that renames a type cannot break a running plugin. The complete runtime DSH coupling is six value imports: defineTool, schemastery's default export, and cordis's Service.

Why the package ships compiled JavaScript

The plugin's sources are TypeScript, and Node runs them directly through type stripping while you work in a checkout. It refuses to do that inside node_modules:

ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING: Stripping types is currently
unsupported for files under node_modules

A package installed by name — from GitHub, from a registry — lands inside the profile's node_modules, so a .ts entry there could never be imported. That is the entire reason this repository commits lib/: lib/ is the same sources compiled to JavaScript, and the package's entry points (main, exports) point at it, so dsh plugin add needs no build step on your machine.

lib/ is generated, never edited: npm run build regenerates it from src/ and dsh-plugin/. The one setting that makes it work is rewriteRelativeImportExtensions, so the emitted JavaScript imports ./x.js where the source said ./x.ts.


Quick start

There are two halves, and you can use either without the other.

A. Use the hub as a library (no DSH needed)

cd dsh-ai-model-hub

npm install                  # typescript + the DSH packages the plugin links against
npm test                     # the suite, ~20 s, no engines or GPU required
npm run typecheck            # strict TypeScript, no errors
npm run build                # compile src/ + dsh-plugin/ into lib/
npm run demo                 # the vertical slice, end to end
npm run demo:workflow        # a multi-step cross-model pipeline
npm run demo:3d              # image → 3D → a real .glb, against a stand-in engine

The demo prints the full path — capability discovery, routing with reasons, invocation, and the artifact on disk:

=== catalog: capability discovery ===
  text_to_image        text → image  via comfyui_z_image_turbo
  text_to_text         text → text  via ollama_text_model
  not available here: image_to_3d, image_to_image, audio_generation, …

=== routing ===
  chose comfyui_z_image_turbo
  rationale: comfyui_z_image_turbo (score 1): …
    [rejected] ollama_text_model: does not declare capability "text_to_image"

=== artifact ===
  valid PNG: yes
  size:      1.2 MiB

It runs against config/models.json, so it needs the engines that catalog names: Ollama for text, ComfyUI for images. node examples/vertical-slice.ts <catalog> takes another catalog, and the last section deliberately asks for a capability nobody serves, to show what a refusal looks like.

npm run demo:3d needs nothing installed. It starts a stand-in 3D engine inside the example and runs the whole real path against it — discovery, health, resource-aware routing, a two-step Gradio protocol, and a .glb written to disk — then prints every HTTP request the engine saw, to make the boundary concrete. To point it at TRELLIS instead, swap the stand-in for your catalog entry; the three capability calls are identical. See docs/three-d.md.

B. Give the capability to the DeepSeek Harness agent

# Installs into the 'web' profile (the one behind `dsh web`), then verifies.
npm run install:plugin

# ...or target another profile:
node --no-deprecation scripts/install-plugin.mjs --profile <name>

Then restart DeepSeek Harness and ask the agent:

List the available AI model capabilities.

Checking an installation

npm run doctor                          # inspects: is it loadable, and if not, why not
node scripts/smoke.mjs --profile web    # acts: applies the plugin and lists what it registered

doctor inspects and reports; smoke actually runs the plugin against a real cordis context and prints the tools it registered. Use smoke when you want proof rather than a checklist:

Registered tools:
  check_model_health   explain_routing    get_model_status   invoke_model
  list_artifacts       list_capabilities  list_models        start_model
  stop_model           refresh_model_discovery
Catalog: model hub ready: 2 model(s), 2 capability(ies) from …/config/models.json

Note on flags. npm run forwards flags to the script, but npm 12 parses --profile as its own config flag and fails. Use the direct node scripts/smoke.mjs --profile <name> form for a non-default profile.

If DSH fails to boot after installing

If the harness dies with:

invalid plugin, expect function or object with an "apply" method, received object

then the row cordis.patch.yml inserts is pointing at something that is not a plugin. In this package that can only happen if the row's name was changed away from the bare package name: dsh-ai-model-hub resolves to the package's . export, which is the plugin (lib/dsh-plugin/index.js, exporting name/inject/apply/Config). The hub library lives behind the dsh-ai-model-hub/library subpath instead, precisely so the two cannot be confused.

The row has two names, and they are easy to conflate: id is the row's name in the composed tree (what a profile's own cordis.patch.yml uses to override or disable it), while name is the module specifier the loader imports. Keep name bare — a subpath such as dsh-ai-model-hub/plugin would load the host half but silently drop the Web UI half, because DSH's client module system only resolves a row's package manifest from a bare specifier. cordis.patch.yml documents this in place, and npm run doctor checks it.

Where does the catalog come from?

The plugin resolves its catalog from ordered anchors, first hit wins:

  1. configPath — an explicit file, if configured. When set it is the only candidate: a wrong path is an error, never a silent fallback.
  2. Each directory in searchRoots, for a deployment that names its own.
  3. The working directory of the DSH host process, walking upward.
  4. This plugin's own installation directory, walking upward.

At every level discovery accepts models.json, model-catalog.json, or dsh-ai-model-hub.json, directly or in a config/ subdirectory.

Anchor 4 is what makes the zero-config case work. DSH installs profile plugins as junction links and Node resolves them to their real path, so the plugin finds the config/models.json shipped by the checkout it was installed from — no matter where the host was launched. Anchor 3 covers the opposite case: a host started inside a project that carries its own catalog.

Why the host's working directory and not the session's? A plugin's apply runs when the profile boots — before any session exists — and one host serves many sessions with different workspaces, so there is no single "session working directory" to read at that moment. Anchors 1 and 2 are the explicit way to name a workspace. Per-session resolution is a deliberate future step, not an oversight; see docs/roadmap.md.

If no anchor yields a catalog the plugin logs model hub disabled together with every directory it tried, registers no tools, and lets DSH boot normally — a missing or broken catalog never makes the agent unusable.

Installing from a checkout instead of from GitHub

dsh plugin add <path> links the working tree — DSH installs profile plugins as junction links — so Node resolves the plugin to its real path in the repository instead of to a copy inside the profile. Peer dependencies then resolve by walking up from the package: from lib/dsh-plugin/ to <repo>/node_modules. Run npm install once in the repository and that directory exists, because dsh plugin add forwards to pnpm, which only warns about peer dependencies instead of installing them.

This is not theoretical: it was found by installing into a throwaway profile, deleting the workspace's node_modules, and watching the plugin fail to resolve @deepseek-ai/schemastery.

A linked checkout also loads lib/, exactly like an installed copy does — so run npm run build after editing src/ or dsh-plugin/. If you would rather not think about that, work on the hub as a library (npm test, npm run typecheck) and only build when you want the plugin to pick the change up.

The historical obstacle on the GitHub route — Node refusing to strip types for files under node_modules — is what committing lib/ removes; see Why the package ships compiled JavaScript.

What the agent can then do

Ask the agentTool it calls
"What AI models can you run?"list_models
"What can this machine actually do?"list_capabilities
"Create a futuristic city image."invoke_model (text_to_image) → router picks the model
"Now make a 3D model from that image."invoke_model (image_to_3d), chaining the artifact id
"Why did it pick that model?"explain_routing — read-only
"Is the image model running?"get_model_status, check_model_health
"Start / stop the image model."start_model, stop_model

The agent never names an engine, a command, or a path. That is the whole design.

The hub ships its own skill

The plugin also teaches the agent how to use it. skills/dsh-ai-model-hub/SKILL.md is registered as a skill provider on ctx.skills, the way DSH's own bundled dsh-badge skill is, so:

  • nothing is copied into ~/.dsh/skills or a project skill root, and no profile file is edited — installing or updating the plugin installs the skill;
  • the body stays the file in this checkout, so a git pull changes what the agent reads on its next load, with no reinstall step and no stale copy;
  • registration lands in the global layer, which is the layer DSH merges into every preset's session catalog, so no preset has to know about it.

The registration is deliberately not a declared inject. It goes through ctx.inject(['skills'], …), so a composition with no skill registry — headless, or the sdk-minimal profile — still gets the model tools and simply never runs the skill callback. A missing or malformed skill file costs one warning and the skill, never the tools beside it.

The file is an ordinary skill, so it also works the old-fashioned way: link or copy skills/dsh-ai-model-hub/ into a .dsh/skills root if you would rather not run the plugin at all. Its frontmatter is the single source of truth for the name and description on both paths.

A settings page for what is on this machine

The agent is not the only one who needs to know where the engines are. The plugin also adds a Local models section to the DSH settings, which answers the question a user actually has: which engines are on this PC, where is each one installed, is it running, and what is inside it.

RowWhere it comes from
Verified installationEngine markers or executables in configured/well-known locations; a configured cwd alone is not proof
Configured pathsCatalog launch paths, displayed separately from verified installation paths
Listens on / availabilityIndependent bounded endpoint checks on both Scan and Refresh
Hub can start itThe descriptor's startable, after the deployment's allowProcessLaunch
Catalog models / capabilitiesThe same live hub the tools read, so the page cannot disagree with behaviour

Scan is the page's main action, and it produces one list of two kinds of thing, each row labelled with what it is:

KindWhat it isWhere it comes from
Ollama ModelAn observed or catalog-known modelLive /api/tags, offline loopback-host manifests (OLLAMA_MODELS or ~/.ollama/models), active catalog, or last-good observations
ComfyUI WorkflowOne complete workflow or an explained unreadable entrySaved /userdata workflows, verified installation's workflow store, configured directories, and active catalog inline/pinned workflows

A ComfyUI workflow is the smallest thing the list exposes. Its checkpoints, LoRAs, VAEs, text encoders, ControlNets and individual nodes are not listed — they are the workflow's own business, which is why changing what a workflow loads never changes anything an agent or a user has to know.

The two halves are discovered independently, so an Ollama instance that is down cannot hide the workflows and a ComfyUI that is down cannot hide the models; each source reports its own status. Scanning twice refreshes the list rather than growing it, because every entry's id comes from its source location.

A saved workflow is discovered without a running ComfyUI. Workflow discovery reads the configured directories, the engine's own saved-workflow store when it can be found on disk, the active catalog, and the workflows earlier scans remembered — so a first launch with the engine stopped still lists the graphs that are on the machine, and an engine that stops mid-session cannot take them off the screen. The list says which evidence it has: ComfyUI stopped when a refused connection proves the process is not running, ComfyUI unreachable when a timeout or a routed address leaves that genuinely unknown, and a cached row whenever the contract came from a previous observation rather than this pass. Only a scan that completely read a source can conclude a workflow was deleted; a failure never can.

The remembered workflows live in ~/.dsh/plugins/dsh-ai-model-hub/workflow-catalog.json, beside the capability policy and for the same reason: it is machine state that changes without anyone editing a file. It is per host and endpoint, so repointing the catalog at another ComfyUI does not resurrect the old engine's workflows. workflowCatalogPath configures it; DSH_AI_MODEL_HUB_WORKFLOW_CATALOG redirects it for a test.

Scan starts the configured engines; looking never does. Opening or reloading the page reads the machine as it finds it, and says what did not answer. Pressing Scan is a deliberate act, so it checks each engine, starts any that is stopped through the lifecycle manager's own launch path, polls its API until it answers within a bounded budget, reads it, and finally releases its hold:

StageWhat happens
Checking enginesEach engine's own readiness route is probed. An engine that answers is adopted and never restarted
Starting enginesA stopped engine is launched with the catalog's configured command; a missing command, a forbidden launch, or a refused spawn is reported with the setting that fixes it
Waiting for readinessBounded polling of the engine's API — a listening socket is not readiness. ComfyUI cold-starts get a longer default budget because loading checkpoints takes minutes
ScanningEvery readable engine is read: Ollama's models, ComfyUI's saved workflows
Complete / Partially completecomplete is false whenever an engine could not be started, timed out, died mid-read, or has no launch command, and the engine's own report says which and why

While the scan reads, the engines it started are held against automatic idle unloading; the hold is released in a finally, on success, failure and cancellation alike, so a scan can never leave an engine permanently unsweepable. An engine that was already running is neither started nor held — it is the user's process, not the scan's. Repeated Scan clicks cannot start a second copy: the launch goes through the same gate a cold invocation uses, which serializes transitions per model, and a later scan simply reuses what is now running.

Engine states distinguish running, installed_unreachable, not_found in the checked locations, and unknown. Connection failure alone proves neither that a process is stopped nor that an installation is absent. The installation is the directory whose contents are the engine — a ComfyUI whose main.py lives under src/ is installed at src/, which is also where its saved-workflow store hangs from. Nothing is ever installed automatically.

Resource contract status is separate from availability. Each resource reports live, local, catalog, or cached freshness and its observation timestamp. An answering ComfyUI server does not verify a workflow's weights or execution; local Ollama manifests do not verify every referenced blob. Failed refreshes keep the last useful snapshot visibly stale; Retry requires no page reload. Superseded requests cannot replace newer results or cache/provider publications.

Workflow contract statuses include:

StatusMeaning
readySaved in API format — the hub queues it as it stands
needs_conversionSaved in the editor's UI format. It is listed and converted for execution, but it is not the same as an API export
invalidReadable, but produces nothing a caller can keep. The reason is shown
unreadableThe file or the engine's copy of it could not be retrieved. The transport reason is shown, because the document was never seen
unavailableThe engine needed to interpret it could not be reached

Running a remembered workflow while ComfyUI is stopped goes through the same lifecycle manager as any other cold engine: the configured instance is started, its readiness is awaited within the declared budget, and the graph is queued once it answers. A startup that is refused or fails leaves the workflow listed with the reason, and never falls through to a subscription provider — a workflow the user picked is not silently swapped for a different one.

Runnable workflows are published to the hub as workflow-backed providers, so the agent can route to them with the existing invoke_model tool by pinning the workflow's id — the same harness, no second execution path. The public parameters a workflow exposes (prompt, negative prompt, image, seed, steps, CFG, width, height) are inferred from its graph and are the only fields a caller may set.

Engines the catalog does not declare are still listed — a1111, comfyui, and ollama are known by name — with the directories that were checked, so "Forge is installed but the hub has not been told" is visible instead of silent.

Two halves, both dependency-free:

  • dsh-plugin/inventory.ts builds the answer and serves it on GET /dsh-ai-model-hub/inventory. Every probe is bounded and failures are contained; the route is injected on demand, so a headless profile with no web server keeps every tool and simply serves no page. The scan itself lives in dsh-plugin/scan.ts, and the graph → public-contract inference it uses is in src/comfy/scan.ts.
  • dsh-plugin/client.js is the browser half. DSH loads plugin clients through window.__ModuleLoader__ as plain side-effect scripts — no top-level import/export, React handed in through the factory's require — so this is hand-written JavaScript with no bundler and no build step, matching the rest of the project. It registers one settings.section contribution.

Configure a workflow directory when your workflows live beside the catalog rather than inside ComfyUI:

config:
  comfyuiWorkflowDir: 'C:/workflows/comfy'   # every *.json under it, recursively
  scanTimeoutMs: 4000                      # per-probe budget; filesystem walks are also capped
  workflowCatalogPath: ''                  # where discovered workflows are remembered

A relative directory is resolved against the catalog that configured it, and %VAR% and ~/ are expanded from the host's own environment, so the same configuration works on Windows and on a POSIX host. The directory is a source of workflows only — nothing in it enumerates weights or custom nodes.

To see what a stopped engine still yields on this machine, with every HTTP probe refusing:

node scripts/verify-live-offline.ts                     # writes nothing
node scripts/verify-live-offline.ts ./scratch-catalog.json

Capability priorities: order and enablement you own

Open Settings → Local models, then use the Edit priorities button at the top left or the Priorities tab. The Inventory tab stays mounted alongside the editor, so switching views or rescanning does not discard an unsaved draft.

The editor uses responsive cards, human-readable capability names, provider search, and All / With options / Enabled filters. Search matches capabilities and their providers; it keeps each matching capability's full ordered list visible so filtering never changes the meaning of a rank. Technical status and the policy file are available in expandable details, and Save / Cancel stay reachable in a sticky action bar. Light and dark colors follow the host theme.

Each capability contains every option that genuinely declares it — local models, ComfyUI workflows, and subscription backends in the same list, because to the hub they are the same kind of thing: a way to serve a capability.

On hosts that only report effective enablement while globally disabled, individual editing is paused rather than guessing at saved permissions. Enable and save the global switch to edit those options. Simply opening this page, switching tabs, or changing filters never changes the policy or starts a model.

Three facts about an option are deliberately kept apart, and confusing them is the failure this section exists to prevent:

FactWhere it livesWhat it means
Enabled / DisabledThe policy you saveMay this option serve this capability
PositionThe policy you saveWhen it is tried, relative to the others
StateThe live runtimeIs it usable right now — ready, stopped, starting, unhealthy, unavailable, unsupported

So "disabled" is not "stopped", and "lower priority" is not "unavailable":

  • A disabled option stays visible, marked Disabled, keeps its position, and is never run — not as a first choice and not as a fallback — and its engine is never started for it. Only you can change that.
  • A stopped but enabled and startable local engine keeps its saved place and is started on demand. It is not demoted behind a remote backend that merely happens to be warm right now.
  • A lower-priority option runs only after every higher-ranked permitted option has definitively failed.
  • Global disable wins over everything. It is one switch, and it overrides every per-capability setting.

Reordering is real drag-and-drop — and every row also has Move up, Move down, Move to top and Move to bottom buttons with accessible names, so the list is not mouse-only. Nothing is written until Save; Cancel reverts to the saved policy. That matters for a list whose whole point is an arrangement: a drag that saved immediately would make a mistake irreversible.

The policy is persisted as its own versioned document, keyed by stable ids (never display names), at ~/.dsh/plugins/dsh-ai-model-hub/capability-policy.json — override it with policyPath. It survives reloads, restarts, and catalog refreshes, and an option that is temporarily undiscovered keeps its saved settings rather than being forgotten.

Nothing new activates itself. A newly discovered option — a pulled Ollama model, a workflow saved in ComfyUI, a subscription backend a plugin just registered — appears in the list immediately, disabled, and stays that way until you enable it. Catalog presence, a descriptor's own enabled flag, and visibility in a picker are not authorization. Options that were already configured when the plugin loaded keep working exactly as they did, so the feature is a no-op on an existing deployment until you touch it.

How a request is served

For every request, in order:

  1. Inputs are resolved and validated against the artifact store.
  2. Global enablement, then the capability's own enablement, are applied.
  3. Your saved order decides the sequence.
  4. Compatibility and live readiness are checked — without demoting a startable stopped engine.
  5. A permitted local engine is made ready: a warm one is reused, a startup already in progress is joined rather than duplicated, and a cold one is started with its configured command. The readiness check gates the submission — process creation or an open port alone is not enough.
  6. The request is dispatched to that exact backend and no other.
  7. On an eligible definitive failure, the next permitted compatible option is tried.
  8. When the permitted list is exhausted, the hub stops and reports every reason.

There is no attempt cap by default: a saved order of ten options really is ten attempts. A deployment that needs a smaller budget sets attemptBudget, and the failure message then says how many options went untried rather than implying they failed.

Readiness, not liveness

A bare 2xx proves that something is listening. It does not prove the engine can serve work — a half-loaded engine or any local web page answers 200 too, and this project has been bitten by exactly that. A health check may therefore demand the shape of a real answer:

"health": {
  "kind": "http",
  "path": "/system_stats",
  "expect": "json-key",
  "expectKeys": ["system", "devices"]
}

any (the default) keeps the historical permissive behaviour; json requires a JSON body; json-key additionally requires one of expectKeys at the top level. A body that fails the expectation is an unhealthy model with the observed content in the reason. The shipped catalog uses this for ComfyUI (/system_stats) and Ollama (/api/tags).

Turning automatic startup on or off

Automatic local-engine startup is the deployment-level allowProcessLaunch switch, and it has always been the one place to decide it — once, not per request:

config:
  allowProcessLaunch: true    # the hub may start an engine its catalog declares startable

Both halves must agree: the descriptor declares lifecycle.startable: true with a start command, and the deployment opts in. With launching off, an engine that is merely stopped is reported as not startable by the hub with that as the reason — never launched anyway, and never confused with a failed launch. See docs/security.md before enabling it.

Seeing what happened

invoke_model reports the requested capability, the policy revision that decided it, the ordered list of permitted options, everything that was skipped with the switch or fact that skipped it, and how each attempt turned out — distinguishing launch forbidden, stopped, startup failed, and generation failed by the stage it failed at. That is reported on success too: "why did this run on the second option?" is answered by the first option's reason, not by a log hunt.

If the outcome cannot be determined

A timeout or a dropped connection after the request reached the backend leaves a question this process cannot answer: did it generate? The hub refuses to guess. It reports an explicit unresolved outcome, with any identifiers that can be used to look the work up, and does not fall back — because generating again could double-charge a subscription or repeat a three-minute render. A refusal the backend stated (quota, a login problem, a 4xx) is a definitive "no", and the plan moves on as usual.

Pointing the hub at your engines

The shipped catalog (config/models.json) declares two real engines — Ollama for text and ComfyUI for images — and includes a specific ComfyUI workflow provider. Ensure Ollama has the configured model and ComfyUI has the workflow's required weights; installing a new checkpoint or saving a workflow does not, on its own, register a new ComfyUI provider.

To change them, or to add more, edit that file or point the plugin at your own with configPath / searchRoots. Everything else is copy-and-adjust from config/examples/real-models.example.json: an A1111/Forge/sd.cpp server uses http_json, llama.cpp uses openai_compatible, and ComfyUI uses its own comfyui adapter driven by a graph template — an example is in config/workflows/z-image-turbo.api.json, an API-format {client_id, prompt} graph. The provider additionally declares exact bindings for caller inputs and an outputs node/type selector; the graph's internals are not inferred. To add a ComfyUI provider, configure the host's adapterConfig.workflows with id, name, workflowPath, capabilities, bindings, outputs and optional routing/resource settings. The static model alternative uses adapterConfig.providerKind: "workflow" and workflowId.

A catalog names those files (workflowPath for ComfyUI, stepsPath for 3D) with paths relative to the catalog itself, because a catalog can live anywhere and the process that reads it can be started from anywhere. The shipped config/models.json therefore says workflows/z-image-turbo.api.json — config/ is that file's own directory — while the same template from inside config/examples/ is ../workflows/z-image-turbo.api.json. Absolute paths also work. A path that does not resolve names itself and its base in the error rather than guessing.

A wrong model name is a loud failure — the engine answers 404 and the invocation fails — never fixture text: there are no mock models in any shipped catalog. See docs/adding-a-model.md.

Let the engines list their own models

For supported enumerating engines such as Ollama, set discoverModels: true on the plugin to register pulled models. ComfyUI remains workflow-only and configuration-driven: discovery does not publish its loose checkpoints or saved workflows. Add a ComfyUI workflow descriptor to the host config (or a static workflow model) and reload the catalog:

- insert:
    - id: dsh-ai-model-hub
      name: 'dsh-ai-model-hub'
      config:
        discoverModels: true

It is off by default, and off means no engine is ever contacted for introspection. With it on:

  • config/models.json still wins. A discovered model whose id a hand-written entry claims is dropped before the catalog is built, so the file stays the place to set exact resources or fix priority. ComfyUI workflow providers are always explicitly configured, independently of this discovery switch.
  • An engine that is down is a warning in the log, never a failed boot.
  • Results are cached per host for discoveryTtlMs (60 s). The refresh_model_discovery tool bypasses the cache — that is the path right after ollama pull.
  • Resource figures for discovered models are estimates (from a reported file size where the engine gives one, from the filename otherwise), good enough to keep an oversized model off a small card and not a specification.

Discovery never bypasses resolveDescriptor(): it produces descriptors in the same shape as a models.json entry, and the router, runtime manager, and adapters are unchanged by it. See docs/architecture.md.

Nothing is launched by default: allowProcessLaunch is false, so the hub talks to engines you run but will not start any. See docs/security.md before turning that on.

Where the output goes

Artifacts land in <workspace>/artifacts, where "workspace" means the calling session's, not the host process's working directory. That distinction is the whole point: dsh web is one long-lived process serving many sessions with different workspaces, and it is built — and used to resolve this path — before any session exists. So the root is resolved per tool call, from DSH's own ctx.sandboxPolicy.resolve({ session }).workspaceRoot, which is the same value the file tools treat as the workspace boundary.

The consequence a user notices: switch workspace in the GUI and output follows, with no configuration. list_artifacts reads the same per-call root, so chaining a workflow still finds what the previous step produced.

Set artifactRoot in the plugin row to pin one absolute directory for every session instead — the right choice when collecting artifacts centrally, the wrong one when you want each conversation's images beside its code. The hub refuses a relative root rather than guessing at a base.

Turn it on and engines start themselves. A descriptor that marks its engine startable — "startable": true plus a start command, as the a1111 host in config/examples/real-models.example.json does — lets start_model bring the engine up, and lets invoke_model do it implicitly for a cold model, waiting for the health check before the request is sent. A stopped ComfyUI then costs one cold start, not a failed task:

invoke_model({ capability: 'text_to_image', prompt: 'a red fox in deep snow' })
  → text_to_image served by comfyui_z_image_turbo in 22620 ms (cold start).

Two switches have to agree — the descriptor's startable and the deployment's allowProcessLaunch — so an 8 GB model cannot be started by a catalog file alone. Starting an engine that is already healthy is a no-op rather than a duplicate process, and only processes the hub started can be stopped by stop_model.


The five-minute mental model

The agent asks for a capability; the router picks a model.

// What the agent does. Note what it does NOT say: no model, no engine, no command.
await tools.invoke_model({ capability: 'text_to_image', prompt: 'a futuristic city' })
// What the hub does, in order:
//   1. resolve the request's artifacts
//   2. filter models by capability, input kinds, tags, resources, availability
//   3. sort deterministically (priority, then id)
//   4. start the chosen model if it is cold
//   5. call its adapter
//   6. persist the outputs as artifacts and return them

A model is data, not code. Adding one means adding a JSON entry:

{
  "id": "a1111_sdxl",
  "name": "SDXL (WebUI)",
  "type": "image_generation",
  "host": "a1111",
  "capabilities": ["text_to_image", "image_to_image"],
  "adapterConfig": { "model": "sd_xl_base_1.0.safetensors", "steps": 30 },
  "priority": 10,
  "tags": ["local", "gpu", "image"]
}

No router change. No prompt change. No DSH change.

Artifacts are the only thing that crosses a model boundary. That is what makes text → image → 3D compose without anything knowing about PNG or GLB:

const image = await hub.invokeModel({ capability: 'text_to_image', prompt: 'a spaceship' })
const mesh  = await hub.invokeModel({ capability: 'image_to_3d', inputs: [image.outputs[0].id] })

What is where

dsh-ai-model-hub/
├── src/                          the hub — imports NOTHING from DeepSeek Harness
│   ├── catalog/                  capability vocabulary, descriptor schema, registry,
│   │                             published JSON Schema
│   ├── router/                   deterministic capability-first selection
│   ├── runtime/                  process lifecycle, health, idle timeout
│   ├── adapters/                 adapter contract + the mock test double,
│   │                             PNG/STL/WAV writers, openai_compatible,
│   │                             http_json, comfyui, three_d
│   ├── artifacts/                artifact contract, the local filesystem store,
│   │                             and 3D container sniffing/measurement
│   ├── discovery/                engine introspection: ollama · comfyui · a1111 ·
│   │                             three_d, and the merge that keeps static first
│   ├── comfy/                    the workflow contract (bindings + typed outputs)
│   │                             and the scan that infers one from a saved graph
│   ├── config/                   catalog discovery and loading
│   ├── util/                     process guardrails, primitive validators
│   ├── machine.ts                RAM/VRAM/GPU/disk probing, free figures included
│   ├── hub.ts                    the facade: the public API
│   └── index.ts                  the public surface
├── dsh-plugin/                   the ONLY DSH-aware code (~1 file per concern)
│   ├── index.ts                  plugin entry: name / inject / apply / Config
│   ├── service.ts                the hub as a cordis service
│   ├── config.ts                 plugin configuration schema
│   ├── types.ts                  the DSH API bridge — where a breaking change lands
│   ├── skills.ts                 the bundled agent skill: one provider, one file
│   ├── inventory.ts              engine discovery + the route the settings page reads
│   ├── scan.ts                   the Scan pass: Ollama models + complete ComfyUI workflows
│   ├── client.js                 the browser half: the "Local models" settings page
│   └── tools/                    discovery · lifecycle · invoke
├── skills/dsh-ai-model-hub/
│   └── SKILL.md                  the agent-facing skill the plugin registers
├── lib/                          src/ and dsh-plugin/ compiled to JavaScript:
│                                 the entry points `dsh plugin add` loads, and
│                                 the reason a GitHub install needs no build
├── cordis.patch.yml              the bundle patch: the row the profile mounts
├── install.ps1                   one-line install from GitHub (Windows/PowerShell)
├── install.sh                    one-line install from GitHub (macOS/Linux)
├── scripts/                      install-plugin · doctor · smoke — install and verification
├── config/
│   ├── models.json               the catalog the package ships: real engines
│   ├── examples/                 more real engines, ready to copy
│   └── workflows/                ComfyUI graph templates and 3D step declarations
├── tests/                        the suite; real local HTTP servers stand in for
│                                 engines, so no GPU or model download is needed
├── examples/                     vertical-slice · workflow · three-d-slice demos
└── docs/                         architecture, components, guides

The dependency rule that makes this survivable: src/ does not import @deepseek-ai/*. Only dsh-plugin/ does. DSH is a 0.1.x-rc line, so breaking changes are expected; when one lands, the blast radius is the plugin layer, and the entire hub keeps working and keeps passing its tests.


Documentation

DocumentRead it when
docs/architecture.mdYou want the design, the data flow, and why each boundary is where it is
docs/components.mdYou are working on one component and want its contract in detail
docs/adding-a-model.mdYou want to add a model — the common case needs no code
docs/three-d.mdYou want local image-to-3D working: engine, catalog, discovery, troubleshooting
docs/security.mdYou are about to enable process launching or widen the command allowlist
docs/roadmap.mdYou want to know what each phase required and what remains

Try it without any engine

npm test          # the whole suite; real HTTP servers stand in for engines
npm run demo:3d   # discovery → routing → a real .glb → the boundary transcript
npm run demo      # capability discovery and routing against your own catalog

Requirements

  • Node ≥ 22.6 — the hub sources run directly through Node's type stripping while you work in a checkout. The installed package loads the compiled lib/ instead, because Node refuses to strip types under node_modules; npm run build regenerates it.
  • pnpm — only for dsh plugin, which forwards to it.
  • No runtime dependencies. The hub is dependency-free by design: a plugin whose node_modules must resolve inside a DSH profile is a plugin that breaks on the next release. The DSH packages it imports are peers, supplied by the profile that loads it.

License

MIT