dsh-thinking-levels
September 11, 2026 · View on GitHub
Per-round thinking-level (reasoning_effort) control for DeepSeek Harness (dsh): pick Auto (a mask) in the session model selector and the plugin schedules low / high / max from the recent tool-call history before submitting the API effort — or fix a wire level (off / on / minimal / low / medium / high / xhigh / max) manually. Cheap tool rounds stay cheap; heavy work never starves.
v0.7.0-beta.1 (2026-09-06): the short-circuit route is retired. This release no longer depends on
dsh-llm-openai-completions— custom-gateway fixes ride the officialllm-pi-aicompat surface (requires dsh ≥ v0.1.2-alpha.1); keep the adapter plugin uninstalled. See the CHANGELOG.
- 中文 README
- 日本語 README
- 한국어 README
- Installation guide
- 中文安装指南
- 日本語インストールガイド
- 한국어 설치 안내
- Changelog
- 日本語 changelog
- 한국어 changelog
▼ DSH version support
This release supports DSH v0.1.2 or newer only.
DSH version Status Notes ≥ 0.1.2-alpha.1 ✅ Supported Covers the 0.1.2 / 0.1.3 / 0.1.4 / 0.1.5 lines < 0.1.2-alpha.1 ⚠️ Not recommended Stay on the previous plugin line (0.7.1-beta.2 or earlier). Do not run an older plugin build against DSH v0.1.2+ — upgrade the plugin instead. The boundary is
0.1.2-alpha.1, where DSH removed@deepseek-ai/dsh-client-runtime. This release importsContextfrom@deepseek-ai/cordisinstead of the deletedClientContext, matching the official client plugins.
Compatibility note: Version
0.6.0includes Japanese (ja) and Korean (ko) dictionaries and selector entries, but the current official DSH releases expose onlyzhandenthroughLocaleRuntime. On stock DSH, selectingjaorkofails withlocale "<id>" is not registered. These languages will work after official DSH adds the locale IDs. Advanced users can use a DSH fork that updatespackages/client/locale/src/locale-settings.ts(LOCALE_IDS) andpackages/client/locale/src/client/index.ts(LOCALESlabels), together with the corresponding core dictionaries and tests, then rebuild and run the forked DSH. Changing this plugin alone cannot extend DSH's global locale list.
In a multi-step tool chain, the model re-thinks before every tool call — and that thinking dominates the wall-clock time (a 50-step agent task can spend minutes reasoning between tools). dsh-thinking-levels plugs into the agent/request waterfall that dsh re-resolves for every step (registered with prepend so the session model-selection assembly cannot overwrite its decision) and injects a thinking level into the next model request.
Preview
Screenshots of the live UI (dsh web):
Levels
| Level | Meaning | Where |
|---|---|---|
off | thinking disabled (manual only — never auto-picked) | model selector / default level |
on | thinking enabled (toggle-only models only): sends enable_thinking, never a think effort | model selector / default level |
minimal | least effort (very light tasks) | model selector / default level |
low | manual pick for simple chat tasks (cheap rounds stay cheap) | model selector / default level |
medium | medium effort | model selector / default level |
high | the official default effort | model selector / default level |
xhigh | extra high effort | model selector / default level |
max | heavy work | model selector / default level |
auto | mask: schedule per step from the recent tool-call history, resolved to a wire level before submission | model selector (injected by the plugin) / default level |
Wire-level facts (verified against the official DeepSeek docs and dsh's llm-deepseek adapter): low maps 1:1 on deepseek-v4-flash / v4-pro, while medium / xhigh collapse onto high. The adapter accepts off | low | high | max and rejects anything else with UNSUPPORTED_REASONING_EFFORT — auto is the plugin's mask layer, never sent to the API, always resolved to a concrete wire level before injection. on is not an effort level: it is advertised only by toggle-only models (Qwen3.6-style), and it only flips enable_thinking true — no reasoning_effort is sent; an effort-capable model never advertises on, so a manual on pick on one is stripped.
Custom wire mapping
For hand-declared llm-pi-ai models the settings card lets you map each level to the exact value your gateway expects (borrowed from dsh-thinking-effort): tick a level and enter its wire value, e.g. high → ultra. The mapping is stored as the model's reasoningEfforts table, so the Composer selection High sends ultra to the gateway. Leaving off empty means "do not send".
- Official preset:
Off / High / Max(official DeepSeek style) - Generic preset:
Off / Low / Medium / High
Context-window presets
The settings card's per-model editor now includes a context window limit control: preset buttons 64K / 128K / 256K / 400K / 512K / 1M, a custom integer input, and a clear button. The value is written to the llm-pi-ai model entry contextWindow (integer 2000–1000000).
Upstream, the harness consumes it through resolveModelInfo(...).context.contextWindow for compaction thresholds, context-overflow detection and context-pressure projections. Because llm-pi-ai re-reads the live config on every resolve and the compat sync does not block model discovery, a settings edit takes effect on the next request without a restart.
The plugin config also accepts models['provider/model'].contextWindow as a validated (integer 2000–1000000) declaration at the composition/config surface.
Model-aware guard (v0.5.0)
The plugin never sends a reasoning_effort to a model that does not advertise one. Custom
openai-completions routes (e.g. a local Qwen3.6 without reasoningEfforts) are classified
non-reasoning via ctx.llm.resolveModelInfo, and any effort — inherited or scheduled — is
stripped instead of sent, so dsh's per-request UNSUPPORTED_REASONING_EFFORT rejection
cannot fire. Unsupported fields are never passed to an API that cannot take them.
Version behavior:
| dsh version | low handling |
|---|---|
| rc.6 (old) | not native: the selector only shows it when a configurer-confirmed models override names it; the level is then advertised (selector + request validation) and passed through verbatim |
| rc.7+ (new) | native: the plugin neither rewrites nor re-injects it; a manual low pick passes through unchanged |
The auto scheduler may still pick low for supporting models — the capability guard above is
what keeps it away from models that cannot take it.
Model-selector Auto
The session model selector (next to the model) now offers Auto after the wire levels (injected into the model-directory metadata by the plugin):
| Model-selector pick | Behavior |
|---|---|
| Auto | plugin schedules via tool history + the upgrade/downgrade toggles, resolves to low / high / max before submission |
off / on / minimal / low / medium / high / xhigh / max | manual choice wins — plugin does not intervene (on stays on on toggle-only models, never lifted to an effort; effort-capable models strip it) |
| unset | the plugin's default level applies (below) |
Auto scheduler
The hub is high (the official default). auto schedules between low / high / max; it never picks off.
| Recent tool calls | Level |
|---|---|
| none (fresh prompt, pure chat) | low |
| ≥75% simple tools, small args, downgrades allowed | low |
| mixed / heavy tools | high |
| very heavy payloads, upgrades allowed | max |
The scheduling policy is the same source as dsh-tool-turbo (same simple-tool whitelist / payload thresholds / 75% ratio rule).
Install
See INSTALL.md for the full official-CLI guide (profile discovery, upgrade, migration, verification, troubleshooting). Quick start:
# 1. install the plugin into a profile from npm (web shown; any profile works)
# (the web profile is a pnpm workspace root, so -w is required)
dsh plugin --profile web add dsh-thinking-levels -w
# GitHub alternative:
# dsh plugin --profile web add https://github.com/drscrewdriver/dsh-thinking-levels.git -w
# local-path alternative (no network needed):
# dsh plugin --profile web add /absolute/path/to/dsh-thinking-levels
# 2. restart dsh web (a running instance does not hot-load new bundle layers)
dsh web
Note: the dsh runtime uses pnpm 11, whose
minimumReleaseAgesupply-chain policy may block a freshly published version withERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION— add the version tominimumReleaseAgeExcludein~/.dsh/profiles/web/pnpm-workspace.yamlto lift the cooling period.
Manual link: registration (alternative to dsh plugin add):
# ~/.dsh/profiles/web/package.json dependencies:
# "dsh-thinking-levels": "link:<absolute path to dsh-thinking-levels>"
# ~/.dsh/profiles/web/cordis.patch.yml:
# - insert:
# - id: thinking-levels
# name: dsh-thinking-levels
cd ~/.dsh/profiles/web && pnpm install && dsh web
Configuration
Two surfaces share one schema:
- Assembly — the plugin row's
config:in the profile composition (e.g.cordis.yml):config: level: auto # off | on | minimal | low | medium | high | xhigh | max | auto — the default level when the session picks nothing allowDowngrade: true # let the scheduler drop below `high` allowUpgrade: false # forbid the scheduler lifting to `max` - Runtime — the dsh-settings namespace
thinking-levels(level,allowDowngrade,allowUpgrade,enabled,models): changes apply to the next model request, no restart needed. A visual editor is available under Settings → Plugins → configurable plugins.
Per-model capability overrides (models, keyed provider/model) confirm what auto-detection
finds; the configurer has the final word:
config:
level: auto
models:
llm-pi-ai/Qwen3.6-35B-A3B: # non-reasoning thinking model (thinking toggle + budget)
vision: false
thinking: true
efforts: false # never send reasoning_effort (stripped at request time)
llm-pi-ai/Qwen3.8-27B: # effort-capable model (rc.6-era adapter without low)
efforts: [low, high] # confirm low → advertised in the selector + passed through
For Qwen thinking on/off + budget, configure the llm-pi-ai route instead:
compat.thinkingFormat: qwen(→ wireenable_thinking+thinking_budgetviathinkingBudgets), orqwen-chat-template(→chat_template_kwargs.enable_thinking) for effort models like Qwen3.8-27B.
Defaults: { enabled: true, level: 'auto', allowDowngrade: true, allowUpgrade: false, models: {} }.
Semantics: the model-selector pick outranks the plugin's default level. Pick
auto(mask) → plugin schedules; pick a wire level → applied directly; pick nothing → the plugin'sleveldefault is used.allowDowngrade/allowUpgradeconstrainautoscheduling only.
Official compat surface: the short-circuit tool is retired (0.7.0-beta.1)
Once custom gateways (vLLM / LM Studio / self-hosted OpenAI-compatible proxies) declare
thinking, this plugin writes the fixes into the official llm-pi-ai compat surface
(introduced in dsh ≥ v0.1.0-rc.8, commit 884f7b9c41) —
dsh-llm-openai-completions
is no longer needed and should stay uninstalled:
- Scans
llm-pi-ai.providersfor routes that are custom openai-completions gateways (api: openai-completionsor a non-official baseURL) and declare areasoningEffortstable on any model (includingmodelOverrides), then writes:- route-level
compat.supportsDeveloperRole: false— the system prompt goes out assystem, fixing the vLLM / SGLangUnexpected message role400; - model-level
compat.thinkingFormat: 'qwen-chat-template'on toggle-style thinking rows (thinking table without row-levelsupportsReasoningEffort) — pi-ai then sendschat_template_kwargs.enable_thinking(bare vLLM servers ignore the top-levelenable_thinkingof the plainqwenformat);
- route-level
- Writes go through the official settings channel (read → pure transform → whole-section
settings.update('llm-pi-ai', …)), so dsh's schema validates the write where it is written: a dsh older than rc.8 rejects the fields with a log warning — no silent misconfiguration; explicit values on any layer are never clobbered; - Triggers on plugin start,
llm/adapters-updated, andllm-pi-aisettings changes — no manual config editing; - The capability card is de-short-circuited too: the provider-level switch is now "gateway rejects the developer role" (writes/clears the route-level flag; unchecking restores inheritance), and the model editor is progressive (thinking/vision → effort support → effort editor);
- Response-side inline
<think>splitting remains a gateway concern: bare vLLM needs--reasoning-parser qwen3(pi-ai parses onlyreasoning_content/reasoning/reasoning_text).
Dependency note
The host half does not value-depend on @deepseek-ai/dsh-settings (settings registration goes through the cordis settings service provided by the dsh runtime) — no need to install official packages into the profile manually. dependencies is just @deepseek-ai/schemastery (installed automatically with the package).
Development
npm run lint # eslint (typescript-eslint flat config)
npm run typecheck # tsc --noEmit
npm test # vitest — 46 tests
Test coverage: level policy (manual pass-through incl. the extended levels, on clamping, auto scheduler, validation, simple-tool boundary), the model-capability guard (reasoningEffortSupported, resolveEffortInjection stripping/passthrough), session-event parsing (guards, window cap, malformed records), the config schema (defaults lockstep, out-of-band rejection, models overrides), and the official-compat sync (identification, explicit-value respect, identity idempotence, write-time schema gating).
License
MIT