dsh-openrouter-models
English · 简体中文
An OpenRouter model catalog and multi-key router for DeepSeek Harness. It adds one settings page where you can:
- browse OpenRouter's live model catalog (hundreds of models, changing with the upstream), search by name / id / vendor, filter by input or output price, toggle free only, and sort by price or context length;
- keep several OpenRouter API keys as named "accounts", each bound to its own subset of models;
- get one native model route per account (
openrouter-<account>), so each key shows up separately in the model picker and in/model.
The plugin is deliberately self-contained: no runtime dependencies, no build
step, and no imports from @deepseek-ai/*. The whole thing is the six files
in lib/.
Why another OpenRouter plugin
OpenRouter speaks the OpenAI Chat Completions protocol, which Harness's
llm-pi-ai service already drives. So instead of shipping a private streaming
adapter, this plugin declares one ordinary llm-pi-ai route per key. You get
the native picker, default-model selection, retries, replay handling, and
credential resolution for free — and the plugin never handles token streams or
speaks to the network on a request path.
Because a route resolves its key from its own credential reference, separate routes are exactly the host's mechanism for several keys against one provider.
Install
Requires DeepSeek Harness 0.1.5-rc.2 or newer and Node.js 20+.
Desktop app — open Settings → Plugins and install dsh-openrouter-models
once it is on npm, or quit the app completely (tray included) and run:
# from npm (once the package is published)
dsh plugin --profile desktop add dsh-openrouter-models
# or straight from GitHub — no npm release needed
dsh plugin --profile desktop add github:type53/dsh-openrouter-models
# or from a local checkout you are editing
git clone https://github.com/type53/dsh-openrouter-models.git
dsh plugin --profile desktop add link:/absolute/path/to/dsh-openrouter-models
The CLI refuses to touch the desktop profile while the app is running, so quit
it first. For a CLI-only profile (web, headless, or a custom name), use that
name instead of desktop.
Restart DSH after installing — the host half mounts only at startup. Then open Settings → OpenRouter 模型 (the panel is Chinese today; see Internationalization).
Usage
- Create an account and name it (for example
mainorbackup). The account id is derived from the name and becomes part of the route, so an ASCII name works best; a purely non-ASCII name falls back toaccount. - Paste the key and save. It is written only to the harness credential
store as
OPENROUTER_KEY_<ACCOUNT>; it never enters the plugin's JSON file and is never sent back to the page. Press Verify key to check quota. - Pick models from the catalog: select an account, then click models to add
or remove them. Filter by name, by an input/output price range in
$/Mtokens, or with free only; sort by price, context, or name. - Use them from the model picker at the bottom of a conversation, under
OpenRouter · <account name>.
A key alone produces no route: llm-pi-ai rejects a hand-declared route with an
empty model list. Bind at least one model to an account for it to appear in the
picker.
How it maps to DSH
One account becomes one hand-declared llm-pi-ai provider:
llm-pi-ai:
providers:
openrouter-main: # account id = main
displayName: OpenRouter · Main
apiKeyEnv: OPENROUTER_KEY_MAIN # credential reference, not the key
api: openai-completions
baseURL: https://openrouter.ai/api/v1
compat: { supportsDeveloperRole: false, thinkingFormat: openrouter }
models: [ ... the models you picked ... ]
- The route is written into the profile's
cordis.patch.yml(the user settings layer), so it takes effect on the next request without a restart. On every boot the plugin re-syncs it from the account file. contextWindow,maxTokens, input modalities, andreasoningEffortsare filled from the live catalog when the route is derived. A copy is stored when you pick a model, so a route built while the catalog is unreachable still declares reasoning support correctly.- Removing an account or clearing its models removes only the route this plugin
owns; other
llm-pi-aiproviders are untouched.
Data and security
| Data | Location |
|---|---|
| Accounts and their selected models | $DSH_HOME/openrouter-models.json (default ~/.dsh/openrouter-models.json), written atomically, safe to inspect, back up, or delete |
| OpenRouter API keys | The harness credential store, reference OPENROUTER_KEY_<ACCOUNT> |
| Derived routes | The profile's cordis.patch.yml |
- Two outbound destinations, both on
openrouter.ai:/api/v1/models(public catalog, no credentials attached) and/api/v1/key(only when you press Verify key, with that account's key). No telemetry, no third-party reporting. - All host routes are registered on Connection's authenticated
/apichannel, so they inherit Host/Origin checks and browser-session authentication. A request from an unauthenticated client is rejected with 401; plainwebServerroutes would not have that fence. - Keys are write-only: the state route reports only whether a key is configured, and verification returns only quota metadata.
See SECURITY.md for the disclosure process.
Development
No pnpm install is needed — the tests run on a bare checkout:
node scripts/unit-test.mjs # pure logic: naming, validation, prices, catalog, route derivation, store
node scripts/host-boot-test.mjs # host half: stub seams + all five HTTP routes
node scripts/client-boot-test.mjs # browser half: slot registration, filter/sort, render smoke test
npm test runs all three. CI runs them on Node 20, 22, and 24.
Source map:
| File | Responsibility |
|---|---|
lib/index.js |
Host half: account → llm-pi-ai route reconciliation, the five HTTP routes |
lib/config.js |
Constants, id / credential naming, account validation |
lib/store.js |
Atomic read/write of the account document |
lib/catalog.js |
Catalog fetch, normalization, price arithmetic |
lib/routes.js |
Pure account → llm-pi-ai route mapping |
lib/client.js |
Settings page UI (hand-written __ModuleLoader__ bundle, no build) |
Client-only edits in lib/client.js apply on a page refresh: the bundle URL is
content-hashed. Host edits need a DSH restart.
Layout note
The settings page is hosted by the platform's settings dialog, which is a fixed 800px panel (188px nav, 24px side padding), leaving roughly 564px of usable width — less in a small window, and a plugin cannot widen it (that CSS belongs to the platform). The layout is designed for that constraint:
- no fixed control widths; inputs fill their grid cell;
- buttons and inputs share one 28px height and one font size, so rows align;
- account settings use a
label | controlgrid with a shared left edge; - filters are a caption-over-control grid that wraps to about four columns;
- accounts stack vertically, one per row;
- model names and ids wrap (
overflow-wrap: anywhere) instead of truncating — OpenRouter ids are long, and the id is what a request actually needs.
Known limitations
- Filters are not persisted; they reset when you leave the page. Accounts and model selections are persisted.
- The filtered list is capped at 600 rows to avoid rendering ~500 items at once.
- Variable-priced models (
openrouter/autoand a few other routers) report-1upstream, meaning "depends on the route chosen". They display as variable, sort last, and match no price range. - All models use one protocol:
openai-completions. OpenRouter serves that protocol for every model on/api/v1, so this does not reduce availability. - Deleting an account, disabling it, or clearing its models removes its
openrouter-<id>route (only that one). - Account ids must match
[a-z][a-z0-9-]*because they become route names and credential references. - A key with no models bound produces no route (see Usage).
Internationalization
The settings page currently ships Chinese strings only, matching the Harness UI this plugin was built against. Extracting strings into a locale layer is a welcome contribution; see CONTRIBUTING.md.
License
MIT © 2026 type53
This project is not affiliated with OpenRouter or DeepSeek. OpenRouter and DeepSeek Harness are the property of their respective owners.
No comments yet. Be the first to write one.