DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

buguoshixc /

buguoshixc/dsh-thinking-effort

Verified

Adjustable thinking intensity (reasoning effort) for third-party API-key providers in DeepSeek Harness (dsh). Declares 推理等级 for custom OpenAI-compatible routes so the composer picker works again.

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@c8e0ff89

dsh-thinking-effort

English | 简体中文

Bring back the thinking-intensity picker (推理等级 / reasoning effort) for third-party API keys in DeepSeek Harness.

You plug an OpenAI-compatible relay into dsh, pick one of its models, and the 「推理等级」 pane in the composer is simply gone. This plugin declares the missing capability for those routes, so the picker comes back — from a slash command, from a settings tab, or by asking the model.


The problem, precisely

A third-party provider is a providers.<route> profile on the llm-pi-ai adapter:

# $DSH_HOME/profiles/<profile>/cordis.patch.yml
- id: llm-pi-ai
  name: '@deepseek-ai/dsh-llm-pi-ai'
  config:
    providers:
      qiyuan:
        apiKeyEnv: QIYUAN_API_KEY
        api: openai-completions
        baseURL: https://api.qiyuanapi.cc/v1
        models:
          - id: deepseek-v4.1
            contextWindow: 1000000

Nothing in that flow writes reasoningEfforts, and @deepseek-ai/dsh-llm-pi-ai decides a model's reasoning capability like this:

const efforts = entry.reasoningEfforts
if (efforts === undefined) return { reasoning: base?.reasoning ?? false }

A hand-declared route has no installed-catalog base, so every one of its models resolves to reasoning: false. The adapter then advertises no effort levels, the composer's picker has nothing to render, and a forced effort is refused with UNSUPPORTED_REASONING_EFFORT. That is the whole symptom.

It is not about the key or the vendor: catalog routes keep their shipped capability, and the same relay reached through llm-deepseek works, because that adapter advertises efforts for every route it serves. It is a missing declaration.

What this plugin does

It writes that declaration — in the one place the adapter reads it, through the supported settings seam. No adapter takeover, no HTTP interception, no harness patch. By default it does so automatically at startup: any third-party route with no declaration is filled in, so installing the plugin is the whole setup.

# what it writes, for the route above
providers:
  qiyuan:
    models:
      - id: deepseek-v4.1
        contextWindow: 1000000
        reasoningEfforts:      # ← the declaration the picker reads
          off:
          low: low
          high: high
          max: max
        compat:
          thinkingFormat: deepseek

Four entry points over one shared, unit-tested planner:

Surface How
Automatic Nothing to run: a load-time pass declares every third-party route that has none
Slash command /thinking · /thinking presets · /thinking adapt · /thinking set <provider>/<model> <preset> · /thinking remove …
Settings tab Settings → Plugins → 思考强度 — per-model dropdown, applies instantly
Model tool thinking_effort_status / thinking_effort_set, so "这个模型调不了思考强度" gets fixed conversationally

A load-time diagnostic also names every route that still cannot be adjusted, so a freshly added key is noticed immediately instead of the first time you open the model picker.

Install

# from a checkout
dsh plugin --profile <profile> add /path/to/dsh-thinking-effort

# from GitHub (replace with your fork)
dsh plugin --profile <profile> add github:<you>/dsh-thinking-effort

dsh plugin forwards to pnpm inside the profile directory. A GitHub install pulls sources and pnpm runs prepare (i.e. tsdown) to build lib/. On pnpm ≥ 10 the first git-dependency build is refused; add the package name pnpm prints to the profile's pnpm-workspace.yaml and retry:

allowBuilds:
  dsh-thinking-effort: true

That allowlist authorizes executing this package's code at install time. Only allow source you trust, and prefer pinning a commit: github:<you>/dsh-thinking-effort#<sha>.

Then restart dsh — bundle membership is only read at boot — and check the layer is composed:

dsh --profile <profile> --dump-config    # expect a row named dsh-thinking-effort

On the Windows desktop build the desktop profile is reserved and file-locked by the running app: quit DeepSeek Harness before installing into it.

Usage

Nothing to do. A freshly added third-party key is declared automatically at startup, and the log says which route got which level map:

# dsh-thinking-effort: 自动适配完成。
# ✅ 已自动声明 1 个模型的思考强度档位:
#   路由 qiyuan → passthrough(1 个模型)
#     端点检测为 openai,写入 passthrough
#     档位:off→(不发) minimal→minimal low→low medium→medium high→high xhigh→xhigh max→max
#   这是可以随时改的起点:换档位名或方言用 /thinking set,预览用 /thinking explain。

Then open the model picker in the composer → 推理等级 → Off / Minimal / Low / Medium / High / XHigh / Max. Picking one also saves it as the Agent default, exactly like a built-in provider.

What automatic adaptation decides

It only touches configuration-declared routes — a catalog route already resolves its models' capability from the installed catalog, so a declaration there would only narrow it. For each route it reads the evidence already in the configuration:

  1. If pi-ai already believes the endpoint does not take reasoning_effort (xAI, Z.AI, Moonshot, Together, NVIDIA, Cloudflare, or your own compat.supportsReasoningEffort: false), it writes openai-null-off. Sending xhigh to an endpoint that does not parse the parameter is the one way this plugin could turn a missing picker into a failing request. Your explicit setting outranks the plugin's inference.
  2. Otherwise, if the endpoint host identifies a vendor (deepseek.com, openrouter.ai, DashScope, …), it writes that vendor's preset.
  3. Otherwise it writes autoAdaptPreset, passthrough by default: all seven levels sent under their own names, no dialect pinned. A gateway with its own ladder gets the whole ladder back that way.

It never overwrites a model you already declared, and never touches reasoningEfforts: false — that is an explicit "this model does not reason", and adaptation must not undo it.

Adaptation supplies the declaration; it cannot verify that the endpoint honours these values, because that is not discoverable from configuration. After seeing the log line, check what each level actually sends with /thinking explain, then try a level in a session.

Adjusting it by hand

What it wrote is a starting point you can change at any time:

/thinking
# 路由 qiyuan(自声明路由)
#   - deepseek-v4.1: 可调 · 档位 off / minimal / low / medium / high / xhigh / max

/thinking explain qiyuan/deepseek-v4.1
# 路由 qiyuan · deepseek-v4.1
#   当前声明(未锁方言,pi-ai 会按端点检测为 openai)
#   实际生效的方言:openai(由端点检测得出)
#   compat.supportsReasoningEffort:true
#   每档实际发送的参数:
#     off      (不发送任何推理参数)
#     low      {"reasoning_effort":"low"}
#     ...

/thinking set qiyuan/deepseek-v4.1 deepseek-relay
# ✅ 已写入 qiyuan/deepseek-v4.1 的思考强度声明。

/thinking adapt runs the pass again on demand: /thinking adapt qiyuan --preset deepseek-relay restricts it to one route and names the level map.

To turn adaptation off (this plugin's own profile config):

- id: dsh-thinking-effort
  name: 'dsh-thinking-effort'
  config:
    autoAdapt: false          # only write when /thinking adapt is run
    autoAdaptPreset: passthrough

Dialect presets

Which wire values the endpoint accepts is provider-specific; reasoningEfforts maps selectable level → value sent. But the levels are only half of it: each thinkingFormat is a separate branch that suppresses the generic OpenAI path, so pinning the wrong dialect can make a level do nothing while appearing to work. That is why the presets are ordered safest first, and why only the vendors that need one pin a dialect at all.

Preset Levels Sends Pins Fits
openai-null-off off / low / medium / high reasoning_effort; off sends nothing no Any OpenAI-compatible endpoint — try this first
openai off / low / medium / high reasoning_effort; off → "none" no OpenAI itself, or a relay that accepts none
deepseek-relay off / low / high / max reasoning_effort no A third-party gateway serving DeepSeek models
deepseek off / low / high / max reasoning_effort + thinking:{type:…} deepseek DeepSeek's own endpoint, or a strict clone
openrouter off / low / medium / high nested reasoning:{effort} openrouter OpenRouter and mirrors
qwen off / low / high enable_thinking (+ reasoning_effort) qwen Qwen family
passthrough all seven level name verbatim no Gateways that know pi-ai's own names
custom yours yours — Anything else — pass levels yourself
inherit unchanged unchanged — Only pin --format / --default

off: with no value means "supported, but dispatch sends nothing" — the right spelling when not thinking is the absence of the parameter. Only off may be left empty.

Why deepseek-relay exists. deepseek pins DeepSeek's private thinking:{type:…} parameter, which an OpenAI-compatible relay generally ignores. Ignoring is worse than failing: with thinking switched off, that branch sends no reasoning_effort either, so Off does nothing while a named level looks fine. If Off seems to have no effect on a relay, this is the cause — switch to deepseek-relay or openai-null-off, both of which send only the standard parameter.

/thinking explain <provider>/<model> [preset] prints, for every level, the exact request parameters it will send — plus what pi-ai would have detected had nothing been pinned, and a warning when two levels collapse to the same request. Nothing is written.

Overrides: /thinking set qiyuan/m deepseek --format deepseek --default high.

How it works

settings.update(entryId, { providers: { <route>: … } }) merges, so supplying one route key leaves every other route — and every field of this route it does not own — untouched. The shape of the patch depends on where the route's capability comes from, because mergeLayers replaces arrays wholesale and llm-pi-ai refuses modelOverrides on a route that also carries a models list:

  • a route with a non-empty models list (every hand-declared third-party route) → the list is rewritten in full, carrying every entry and every unowned field through;
  • a route inheriting the installed catalog → a modelOverrides entry, a dict that merges and never narrows the served catalog.

One side effect is worth knowing before you read the diff. The rewritten models array is built from the resolved section, so schema defaults the resolver had already materialized ride along into the stored document — after declaring a level on a hand-declared route, that model entry typically gains compat.chatTemplateKwargs: {} and compat.chatTemplateArgs: {} beside the thinkingFormat this plugin actually wrote. They are the schema's own defaults, so the resolved value is unchanged and nothing behaves differently; the document just becomes explicit about fields it previously left implicit. The write stays narrow at the route level: only the route you named appears in the user layer, and no route-level default is materialized.

Writes are revision-fenced against the descriptor read in the same pass (a concurrent editor is reported, then retried once against a fresh read), and a declaration that is already in force writes nothing at all. That is what makes automatic adaptation idempotent: the first boot writes, every later boot is a zero-write no-op, and it needs no memory of what it wrote — an in-force declaration is itself what leaves it with nothing to do.

Adaptation and the diagnostic run at the same injection point, adaptation first, so a log line saying "every model is adjustable" describes the state the pass just produced rather than one about to arrive. The settings service can register before the pi-ai adapter does, so a pass that finds no descriptor retries up to five times at half-second intervals before giving up and pointing at /thinking adapt.

The settings entry id is taken from the adapter's own configurable-provider directory (listConfigurableProviders().settingsNs), so a profile that renames the loader entry still works; llm-pi-ai is only the fallback.

What it deliberately does not do

  • No route-level removal. remove strips reasoningEfforts and the thinkingFormat this plugin wrote. It leaves reasoning / thinkingBudgets alone: update merges, JSON has no undefined to merge in, and unsetting fields this plugin does not own would need a path op against somebody else's config.
  • No HTTP-layer rewriting. There is no supported interceptor for the request body, and mutating the frozen call config throws. Declaring the capability is the supported path.
  • No probing of the endpoint. Automatic adaptation judges only the evidence already in the configuration: it neither spends your API key on test requests nor infers a dialect by watching which level has an effect. A single request cannot tell "the parameter was ignored" apart from "the parameter worked", so that conclusion is yours to draw in a session; renaming a level is one /thinking set.

Development

pnpm install
pnpm typecheck     # tsc --noEmit
pnpm build         # tsdown → lib/index.js + lib/client.js
pnpm test          # node --test over the pure modules and the build output
pnpm verify        # all three

pnpm test runs without a dsh installation. test/smoke.test.mjs registers a resolver hook (test/resolve-hook.mjs) that redirects @deepseek-ai/dsh-tools to a faithful local stub: the real package resolves a chain of harness-internal peers a plugin repo has no business installing, and defineTool is an identity helper at runtime — its real signature still type-checks from the devDependency. The hook is registered inside the test file itself, so pnpm test behaves the same whether or not the runner forwards its own flags to per-file child processes.

Layout:

src/
  index.ts        host entry: Config, registrations, load-time adaptation + diagnostic
  presets.ts      thinking levels, wire dialects, the validator (pure)
  plan.ts         declaration → settings patch (pure, shared with the browser)
  auto.ts         which preset an undeclared route gets (pure)
  diagnose.ts     capability report (pure)
  wire.ts         what each level actually puts on the wire (pure)
  types.ts        stored/read shapes, the settings port, dialect constants
  json.ts         isPlainObject / deepEqual / omit (dependency-free)
  apply.ts        the settings port: read descriptor, plan, merge the write
  commands.ts     /thinking, including the `adapt` batch entry
  tools.ts        thinking_effort_status / thinking_effort_set / thinking_effort_explain
  format.ts       shared text rendering
  client/
    index.ts      browser half entry: inject / apply
    card.ts       the settings tab
    styles.ts     its stylesheet
    types.ts      structural types for the two browser services used
test/             unit tests + a smoke test over the build output

Working notes for anyone (human or agent) about to change the code — the invariants, the build constraints, and a verified pnpm verify baseline — live in AGENTS.md (简体中文).

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit c8e0ff89ba5b

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout