dsh-openrouter-live
An OpenRouter provider for DeepSeek Harness (dsh) that reads OpenRouter's live model list, so a model shows up in the picker as soon as OpenRouter serves it.
The harness's built-in openrouter route lists models from a catalog bundled with each release. A model released after that snapshot, such as deepseek/deepseek-v4.1-flash, does not appear in the picker until the next harness release. This plugin adds a second route, OpenRouter (live). It reads OpenRouter's /models endpoint and takes each model's metadata from there.
What it does
-
Live model list. It lists every OpenRouter model that accepts tools, sorted by name, so models from one vendor stay together. Each name ends with the input/output price per million tokens. The list is refreshed every 10 minutes. If a refresh fails, the previous list stays in use.
Model picker with prices
-
Per-model metadata. The context window and output cap come from OpenRouter. The reasoning levels come from the model's own reasoning.supported_efforts, so the composer offers only levels the model accepts, including Off when reasoning is optional.
-
Image input. It inlines images for models whose /models entry lists image input. It follows the harness image budget: images are downscaled, and the oldest images are offloaded when a request grows too large.
-
Reasoning continuity. It sends reasoning_details back on later requests, including signed and encrypted entries. This lets models such as Claude and DeepSeek keep their reasoning across tool calls.
-
Prompt caching.
- Anthropic models get cache breakpoints on the system prompt and the newest message. In a live check, a second request read 7209 of 7223 prompt tokens from cache.
- Other providers cache automatically. The
x-session-id header keeps each conversation on one upstream provider, so its cache stays warm.
-
Classified failures. Errors map to the harness failure codes, so the harness retry plugin retries only what is worth retrying:
- Retried: rate limits, with OpenRouter's
Retry-After honored; upstream 5xx errors; idle streams; truncated streams; empty responses.
- Not retried: bad keys, exhausted credits, and context overflow.
-
Actual spend, including subagents. The plugin records the charge OpenRouter reports for every response (usage.cost), so these are real charges, not token estimates. Each charge is attributed to the session that made the request and to every session it was delegated from. Type /cost in the CLI or Web UI:
OpenRouter spend in this session: $0.0022 (5 requests)
this agent: $0.0011 (3 requests)
subagents: $0.0011 (2 requests in 1 session)
All recorded sessions: $0.0022 (5 requests).
In the Web UI the session total is always visible under the composer, next to the built-in statistics and context meter. It updates during a turn, including subagent charges, and its tooltip splits the total between the agent and its subagents. It is hidden in sessions with no OpenRouter charges.
Spend under the composer
Charges are appended to ~/.dsh/openrouter-live/costs.jsonl, one JSON line per response, so totals survive restarts. Each line carries the OpenRouter generation id, so any charge can be matched against your OpenRouter activity page.
-
Shared credential. By default the route reads the same OPENROUTER_API_KEY credential as the built-in openrouter route, so a key you already stored works without extra setup.
Requirements
- DeepSeek Harness
0.1.6-alpha.2 through 0.1.7. It has been tested on 0.1.6-alpha.2 and 0.1.7-alpha.2. The harness API is pre-stable, so later harness versions are added to the peer range after they are tested.
- Node.js
^22.19 || >=24.
- An OpenRouter API key.
Install
From npm:
dsh plugin --profile web add dsh-openrouter-live
From a local checkout:
git clone https://github.com/Boomerangz/dsh-openrouter-live.git
cd dsh-openrouter-live
npm install
npm pack --pack-destination dist # runs typecheck and tests first
dsh plugin --profile web add "$PWD/dist/dsh-openrouter-live-0.2.0.tgz"
[!IMPORTANT]
Install the packed .tgz, not the directory. When you add a directory, pnpm links it, Node resolves the link to its real path outside the profile, and the harness packages (@deepseek-ai/*) can't be found there. The whole profile then fails to boot.
Restart the profile after installing. OpenRouter (live) then appears in the model picker. If you haven't stored a key yet, choose one of these:
- Enter it on the built-in OpenRouter card under Settings → Models. It is stored as
OPENROUTER_API_KEY, which both routes read.
- Export
OPENROUTER_API_KEY before launching dsh.
To choose it as the default model, pick it in the composer's model picker. You can also set it in ~/.dsh/settings.yaml:
agent-default-model:
provider: openrouter-live
model: deepseek/deepseek-v4.1-flash
Configuration
Every field is optional. Set fields in the profile's patch layer, ~/.dsh/profiles/<profile>/cordis.patch.yml, under the plugin's entry id openrouter-live:
- id: openrouter-live
config:
models: ['deepseek/*', 'anthropic/claude-*', 'openai/gpt-5*']
extraBody:
provider:
sort: throughput
| Field | Default | Meaning |
|---|
models | [] | Glob patterns over model ids. An empty list lists every model. Ids outside the filter still work when typed. |
toolsOnly | true | List only models whose OpenRouter entry accepts tools. Agent turns need tools. |
priceInName | true | Append input/output prices per 1M tokens to model names, such as DeepSeek: DeepSeek V4.1 Flash ($0.15/$0.60), because the pickers show names but not descriptions. The price then also appears in the composer's model button. |
apiKeyEnv | OPENROUTER_API_KEY | Credential reference: a credential-store entry or environment variable. |
provider | openrouter-live | Route id. Give each instance its own id to run several side by side, for example one with a *:free filter. |
displayName | OpenRouter (live) | Provider name in the picker. |
baseURL | https://openrouter.ai/api/v1 | API root. |
modelListTtlMs | 600000 | How long one fetched model list is reused. |
maxTokens | 32768 | Output cap for requests that set none. A lower model limit wins. |
defaultContextWindow | 262144 | Context size assumed for an id the list does not describe. |
streamIdleTimeoutMs | 300000 | Longest silence inside a response stream before failing with TIMEOUT. |
maxRequestImageBytes | 20971520 | Base64 image budget per request. Older images are offloaded beyond it. |
requestImagePixelBudget | 4194304 | Pixel budget of one request image. |
requestImageMaxBytes | 1048576 |
How it maps onto OpenRouter
| Harness | OpenRouter chat completions |
|---|
| System prompt, user and assistant text | system, user and assistant messages |
| Tool definitions and tool calls | tools (function type) and tool_calls |
| Tool results | tool messages. Images returned by a tool follow in a user message, because tool messages carry text only. |
| Images | image_url data URLs, each preceded by the harness's image handle line |
| Reasoning effort | reasoning.effort |
| Reasoning output | The reasoning stream becomes reasoning blocks. reasoning_details is kept for replay. |
| Usage | prompt_tokens is split into uncached input, cache reads and cache writes. Reasoning tokens are reported separately. |
Development
npm install
npm run check # tsc --checkJs (strict) + node:test suite
OPENROUTER_API_KEY=... npm run smoke # live checks against OpenRouter; costs a few cents
The plugin is plain ESM JavaScript with JSDoc types, so no build step is needed to run it. npm pack generates the .d.ts files in types/. The test suite includes a composition test that boots the real LlmRuntime from @deepseek-ai/dsh-llm in a Cordis context and streams through it.
Known limitations
-
Spend covers only requests through this plugin's routes. The built-in openrouter and deepseek-official routes are not counted.
-
A charge is recorded when OpenRouter reports it at the end of a response. A request that is cancelled or fails mid-stream may still be billed by OpenRouter, but it does not appear in /cost.
-
The Web UI indicator reads a route that answers only same-origin requests to a loopback address (127.0.0.1, localhost), because the harness web server does not authenticate plugin routes. When the Web UI is opened over the network, the indicator stays hidden; /cost still works.
-
/cost needs the harness command service, which the CLI and Web profiles mount. Headless runs still write the ledger.
-
Only OpenRouter's text output is used. Image generation and audio models are listed only when they also accept tools, and their non-text output is ignored.
-
Prices in the picker are OpenRouter's base prices. Time-of-day price overrides are not shown.
-
Explicit cache breakpoints are sent only for anthropic/* models. Every other model relies on its provider's automatic caching.
-
A model that reasons but publishes no supported_efforts gets no reasoning selector, so OpenRouter's default applies.
License
MIT