# dsh-model-settings [English](README.en.md) | [中文](README.md) ## Overview One panel to configure the model for the three kinds of independent model consumers in dsh: - **Main agent** (the default model for new sessions) - **Subagents** (subagent / workflow-spawned children) - **Goal rounds** (rounds while a goal is active) Each slot sets its own **provider / model / reasoning effort** independently — saved settings take effect immediately, no restart needed. Two actions, kept distinct: **Apply globally** writes the config and switches the current conversation; **Apply to this conversation only** switches just the current session without writing config. ![Model Settings panel](docs/dsh-model-settings.png) ## Origin This plugin was born from a real billing accident. On 2026-08-19, the author switched dsh's main session to kimi (a subscription plan, to save some money). Around that time, DeepSeek had just introduced **peak/valley pricing** — and in the community's telling, the daytime bill belongs to *Liang Wenfeng*, while the nighttime rate belongs to *Liang Wengu*. (See the note below for what that means.) Since daytime is peak-priced, metered DeepSeek should be saved for the night; daytime work should run on a subscription. That evening, the DeepSeek balance had gone from ~¥50 to **-1.6**. An investigation — decompressing all 76 session logs and checking them line by line — found the truth: the main session was indeed kimi, but the 12 subagents spawned that afternoon had all hit the DeepSeek official API (`deepseek-v4-flash`), along with 40 more the previous evening. About **¥41–52 over two days**. No leaked key — it's dsh's model routing design: subagents inherit the **default model at creation time** (`agent-default-model`), not the model selected in the main session's UI. In other words: once the main session is switched to another model (say, kimi), subagents do not follow — they keep the default model from creation time. Hence this plugin: one panel to control the model and reasoning effort for the **main model**, **subagents**, and **goal rounds**, hot-reloaded on save. > **About the joke**: DeepSeek's founder is Liang Wenfeng (梁文锋). Peak/valley pricing (like off-peak electricity tariffs) splits the day into expensive *peaks* and cheap *valleys* — 峰 (fēng, "peak") is a homophone of 锋 (fēng, in *Wenfeng*), and 谷 (gǔ) means "valley". So the gag goes: daytime (peak) billing is run by "Liang Wenfeng", nighttime (valley) rates by his fictional sibling "Liang Wengu" (梁文谷). It's just a pun — there is no second founder. ## What it does Adds a **Model Settings** entry to the bottom-left of the dsh web UI (below the workspace, above Settings — pinyin order puts it right before "用量/余额"). One panel controls the three kinds of independent model consumers: | Slot | Controls | Takes effect | | --- | --- | --- | | Main model | New-session default (writes `agent-default-model`) | On save: writes global config **and switches the current conversation immediately** | | Subagents (incl. workflow) | Every subagent request (global `agent/request` interception) | Hot: the next subagent request in a running session | | Goal rounds | Rounds while a goal is active | Hot: the next goal round | Each slot sets its own **provider / model / reasoning effort**, or follows the default. ### Two buttons, don't mix them up | Button | Meaning | In one line | | --- | --- | --- | | **Apply globally** | Writes global config **and** switches the current conversation too | Now *and* the future | | **Apply to this conversation only** | Switches only the open conversation, no config write | Just now, no side effects | **Hot reload**: no dsh restart needed. New sessions use the new main model; the next subagent request or goal round in a running session uses the new config. ## Compatibility **The latest `@deepseek-ai/dsh` is `0.2.1-rc.2`, exactly the kernel DSH NEXT 2.0.19-next runs — that is the primary target**: `SettingsForms` + volatile Config, verified with a real host boot and a real browser. Older versions are additive compatibility, not the main path. The host half never statically imports a named helper from one `dsh-settings` generation; it picks the available integration at runtime. The client half falls back across both icon naming schemes: | dsh version | settings integration | Icon exports | Status | | --- | --- | --- | --- | | **`0.1.7`–`0.2.x` (incl. latest `0.2.1-rc.2`)** | `SettingsForms` + volatile Config | `Icon*Regular/Medium` | **Primary, verified** | | `0.1.2`–`0.1.6` | `SettingsProvider.installSection` | `Icon*16` | Supported | | `0.0.1`–`0.1.1` | `installSettingsSection` + `settingsNamespace` | `Icon*16` | Supported | Verified on `dsh 0.0.1-rc.1`, `0.1.0-rc.7`, `0.1.5-rc.2`, `0.1.7-rc.2`, and `0.2.1-rc.2` (DSH NEXT 2.0.19-next); the latest `0.2.1-rc.2` was also booted as a real web profile with `state` / `save` / clear-overrides / restart-restore exercised end to end, and the panel was opened in a real browser without console errors. ## Requirements - **dsh Web UI**: the panel is web-side UI (`dsh.client.platform: "web"`), loaded by both a web profile and the DSH NEXT desktop app (which hosts the same web client). - **Peer dependencies**: | Package | Range | | --- | --- | | `@deepseek-ai/cordis` | `^4.0.1` | | `@deepseek-ai/dsh-llm` | `0.0.1-rc.1`–`0.2.x` (prereleases included, explicit branches) | | `@deepseek-ai/dsh-settings` | `0.0.1-rc.1`–`0.2.x` (prereleases included, explicit branches) | | `@deepseek-ai/schemastery` | `>=3.18.1 <4.0.0` | The full `dsh-llm` / `dsh-settings` ranges live in `package.json`: plain semver would not let a prerelease match `<0.3.0`, so the range uses one `||` branch per patch line; the latest `0.2.1-rc.2` is covered. - **Required host services**: `agents`, `webServer` (server side); `slots`, `locale` (client side). - **Optional services** — when absent the matching capability degrades instead of crashing: | Service | When absent | | --- | --- | | `settings` | Panel is read-only; saving returns `settings service unavailable` | | `llm` | Empty model catalog; the panel shows "No configurable model providers" | | `agentDefaultModel` | Current default is unreadable; the main route shows "Not configured" | | `goals` | The goal slot has no effect; the other two work normally | ## Installation ### Web edition (dsh 0.1.x / standalone web profile) ```bash # Install from GitHub dsh plugin --profile web add github:Howthemeaning/dsh-model-settings ``` `dsh plugin` installs the package into the profile and automatically appends `dsh-model-settings` to the `dsh.profile.bundles` layer (the package declares `dsh.bundle.patch`). **Restart dsh** for it to take effect. ### DSH NEXT desktop (0.2.x) DSH NEXT's `desktop` profile is managed by the Electron app and its CLI rejects `--profile desktop`. Open **Plugins → Add plugin → Install a third-party plugin** in the sidebar, enter `github:Howthemeaning/dsh-model-settings` (a local directory also works), then restart the client. Manual route for local debugging only: place the package at `~/.dsh/profiles/desktop/node_modules/dsh-model-settings`, add `dsh-model-settings` to both `dependencies` and `dsh.profile.bundles` in `~/.dsh/profiles/desktop/package.json`, then restart DSH NEXT. The desktop app may rewrite this profile, so prefer the Plugins page for day-to-day use. ### Uninstall ```bash # Web edition dsh plugin --profile web remove dsh-model-settings ``` On DSH NEXT, uninstall from the sidebar **Plugins** page; for a manual install, remove `dsh-model-settings` from both the `dsh.profile.bundles` array and `dependencies` in `~/.dsh/profiles/desktop/package.json`, then delete `~/.dsh/profiles/desktop/node_modules/dsh-model-settings`. ## Configuration Day to day, just open the panel — it writes the settings *user* layer, which takes precedence over the defaults below. To pre-seed defaults at deploy time (say, pinning every subagent to a cheaper model across a team), override this plugin's `config` with an `update` entry in the profile's `~/.dsh/profiles//cordis.patch.yml` (`web` for the web edition, `desktop` for DSH NEXT): ```yaml - update: - id: model-settings config: subagent: provider: deepseek-official model: deepseek-v4-flash reasoningEffort: low ``` Two optional slots, same fields: | Field | Type | Notes | | --- | --- | --- | | `subagent` | object | Override for subagents (workflow children included); omit to follow the default | | `goal` | object | Override for rounds while a goal is active; omit to follow the default | | `.provider` | string | Provider id, matching the panel's "Provider" dropdown | | `.model` | string | Model id | | `.reasoningEffort` | string | Reasoning effort id; omit to use the model's own default | The main-model slot is not configured here — it writes the `agent-default-model` namespace, owned by the panel or by dsh's own settings. ## Development This package is hand-written ESM with no build step: `lib/index.js` is the host half, `lib/client.js` the browser half (a hand-written `__ModuleLoader__` bundle with the CSS inlined). The peer dependencies ship with dsh; point `node_modules` at the matching dsh installation for local work, and swap it to run the compatibility matrix against 0.0.1 / 0.1.x / 0.2.x: ```bash ln -sfn "$(npm root -g)/@deepseek-ai/dsh/node_modules" node_modules npm run check ``` | Command | What it does | | --- | --- | | `npm run build` | Verifies every published artifact exists and `node --check`s both js files (no transpile) | | `npm run check` | `build`, then `test/smoke.mjs` and `test/client-smoke.mjs` | `test/smoke.mjs` mocks a minimal cordis ctx and covers the `agent/request` slot precedence plus all three endpoints; it picks the 0.0.1 / 0.1.x / 0.2.x integration automatically from the installed `dsh-settings` exports. `test/client-smoke.mjs` loads the client bundle with both the old `Icon*16` and the new `Icon*Regular/Medium` primitives, so a missing icon cannot silently break the first render. Two more scripts are investigation tools, not part of `check`: `test/read-log.mjs` decompresses a session log and prints its `request/header` entries, and `test/render-debug.mjs` force-renders the panel under node to reproduce crashes. After editing `lib/client.js`, sync it to the installed copy and hard-reload the page to see the change (the CSS is injected into a `