dsh-openrouter-plus
Service-tier routing for OpenRouter — as a DeepSeek Harness plugin, with no patched packages.
OpenRouter prices and schedules by service tier: flex halves token cost in exchange for a
slower pool, while priority / fast / ultrafast pay a multiplier for lower latency. In
@deepseek-ai/dsh-llm-pi-ai that tier is unreachable — provider profiles expose baseURL and
headers, but no request-body fields
(discussion #9787).
This plugin reaches the same wire result from the outside. It runs a loopback relay; a provider
route points at it, and the relay writes the service_tier key into the request body on the way
upstream. Because it only uses schema fields the profile already has, it survives
bun install -g upgrades and never touches ~/.bun/install/global.
model picker
│ baseURL = http://127.0.0.1:8791/flex/v1
▼
dsh-llm-pi-ai ──► dsh-openrouter-plus relay ──► https://openrouter.ai/api/v1
│ injects {"service_tier":"flex"}
└── /__openrouter-plus/status ──► OpenRouter Plus settings page
What you get
- Tier mounts —
/flex/v1,/priority/v1,/fast/v1,/ultrafast/v1,/scale/v1,/auto/v1,/default/v1, plus a/v1passthrough that sends no tier at all. Each mount is abaseURLyou can paste into any OpenAI-compatible provider route. - Its own settings page (Settings → OpenRouter Plus) showing relay state, every mount with a copy button, live request/token counters per tier, the weighted cost multiplier, and recent requests.
- One-click provider routes — the page creates a real
llm-pi-airoute (a clone of an existing one, withbaseURLpointed at a tier mount) so it appears in the model picker like any other provider, and removes it again. Routes the plugin did not create are never touched. - Zero risk to your existing setup — the relay holds no API key, forwards your
Authorizationheader verbatim, binds loopback only, and the plugin's rows are additive.
Install
git clone https://github.com/GnaneshKunal/dsh-openrouter-plus
dsh plugin add ./dsh-openrouter-plus # or, from a session:
# plugin_manager { action: install_bundle, target: "./dsh-openrouter-plus" }
The plugin adds one entry to Settings navigation: OpenRouter Plus, directly after Models. If
the relay is up, the page shows the origin (http://127.0.0.1:8791) and its mounts.
Use it
Through the settings page (recommended)
- Pick a source route (for example
openrouter-custom) and a tier (flex). - Press Create route. The page writes a new provider route named
<source>-<tier>whosebaseURLishttp://127.0.0.1:8791/<tier>/v1and whose models are cloned from the source. - Choose one of that route's models in the model picker. Every request it makes now carries
service_tier: "<tier>". - Remove the route from the same page when you are done. Its deletion is safe and leaves your original route untouched.
By hand
Point any OpenAI-compatible provider route's baseURL at a mount:
- id: llm-pi-ai
config:
providers:
my-flex-route:
api: openai-responses
apiKeyEnv: OPENROUTER_API_KEY
baseURL: http://127.0.0.1:8791/flex/v1
models:
- id: deepseek/deepseek-v4.1-flash
# Optional but recommended — see "Reasoning effort" below.
reasoningEfforts:
off: null
low: low
medium: medium
high: high
max: max
Reasoning effort on a relay route
A relay route is a provider key the installed catalog has never seen, so
@deepseek-ai/dsh-llm-pi-ai cannot inherit a model's reasoning capability for it. Without an
explicit reasoningEfforts the model materializes as reasoning: false and the composer shows
"This model provides no reasoning effort levels." — no effort selector, no error, nothing in the
log. The relay itself is not involved; it never touches reasoning fields.
The plugin fills that gap when it creates a route, from the best evidence available:
- a capability the model already declares (in the source route), kept byte-identical;
- a declaration for another variant of the same id —
google/gemini-4-argon:flexteaches the plugin aboutgoogle/gemini-4-argon; - the configured
reasoningEffortsmap, as a fallback for everything else.
Set reasoningEfforts: false to switch filling off, or omit it to fill only from evidence 1–2.
A model that declares reasoningEfforts: false is itself opted out: the fill leaves it alone.
The fill also re-applies itself. Re-picking a model in Settings → Models rebuilds that row from
the catalog — id, name, context window, max tokens — and drops reasoningEfforts along with
every other hand-declared field. That is the shape of this bug: the effort selector works, you touch
the model list afterwards, and it is gone. The cause is upstream rather than here (adopt() in
@deepseek-ai/dsh-client-ui-settings-models copies a fixed field list), so the plugin watches the
profile instead: a debounced pass runs on every config settle and restores the capability a moment
after the edit lands. Set autoRepairReasoning: false to keep the plugin out of your config
entirely, and use Add reasoning to N models in the settings page (or
POST /__openrouter-plus/routes/repair) when you want the fill on demand.
Either way it only ever touches routes whose baseURL points at the relay, never overwrites a
declared map, never resurrects an explicit false, and writes nothing when there is nothing to do.
Control API
Served on the same loopback listener, outside the proxy path space:
| Endpoint | Purpose |
|---|---|
GET /__openrouter-plus/health |
liveness |
GET /__openrouter-plus/status |
address, upstream, mounts, metrics |
GET /__openrouter-plus/targets |
source routes + routes this plugin created |
POST /__openrouter-plus/routes/create |
{ source, tier, name?, displayName? } |
POST /__openrouter-plus/routes/remove |
{ name } |
POST /__openrouter-plus/routes/repair |
adds missing reasoningEfforts to the routes this plugin owns |
POST requires x-oorp-client: web. CORS is reflected only for loopback GUI origins.
curl -s http://127.0.0.1:8791/__openrouter-plus/status | jq .metrics.byTier
Configuration
cordis.patch.yml ships the defaults; override them in your profile's patch layer:
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
false keeps the plugin installed but binds nothing |
host |
127.0.0.1 |
bind address — loopback only, deliberately |
port |
8791 |
preferred port |
portFallback |
true |
fall forward to the next free port (the page probes 8791–8794) |
upstream |
https://openrouter.ai/api/v1 |
where requests are forwarded, no trailing slash |
routes |
tier mounts | mount path → tier; null tier is a pure passthrough |
upstreamTimeoutMs |
0 |
0 disables the upstream timeout (long SSE completions) |
reasoningEfforts |
false-able map |
levels written onto created routes' models; false disables, omit for evidence-only |
autoRepairReasoning |
boolean, default true |
re-apply that fill whenever the profile changes, so editing a route's models cannot silently strip the effort selector |
Security model
- Loopback only. The relay forwards the caller's
Authorizationverbatim and stores no key. Do not bind a routable interface. - Read-only telemetry. The status API exposes counts and token sums, never prompts or keys.
- Reversible by construction. The plugin only ever adds a provider route whose
baseURLpoints at its own origin, and only removes routes that pass that same test.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Card says relay offline | The port range is busy, or the row is enabled: false. Check the Host log for [openrouter-plus]; set a fixed port. |
| A model errors after disabling the plugin | That model belongs to a route pointing at the relay. Re-enable the plugin or remove the route on the OpenRouter Plus page. |
| Requests work but no tier is applied | The route's mount has a null tier (/v1), or the request is not POST with a JSON body. |
flex still bills full price |
The provider decides the served tier; a pinned tier only requests it. Confirm with usage / the OpenRouter dashboard. |
| No effort selector for a relay route's model | The model has no reasoningEfforts — usually because the model list was edited in Settings → Models afterwards. It is restored automatically; if autoRepairReasoning is off, press Add reasoning to N models on the OpenRouter Plus page. See "Reasoning effort on a relay route". |
Uninstall
Remove the route(s) in Settings → Models, then:
dsh plugin remove dsh-openrouter-plus
Development
node --check index.js && node --check client.js
node --test test/
See docs/ARCHITECTURE.md for the module contract, and
docs/DESIGN.md for the design rationale against the upstream discussion.
MIT.
No comments yet. Be the first to write one.