deepseek-harness
deepseek-ai
DeepSeek Harness: Everything is a Plugin.
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:pc439527/dsh-model-provider
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
License: MIT · Platform: DSH Web (client plugin)
Provider-first model selector for DeepSeek Harness (DSH): pick a provider first, then a model from that provider. The current model always reads as Model · Provider — without touching the harness's original model-calling or session-state logic.
In the DSH Web interface, the composer model seat is upgraded from "provider groups + full model list" to a three-level selector (v0.3.0):
Before After (this plugin v0.3.0)
DeepSeek V4 Flash DeepSeek V4 Flash · OpenCode Go
Try opening the dropdown (still tidy even with many models):
Level 1 (root menu) Level 2 (Provider) Level 3 (Model)
├─ Model DeepSeek V4 Flash > ├─ opencode-go 8 models · current > opencode-go
└─ Effort High > ├─ luckikey 2 models > ├─ MiniMax-M3
🔍 Search providers ├─ OpenRouter 20 models > ├─ Qwen3.7 Max
(filters Provider rows) ├─ DeepSeek 4 models > ├─ DeepSeek V4 Flash ✓
└─ OpenRouterX failed ⚠ retry ├─ DeepSeek V4 Pro
🔍 Search opencode-go models └─ GLM-5.1
(current provider only)
Model · Provider by default; "· Effort" is appended only when you actively pick a non-default reasoning effort — no wasted horizontal space for "Default".Model · Provider [· Effort] trigger always matches the harness session state.| Concern | Detail |
|---|---|
| Extension point | Official Slot system: conversation.input.model (single slot, session scope) — no DOM hack |
| Override | Same-named slot registered at priority: -1 — the slot renders the lowest-priority entry, so this component wins and the original seat is shadowed |
| Data | Reuses the harness's native modelDirectories service (per-session shared ModelDirectory); selection semantics and disable logic are unchanged; the provider list comes straight from state.groups, failures from state.failures |
| Fallback | The registration disappears on plugin unload (slots.inject effect teardown) and the original model seat is restored instantly and unchanged |
| Namespace | Own locale dictionary modelProvider (zh/en) — no intrusion into harness copy |
Verified against DeepSeek Harness 0.1.1-rc.2 (dsh CLI 0.1.1-rc.2 + web frontend). The APIs this plugin depends on are unchanged across the rc.8 → rc.2 release line:
| Surface | rc.2 status |
|---|---|
| Slot composition | ctx.slots.inject(key, cb) / slots.register with priority (ascending, lowest renders) + optional registrant — unchanged; same key at the same priority throws, a different priority shadows |
| Seat contract | conversation.input.model (single slot, session scope): owner share locked, locale t seat, inject face { available, directory, load, select } — unchanged |
| Directory face | Shared per-session ModelDirectory snapshot { current, routable, groups, failures, status, error } — unchanged |
| Primitives | IconChevronDown/Left/RightOutline14, IconCheck/Search/WarningOutline16, Toast { text, icon, anchor, onDone } — unchanged |
| Design tokens | --dsw-alias-*, --dsw-specific-menu, --dsw-shadow-lv3, --dsh-scrollbar-* — unchanged |
peerDependencies now track the rc.2 line (^0.1.1-rc.2, same pins the first-party client packages use).
The component keeps the original ModelSelect interaction baseline (shared directory & selection RPC / keyboard arrows & Esc / failure retry & Toast / effort page), only turning the two-level flat list into three-level navigation:
providerId + modelId key — same-named models across providers don't collide)defaultEffort into the selection (selectionFor is the single place that builds a Selection, so prebuilt choice and click-path semantics stay identical)dsh-model-provider/
|- package.json # dsh.client declaration (platform: web, inject list)
|- pnpm-workspace.yaml # pnpm 11 setup (approves the esbuild build script)
|- build.mjs # esbuild bundle script → lib/client.js (ModuleLoader format)
|- tsconfig.json # noEmit typecheck (src + test)
|- src/
| |- index.ts # host half: empty apply (pure browser-surface plugin)
| |- client.tsx # client entry: apply() + slot wiring + compat exports
| |- locale.ts # modelProvider dictionary (zh/en)
| |- model/
| | ├─ types.ts # directory/selection wire types
| | └─ selection.ts # pure functions: selectionFor / sortGroupsForCurrent /
| | # search filtering / Esc stack (shared by UI and tests)
| |- components/
| | ├─ ModelSelector.tsx # trigger + menu shell + state orchestration
| | ├─ RootPane.tsx # level 1: model / effort
| | ├─ ProviderPane.tsx # level 2: providers (search + failed rows)
| | ├─ ModelPane.tsx # level 3: single-provider models (search + composite key)
| | ├─ EffortPane.tsx # reasoning effort
| | └─ StatusBlock.tsx # directory load/error banner
| |- hooks/
| | └─ useKeyboardNavigation.ts # Esc stack + arrow-key focus
| └─ model-provider.css # scoped styles (dshmp- prefix + design tokens)
|- test/model.test.ts # node --test unit tests (pure-function layer)
|- lib/ # build output (host serves /plugins/<id>/client.js)
|- assets/icon.svg
pnpm install # first time: installs esbuild / typescript (npm works too, see below)
pnpm build # node build.mjs → lib/client.js
pnpm typecheck # tsc --noEmit
pnpm test # node --test (no DOM needed; runs the pure-function layer directly)
The build script resolves esbuild via require.resolve("esbuild") from this package's own devDependency — no machine-specific hardcoded paths, so it builds on any machine. (With npm: npm i && npm run build; the committed lockfile is pnpm-generated and npm resolves it itself.)
The output is the standard DeepSeek Harness client-plugin format:
window.__ModuleLoader__.load({ id: "dsh-model-provider", factory: (require) => { ... return module.exports; } });
The right way (since v0.1.0): this package declares
dsh.bundle.patch+ shipscordis.patch.yml, i.e. the standard dsh plugin shape (same as dsh-balance-meter / dsh-context). Don't write it as a "bundle without metadata" — that triggers the startup check error:profile bundle "dsh-model-provider" declares no dsh.bundle in its package.jsonand a restart loop.
Link the dependency into the profile (/data/.dsh/profiles/web/package.json):
"dependencies": { "dsh-model-provider": "link:/data/opt/dev/dsh-provider-model" }
"dsh": { "profile": { "bundles": [ ..., "dsh-model-provider" ] } }
(Or use the official dsh CLI: dsh plugin --profile web add /data/opt/dev/dsh-provider-model — it reconciles into bundles automatically per the dsh.bundle.patch declaration.)
pnpm install (materializes the link)
Restart the web host (client-plugin set changes apply on restart): docker restart deepseek-harness or the equivalent dsh web restart
Verify:
curl -s http://127.0.0.1:3080/ | grep -o '"id":"dsh-model-provider"'
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3080/plugins/dsh-model-provider/client.js # 200
Changing only client.js (no plugin add/remove) needs no restart — rebuild and refresh the page.
Remove this package from dsh.profile.bundles (or uninstall the plugin) and restart — the original ModelSelect seat becomes the only entry again and the UI is restored.
Shipped (v0.3.1):
Model · Provider [· Effort] trigger, consistent default effortpeerDependencies aligned to ^0.1.1-rc.2Candidates:
MIT
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。