dsh-provider-proxy

Per-provider and per-model outbound proxy routing for DeepSeek Harness.
DeepSeek Harness applies one proxy policy per process (HTTP_PROXY / HTTPS_PROXY / NO_PROXY / ALL_PROXY). This plugin layers two routing axes on top of it, configurable per provider host and per model ID — from a Plugins settings page, without touching YAML unless you want to.
flowchart LR
A["ctx.llm.stream<br/>{provider, model}"] --> B["llm/stream waterfall<br/>AsyncLocalStorage per pull"]
B --> C["adapter → fetch"]
C --> D["RoutingDispatcher"]
D -->|model rule| E["direct / proxy / inherit"]
D -->|host rule| E
D -->|loopback| F["always direct"]
D -->|no match| G["process policy<br/>env + NO_PROXY"]
- Host axis (
providers): matches the request URL host — exact, subdomains, optionalhost:port. Applies to everyfetch()in the process (LLM, web search, MCP). - Model axis (
models): matches theprovider/modelpair of the model call that issued the request. Exact ids and trailing-*prefixes, with an optional provider pin. - Precedence: provider-pinned exact model → bare exact model → provider-pinned prefix → bare prefix → provider host rule →
default(inheritordirect).
Install
Prerequisite: a DSH profile with a plugin manager (the Desktop app and dsh web both qualify).
From npm (recommended):
dsh plugin --profile <profile> add @apolenkov/dsh-provider-proxy
From git (a release tag, no registry involved):
dsh plugin --profile <profile> add git+https://github.com/apolenkov/dsh-provider-proxy.git#v0.1.0
From a local checkout:
dsh plugin --profile <profile> add /absolute/path/to/dsh-provider-proxy
# or: link the directory in the profile's package.json and run pnpm install there
Then restart DSH. The bundle inserts the provider-proxy row; routes live in that row's config.
npm note: the package is
@apolenkov/dsh-provider-proxy; the unscoped namedsh-provider-proxybelongs to an unrelated package on the public registry, so neverpnpm add dsh-provider-proxy.
Configure
In the UI (recommended)
Open Plugins in the sidebar → open the dsh-provider-proxy bundle → Configure. The page shows:
- Default route (
inheritordirect); - Provider rules: label, hosts (comma or newline separated), proxy (
inherit/direct/ customhttp(s)://URL); - Model rules: model id (exact or trailing
*), optional provider pin, proxy.
Edits are staged locally and applied on Save with revision fencing: if someone changed the config meanwhile, the save is refused instead of silently overwriting. Clearing the form is never written; leaving the page drops staged edits.
In YAML
Add or edit the row in the profile’s cordis.patch.yml (HMR applies it live, no restart):
- id: provider-proxy
name: "dsh-provider-proxy"
config:
default: inherit
providers:
corp:
hosts: ["llm.internal.example"]
proxy: direct
opencode-go:
hosts: ["opencode.ai"]
proxy: "http://127.0.0.1:1301"
models:
"muse-spark-1.3-contributor":
provider: opencode-go
proxy: "http://127.0.0.1:1301"
"longcat-*": inherit
An invalid entry refuses the whole configuration (fail-closed): the plugin logs the reason, keeps the previous routes, and never partially applies them.
Proxy values
| Value | Meaning |
|---|---|
inherit |
Use the launcher’s process proxy policy (env + NO_PROXY). |
direct |
Connect to the origin directly, ignoring the process proxy policy. |
http(s)://… |
Route through that proxy (ProxyAgent). SOCKS and PAC are rejected. |
The UI additionally refuses proxy URLs with embedded credentials — keep secrets out of the profile patch.
Muse Spark example
Route one model through its own proxy while everything else follows the launcher policy:
- id: provider-proxy
name: "dsh-provider-proxy"
config:
default: inherit
models:
"muse-spark-1.3-contributor":
provider: opencode-go
proxy: "http://127.0.0.1:1291"
See examples/models-muse-spark.yaml (sanitized: loopback endpoints, no credentials). The same rule can be created in the Plugins page — the walkthrough is captured with assets/scripts/record-proxy-ui.sh into assets/gifs.
Verify it works
The plugin writes its last apply outcome (never secrets) to $DSH_HOME/provider-proxy/state.json:
{
"appliedAt": "2026-10-22T07:59:22Z",
"pid": 42120,
"status": "active",
"routes": "muse-spark-1.3-contributor→http://127.0.0.1:1301, llm.internal.example→direct, default→inherit"
}
A live rule can be proven by pointing it at a dead proxy (http://127.0.0.1:9) and watching the request fail with ECONNREFUSED 127.0.0.1:9 instead of reaching its target — then restore the real proxy.
Limits
- WebSocket transports are not guaranteed to pass through the undici dispatcher; use an
http/httpstransport for strict routing. web_fetchpins validated addresses through the launcher's own policy and may bypass these routes; model traffic is covered.- Model rules apply only inside
llm/stream; web search and MCP use the host axis. - A model rule never fires for non-model traffic that happens to hit the same host, and a host rule never overrides
localhost/ loopback. - Proxy URLs with credentials are accepted by the transport but rejected by the UI; prefer credential-free proxy endpoints.
Walkthrough

Recorded in a scratch profile with a loopback proxy: open the bundle’s Configure page, add the Muse Spark model rule, trigger and fix a validation error, save. Individual frames live in assets/screenshots/; reproduce or re-record with assets/scripts/record-proxy-ui.sh.
Publishing
Owner-side checklist for the first public release (repository URL, npm scope, market index, hub listing): docs/publishing.md. Automated gates:
pnpm check:release # strict manifest + secrets + generated market index + tests
pnpm release:market # writes market/dsh-market.json from package.json + HEAD
Development
pnpm install --ignore-scripts # or: pnpm install --frozen-lockfile
pnpm test # node:test, 44 tests: matching, precedence, ALS isolation, egress, wiring, volatile updates, agent retirement, client contract, UI form logic
pnpm check # manifest + secrets + market validation + full test suite
See CONTRIBUTING.md (workflow, commit style, release checklist), CODE_OF_CONDUCT.md, and docs/ — an index with architecture, configuration reference, UI guide, publishing checklist, marketplace notes, troubleshooting, and a glossary.
Marketplace
To list this plugin in a DSH market, ship a format-1 index validated with pnpm check:market. A documented example lives in market/dsh-market.example.json; production indexes must replace the example repository URL and commit. Listing on community hubs (e.g. dsh.market) additionally requires the dsh-plugin topic on the GitHub repository or a submission through the hub’s own flow — see docs/marketplace.md.
License
MIT — see LICENSE. DeepSeek Harness itself is a separate project by DeepSeek; this plugin is community work and is not affiliated with or endorsed by them.
No comments yet. Be the first to write one.