DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

joslynSmall /

joslynSmall/dsh-plugin-reasoning-effort-sync

Verified

Synchronize DeepSeek Harness model reasoning-effort levels with gateway metadata and models.dev

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@5d3a8f28

dsh-plugin-reasoning-effort-sync

Keep a DeepSeek Harness model route's reasoning-effort levels in sync with what its gateway already advertises, so adding a route or a model stops meaning hand-writing a reasoningEfforts block per model.

中文

The problem this solves

DSH's effort picker is driven by per-model reasoning metadata. For a route that @deepseek-ai/dsh-llm-pi-ai serves from pi-ai's installed catalog, that metadata comes for free. For a route you declare yourself — any self-hosted or relayed gateway — it does not: the route name is unknown to the catalog, models replaces the catalog rather than extending it, and a model entry that omits reasoningEfforts is reported as a non-reasoning model. The effort picker then hides its Effort row, which is correct behaviour and a silent one.

The levels are not actually unknown: your gateway publishes them at GET {baseURL}/models. This plugin reads them from there and writes them into the profile patch.

Requirements

  • DSH 0.2.0-rc.2 (the peerDependencies range is checked at install time).
  • A route under an llm-pi-ai row in the profile's cordis.patch.yml — the plugin annotates that row, and only routes declared in the profile layer are addressable.
  • Levels from at least one of two sources. The route's own endpoint is asked first: reasoning_efforts (a list) or reasoning_fixed_effort (a single level) on each entry of GET {baseURL}/models. For the models it does not describe, the plugin consults models.dev and offers only the levels that every declaration of that model name agrees on. A model neither source can place is left alone and still needs a hand-written block.

Install

From the Web GUI's Plugins page, install by registry name, absolute path, Git address, or tarball, then enable the bundle:

C:\documents\dsh\dsh-plugin-reasoning-effort-sync

The Desktop profile is managed by the Electron application, so dsh plugin --profile desktop … is refused; use the Plugins page. The plugin ships no lifecycle scripts, so no build-script approval is needed.

Configuration

The bundle's patch registers one row with these defaults:

- id: reasoning-effort-sync
  name: dsh-plugin-reasoning-effort-sync
  config:
    enabled: true
    syncOnLoad: true
    intervalMinutes: 0        # 0 disables the periodic sync
    requestTimeoutMs: 10000
    dryRun: false
    routes: {}                # empty means every route the pi-ai row declares
    modelsDev:                # fallback source, read only for models the endpoint does not describe
      enabled: true
      url: https://models.dev/api.json
      ttlHours: 12
      timeoutMs: 30000
      minShare: 0.5           # agreement the dominant declaration needs before it is trusted
Field Meaning
enabled Master switch. Off means no request and no write.
routes Empty processes every route. A populated dict is an allowlist: name a route to include it, and enabled: false under that name to exclude it.
syncOnLoad Sync once when the profile loads.
intervalMinutes Also sync on this interval. 0 (the default) disables periodic sync; load and configuration-change sync remain active.
requestTimeoutMs Deadline for one listing request.
dryRun Compute and log the result without writing. Start here.
modelsDev.enabled Consult models.dev for models whose endpoint published no levels.
modelsDev.url Dataset address. The default is models.dev's full dataset, about 5 MB.
modelsDev.ttlHours How long one fetched dataset is reused in memory.
modelsDev.timeoutMs Deadline for that one fetch.
modelsDev.minShare Agreement the dominant declaration needs. 1 demands unanimity (kimi-k3 would then be skipped), 0 accepts any plurality, 0.5 accepts a majority.

The load-time sync is not awaited, so an unreachable gateway never delays a profile. The plugin observes app-boot/config-reload, emitted after DSH finishes reconciling a profile save, and synchronizes when the addressable pi-ai configuration changes. A channel or model added in the Models page is picked up without restarting DSH or refreshing the page. Unrelated or unchanged profile saves do not fetch model metadata. Changes arriving during a sync are coalesced into a subsequent serial pass, so a slow gateway cannot discard a new model. Every pass is diff-first: a sync that resolves nothing new writes nothing. When HMR is present, sync work starts outside the profile transaction's async context, so its later config-editor write can queue normally rather than being rejected as nested.

Where the levels come from

  1. The route's endpoint, when it names them. It describes the very gateway being configured, so its answer is final.
  2. models.dev, for the models the endpoint did not describe. The same model name is declared differently by dozens of providers there — deepseek-v4.1-flash had 15 distinct level sets across 75 declarations when this was written — so one rule has to pick. The offered levels are the dominant declaration: the exact set the largest number of declarations agree on, taken only when at least modelsDev.minShare of them agree and at least two providers say it. A weaker or tied plurality falls back to the intersection, so lowering minShare can only add levels some declaration already named.

Two details make that rule work in practice:

  • "Off" levels are dropped before anything is compared. none, off, disabled and false say "do not reason", which the picker's Default row already expresses by sending no parameter. Counting them would duplicate that row and let a reseller recording only "off" veto every real level. Dropping them also merges declarations that differ only in whether they mention it — which is why deepseek-v4.1-flash resolves to low, high, max, the very set the unrelated WorkBuddy gateway declares authoritatively for that name.
  • A weak or tied plurality is refused. Under-offering is recoverable; a level the upstream rejects is a hard failure at request time. A model neither rule can place is left untouched, so some names still need a hand-written block even with the fallback on.

What it writes

Before:

providers:
  workbody:
    apiKeyEnv: WORKBODY_API_KEY
    api: openai-completions
    baseURL: http://127.0.0.1:8788/v1
    models:
      - id: hy3
        contextWindow: 192000
        maxTokens: 64000

After a sync against a gateway advertising reasoning_efforts: [low, high] for hy3:

    models:
      - id: hy3
        contextWindow: 192000
        maxTokens: 64000
        reasoningEfforts:
          low: low
          high: high

Every other key on the row — apiKeyEnv, api, baseURL, compat, headers, routes this sync did not touch, and every model field it did not change — is preserved as written. The write goes through the config editor, so it is validated, serialized against profile changes and hot reloads, atomic, and rolled back if the Host rejects the result.

Level names

Known levels are emitted in DSH's escalation order (off, minimal, low, medium, high, xhigh, max) and anything else the gateway names is emitted after them, alphabetically. Values are the levels' own spellings.

A "do not reason" level is normalized. off, none, disabled and false are written as a valueless off: key, which pi-ai dispatches by sending no parameter at all. The literal wire value is deliberately never emitted: reasoning_effort: "off" is not the same request, and gateways have answered 400 for it.

What it will not do

  • It never adds or removes a model. Only entries already present in models are annotated, so the catalog stays yours to decide.
  • It never writes a default effort. DSH's defaultEffort is a route-level single value, and models on one route disagree about their levels — writing one would make every unsupported model fail with UNSUPPORTED_REASONING_EFFORT. The picker's "Default" row means "send nothing", which is the model's own default.
  • It never guesses. A model the endpoint does not mention is untouched, and reasoningEfforts: false is treated as your explicit pin and left alone.
  • It does not replace dsh-llm-pi-ai. Transport, compatibility quirks, and image handling all stay with the adapter.

Once a route is in scope, its models' reasoningEfforts blocks are managed by this plugin. That is the trade for not writing them by hand.

Failure behaviour

Routes are atomic. Each route's own resolution is written; a route that resolves nothing is left exactly as it was. One misbehaving gateway therefore cannot suppress the work for every other route — which matters as soon as two of them are configured.

Both sources are best-effort, and neither can fail the sync:

  • An endpoint that cannot be reached, refuses the credential, or answers something unreadable is recorded as a note, and that route falls through to models.dev.
  • A models.dev fetch that fails is recorded as a note, and the endpoint-derived work still lands.

Every sync outcome is logged as up to date, annotated N model(s), or the reason it did neither. A route declaring no baseURL or no models list is skipped with a note: it is reached through pi-ai's installed catalog, whose own levels this plugin cannot see and must not narrow.

An annotation write updates pi-ai through the configuration editor and emits the same profile-reload event. A subsequent pass finds no changes and stops. At commit time the plugin annotates the editor's latest model list, retaining concurrent additions, removals, renames, and explicit reasoningEfforts: false pins. Results fetched for a channel whose connection configuration changed are discarded; the queued pass resolves its current endpoint. Unloading the plugin stops pending passes and cancels in-flight work before its editor callback derives a commit. Once that callback returns a changed configuration, the Host owns completion of the serialized transaction.

Troubleshooting

Symptom Cause
... answered 401; check the route's credential apiKeyEnv names a reference the credential store does not hold.
no "llm-pi-ai" configuration row is addressable The profile has no llm-pi-ai row, or its id is not unique.
route "x" declares no baseURL Add baseURL to the route, or give the model entries one.
route "x" declares no models list The route relies on pi-ai's installed catalog; there is nothing to annotate.
Nothing logged at all Check the row's enabled, and that the bundle is selected in the profile's dsh.profile.bundles.
incompatible-version at install The DSH runtime and this package's peer range disagree. Grant the pair explicitly — plugin_manager's allow-version, or dsh plugin --profile <profile> allow-version <package@version> --dsh-version <runtime> --accept-risk — after reading the risk warning. Exemptions do not survive a DSH upgrade.

Development

pnpm install
pnpm typecheck     # tsc --noEmit
pnpm test          # node --test "tests/*.test.mjs"
pnpm build         # esbuild -> lib/index.js

src/listing.ts, src/merge.ts, src/options.ts, src/routing.ts and src/sync.ts import no Host package, so the test suite drives them directly and with doubles, without the Host or a network. src/config.ts is the only module that loads a peer.

The single dependency at runtime is @deepseek-ai/schemastery, declared in peerDependencies. It stays external in the bundle and is deliberately not a dependencies entry: DSH routes a package name listed in a plugin's peerDependencies to its own installed copy, and a bundled or locally installed second copy would put two schema builders in one process.

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 5d3a8f2897bd

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