dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH plugin: ordered web-search provider fallback (omp-style webSearchOrder) over the providers already registered in ctx.web
Ordered fallback for DSH's web_search: providers are tried in the order you configure, and one that is unavailable, errors, exceeds its budget, or returns nothing usable hands the turn to the next.
It implements no search backend and touches no credentials. It only chains the providers a deployment has already registered with ctx.web (exa, deepseek-official, ...). The model-facing web_search tool, agent presets, and web_fetch are untouched. This is the DSH counterpart of omp's providers.webSearchOrder.
web_search → ctx.web.search() → auto-fallback (this plugin)
├─ providers named by `order` first, then registry order
└─ tried in turn; the first usable result is returned as-is
dsh plugin --profile web add github:killcerr/dsh-web-search-order
The package ships its own cordis.patch.yml, and dsh plugin appends it to the profile's dsh.profile.bundles. At boot that patch points web.searchProvider at this plugin and mounts the router row. Restart DSH to take effect.
For local development, install from a path. link: means source edits need a restart, not a reinstall:
dsh plugin --profile web add C:\path\to\dsh-web-search-order
Updating is the same command again; pin a version by appending #v0.1.0.
You will only ever type two names: the package dsh-web-search-order and the provider id auto-fallback. The patch row id is filled in by the bundled patch — you write it by hand only if you wire the plugin up manually. (The 0.1.5 line also has a settings section named web-search-order; the 0.1.6+ lines configure through the row config instead.)
Bundles are per profile, so install once per profile. To cover every profile on the machine at once instead, use a machine-level patch ($DSH_HOME/cordis.patch.yml: override the web row, then insert the router row). Pick one or the other — the row id may appear only once.
| DSH | Status | Where configuration comes from |
|---|---|---|
0.1.5 line (alpha / rc) | supported | the web-search-order: section of settings.yaml (live) or the row config |
0.1.6 line (alpha) | supported | the row config (Settings-page edits write back to the profile patch) |
0.1.7 line (alpha / rc) | supported; verified on 0.1.7-rc.2 | the row config (Settings-page edits write back to the profile patch) |
Declared range (dsh.engines.dsh, and the dsh-web peer):
>=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0
All three clauses are load-bearing. Under semver, a version carrying a prerelease tag only
satisfies a comparator set when some comparator in that set sits on the same
[major, minor, patch] tuple, so an 0.1.5/0.1.7-only union silently excludes the whole 0.1.6
line.
0.2.0 and up are not claimed.
On the
0.1.7linesettings.yamlno longer applies. That line replacedctx.settingswithSettingsForms: the plugin no longer registers asettings.yamlsection, and the host instead projects this plugin's own rowConfiginto the Settings page, writing edits back to the profile'scordis.patch.yml. Put configuration in the rowconfig(or use the Settings page). That is the host's behaviour, not a downgrade in the plugin.
On the 0.1.7 line, configuration goes in the plugin row's config (the Settings page projects
it into a form and writes edits back to the profile patch):
- id: web-search-order
name: 'dsh-web-search-order'
config:
order: [exa, deepseek-official]
timeoutSeconds: 20
On the 0.1.5 line, you can additionally use the plugin's section in ~/.dsh/settings.yaml,
which applies live with no restart:
web-search-order:
order: [exa, deepseek-official]
exclude: []
timeoutSeconds: 20
fallbackOnEmpty: true
| Key | Default | |
|---|---|---|
order | [] | Provider ids, most preferred first. Providers you leave out follow in registry order; an empty list is registry order. Ids that are not installed are ignored, with a one-time note. |
exclude | [] | Never tried, and this outranks order. |
timeoutSeconds | 20 | Per-attempt budget; see below. |
fallbackOnEmpty | true | A result with neither sources nor answer text counts as a miss and hands over to the next provider. Set false to return the empty result as-is. |
On the 0.1.5 line precedence is schema defaults < row config < settings.yaml; the 0.1.6+ lines
have no settings.yaml layer.
DSH's seam has no per-provider timeout: a provider is handed an AbortSignal and nothing else. So this budget is the plugin's own, and the plugin is the only thing that can guarantee it. Each attempt waits on the deadline, on caller cancellation, and on the provider itself, and whichever arrives first wins. A provider that ignores the signal still loses its turn on time, and a result that lands after the deadline is not used.
Both directions bite. Too large, and a slow first provider eats the whole tool-call budget (tool-web.searchTimeoutMs, 30 s by default), so the later providers never get a turn. Too small, and you kill providers that would have succeeded — a real deepseek-official search takes more than five seconds, so a 5 s budget kills it while 20 s is fine.
One known limit: the deadline ends the attempt, it does not kill the request in flight. Providers that honour the contract — the shipped deepseek and exa both pass the signal to fetch — cancel with it; an adapter that ignores the signal runs its request to completion.
A fully failed chain throws one WebError with code WEB_PROVIDER_ERROR (WEB_PROVIDER_UNAVAILABLE when there was no candidate at all). The message lists every provider's outcome with the provider's own text left intact, so the real cause — a 401, a missing key — is right there:
every candidate web search provider behind "auto-fallback" failed:
- exa: error: WEB_PROVIDER_ERROR: Exa API error (HTTP 401)
- deepseek-official: timeout: timed out after 20000 ms
- brave: skipped (excluded)
Cancellation — a tool timeout, or the user stopping the turn — throws WEB_ABORTED immediately and does not try the next provider. That call was cancelled; this provider was not judged.
| Symptom | What to do |
|---|---|
| Searches still reach the old provider | The web row's searchProvider does not name auto-fallback. A profile layer overrides a bundle layer. |
WEB_PROVIDER_CONFIGURED_MISSING: auto-fallback | This profile does not have the plugin installed, or an insert row's name is wrong. |
... has no candidate provider to try (...) | The chain has no candidate. The message carries order, exclude, and the registry count; read it. |
| The second provider is never tried | timeoutSeconds is too large and the outer tool-web.searchTimeoutMs cuts the call first. |
A log line about does not expose a readable search-provider registry | ctx.web changed shape. The router reads ctx.web.searchProviders (a Map); the verified version is @deepseek-ai/dsh-web@0.1.5-alpha.1. |
That last one is the plugin's most fragile edge: ctx.web exposes no enumeration API, so the router has to read that field. When it cannot, it fails closed quietly instead of throwing from deep inside a search, and test/contract.test.js fails loudly the moment the shape changes.
pnpm install
npm test # node --test, 46 cases, no network
lib/order.js holds the pure logic (ordering and the fallback walk; no DSH imports, the error constructor is injected), lib/registry.js is the only place that reads ctx.web.searchProviders, lib/provider.js is a thin adapter, and lib/index.js is the cordis plugin itself. test/seam.test.js and test/contract.test.js run against the shipped WebRuntime and a real cordis Context rather than a mocked seam.
Search only, not fetch, and only providers that are already installed. If you want bundled backends, key pools, cooldowns or circuit breaking, see dsh-web-search-router, dsh-web-search-aggregation, or websearch-dsh. This plugin has none of that, and no browser settings card yet.
MIT