dsh-model-palette
Global provider-aware model command palette for DeepSeek Harness Web
Alt+M · Fuzzy search · Provider filter · Model config · OpenRouter media
dsh-model-palette is a plugin for DeepSeek Harness that replaces the native model selector with a powerful, global command palette. Press Alt+M from anywhere in the Web UI — or click the composer trigger — and instantly search, filter, favorite, and switch models across all your providers.
Project site: jensen-yao.github.io/dsh-model-palette
Current release: v0.19.1
The plugin bridges DSH 0.1 native settings and DSH 0.2 profile-backed SettingsForms. Version 0.17 adds selected-provider JSON backups, batch import previews, model-type filters, sorting and progressive results. The bundled preset registry contains 487 entries.
Quick profiles and previous-model switching
Open Quick profiles to name the current provider/model/reasoning combination, such as Coding, Quick questions or Deep thinking. Save up to twelve profiles; the first three appear beside the composer and use Alt+Shift+1/2/3. Rename, update from the current selection, reorder or delete with undo. Saving an existing name updates its combination. Profiles stay in this browser origin, contain no credentials, and synchronize across tabs and composer seats. Missing models retain their profiles and show why selection is unavailable.
The ↶ composer button or Alt+Shift+M switches back to the previous model in this session, including its prior reasoning effort. Changing only the reasoning level does not replace previous-model history. Failed selection preserves the active model; locked seats and in-flight selections reject repeated shortcut actions.
Search accepts compact spellings such as gpt5, full-width characters, is:free is:reasoning, Chinese 免费 推理, and @provider terms. Precise matches rank before favorites while searching. Reset filters clears all filters and sorting. Closing a dirty configuration editor or leaving it through the sidebar now asks before discarding its draft; active settings writes must finish first.
Keyless free models and local gateway
The native dsh-free-models provider combines the keyless Kilo free pool with OpenCode's explicit free ids. Kilo contributes only rows its own catalog marks isFree; OpenCode keeps its public-pool session wire. Availability probes use small real requests but remove a model only after an explicit 400/404/422 routing refusal — a 429, timeout, or 5xx leaves it selectable. Free pools can log prompts upstream, so do not send secrets through them.
Catalogs re-read on every probe interval, and the provider's rail row has an inline refresh button (POST /model-palette/api/free-models/refresh) reporting added/removed counts. Context windows, output limits, reasoning ladders, and display names are enriched from the public models.dev catalog (cached 12 hours; the refresh button forces a re-read), so newly published free models arrive with capabilities instead of stale guesses. In the palette the lane is pinned right below the current provider with a localized name and a FREE pill, and the free filter plus 免费/is:free search cover the keyless lane as well as :free routes.
A loopback OpenAI-compatible gateway also listens on http://127.0.0.1:4180/v1 by default and serves /models plus streaming/non-streaming /chat/completions. It binds only to loopback; set freeModels.gateway.credentialRef to require a bearer token from a configured DSH credential. Response framing is detected from body bytes, so an SSE stream mislabelled as JSON remains readable.
freeModels:
enabled: true
baseURL: https://ai-gateway.kilo.ai/api/v1
opencodeBaseURL: https://opencode.ai
probeOnStartup: true
probeIntervalMinutes: 60
probeConcurrency: 2
gateway:
enabled: true
port: 4180
credentialRef: ''
Configuration backup and batch import
Open Model config → Configuration backup & batch import. Choose saved providers and export their model parameters, retry rules and free-sync schedules as JSON. Unsaved drafts, API keys, request headers and sync diagnostics are excluded; credential references remain names. URLs containing user information, query parameters or fragments are omitted and listed for review. Configure credentials separately on the destination host.
Choose a JSON file up to 2 MiB to preview an import, select the providers, and choose Skip existing (default), Merge incoming fields, or Create copies. Merge preserves unmentioned provider fields and replaces incoming model lists; copies retain the source credential reference. Only Apply import writes settings. Revisions protect concurrent changes. Provider settings and palette rules are separate writes: if the second write fails, the panel reports the saved provider settings and asks you to reload and review the rules.
Model filtering and synchronization
The palette combines provider, search, favorites and recent filters with Reasoning or explicit :free routes. Sort by frequent use, model name or provider name. Results render 80 rows at a time; Show more and arrow-key navigation reveal later results. Capacity badges resolve the displayed models through DSH rather than relying on fields absent from the native model directory. Modal focus stays inside the palette and returns to its trigger on close; IME composition does not select models.
Free sync retains configured models when OpenRouter returns an empty compatible catalog, rechecks route settings after fetching, and aborts catalog requests during plugin disposal. intervalHours: 0 disables interval synchronization while retaining startup and manual synchronization. Unique renamed SettingsForms entries are recognized by their editable fields; ambiguous entries fail with a list of candidate ids.
✨ Features
🎯 Global Command Palette
|
🏷️ Provider Filter Rail
|
⭐ Favorites & Recents
|
🧠 Universal Reasoning Effort Selector
|
⚙️ Provider & Model ConfigurationAdd, edit, or remove provider profiles directly from the UI:
|
📦 Model Presets
|
🖼️ OpenRouter Media Tools (Optional)
|
🔒 Safety & Privacy
|
🚀 Quick Start
Install
dsh plugin --profile web add github:Jensen-Yao/dsh-model-palette#v0.12.0
Restart dsh web, then press Alt+M or click the model trigger in the composer area.
Enable OpenRouter Media Tools (Optional)
Media tools are enabled by default with zero configuration: they reuse the OPENROUTER_API_KEY already stored in your DSH credentials (the same key your OpenRouter chat routes use) and write outputs to ~/.dsh/media-output. Free image/video endpoints are used automatically; paid generation asks for per-run confirmation in the panel unless you allow it in config.
Opt out or customize via a profile patch — every field is optional:
# cordis.patch.yml or your profile patch file
- id: dsh-model-palette
config:
openrouterMedia:
enabled: false # set false to turn the tools off
credentialRef: OPENROUTER_API_KEY # any DSH credential reference
outputDir: 'D:\AI\openrouter\outputs'
allowPaidImages: false
allowPaidVideos: false
preferredImageModels: []
preferredVideoModels: []
If the key is missing you get a clear Credential OPENROUTER_API_KEY is not configured in DSH message — add it on the config panel like any other provider key.
This registers five agent tools:
| Tool | Description |
|---|---|
openrouter_media_models |
Browse live OpenRouter image/video models and pricing |
openrouter_generate_image |
Generate images via OpenRouter |
openrouter_generate_video |
Submit asynchronous video generation |
openrouter_video_status |
Poll a video job's status |
openrouter_download_video |
Download a completed video to the configured output directory |
B.AI direct-connection recovery without a VPN
If https://api.b.ai/v1/models times out locally while the key is known to be valid, the network is usually blocking the api.b.ai DNS or TLS route rather than rejecting the key. The plugin includes a loopback-only B.AI relay: it forwards DSH /v1/* requests through a reachable B.AI entry while using the api.b.ai host and certificate name. No VPN is required, and the key remains in DSH credentials. “Switching strategy” always stays inside B.AI: the provider, model, API key, and request path remain unchanged; only the connection host, DNS address, or direct-versus-accelerator route changes. The relay opens a fresh HTTPS socket for every attempt, dynamically resolves and rotates DNS addresses, and moves replayable requests to the next B.AI strategy after a transient failure. Press Alt+M and open Relay config to copy a 127.0.0.1 Base URL and provider template using the current DSH port.
Change the affected B.AI provider baseURL to the following value and restart dsh web:
llm-pi-ai:
providers:
bailsb:
apiKeyEnv: BAILSB_API_KEY
api: openai-responses
baseURL: http://127.0.0.1:3080/model-palette/api/bai-relay/v1
models:
- id: deepseek-v4-flash
baiwhr:
apiKeyEnv: BAIWHR_API_KEY
api: openai-responses
baseURL: http://127.0.0.1:3080/model-palette/api/bai-relay/v1
models:
- id: deepseek-v4-flash
The relay accepts only direct 127.0.0.1 / ::1 requests and has a fixed B.AI destination; it is not an open proxy. Its default strategies are: the current DNS address for the AWS Global Accelerator entry, the next address for that same accelerator hostname, and direct api.b.ai. 503, 502, 504, explicit Cloudflare 403 pages, rate limits, and transient socket failures move to the next B.AI strategy; the relay returns 503 UPSTREAM_TRANSIENT only after all attempts fail. A 401 from /v1/models means the request reached B.AI, so check the credential first. Large non-replayable requests preserve the original B.AI HTTP error instead of being replayed. If B.AI changes its accelerator entry, override the full chain with baiRelay.strategies in the plugin configuration.
The built-in relay retry settings belong to the plugin config, not the provider profile:
- id: dsh-model-palette
config:
baiRelay:
upstreamRetries: 2
retryDelaysMs: [250, 1000]
retryBodyLimitBytes: 16777216
strategies:
- id: aws-global-accelerator
upstreamHost: a18ccd091ab831ac3.awsglobalaccelerator.com
hostHeader: api.b.ai
tlsServerName: a18ccd091ab831ac3.awsglobalaccelerator.com
certificateHost: api.b.ai
addressIndex: 0
- id: aws-global-accelerator-next-address
upstreamHost: a18ccd091ab831ac3.awsglobalaccelerator.com
hostHeader: api.b.ai
tlsServerName: a18ccd091ab831ac3.awsglobalaccelerator.com
certificateHost: api.b.ai
addressIndex: 1
- id: direct-api
upstreamHost: api.b.ai
hostHeader: api.b.ai
tlsServerName: api.b.ai
certificateHost: api.b.ai
addressIndex: 0
upstreamRetries is the number of extra attempts for the same B.AI request; the default 2 covers the three strategies above in order. Relay transport recovery and the provider/model request-retry rules below are separate layers, and neither layer switches to another provider.
Extend the relay to another provider
For another provider whose key works but canonical domain is unreachable locally, declare a named fixed-destination relay in plugin configuration. Each relay has an ID, fixed HTTPS connection host, original API Host, TLS SNI, certificate validation name, and allowed path prefix. Browser requests cannot choose an upstream dynamically.
- id: dsh-model-palette
config:
providerRelays:
example-provider:
upstreamHost: reachable-entry.example.net
hostHeader: api.provider.example
tlsServerName: reachable-entry.example.net
certificateHost: api.provider.example
allowedPathPrefix: /v1/
timeoutMs: 180000
upstreamRetries: 2
retryDelaysMs: [250, 1000]
retryBodyLimitBytes: 16777216
After restarting dsh web, set the provider Base URL to http://127.0.0.1:3080/model-palette/api/relay/example-provider/v1. If DSH uses another port, copy the current loopback address from Relay config. Configure only a verified alternate entry for the same provider API; never place an unknown site, secret, or dynamic URL in relay configuration.
🎮 Using the Palette
Basic Usage
| Action | How |
|---|---|
| Open palette | Press Alt+M or click the ⌘ model trigger |
| Close palette | Press Esc or click outside the dialog |
| Search | Type into the search field — matches model name, model ID, provider name, provider ID, and description |
| Navigate | ↑ / ↓ arrow keys |
| Select | Enter on the highlighted row |
| Filter by provider | Click a provider in the left rail |
| Show favorites only | Click Favorites only in the left rail |
| Show recent models | Click Recent models in the left rail |
| Toggle favorite | Click the ★ star on any model row |
| Switch reasoning effort | Use the footer dropdown; missing model declarations are added live |
Configuration Panel
Press Alt+M and select Model config in the left rail to:
- Select or create a provider — browse endpoint-grouped routes, filter missing keys or free sync, or click "Add provider"
- Duplicate a provider when useful — start from a working route without reusing its credential reference
- Configure the endpoint — set base URL, protocol, and credential reference
- Manage API keys — enter a new key or load the stored one (loopback only)
- Test the connection — click "Check connection" to discover models and immediately fill their disclosed or preset context, output, input, and reasoning metadata
- Choose OpenRouter free models — scan the current usable
:freecatalog, search and inspect capacities, then import only the checked models - Validate the API key — send a minimal request through the selected model and protocol, then distinguish the active DSH runtime credential from a different unsaved key in the input
- Check all API keys — sequentially test every configured runtime credential and open a failing provider directly from the results
- Test one live model — send one minimal request to
/chat/completionsand/responsesand show which actually works - Check every model protocol — classify explicit models plus live DSH catalog entries and
modelOverrides; large providers are scanned in batches and no configuration changes until confirmation - Split Re/CC protocols — keep Responses-capable models on the original provider and create a
-completionsbranch containing only Chat Completions-only models, after resolving capacities and text/image inputs - Configure provider retries — keep the DSH policy or set an exact transient-failure retry count for the route
- Override retries per model — inherit, explicitly disable retries, or set a model-only count
- Configure models — filter long lists, copy parameters, and set context window, max output, input types, and reasoning wire values
- Enable universal reasoning — add all seven DSH levels to one model or every declared model on the route
- Apply presets — auto-fill from the registry or select manually
- Repair known dialect compatibility — use Repair and apply when a DeepSeek-compatible route lacks historical
reasoning_contentreplay fields; normal model selection also runs the preflight automatically - Save safely — duplicate IDs are rejected, and unsaved edits are clearly marked before switching or reloading
- Delete a provider — remove its settings profile after confirmation; its credential is intentionally kept
API key validation reports whether the credential is usable, invalid, blocked, unavailable, missing, or inconclusive. It never treats a public /models response as proof that a key can run conversations and uses the same streaming mode as DSH conversations. Check all API keys runs real requests sequentially to reduce rate-limit pressure and shows the provider, credential reference, source, protocol, model, and diagnostic. The active DSH runtime credential is tested separately from a different unsaved input key, and all keys stay on the plugin backend. Each request may incur a small charge.
Selective request retry rules live in the plugin's dsh-model-palette settings namespace and apply live. A provider rule owns recovery only for that route; an exact model override wins over it. B.AI/BankOfAI aliases (b.ai, bai, bailsb, baiwhr, and bankofai) default to 50 retries. N means retries after the first failed request, so 50 can produce at most 51 billable attempts. Only transport errors, timeouts, rate limits, server errors, empty responses, and explicit Cloudflare/WAF 403 pages retry. Invalid credentials, quota exhaustion, malformed requests, missing models, and context overflow remain terminal. Setting a model override to 0 explicitly disables recovery for that model; routes without plugin rules continue through the normal DSH recovery chain. These provider/model rules are separate from the relay's own transport recovery: the relay handles socket setup failures before DSH receives a response, while this namespace handles failures reported by the DSH request path.
If an explicit Cloudflare/WAF 403 keeps failing after its retry budget, the failure is relabeled as a provider block so the conversation does not misleadingly report API key is invalid. Retries cannot bypass a permanent WAF rule; request content, size, frequency, account policy, or the gateway itself may still need correction.
Reasoning effort is a model capability declaration, not a protocol selector. New OpenAI-compatible routes start on openai-responses, but real endpoint checks remain authoritative. Check every model protocol sends bounded requests for explicit models and for models inherited from the live DSH catalog, including entries customized through modelOverrides. Models that accept Responses stay on the primary route even when both protocols work; models that reject Responses but accept Chat Completions can be moved, after a preview and confirmation, to a generated collision-safe provider-completions branch. Before a catalog route is split, the plugin resolves installed context capacities, configured output caps, and text/image input modalities from the running adapter and fills exact presets; if that metadata cannot be resolved, it cancels rather than writing an inaccurate branch. Provider-only edits preserve the compact catalog plus modelOverrides; an explicit model edit or split materializes the full list. Undetermined models are never moved automatically. The split preserves the endpoint, credential reference, model capacities, inputs, reasoning declarations, valid compatibility fields, and provider/model retry rules.
OpenRouter free-model discovery uses the public /api/v1/models catalog and accepts only explicit :free variants that produce text and have at least one DSH-supported input (text or image). The check requires no API key and changes no provider settings. The picker leaves everything unchecked by default, supports search and manual selection, previews context/output/input capabilities, and imports only the checked entries. Import fills missing metadata and presets without overwriting manual fields or deleting unselected local models.
Live protocol probing sends real requests capped at 16 output tokens and may incur a small charge. The cap avoids false negatives from gateways that reject one-token probes. Full-catalog checks use batches of at most 100 models per backend request and may send up to two model requests per entry. Authentication failures, WAF blocks, rate limits, server failures, and transport errors are reported as undetermined rather than protocol incompatibility. The plugin never changes protocol based solely on reasoning capability and never writes a split before explicit confirmation.
Media Tools Panel
Press Alt+M and select Media tools in the left rail:
- The panel loads the live OpenRouter image/video catalog
- Models reported as free run without restrictions
- Models not reported as free require a one-time charge acknowledgement for each request
- Image generation runs synchronously and saves to the output directory
- Video generation submits an async job — check status and download when complete
🛠️ Development
# Clone the repository
git clone https://github.com/Jensen-Yao/dsh-model-palette.git
cd dsh-model-palette
# Install dependencies
pnpm install
# Run type check, tests, and build
pnpm check
# Build only
pnpm build
# Run tests (keyless, with a deterministic OpenRouter catalog)
pnpm test
# Open http://127.0.0.1:4178/tests/browser/index.html for a keyless browser smoke
pnpm preview:smoke
Prebuilt lib/ files are committed to the repository so GitHub-based installs do not need build scripts.
Project Structure
dsh-model-palette/
├── src/
│ ├── client/ # Frontend React components
│ │ ├── index.tsx # Client entry point & slot registration
│ │ ├── ModelPalette.tsx # Main palette component
│ │ ├── ConfigPanel.tsx # Provider & model config UI
│ │ ├── MediaPanel.tsx # OpenRouter media tools UI
│ │ ├── model.ts # Search, ranking, favorites logic
│ │ ├── model-config.ts # Configuration data utilities
│ │ ├── selection-compatibility.ts # DeepSeek compatibility preflight before selection
│ │ ├── model-presets.ts # Preset registry management
│ │ ├── config-api.ts # Config API client
│ │ ├── media-api.ts # Media API client
│ │ ├── locales.ts # i18n (en & zh)
│ │ ├── types.ts # TypeScript types
│ │ └── style.css # Component styles
│ ├── index.js # Plugin entry point (server-side)
│ ├── openrouter-media.js # OpenRouter media backend
│ ├── model-config-api.js # Model config API backend
│ ├── request-retry-settings.js # Live provider/model retry settings
│ └── media-protocol.ts # Shared protocol constants
├── assets/
│ ├── icon.svg # Plugin icon
│ └── model-presets.json # Bundled model presets
├── docs/
│ └── openrouter-media.zh-CN.md # Media tools documentation (zh)
├── tests/
│ ├── model.test.ts
│ ├── model-config.test.ts
│ ├── model-presets.test.ts
│ ├── media-api.test.ts
│ ├── model-config-api.test.js
│ └── openrouter-media.test.js
├── lib/ # Prebuilt output
├── package.json
├── tsconfig.json
├── tsdown.config.ts
└── cordis.patch.yml
📋 Requirements
- Node.js >= 22.19
- DeepSeek Harness 0.1.6-alpha.2 or the supported 0.2 alpha series (see package peer dependencies); Cordis has its own version
- pnpm >= 11.21 (for development)
📄 License
MIT © Jensen Yao
No comments yet. Be the first to write one.