dsh-websearch-multi
Verified@d0nj/dsh-websearch-multi Β· v1.1.1 Β· MIT
Multi-provider web search and fetch backends for the DeepSeek Harness web capability seam (ctx.web).
Install
dsh plugin add @d0nj/dsh-websearch-multi Confirm the layer applied with dsh --profile default --dump-config β see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Readme
@d0nj/dsh-websearch-multi
Thirteen web-search backends and four page-fetch backends for the DeepSeek Harness ctx.web seam, so web_search and web_fetch keep one model-facing contract while the deployment picks the engine.
Why
web_search talks only to ctx.web, which selects one registered provider. A deployment that wants a different engine β or a working engine when the first one's balance runs out β normally swaps packages. This bundle registers many backends at once and lets web.searchProvider / web.fetchProvider choose, so switching engines is a config edit rather than an install.
Providers
| Id | Product | Auth | Answer | Notes |
|---|---|---|---|---|
tavily |
Tavily Search | TAVILY_API_KEY |
yes | max_results is capped at 20 by the API |
exa |
Exa | EXA_API_KEY |
no | has no snippet field; highlights[] is used as the snippet |
parallel |
Parallel | PARALLEL_API_KEY |
no | single query goes in search_queries[]; count nests under advanced_settings |
tinyfish |
TinyFish Search | TINYFISH_API_KEY |
no | GET, not POST; free up to a daily allowance |
firecrawl |
Firecrawl Search | optional FIRECRAWL_API_KEY |
no | runs keyless from a residential address; a key raises the limit |
linkup |
Linkup | LINKUP_API_KEY |
yes | title field is name |
brave |
Brave Search | BRAVE_API_KEY |
no | sends X-Subscription-Token, not Authorization |
kagi |
Kagi Search | KAGI_API_KEY |
no | paid only |
perplexity |
Perplexity Search | PERPLEXITY_API_KEY |
no | results-only endpoint; the Sonar chat endpoint was retired 2026-09-27 |
serper |
Serper (Google SERP) | SERPER_API_KEY |
knowledge graph | organic results use link, not url |
jina |
Jina Reader search | JINA_API_KEY |
no | response is a bare top-level array |
searxng |
SearXNG | none | answerers | self-hosted; requires baseURL, and format: json enabled on the instance |
mojeek |
Mojeek | MOJEEK_API_KEY |
no | key goes in the query string, not a header |
Fetch backends: tavily (extract), exa (contents), firecrawl (scrape), jina (Reader). web_fetch already has a safe local HTTP provider; these are opt-in for JavaScript-heavy pages, PDFs, or a deployment that already pays for an extractor.
Install
pnpm install
pnpm --filter @d0nj/dsh-websearch-multi build
Then install the package directory into a profile with the plugin_manager tool.
Configure
- id: websearch-multi
name: '@d0nj/dsh-websearch-multi'
config:
providers: [tavily, exa, parallel, tinyfish, firecrawl, linkup, brave, kagi, perplexity, serper, jina, searxng, mojeek]
fetchProviders: []
overrides:
searxng:
baseURL: https://searx.example.org
firecrawl:
tbs: qdr:w
country: GB
Which provider answers is web.searchProvider, not the order of providers:
- id: web
name: '@deepseek-ai/dsh-web'
config:
searchProvider: tinyfish
fetchProvider: http
Each backend resolves a credential reference that defaults to its conventional environment variable, so an exported TAVILY_API_KEY needs no config. Override the reference per provider, or set a literal key, with overrides.<id>.apiKeyEnv / overrides.<id>.apiKey.
Combining providers
A combo registers as one provider that fans a query out to several backends and merges their results. It is selected like any other provider through web.searchProvider, so web_search needs no change.
- id: websearch-multi
name: '@d0nj/dsh-websearch-multi'
config:
providers: [tinyfish, tavily, exa, parallel, firecrawl]
combos:
- id: consensus
members: [tinyfish, tavily, exa, parallel]
merge: consensus
- id: cheap
members: [tinyfish, firecrawl, tavily]
merge: append
concurrent: false
stopAfterResults: 8
- id: shuffled
members: [tinyfish, tavily, exa, brave]
merge: random
- id: web
name: '@deepseek-ai/dsh-web'
config:
searchProvider: consensus
| Field | Default | Meaning |
|---|---|---|
id |
required | Provider id the combo registers under; must not shadow a backend id |
label |
the id | Display name used in diagnostics |
members |
required | Backend ids to fan out to, in priority order |
merge |
consensus |
How results are combined |
concurrent |
true |
Query members in parallel; false queries in priority order and can stop early |
stopAfterResults |
none | With concurrent: false, stop once this many sources exist |
snippetBudget |
600 |
Characters kept per merged snippet when several backends describe one page |
Merge strategies
consensusranks a page higher the more backends returned it, then by the better mean position. This is the default because agreement is the strongest relevance signal across engines that rank very differently.round-robintakes the best result of each backend in turn, then the next of each, so the first page represents every backend.appendlists each backend whole, in configured priority order, for when the first backend's judgement is trusted most.randomshuffles the deduplicated pool. The seed derives from the query, so replaying a turn cites the same pages rather than reshuffling history.
What fusion actually does
Merging is mostly about agreement, not concatenation. Two engines return the same article as https://example.com/a, http://www.example.com/a/, https://example.com/a#top, and https://example.com/a?utm_source=x. Comparing raw strings makes those four separate sources and the model cites one page four times. The fusion normalizes scheme, www./amp./m. prefixes, mobile hosts (old.reddit.com β reddit.com), AMP path suffixes, trailing slashes, fragments, tracking parameters, and parameter order before comparing, then shows the cleanest spelling.
Snippets from every backend that returned the page are blended longest-first within snippetBudget, and the earliest publication date wins, since a news index and a general index should report the article's own date rather than the most recent crawl.
Behavior worth knowing
- A combo survives a member's outage. If one member fails and others answer, the search succeeds with those; only when every member fails does the first error surface, unchanged and with its structured code.
- A combo is also a fallback chain. A member with no credential is skipped, so
members: [tinyfish, tavily]answers through tavily when only that key is configured. - Registration follows configuration, not credentials. A provider listed in
providersis registered even when its key is missing, so the seam reportsWEB_PROVIDER_CONFIGURED_UNAVAILABLEnaming that provider instead of a generic "no provider" error. - Keys are read per search, so storing a key in the credentials service reaches the next call without a restart.
- The seam owns the result cap. Providers return what the API gave them;
ctx.webtruncates and flagstruncated. - Failures are structured. Missing credential is
WEB_PROVIDER_CREDENTIAL_MISSINGand names the reference to store; provider failures areWEB_PROVIDER_ERRORwith the HTTP status and the provider's own message; cancellation isWEB_ABORTED.
Tests
pnpm test
Adapters are tested offline against payloads recorded from each provider's documentation, the provider classes against a stubbed fetch, and fusion against URL spellings that engines genuinely disagree about. The suite needs no network and no API keys.