DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-web-search-failover

0

dsh-web-search-failover

Discovered

DeepSeek Harness (dsh) web_search provider: Exa primary with the official DeepSeek search provider as an automatic fallback.

dsh-web-search-failover

English | 中文

A DeepSeek Harness (dsh) plugin that gives the built-in web_search tool an Exa primary and the official DeepSeek search provider as an automatic fallback — so a dead Exa endpoint, an expired key, or an Exa outage no longer leaves the agent without web search.

Why this plugin exists

ctx.web — the harness web capability seam — resolves exactly one provider per call: a pinned searchProvider id wins, otherwise the single usable provider is chosen. available() is only a cheap local check, and when a provider throws, the seam does not retry another backend. Mounting @deepseek-ai/dsh-web-search-exa and @deepseek-ai/dsh-web-search-deepseek side by side therefore gives you no failover at all: the pinned provider either answers or the tool call fails.

This plugin registers one extra provider that composes both official implementations, so the failover lives inside a single provider and everything above it — the web_search tool name, its schema, its result formatting, the system-prompt guidance — stays untouched.

How it works

web_search (dsh-tool-web)
      │
      ▼
ctx.web  ── searchProvider: exa-deepseek
      │
      ▼
FailoverSearchProvider  (this plugin)
      ├── ExaSearchProvider        ← primary   (@deepseek-ai/dsh-web-search-exa)
      └── DeepSeekSearchProvider   ← fallback  (@deepseek-ai/dsh-web-search-deepseek)

Both legs are the official classes, imported from the official packages (ExaSearchProvider, DeepSeekSearchProvider, and the DEEPSEEK_DEFAULT_* constants). This plugin adds only the ordering, the cancellation guard, and the combined error — it re-implements no search logic.

Fallback rules

SituationBehavior
Exa locally unusable (empty key, unparseable baseURL)Answered by DeepSeek directly; no error
Exa throws (network failure, timeout, 401/402, 5xx, unparseable body)One warn on the harness logger, then answered by DeepSeek
Call cancelled / timed out (signal.aborted, WEB_ABORTED)Rethrown as is — no fallback, so a timed-out call never starts a second paid search
Exa succeeds with zero sourcesReturned as is — an empty result is a real answer, not a failure
Both legs failThe Exa error keeps its code and message, with the fallback failure appended and chained as cause

When the DeepSeek leg answers, it still records the official web/deepseek-search-llm-request session event, so a fallback search stays auditable in the session log. (That event is also the cleanest way to prove the fallback fired — see Verifying.)

Requirements

  • dsh 0.1.7-rc.2 (the peers in package.json are pinned to the runtime version — see Upgrading dsh)
  • @deepseek-ai/dsh-web-search-exa@0.1.7-rc.2 installed in the profile
  • An Exa API key (EXA_API_KEY) and a DeepSeek credential (DEEPSEEK_API_KEY)

Install

dsh plugin --profile web add github:0x-0cd/dsh-web-search-failover

A git install runs whatever the repository publishes, so pin a revision when you want a fixed one: dsh plugin --profile web add github:0x-0cd/dsh-web-search-failover#v0.1.0.

The package deliberately declares no dsh.bundle, so it installs as a plain dependency (the CLI prints declares no dsh.bundle — installed as a plain dependency, not a profile layer; that is expected) and you mount its row from the profile patch. Mount the Exa provider too, then pin the seam to the failover provider:

dsh plugin --profile web add @deepseek-ai/dsh-web-search-exa@0.1.7-rc.2
# $DSH_HOME/profiles/web/cordis.patch.yml  (see examples/cordis.patch.yml)
- insert:
    - id: web-search-failover
      name: dsh-web-search-failover
      config:
        exaSearchType: auto
    - id: web-search-exa
      name: '@deepseek-ai/dsh-web-search-exa'
      config:
        searchType: auto
- id: web
  name: '@deepseek-ai/dsh-web'
  config:
    searchProvider: exa-deepseek
    fetchProvider: http   # a patch replaces the whole config; never drop this line

Put the keys where the launch environment can see them — $DSH_HOME/.env works:

EXA_API_KEY=exa-...
DEEPSEEK_API_KEY=sk-...

Restart dsh web after installing packages or changing .env (the Exa provider reads the launch-environment snapshot at mount). Editing the patch file alone is hot-reloaded.

Meanwhile web_fetch is untouched: keep fetchProvider: http and it keeps working as before.

Configuration

Every field is optional.

FieldDefaultMeaning
exaApiKey$EXA_API_KEYLiteral Exa key; empty/blank falls back to the launch environment
exaBaseURLhttps://api.exa.aiExa endpoint base; /search is appended
exaSearchTypeautoauto, keyword, or neural
exaNumResultsunsetDefault result count when a request carries no maxResults
exaHighlightsPerResult1Highlight sentences requested per result
deepseekApiKeyEnvDEEPSEEK_API_KEYCredential reference for the fallback leg, resolved per search through ctx.credentials
deepseekBaseURL$DEEPSEEK_SEARCH_BASE_URL → https://api.deepseek.com/anthropic/v1Messages endpoint for the fallback leg
deepseekModeldeepseek-v4-flashModel used for the auxiliary search turn
deepseekApiVersion2023-06-01anthropic-version header
deepseekMaxTokens4096Generated-token cap for the auxiliary request
deepseekMaxUses5Native web_search server-tool uses per auxiliary request

Switching backends

Change one line in the profile patch — hot-reloaded, no restart:

searchProviderEffect
exa-deepseekExa primary, DeepSeek fallback (this plugin)
exaExa only
deepseek-officialThe built-in DeepSeek search only

Verifying that failover really works

Point the Exa leg at an address that cannot answer, save the patch (hot-reloaded), and search once:

- insert:
    - id: web-search-failover
      name: dsh-web-search-failover
      config:
        exaBaseURL: http://127.0.0.1:9   # temporary fault injection

A working fallback means: the tool call still returns results, the harness logs web-search-failover: Exa failed, using the DeepSeek fallback — …, and the session log gains exactly one web/deepseek-search-llm-request event. Remove the injected exaBaseURL when done.

With session logs stored as session.v4.jsonl.zstd, the audit event can be counted with:

zstd -dc "$DSH_HOME"/sessions/*/session-*/session.v4.jsonl.zstd \
  | python3 -c "import sys,json; print(sum(1 for l in sys.stdin if json.loads(l).get('type')=='web/deepseek-search-llm-request'))"

Caveats

  • The fallback leg reads this plugin's config and the launch environment, not the GUI page. Settings → Plugins → Web search edits the official web-search-deepseek row (its endpoint and per-request search budget); those overrides apply to that row's provider, not to the fallback built here. Set deepseekBaseURL / deepseekMaxUses in this row if you need the fallback to match. Credentials are shared: deepseekApiKeyEnv resolves through the same ctx.credentials store the Models page writes.
  • EXA_API_KEY comes from the launch-environment snapshot, so changing it in $DSH_HOME/.env requires a dsh web restart. Config fields in the patch hot-reload.
  • file: installs are copies. If you develop from a local checkout and edit the source, re-run dsh plugin --profile web add file:/path/to/dsh-web-search-failover (or pnpm install in the profile) to refresh what the profile loads.

Upgrading dsh

The five @deepseek-ai/dsh-* peers are pinned to the runtime version. After upgrading dsh, bump them to the new version and reinstall, or the startup compatibility check refuses the plugin (it prints the exact dsh plugin allow-version … --accept-risk command; granting it is a real risk, not a formality). This plugin depends only on public exports, so if a future release renames a provider option, the load fails loudly and the fix is a small edit to index.js.

Uninstall

dsh plugin --profile web remove dsh-web-search-failover

Then remove the two inserted rows and point searchProvider back to exa or deepseek-official.

Development

index.js                     the whole plugin (Cordis function plugin: name, inject, Config, apply)
package.json                 peers pinned to the dsh runtime; no dsh.bundle on purpose
examples/cordis.patch.yml    ready-to-copy profile patch

Local iteration against a live profile:

dsh plugin --profile web add link:/absolute/path/to/dsh-web-search-failover

link: symlinks the checkout so edits take effect on the next plugin load; file: copies it.

License

MIT

Comments

Loading…

Similar plugins

dsh-web-search-ext

by fno2010

Multi-backend web_search provider for DeepSeek Harness (DSH): Exa + Firecrawl with automatic failover, key-free capable

Tools & CapabilitiesTerminal & ClientsManifest valid

★ 3

↓ 261/wk

MIT

JavaScript

Sep 11, 2026

dsh plugin --profile web add @fno2010/dsh-web-search-ext

by A3Boy

Multi-provider Web Search & Fetch for DeepSeek Harness — 8 deeply adapted providers, SearchHints, resilient fallback, and native X / Xiaohongshu retrieval.

Tools & CapabilitiesManifest valid

★ 34

MIT

JavaScript

Oct 3, 2026

dsh plugin --profile web add dsh-web-tools

by abcdream-Lary

DeepSeek Harness (DSH) 的多提供商网页搜索和页面获取插件:AnySearch、Bing、DuckDuckGo、Tavily、Exa

Tools & CapabilitiesTerminal & ClientsManifest valid

★ 2

↓ 47/wk

MIT

JavaScript

Aug 22, 2026

dsh plugin --profile web add dshsearch-multi

by Yurzi

Multi-protocol web_search provider for DeepSeek Harness

Tools & CapabilitiesManifest valid

★ 4

↓ 855/wk

MIT

TypeScript

Sep 30, 2026

dsh plugin --profile web add dsh-web-search-enhanced

by rogerdigital

DeepSeek Harness (dsh) plugin that adds a SearXNG-backed web_search provider to the ctx.web seam — free, self-hosted, key-less search instead of paid Exa/Perplexity APIs.

Tools & CapabilitiesManifest valid

★ 7

↓ 449/wk

MIT

TypeScript

Sep 21, 2026

dsh plugin --profile web add dsh-searxng

by Kerberos255

Priority-ordered multi-provider web_search router for DeepSeek Harness with automatic fallback.

Tools & CapabilitiesManifest valid

★ 1

MIT

JavaScript

Aug 21, 2026

dsh plugin --profile web add dsh-web-search-router