misswell/dsh-advanced-provider-settings

dsh-advanced-provider-settings

Advanced provider configuration UI for DeepSeek Harness (DSH): headers, User-Agent, retry policy, timeouts, vision, reasoning and compatibility — without editing settings.yaml by hand.

未分類deepseek-harnessdshllm

Advanced Provider Settings for DeepSeek Harness

English · 简体中文

Advanced Provider Settings is a WebUI plugin for DeepSeek Harness (DSH) that turns the hidden half of an OpenAI-compatible provider into something you can see and edit. Instead of hand-editing the profile YAML (~/.dsh/profiles/<profile>/cordis.patch.yml), you get a real control panel — on its own settings page and on every provider card — and it is built around the model: pick one, then give it its own input modalities, reasoning-effort mapping and compatibility flags (and, on a route served by the installed catalog, its own name, context window and output-token cap). Everything that can only apply to the provider as a whole — request Headers, User-Agent control, Retry Policy, Timeout and transport, Vision image budgets, Reasoning thinking levels, route-level Compatibility flags — sits in provider-level cards that each say why they are not on the model. All of it is written through DeepSeek Harness's own revision-fenced settings transport, so your YAML keeps its comments and every field you never touched stays exactly as it was.

It exists above all for the case DSH serves worst out of the box: pointing Harness at a third-party DeepSeek endpoint — a cloud gateway, a corporate proxy, a self-hosted vLLM or llama.cpp behind nginx — where nothing in the URL tells the adapter that the models behind it think in DeepSeek's dialect. Compatibility is detected from the provider id and the base URL, so such a route silently gets OpenAI-shaped defaults: the thinking level you pick is shaped in a way the endpoint does not understand, the output cap is spelled with the wrong field name, and a request field your vendor does not implement is sent anyway. This plugin lets you declare the dialect per route and per model, set the thinking ladder and its token budgets, and see what will leave the machine before a request fails halfway through a session.

Status: v0.4.8. Verified against DeepSeek Harness 0.2.0-rc.2, 0.2.1-alpha.4 and 0.2.1-rc.2 (none changes what this plugin reads).


Table of contents


Why this exists

DeepSeek Harness exposes a large, well-designed provider schema, and its Models page deliberately edits only the common fields: endpoint, credential, model list. The rest — the fields that decide whether a flaky gateway succeeds on the third try, whether a corporate proxy accepts your client, whether an image-heavy prompt is rejected before it is sent, and whether a DeepSeek-compatible endpoint ever receives the thinking level you asked for — live in that YAML and nowhere else.

That last one is the sharpest case, because it fails without saying anything. thinkingFormat, supportsReasoningEffort and maxTokensField are detected from the base URL, and only a URL containing deepseek.com is detected as DeepSeek. Every other host that serves DeepSeek models — Ark, a cloud aggregator, your own vLLM behind a reverse proxy — is treated as OpenAI itself: the thinking parameter is built in OpenAI's shape, max_completion_tokens is sent where the endpoint wants max_tokens, and store is sent to servers that reject it. Nothing errors at configuration time; the request simply does not do what the level you selected said it would.

This plugin surfaces those fields without touching a single line of Harness source, without patching anything in node_modules, and without owning a second copy of your configuration. It adds UI through the Models page's declared extension slots, and it reads and writes the llm-pi-ai settings namespace through the framework's public settings API.

At a glance

  • Thinking levels you can actually land. The full ladder — off, minimal, low, medium, high, xhigh, max — as a route default and as a per-model map that decides the exact string each level becomes on the wire, plus the per-level token budgets.
  • Third-party DeepSeek, declared by hand. thinkingFormat: deepseek, reasoning_effort support, the max_tokens spelling, DeepSeek-style reasoning replay and the fields your gateway rejects — all switchable per route and per model, because URL detection will not do it for you.
  • Two per-model channels, correctly separated. A route that declares its own models list is edited in place; a route serving the installed catalog is edited through modelOverrides.<id>.
  • Not only reasoning. Headers, User-Agent, retry policy with a drawn backoff curve, timeouts and transport, image byte budgets, cache retention and all 26 compatibility flags — each labelled with what it changes and filtered to what your protocol actually reads.
  • Safe by construction. Path-addressed writes through the revision-fenced settings transport, no DOM scraping, no Harness source modification, no credentials in the preview.

What you can configure

Configuration has two levels: a model setting is edited one model at a time, and a provider setting applies to every model on the route. That split is not a UI preference — it is the Harness schema. A model entry created by the route's own models list accepts only input, reasoningEfforts and compat; a route that serves the installed catalog is configured per model through the schema's other channel, modelOverrides.<id>, which accepts six fields. Everything else — headers, retry, network, image byte budgets, cache retention — exists only at route level, and a model entry that sets it is rejected by name.

AreaLevelWhat you get
Per modelModelFor the model you select: what input it accepts (text / image), the thinking level → wire value map it actually sends, and its compat overrides — each flag showing the route value it falls back to. On a catalog route the same editor also covers the model's display name, context window and max output tokens, because nothing else edits that channel.
HeadersProvider / globalPer-provider and global request headers, with validation, secret masking and duplicate detection.
User-AgentGlobalPresets (Chrome, Safari, Firefox, opencode, Codex CLI, Claude CLI) plus free text, for gateways that whitelist clients.
Retry PolicyProviderHarness default / Conservative / Aggressive presets, or a custom policy: mode, max retries, retryable codes, initial delay, max delay, jitter.
NetworkProviderTransport (sse, websocket, websocket-cached, auto), request timeout, stream idle timeout, WebSocket connect timeout, cache retention.
VisionProviderThe fallback input type for models that declare none, max image bytes per request, pixel budget, max bytes per image — with human units (MiB) instead of raw byte counts.
ReasoningProviderThe route's default thinking level (off … max) and the per-level token budgets. What each level becomes for a given model is set under Per model.
CompatibilityProviderAll 26 compat flags, filtered to the ones your route's protocol actually reads — including the thinking dialect (openai, deepseek, openrouter, qwen, chat-template, …) and the field that carries a thinking budget. The model-level half lives under Per model and is filtered harder, because a mismatch there is a hard error in Harness.
Test ProviderProviderInterrogates the endpoint's model list using the headers currently in the form, saved or not — the fastest way to prove a whitelist header works.
Effective configurationProviderA preview of exactly what the provider will send, layer by layer, with sensitive values masked.
DiagnosticsGlobalRead-only compatibility report you can paste into a bug report, plus one-click import from the retired dsh-custom-provider-settings plugin.

Install

Requires DeepSeek Harness 0.1.7–0.2.1 or a compatible build, and Node.js 20+.

Both profiles run the same artifact — one package, no build on your machine. What differs is who is allowed to write to the profile.

Web UI — the web profile

dsh plugin manages this profile, and both sources work:

# Straight from the repository — no build step
dsh plugin --profile web add github:misswell/dsh-advanced-provider-settings

# From a GitHub release tarball (a fixed, content-hashed artifact)
dsh plugin --profile web add https://github.com/misswell/dsh-advanced-provider-settings/releases/download/v0.4.0/dsh-advanced-provider-settings-0.4.0.tgz

Both work without a build on your machine. lib/ is committed on purpose: dsh plugin runs no build step, and pnpm 10+ blocks a git dependency's prepare script, so a plugin that needs building must ship its output. The package is not published to npm, so a bare-name install is not offered.

A profile created by the DSH CLI is a pnpm workspace root, and pnpm refuses a first dependency at the root: ERR_PNPM_ADDING_TO_ROOT. Add once to that profile's .npmrc and the install succeeds — ignore-workspace-root-check=true. The failure names the file it wants.

Then restart the web UI:

dsh web

Desktop app — the desktop profile

The app owns this profile, and the CLI refuses to touch it:

$ dsh plugin --profile desktop add github:misswell/dsh-advanced-provider-settings
error: profile "desktop" is managed exclusively by the Electron application

Install it from inside the app instead: open Plugin Marketplace and install misswell/dsh-advanced-provider-settings, then restart the app. The app writes exactly what the CLI writes for a web profile — a dependency in ~/.dsh/profiles/desktop/package.json and an entry in that manifest's dsh.profile.bundles list, the entry that makes the plugin a profile layer rather than a plain dependency. (If the app has no Plugin Marketplace page yet, install dsh-plugin-marketplace first; the marketplace is itself a plugin.)

If an install ends with spawn pnpm ENOENT, the app was launched with a minimal PATH — what a Finder- or Dock-launched macOS app inherits. Make a pnpm that matches the profile's store visible to GUI apps (launchctl setenv PATH …, then relaunch) and install again.

Uninstall

dsh plugin --profile web remove dsh-advanced-provider-settings

In the desktop app, remove it on the same Plugin Marketplace page.

Removing the package removes the UI, and with it this plugin's own settings namespace: on 0.1.7 a namespace is a profile entry's Config, so dropping the entry drops the global header list and the UI preferences stored under it. Your provider configuration is untouched — the advanced fields keep working, because they were written to Harness's own llm-pi-ai entry, which this plugin never owned. Both live in the active profile's user layer, ~/.dsh/profiles/<profile>/cordis.patch.yml; to clear the fields this plugin set by hand, delete the providers.<id> keys under the llm-pi-ai entry (or use Reset all advanced settings on each provider card first, which shows what is about to go).

Using it

  1. Open Settings → Provider Advanced, expand Provider configuration, and pick a provider; or open Settings → Models and expand any OpenAI-compatible provider card. Both surfaces edit the same configuration.
  2. Both disclosure levels start folded: the three cards on the settings page (global headers, provider configuration, diagnostics) and every section inside the editor. Each card header carries its own status chip — 2 items, Always retrying, high, Custom, Default — so you can see where configuration lives, and how much, without opening anything, and expand only the card you came for.
  3. Edit, then Save. Changes are written as path-addressed operations against the namespace revision you were reading, so a concurrent edit is refused rather than silently clobbered.
  4. The Provider Advanced page also owns the two cross-provider surfaces: the global header list every request carries, and diagnostics.

Reading the controls

  • Pick the model first. The top of Per model is a row of model pills: an IMG mark means that model declares image input, and a dot on the right means the model already carries overrides. A filter box appears past 8 models. You edit one model at a time; the editor header shows its id and offers Clear every override on this model.
  • Which channel is being edited is stated, not implied. A route that declares its own models list is edited in place; a route served by the installed catalog is edited through modelOverrides.<id>, and the card says so — with the picker listing the catalog's live models and naming the layer that failed if the listing could not be read. An override whose model has since left the catalog still appears, marked, so it can be cleared rather than silently kept.
  • A control that is missing from the model is one Harness refuses. On a listed model the entry accepts only input modalities, the reasoning-effort mapping and compat, so headers, retry, network, image byte budgets and cache retention appear at provider level only — and the model editor states that boundary at its foot instead of offering buttons that would be rejected on save. Name, context window and max output tokens are editable on a catalog override but deliberately left to the Models page on a listed model: one path, one writer.
  • One row is one setting. A card holds its rows separated by a hairline, and each row is a label, at most one line of explanation, and one control. A row whose value you set yourself carries an accent bar on its left edge — that bar is the only per-field status mark, so a warning still stands out from it.
  • The unit lives inside the number box, and the inherited default is the placeholder rather than a second widget. Inherit appears only on a row that actually overrides something, and it removes the override so the value tracks Harness again instead of being pinned to today's number.
  • A model-level compatibility flag names what it inherits. When the model does not override a flag, its explanation ends with Inherited from the route: Supported, so you never have to switch back to the provider-level list to compare.
  • Durations and budgets are said out loud: 2 min, 5 min, 10 MiB. A value that is off by a factor of 1000 stops looking plausible. The pixel budget is the one row that restates its own value, because turning it into ≈ 1448 × 1448 pixels says something the box cannot.
  • A coloured box always means a decision is needed. Explanatory prose is plain text; only warnings, blocking errors and the result of an action get a fill.
  • Thinking levels are pills, and the fold is stated in words. xhigh and max are accepted by the schema but folded to high before the request leaves, and the row says so rather than offering a granularity that does not exist. A model that does not reason at all — reasoningEfforts: false, a model-level value with no provider-level equivalent — is declarable from the same editor, and Inherit clears it again.
  • The backoff curve draws the retry policy: one bar per attempt, sized by the real delay, with the total wait. 4 retries, 500 ms, doubling, capped at 8 s is a shape, and it is easier to sanity-check as one.
  • Compatibility flags are grouped by what they change — request fields, streaming, reasoning, tool calls, caching — each with a plain-language name and a one-line explanation. The wire identifier is kept as a monospace subtitle so you can still match it against provider docs, and a filter appears once the list is long enough to need one. Three states are kept per flag: Inherit is not the same as Off, because it leaves the decision to the adapter.

Thinking levels, in depth

Everything about reasoning in Harness is two-layered, and the two layers answer different questions: what level do we want and what does this endpoint call it. This plugin exposes both.

The ladder

off → minimal → low → medium → high → xhigh → max. The last two are accepted by the schema but folded onto the high budget before the request leaves, so only high ever needs a budget of its own; the Reasoning card says this in words next to the pills. Budgets exist for minimal, low, medium and high (Harness defaults: 1024, 2048, 8192, 16384), and a budget is clamped to the room the request leaves for an answer — a 32000-token budget on a request whose output cap is 8000 becomes 8000.

A budget reaches the wire only if the route says which field carries it: pick Budget field name (thinking_token_budget, thinking_budget, thinking_budget_tokens) or the alias switch in the Compatibility card, because no adapter can guess it. Servers that express the budget inside their chat template instead are served by the chat-template dialects.

The route default

Reasoning → Thinking level sets the level this route uses when a request does not name one. It is a route-wide default, and Harness validates it against the model that actually receives the request: a level a model pins as unsupported is refused with UNSUPPORTED_REASONING_EFFORT rather than downgraded. On a route that mixes thinking and non-thinking models, leave it on Inherit and let the per-model maps decide (see behaviour 4).

The per-model map

Per model → Model level mapping is one box per level, and the box is the exact string that level sends. Each box is followed by the state that level is in — sends low, Not supported, Inherit, sends nothing — because a blank box cannot say it on its own: a level left blank is pinned unsupported for that model, so choosing it is refused by name instead of being silently dropped, while leaving every box blank keeps whatever the catalog declares. The consequences are worth stating precisely:

What you leave in the mapWhat it means
low: low, high: highThose levels exist for this model and send exactly those strings.
a level left blankPinned unsupported: the level is hidden from the model's effort list and a request naming it fails with a named error.
off left empty (the Offered, sends nothing switch on)off stays selectable instead of being pinned unsupported. The dialect then expresses "do not think" its own way.
off given a stringoff is offered and sends that string as the thinking parameter.
reasoningEfforts: false (No reasoning)The model is declared non-reasoning: Harness reports no reasoning capability at all, and asks for nothing. Model level only — the schema has no provider-level equivalent.

The map is not free-form: Harness refuses a map that is empty, that offers no level beyond off, that maps anything but off to nothing, or that contains an empty string. The editor's grid cannot produce those shapes, and the draft check names one if a hand-edited YAML has it.

What the dialect does with it

The chosen level goes through your compat switches, which is where "third-party DeepSeek" is won or lost. With thinkingFormat: deepseek and supportsReasoningEffort: true, a request asking for high carries thinking: {type: "enabled"} together with reasoning_effort: "high"; a request asking for nothing carries thinking: {type: "disabled"}, the dialect's way of saying so. With the default openai format the same level becomes a bare reasoning_effort, and "no thinking" is the absence of any reasoning field — which is why a DeepSeek-compatible endpoint that requires an explicit disabled marker must be told which dialect it speaks.

Worked examples

Example 1 — a third-party DeepSeek endpoint

The situation. An OpenAI-compatible endpoint serves DeepSeek models: deepseek-reasoner thinks, deepseek-chat does not. Its host is a cloud gateway or a corporate proxy — nothing like api.deepseek.com — so Harness cannot recognise what is behind it.

Why it needs configuring. Compatibility is detected from the provider id and the base URL. An unrecognised URL is treated as OpenAI itself, which is wrong in five specific ways here:

Assumed for an unrecognised URLWhat a DeepSeek-compatible endpoint expects
thinkingFormat: openai — a bare reasoning_effortthinkingFormat: deepseek — an explicit thinking marker plus reasoning_effort
maxTokensField: max_completion_tokensmax_tokens, the spelling DeepSeek's own API and most mirrors use
supportsStore: true — sends storeDeepSeek's own API does not take store — Harness already treats it as non-standard — and most mirrors follow
supportsDeveloperRole: true — sends instructions as developerDeepSeek-style servers expect instructions as system
requiresReasoningContentOnAssistantMessages: falseservers that replay DeepSeek turns expect an empty reasoning_content alongside them

Step 1 — declare the route on the Models page. Identity, endpoint, credential and the model list belong to DSH's own page; the plugin deliberately does not edit them.

# Settings → Models
displayName: Third-party DeepSeek
apiKeyEnv: THIRDPARTY_DEEPSEEK_API_KEY   # the name of a credential, never the key
api: openai-completions
baseURL: https://llm-gateway.example.com/v1
models:
  - id: deepseek-reasoner
    name: DeepSeek R1
    contextWindow: 131072
    maxTokens: 65536
  - id: deepseek-chat
    name: DeepSeek V3

Because this route declares a models list, that list replaces the installed catalog and the per-model channel is those entries themselves — the Per model card edits them in place, addressed by position.

Step 2 — Compatibility, at route level. The mismatches above, as six switches:

  • Reasoning wire shape → DeepSeek
  • Understands reasoning_effort → Supported
  • Output cap field → max_tokens, legacy
  • Sends store → Not supported
  • Uses the developer role → Not supported
  • Replay empty reasoning → Supported

Step 3 — Reasoning, at route level. Leave Thinking level on Inherit here: the two models do not share a ladder, and a route default a model cannot take is refused at request time. Set the budgets instead — they are route-wide and safe:

  • Low 2048, Medium 8192, High 32768

Step 4 — Per model. Select deepseek-reasoner and fill the mapping grid:

LevelBoxMeaning
OffOffered, sends nothing switch onoff stays selectable; the dialect expresses it as thinking: {type: "disabled"}
Lowlowsends reasoning_effort: low
Mediummediumsends reasoning_effort: medium
Highhighsends