# dsh-set-model
简体中文 ·
English
A DeepSeek Harness (DSH / cordis) plugin: through Jev or through its own tool calls, the agent switches model presets and thinking depth on its own, following the task at hand.
- **Automatic switching (Jev)**: the TypeSafe Jev decision model reads your latest instruction at the start of every turn, classifies the task and moves between your declared presets by itself — deep reasoning for hard debugging, the cheap tier for copy edits and test runs; no manual intervention, and no need to say "think harder" in the prompt. A preset that declares reasoning effort only moves **thinking depth on the same model**;
- **Free model tools**: the agent switches provider, model and reasoningEffort by task difficulty via `set_model` / `list_models`;
- **Preset tools**: the deployment declares a set of model presets (model + reasoning effort) and the agent may only switch between them (`switch_preset` / `list_presets`). A preset may omit `provider` / `model` (empty = keep the active model) so it tunes thinking depth alone; an empty `presets` list falls back to the built-in `low` / `medium` / `high`;
- The automatic switch and the two tool switches are **fully independent** and combine freely (Jev only, tools only, both, neither);
- Accepts a **policy prompt**: one text tells both the agent (while it holds a switching tool) and Jev which preset each stage should use;
- The plugin injects the active model and preset into the runtime context, and switches models automatically alongside Plan Mode.
Switch to deep reasoning for hard problems, stay on the cost-effective tier for routine work — with Jev enabled that policy runs itself every turn, no manual configuration changes needed.

## Three independent switches
| Switch | Default | Effect |
|---|---|---|
| `enableModelTools` | `true` | injects `set_model` / `list_models`, letting the agent switch to any registered provider / model / reasoning effort |
| `enablePresetTools` | `false` | injects `switch_preset` / `list_presets`, restricting the agent to the declared `presets` (the built-in three tune thinking depth only) |
| `jevEnabled` | `false` | Jev decides the preset every turn — independent of, and unaffected by, the two tool switches |
> The preset tools constrain the **agent's tool surface**. Your own switching through the GUI model picker is unrestricted (DSH exposes no plugin hook to intercept user selections) and remains a manual fallback. None of the switches changes existing GUI behaviour.
## Policy prompt
`policyPrompt` is the single source of that policy, and it has two readers:
| Reader | Reads it when | Sees it as |
|---|---|---|
| Agent | at least one tool switch is on | a `## Model policy` section in the system prompt |
| Jev | `jevEnabled` is on | part of every decision request (`state.policy`), next to the current instruction and the previous reply |
With neither reader present (both tool switches off and Jev off) nothing reads it, and the plugin logs a warning. When the preset tools are on, the generated preset roster is appended to the same section:
```yaml
policyPrompt: |
Always switch to deep for planning and hard debugging; use daily for routine work, edits and tests.
```
What the agent sees once the preset tools are enabled (generated from each preset's `id`/`name`/`when` — nothing to hand-write):
```
## Model policy
Always switch to deep for planning and hard debugging; use daily for routine work, edits and tests.
本部署只允许在下列模型档位之间切换;进入对应阶段时调用 switch_preset 换档:
- **daily(日常执行)** — deepseek/deepseek-chat · reasoning low — 适用:常规实现、修改、测试
- **deep(深度攻坚)** — deepseek/deepseek-reasoner · reasoning max — 适用:疑难排查、架构设计
```
With nothing configured the section is empty and leaves no trace in the prompt.
## What the model sees
Via `systemPrompt.context`, the plugin contributes the current active model (and preset) line to the Runtime Context snapshot assembled on every step. That snapshot is committed only when its text changes, so a stable turn keeps a single cache-safe append (the model can check itself at any time without calling a tool):
```
[Current active model: deepseek/deepseek-chat · reasoning: low · preset: daily]
```
While Plan Mode is active, a status marker is attached:
```
[Current active model: deepseek/deepseek-reasoner · reasoning: max · preset: deep] [Plan Mode Active]
```
`preset: xx` appears only when the active selection matches a declared preset.
## Tools
### set_model (free model tools)
`set_model({ provider?, model?, reasoningEffort?, reason })` — `reason` is mandatory (every switch invalidates the session's KV cache, so the tool description requires the agent to justify the switch and avoid frequent changes):
```json
{ "model": "deepseek-reasoner", "reasoningEffort": "high", "reason": "Hard concurrency debugging, deep reasoning required" }
```
### list_models (free model tools)
`list_models({ provider? })` lists registered providers and model capabilities (reasoning efforts, context window):
```
# Available Providers & Models:
## Provider: deepseek
- **deepseek-reasoner** (DeepSeek Reasoner) | Reasoning: [low, high, max] | Context: 128000
- **deepseek-chat** (DeepSeek Chat) | Context: 128000
```
### switch_preset (preset tools)
`switch_preset({ preset, reason })` — `preset` is a configured preset id, `reason` is mandatory; an unknown id returns the available presets:
```
Model preset switched to deep (深度攻坚) — deepseek/deepseek-reasoner · reasoning: max. Effective from the next step.
Reason: Hard concurrency debugging, deep reasoning required
```
### list_presets (preset tools)
`list_presets({})` lists the allowed presets, the stage each suits, and the one currently in use:
```
# Model presets
Mode: preset · Plan Mode active
Current: deepseek/deepseek-reasoner · reasoning max · preset deep
- **daily(日常执行)** — deepseek/deepseek-chat · reasoning low — 适用:常规实现、修改、测试
- **deep(深度攻坚)** — deepseek/deepseek-reasoner · reasoning max — 适用:疑难排查、架构设计 ← current
```
A switch takes effect from the next step: the plugin appends a `model/selection` event to the session (the same mechanism as the GUI model picker), and the Web UI state stays in sync.
## Plan Mode auto-transition
The plugin watches the plan projection state on `agent/pre-step` — zero manual intervention:
- **Entering Plan Mode** (`/plan on`): stashes the current model and switches automatically — to the `planPreset` preset when one is set (it wins), otherwise to the `planModel` route;
- **Leaving Plan Mode** (`exit_plan_mode` approval / `/plan off`): automatically restores the pre-entry model (`autoRestorePlanModel`, on by default);
- A failed auto-switch only degrades to a warn and never interrupts the step.
## Jev Autonomous Model Switching
With `jevEnabled` on, the plugin asks the **TypeSafe Jev decision model** once per turn, on the first step: what kind of task is this instruction, and which preset fits it. The decision flow:
1. take the latest user instruction of the current turn (even before it is committed to the session), append the previous assistant response as background, then end-align and truncate to 2000 characters;
2. send that context together with the policy prompt (`state.policy`), and offer every preset in `presets` (`when` / `name` / `id`) plus `keep` (stay as-is) as the candidate set, letting Jev return the best preset with a confidence score;
3. below `jevConfidenceThreshold` (default 0.7) nothing happens, so there is no pointless switching;
4. on a hit the plugin writes a `model/selection` event — the same mechanism as the GUI model picker, so the Web UI stays in sync; the switch applies to requests from the next step onward.
"tweak the color of this button" stays on the daily preset; "orders occasionally get lost in production, help me find out why" moves to the deep preset. The actual decision shows up in the host log:
```
dsh-set-model: [Jev] auto-switched session to preset "deep" (deepseek/deepseek-reasoner · reasoning: max) with confidence 0.87
```
When a preset declares `reasoningEffort` only (`provider` / `model` left empty), switching means **adjusting thinking depth on the same model** — `low` for routine work, `max` for hard problems: same model, different thinking budget.
- **Ultra-low cost & sub-second latency**: based on OpenRouter Decisions API (~200–300ms, ~$0.000015 / request);
- **Generic credentials**: configure `jevApiKey` / `jevBaseUrl` / `jevModel` on the settings page, or use the `JEV_API_KEY` / `JEV_BASE_URL` / `JEV_MODEL` environment variables — no tie to a specific platform or to a machine-local vault (api-vault);
- **Robust guardrails**: 3s timeout cutoff, per-session generation race fences (a newer instruction voids the in-flight decision) and mutual exclusion with Plan Mode; any failure only logs a warning and keeps the current preset, never blocking the turn;
- **Decoupled from the tool switches**: `jevEnabled` is independent of `enableModelTools` / `enablePresetTools` — you can run Jev alone, without exposing model tools to the agent.
## Safety guardrails
- **Provider whitelist**: when `allowedProviders` is non-empty, switching to providers outside the whitelist is rejected outright; it also constrains the presets (a preset whose provider is not whitelisted is dropped at load with an error log);
- **Context-window guard**: if the current session's token pressure exceeds the target model's context window, the switch is rejected with a prompt to compact context first (e.g. clear_mind):
```
Current session token pressure (70000 tokens) exceeds target model context window (65536 tokens).
Please compact context (e.g. clear_mind) before switching to this model.
```
- **Route validation & adaptive reasoning effort**: supports all 7 reasoning levels (`off | minimal | low | medium | high | xhigh | max`), automatically clamping to the nearest level the target model supports (e.g. `xhigh`/`max` → `high`) or omitting it on non-reasoning models before validating via `ctx.llm.resolveCallConfig`;
- **Configuration problems are not fatal**: duplicate preset ids, missing provider/model, a `planPreset` naming no preset, and similar issues are logged (`dsh-set-model: …`) and skipped — the plugin still loads, so a typo cannot take the whole plugin tree down;
- **Root agents only**: the tools are registered for root agents only (subagents get no self-switching tools); turning both tool switches off disables them entirely.
## Settings page
The plugin ships a Web settings page ("动态模型", Settings → 动态模型) where you can maintain:
- the preset table — add, remove and reorder rows; provider, model and reasoning effort are pickers fed by the live model catalog; plus `id`, display name and target stage;
- the policy prompt;
- agent switching capability: the two tool switches and `allowedProviders`;
- the Plan target (preset or model) and `autoRestorePlanModel`;
- the Jev auto-switching toggle and its advanced options (API Key / Base URL / model / confidence threshold), plus a "test connection" button that probes the Decisions API for reachability and latency.
Saving re-applies the plugin configuration on the host immediately, with no restart. Fields must be marked `volatile` to appear in the settings form (a DSH settings-system rule).
## Configuration
The settings namespace is the plugin's profile loader entry id: **`set-model`** (configurable in the Web Settings UI; changes take effect at runtime).
```yaml
# the profile's cordis.patch.yml (or the user layer the settings page writes)
- id: set-model
name: dsh-set-model
config:
enableModelTools: true # inject set_model / list_models (default true)
enablePresetTools: false # inject switch_preset / list_presets (default false)
jevEnabled: false # Jev picks the preset per turn (default false, independent of the tool switches)
presets: # defaults to the built-in low / medium / high, none pinning a model
- id: daily # stable id the agent switches by
name: 日常执行 # display name; falls back to the id
when: 常规实现、修改、测试 # target stage: rendered into the policy prompt and used as Jev's criterion
provider: deepseek # empty provider AND model = keep the active model, tune effort only
model: deepseek-chat
reasoningEffort: low # off | minimal | low | medium | high | xhigh | max; defaults to the model default
- id: deep
name: 深度攻坚
when: 疑难排查、架构设计
provider: deepseek
model: deepseek-reasoner
reasoningEffort: max
policyPrompt: | # optional policy text, shared by the agent (when it has tools) and Jev
Always switch to deep first for production incidents.
planPreset: deep # preset activated on entering Plan Mode (takes precedence)
planModel: # model activated on entering Plan Mode when no planPreset is set
provider: deepseek
model: deepseek-reasoner
reasoningEffort: high
autoRestorePlanModel: true # restore on leaving Plan Mode (default true)
allowedProviders: [] # optional: whitelist of switchable providers (empty = unrestricted)
jevApiKey: "" # optional Jev API Key (falls back to JEV_API_KEY environment variable)
jevBaseUrl: "" # optional Jev Decisions API URL (falls back to JEV_BASE_URL or default endpoint)
jevModel: "" # optional Jev model (falls back to JEV_MODEL or typesafe/jev-1.13)
jevConfidenceThreshold: 0.7 # confidence threshold for Jev switches (0.0–1.0, default 0.7)
```
## Install
```bash
dsh plugin --profile web add dsh-set-model
```
No manual configuration needed after install — the bundled `cordis.patch.yml` mounts automatically.
Install straight from GitHub (source install; pnpm ≥10 requires allowing the build script):
```bash
dsh plugin --profile web add github:john-walks-slow/dsh-set-model
# The first add is blocked by pnpm: add the package name pnpm prints to
# allowBuilds in ~/.dsh/profiles/web/pnpm-workspace.yaml, then re-run
```
## Permissions & compatibility
- **No direct side effects (standard path) & lightweight decision network (Jev path)**: the standard path runs without external services, direct network requests, or direct filesystem writes; when autonomous Jev switching is enabled, only a lightweight task summary is sent to the configured Decisions API on the first step of each turn (the current instruction plus the previous assistant response, end-truncated to 2000 characters, together with the policy prompt and the preset list; a 3-second timeout fuse means failures degrade gracefully to warnings without blocking normal turn execution). Changes are recorded strictly via standard Session events (`model/selection`) and projected via Runtime Context and system prompt sections;
- **Browser half**: the settings page registers a `settings.section` from `lib/client.js` (a build artifact) and only calls DSH's `remote.settings` / `remote.session` RPCs — no network requests of its own
- **Host version requirement**: DSH `^0.1.7-rc.2 || ^0.2.0-rc.1` (on 0.1.2–0.1.6 the installer reports an explicit plugin/dsh version incompatibility; upgrade DSH)
- **Dependency declaration**: every DSH runtime package (`cordis`, `dsh-agent`, `dsh-llm`, `dsh-session`, `dsh-tools`, plus optional `dsh-settings`) is declared as a `peerDependency`, so the DSH module resolver routes the plugin's `@deepseek-ai/*` imports to the **host-provided copies** — the plugin never ships or hoists its own copies of them; the only runtime dependency is the leaf schema library `@deepseek-ai/schemastery` (`react` / `esbuild` / `@types/react` build the browser half only; React is provided by the web shell at runtime). Node ≥ 22.5
- **Failure degradation**: a failed Plan Mode auto-switch only warns and never breaks the step; tool validation failures return a clear error to the agent without polluting session state; a failed settings-page mount only logs to the console and never takes the GUI down
## Local development
```bash
npm install
npm run check # tsc(host) + tsc(browser half)
npm test # tsc(incl. test) + node --test dist/test/*.test.js
npm run build # tsc → dist/src, esbuild → lib/client.js
npm run build:client # rebuild the browser half only
```
For GUI verification, use a minimal dsh-e2e instance (see `scripts/dev-worktree.sh`):
```bash
dsh-e2e start --wait-ready --patch e2e/fixture/cordis.patch.yml
dsh-e2e run e2e/verify-policy-page.mjs
```
The e2e seed config lives in `e2e/fixture/cordis.patch.yml` and is injected with `--patch`; override the playwright / camoufox paths with `DSH_E2E_PLAYWRIGHT` / `DSH_E2E_BROWSER`.
After changing the browser half, always rebuild `lib/client.js`; the build script already runs `node --check lib/client.js`.
## License
MIT
## Release a new version
One command runs tests, bumps the version and packs (`npm version` also commits and tags):
```bash
npm run release # patch; for bigger changes: npm version minor or major
```
Then publish with the fingerprint flow and push:
```bash
node ~/.agents/skills/npm-publish/scripts/publish-webauthn.cjs /tmp/dsh-set-model-.tgz
git push --follow-tags
```
Verify with `npm view dsh-set-model version`. When releasing several packages, check "do not challenge for the next 5 minutes" on the webauthn page to publish them all with one fingerprint.
## Community
- Thanks to the [LINUX DO](https://linux.do/) community for providing the discussion environment.