DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Web Tools Deepseek — DSH Plugin for DeepSeek Harness
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins
W

dsh-web-tools-deepseek

Web Tools Deepseek

DeepSeek native Search/Fetch API providers for the DSH web seam — Rust engine, billed per successful call (¥5 / 1,000)

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:CarbonAnime/dsh-web-tools-deepseek#1a9c1c015cd82e105a85fd998e9c51fcf25d2102
READMECompatibilityVersions

Compatibility and provenance

Web Tools Deepseek is published as dsh-web-tools-deepseek and currently resolves to version 0.2.0. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
any
Release source
github
Registry updated
10/16/2026

Versions

0.2.0stable
10/16/2026

Related plugins

Loading related plugins…

Latest
0.2.0
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
any
License
MIT
Source
github
GitHub
★ 0
Weekly downloads
0
Last push
10/16/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue

Related plugins

More verified plugins in search-research.

Ab Wechat Scrapedsh-ab-wechat-scrape--- description: "The out-of-tree wechat_scrape tool: retrieves WeChat official-account articles, filters promotional blocks, and saves each as a Markdown file, for maintainers of this machine's dsh profiles." kind: "package-reference" ---Ab Market Researchdsh-ab-market-researchAn out-of-tree DeepSeek Harness plugin: the trade_search tool (multi-round aspect-broadening query planning, publisher-tier ranking, source diversity, JSON snapshot) and its browser card in one package.Weknora@wxg-prc-cpg/dsh-weknoraWeKnora knowledge retrieval tools for DeepSeek Harness (dsh): semantic search, document reading and RAG/agent answers over your own knowledge bases.Dejadsh-dejadeja-vu memory for DeepSeek Harness: the session history of forty other coding agents, searchable and recalled before each step — a local index, no LLM.

README

dsh-web-tools-deepseek

English · 简体中文

A provider bundle for the DeepSeek Harness (DSH) ctx.web seam. It routes web_search and web_fetch to DeepSeek's own native Search and Fetch APIs instead of the model-backed web-search tool, and falls back to the native provider when the tools API fails or returns nothing.

The HTTP work lives in a small Rust engine (crates/engine, ~900 KiB) spoken to over NDJSON on stdio; the cordis plugin is a thin JavaScript bridge around it.

tool-web ──▶ ctx.web ──▶ this bundle ──┬─▶ WebSearchProvider ─┐
                                       └─▶ WebFetchProvider  ─┤ NDJSON/stdio
                                                              ▼
                                          dsh-web-tools-deepseek(.exe)   (Rust)
                                                              │
                                        POST /v1/tools/search │ POST /v1/tools/fetch
                                                              ▼
                                                    https://api.deepseek.com

Why — and what it costs

DSH ships two web providers by default:

providerhow it worksbilled
searchdeepseek-officiala server-side tool (web_search_20250305) run inside the model calltokens — the results land in your context
fetchhttpfetches the URL locally, anonymouslyfree

This bundle replaces both with DeepSeek's dedicated endpoints:

endpointbilled
searchPOST /v1/tools/search¥5 per 1,000 successful requests (≈ ¥0.005 each)
fetchPOST /v1/tools/fetch¥5 per 1,000 successful requests (≈ ¥0.005 each)

Only successful requests are billed. A request counts when it returns HTTP 200 and produced a result — for search a non-empty results, for fetch page content. A 403, a 404, an empty result set, a timeout and a failed call are all free. count changes how many results you get, never the price. See Pricing and limits.

The trade: a predictable per-call price instead of token cost, plus search results that arrive as clean structured fields (title / url / summary / date) rather than as text the model has to parse out of its own tool output. To go back, see Rolling back.


Install

From a clone or directory

git clone https://github.com/CarbonAnime/dsh-web-tools-deepseek.git
dsh plugin --profile desktop add "/absolute/path/to/dsh-web-tools-deepseek"

From GitHub

dsh plugin --profile desktop add "github:CarbonAnime/dsh-web-tools-deepseek"

What the install does

The package declares dsh.bundle.patch, so adding it appends to dsh.profile.bundles and its cordis.patch.yml is applied. That patch:

  1. inserts the plugin row (registering deepseek-tools-api for both search and fetch), and
  2. overrides the web row so searchProvider and fetchProvider point at it.

Both halves are required. A patch entry replaces that row's whole config rather than deep-merging it, so the override must restate both keys; and because three providers are then registered (deepseek-official, http, deepseek-tools-api) the seam would otherwise refuse to guess and raise WEB_PROVIDER_AMBIGUOUS.

Restart DSH after installing. Cordis re-runs apply() on a config reload, but the plugin module is ESM-cached — code edits and a first mount need a process restart (verified experimentally; see Troubleshooting).

Requirements

DSH@deepseek-ai/dsh-desktop 0.2.x (targets the ctx.web provider seam)
Node≥ 18.17
CredentialDEEPSEEK_API_KEY in $DSH_HOME/.credentials.yaml, or a literal apiKey
Rustnot needed — bin/dsh-web-tools-deepseek-win32-x64.exe is committed. Build it for Linux/macOS, or take a release asset (zip uploads do not keep the executable bit: chmod +x it).

Building the engine

cargo build --release          # or: npm run build:engine
node scripts/stage-engine.mjs  # → bin/dsh-web-tools-deepseek-<platform>-<arch>

The engine is resolved in this order, so a source checkout, an installed copy and a packed tarball all work:

  1. $DSH_WEB_TOOLS_DEEPSEEK_BIN
  2. the enginePath config field
  3. bin/dsh-web-tools-deepseek-<platform>-<arch>[.exe]
  4. bin/dsh-web-tools-deepseek[.exe]
  5. target/release/…, then target/debug/… (an unstaged checkout)

Configuration

Every field has a working default, so an empty config block is valid. Override fields on the plugin row in a cordis.patch.yml at or after this bundle:

- id: web-tools-deepseek
  name: dsh-web-tools-deepseek
  config:
    count: 8
    qps: 4
fielddefaultmeaning
apiKey''Literal API key. Prefer apiKeyEnv — a literal ends up in the config file.
apiKeyEnvDEEPSEEK_API_KEYCredential reference, resolved through the credentials service on every call, so a rotated key is picked up without a restart.
baseURLhttps://api.deepseek.comAPI origin.
searchProviderIddeepseek-tools-apiId the search provider registers under; must match web.searchProvider.
fetchProviderIddeepseek-tools-apiId the fetch provider registers under; must match web.fetchProvider.
count10Results requested when the caller passes no maxResults (1–50). Price-neutral.
snippetSourcesummaryWhich API field becomes each source's snippet: summary (600–800 chars, "for models to read") or snippet (100–200 chars, "for front-end display"). See below.
oriQuestion''Sent as ori_question when non-empty; over 128 characters is truncated with a warning.
localization''Sent as localization when non-empty. This is an IP address used to prefer results local to it — not a language tag. For language, put lang:zh,en in the query.
blockedAsResulttrueReturn fetch 403/404 as a result carrying a clear non-content notice (true), instead of throwing (false).
includeTitletruePrepend the API's title as an H1 when the Markdown does not already carry it.

Why snippetSource defaults to summary

DeepSeek's documentation labels the two fields by consumer:

snippet — short summary, relevant to the query, usually 100–200 characters, suitable for front-end display. summary — long summary, relevant to the query, usually 600–800 characters, suitable for models to read.

The only consumer of a DSH WebSource is a model, so summary is the faithful default even though it costs several times the context (8 results ≈ 5K characters ≈ 2K tokens). Set snippetSource: snippet to spend less.

Why need_content is never sent

need_content: true makes the API return each result's full page body in that result's content field — a payload the seam's closed WebSource type has nowhere to put, so it would be bytes paid for and thrown away. (The API's own curl example includes need_content: true, which is easy to misread as required; it is not.) Full page text comes from web_fetch, which is what it is for.


What it does

web_search → POST /v1/tools/search

  • Sends query and count (from maxResults when given, otherwise the count field), plus ori_question / localization when non-empty.
  • Rejects a blank query and a query longer than the documented 128 characters locally, spending no request. Counting is by Unicode code point, so an emoji is never split.
  • Maps each result onto the seam's WebSource and drops what the type cannot carry (rerank_score, authority, site_name, site_icon, content) rather than smuggling it into snippet.
  • Validates date into publishedAt (YYYY-MM-DD); a null date produces no publishedAt at all.
  • Never sets the top-level content: WebSearchResult.content is a prose answer for the model, and stitching N summaries together to fake one would be fabrication.
  • Reports truncated: false — one request, no pagination, no invented truncation.

Search syntax passes through verbatim (the API applies it; this plugin never rewrites the query):

operatorexamplemeaning
site:DeepSeek site:github.comrestrict to a site; several OR-joined: site:github.com OR site:huggingface.co
-site:DeepSeek -site:example.comexclude a site
-DeepSeek -广告exclude pages containing a term
-"…"DeepSeek -"release notes"exclude a phrase
"…"DeepSeek "exact phrase"exact phrase
lang:DeepSeek lang:zh,enrestrict language
after:after:2025 / after:2025-01-01published after
before:before:2026 / before:2026-06-01published before

web_fetch → POST /v1/tools/fetch

  • Validates the URL locally first — non-empty, ≤ 8192 characters, parseable, http/https, has a host, carries no credentials — and passes the string through unchanged when it does.
  • On 200: {url, statusCode: 200, body: {kind: 'text', content}}, with the API's title prepended as an H1 unless the Markdown already carries it. The comparison strips Markdown escapes, because the API escapes # and -.
  • On 403 / 404 — the API's verdicts for "refused by safety policy" and "could not obtain usable page content" — returns them as results whose body opens with [FETCH FAILED — NOT PAGE CONTENT] and states the URL, the status and the API's own message. A fetch verdict is a result, not an exception: a model that asked to read a page should be told the page was refused, not handed a stack trace.
  • Every other non-2xx becomes a WebError carrying the API's code.

Fallback to the native provider

A search that fails or returns nothing can fall back to another registered provider (deepseek-official by default):

fallbackOnruns when
nevernever (pure tools-API behaviour)
errorthe tools API call failed
emptythe tools API returned zero sources
both (default)either

The target is resolved per call from the live registry — plugin load order is not guaranteed and a config reload can replace the other provider — never through ctx.web.search(), which would resolve back to this provider and recurse. Cancellation is never a reason to spend money: an aborted call throws instead of falling back, and a fallback that also fails reports the primary failure, because that is what made the call unusual.

Concurrency, retries and cancellation

  • Search and fetch each hold their own throttle (the account allows 5 QPS per endpoint), as a fixed interval (ceil(1000 / qps) ms), not a token bucket — a bucket permits a burst of qps requests inside one second, which is exactly the sliding window the server answers with 429.
  • 408 / 429 / 5xx are retried, honouring Retry-After (capped at 30 s) and using exponential backoff otherwise. Transport failures are retried too: no response arrived, so nothing about the request was rejected.
  • 400 / 401 / 402 / 403 / 404 / 413 are never retried, matching DeepSeek's guidance: "for other 4xx errors, do not retry unless you modify the request."
  • timeoutMs applies per attempt. A caller abort cancels the in-flight request through a cancel frame and surfaces WEB_ABORTED.
  • Response bodies are read incrementally and abandoned as soon as they exceed 32 MiB, so a broken or hostile endpoint cannot exhaust memory.

Architecture

lib/index.js        cordis entry: config schema, registration, wiring self-check
lib/engine.js       spawn + NDJSON framing + request/response correlation + restart
lib/search.js       search provider: engine call, native-provider fallback
lib/fetch.js        fetch provider: engine call
lib/credentials.js  credential resolution (re-read on every call)
lib/diag.js         one JSON line per HTTP attempt
lib/web-error.js    a WebError-shaped error for the seam
crates/engine/      the Rust engine: HTTP, throttle, retry, deadline, mapping
test/unit.mjs       32 checks — JS bridge, fake child process, no cost
test/engine.mjs     29 checks — real binary, mock HTTP server, no cost
test/smoke.mjs      33 checks — the real API, ~¥0.02

Why split it? The seam is a cordis (TypeScript/JavaScript) extension point, so the plugin must be JS. Everything that has nothing to do with cordis — the HTTP client, the rate limiter, the retry policy, the deadline, the cancellation, the request payloads and the response mapping — lives in Rust instead: one binary, no runtime dependencies to keep current, and the mapping sits next to the response it reads.

The engine protocol

Newline-delimited JSON over stdio. The bridge writes:

{"id":1,"method":"configure","params":{"baseURL":"https://api.deepseek.com","count":10}}
{"id":2,"method":"search","params":{"query":"…","maxResults":8,"apiKey":"sk-…"}}
{"id":3,"method":"fetch","params":{"url":"https://…","apiKey":"sk-…"}}
{"id":4,"method":"cancel","params":{"id":2}}
{"id":5,"method":"ping","params":{}}
{"id":6,"method":"shutdown","params":{}}

The engine answers {"id":N,"ok":true,"result":…} or {"id":N,"ok":false,"error":{"code":"WEB_…","message":"…"}}, plus unsolicited {"event":"ready"|"log"|"call",…} frames. The bridge resolves a call only after ready, and a call event becomes a diagnostics line — which is why the engine never writes logs by hand.

If the engine dies, every in-flight call is rejected and the next call starts a fresh one. A missing binary is reported as WEB_PROVIDER_ERROR listing the paths that were tried, not as a crash.


Pricing and limits

Official documentation: Search guide · Search API · Fetch API

Price¥5 per 1,000 successful requests per endpoint (≈ ¥0.005 each). No token usage.
Billed whenHTTP 200 and a result was produced (non-empty results for search, page content for fetch).
Not billedfailures, empty results, timeouts, 4xx/5xx.
Rate limit5 QPS per account, independently for search and fetch, regardless of which API key is used, and independent of model concurrency. Fetch additionally has a per-target-domain limit. Exceeding either returns 429.
SettlementUsage appears on the console usage page as separate tool-search / tool-fetch entries, after a 5–15 minute delay.

Errors use the OpenAI-compatible shape with param always null:

statusmeaning (search / fetch)retried here
400malformed headers or bodyno
401missing or invalid keyno
402insufficient balanceno
403fetch refused by safety policyno
404fetch could not obtain usable contentno
408read timeoutyes
413request body too largeno
429QPS exceededyes, Retry-After first
500server erroryes
504downstream timeoutyes

Testing

npm run test:unit     # 32 checks — no engine, no network, no cost
npm run test:engine   # 29 checks — real engine, local mock server, no cost
npm run test:smoke    # 33 checks — the real API, ~¥0.02
  • test/unit.mjs drives the JS bridge with a fake child process: config sanitizing, engine-path resolution, NDJSON framing (split frames, unparsable lines, correlation), cancellation, crash-and-restart, and the fallback matrix — including that a cancelled call never spends a fallback.
  • test/engine.mjs spawns the built binary against a mock HTTP server and asserts the documented contract: request payloads (no need_content, count from maxResults), the 2xx/403/404 mapping, retry and Retry-After behaviour, the fixed-interval throttle, cancellation, maxResults clamping, and a clean shutdown.
  • test/smoke.mjs runs the plugin end-to-end against api.deepseek.com through a stub cordis context, cross-checking every mapped value against a raw HTTP call made in the same script, so a mapping bug cannot hide behind a passing assertion. The fallback path is proved for free by pointing the primary at a closed port.

test/smoke.mjs needs DEEPSEEK_API_KEY (or $DSH_HOME/.credentials.yaml) and never prints the key.


Diagnostics

One line per HTTP attempt is appended to %TEMP%/dsh-web-tools-deepseek.log (or $TMPDIR), and echoed to the cordis logger:

2026-10-17T02:20:11.402Z [info] [web-tools-deepseek] registered search="deepseek-tools-api" fetch="deepseek-tools-api" baseURL=https://api.deepseek.com key=credential:DEEPSEEK_API_KEY count=10 snippetSource=summary qps=5 timeoutMs=30000 retries=1 blockedAsResult=true includeTitle=true fallback=deepseek-official:both engine=… diagFile=…
2026-10-17T02:20:11.405Z [info] [web-tools-deepseek] seam row is wired to this plugin (search="deepseek-tools-api", fetch="deepseek-tools-api")
2026-10-17T02:20:11.899Z [call] {"label":"deepseek search","endpoint":"https://api.deepseek.com/v1/tools/search","status":200,"ok":true,"ms":412,"attempts":1}
variableeffect
DSH_WEB_TOOLS_DEEPSEEK_DIAG_FILEwrite the log elsewhere
DSH_WEB_TOOLS_DEEPSEEK_DIAG=0turn the log off
DSH_WEB_TOOLS_DEEPSEEK_BINuse a specific engine binary

The second startup line is the one to read first. "Registered" and "selected" are different facts: without the - id: web override this plugin sits inert while deepseek-official and http keep serving every call, which is invisible from the outside. If the log says "registered but NOT selected", that override is missing. (The check reads WebRuntime's TypeScript-private fields, which are ordinary runtime properties; it is diagnostics-only and degrades to (unset).)


Troubleshooting

symptomcause
WEB_PROVIDER_AMBIGUOUSmore than one provider is registered and the web row names none. Add the - id: web override.
WEB_PROVIDER_CONFIGURED_UNAVAILABLEthe web row names this provider but available() is false: no resolvable credential, or the engine binary is missing.
[web-tools-deepseek] engine is not usable: …the engine could not start; the message lists the paths that were tried.
WEB_MISSING_CREDENTIALapiKeyEnv does not resolve to a value in $DSH_HOME/.credentials.yaml.
Edits to lib/*.js have no effectthe plugin module is ESM-cached; restart DSH. Config reloads do re-run apply() — proven by adding a real override (qps: 4) and watching the startup line change — but the module itself is not re-imported. A comment-only edit does not even trigger that much, because the parsed config is unchanged.
Usage page shows no tool-search / tool-fetchsettlement takes 5–15 minutes.

Rolling back

  1. Point the web row back at the defaults and drop the plugin row, then restart:

    - id: web
      name: '@deepseek-ai/dsh-web'
      config:
        searchProvider: deepseek-official
        fetchProvider: http
    

    Both keys must be written together: a patch replaces the whole config block, so changing one would drop the other. And do not leave the web row pointing at deepseek-tools-api after removing the plugin — that raises WEB_PROVIDER_CONFIGURED_UNAVAILABLE.

  2. Or remove the bundle:

    dsh plugin --profile desktop remove dsh-web-tools-deepseek
    
  3. Or disable it in the plugin manager.

Known limitations

  • Fetch 403 is common, including for DeepSeek's own documentation site (measured: api-docs.deepseek.com → 403 URL access denied); zh.wikipedia.org and news.ycombinator.com measured 404 failed to fetch URL. DeepSeek states that fetch results depend on the target site's availability and access policy — this is the service's verdict, not a plugin bug. Switch fetchProvider back to http to fetch those sites.
  • No pagination — one request returns at most 50 results and truncated is always false.
  • No streaming or progress events — a call resolves when the API answers.
  • need_content is deliberately unused, so page text always needs a second fetch call (and its own ¥0.005).
  • The fallback target is read from a private seam field (ctx.web.searchProviders). There is no public way to look up another provider, and ctx.web.search() would recurse. If the seam renames that field the fallback degrades to "no fallback", with a warning, rather than breaking search.
  • The committed binary is Windows x64. Other platforms build their own or take a release asset; the workflow covers Windows, Linux and both macOS architectures. Release assets do not carry the executable bit, so chmod +x after downloading.
  • HTTP uses reqwest with default-tls (schannel on Windows), not rustls: a rustls crypto provider needs a C toolchain and NASM to build, which would make cargo build fail on an ordinary machine.
  • dsh --profile desktop --dump-config is unavailable (profile "desktop" is managed exclusively by the Electron application), so acceptance evidence comes from live behaviour plus the wiring self-check log.

License

MIT © CarbonAnime

Not affiliated with DeepSeek. The Search and Fetch APIs, their pricing and their limits are DeepSeek's and may change; check the official documentation.

timeoutMs30000Per-attempt deadline; 0 disables it.
retries1Extra attempts for 408 / 429 / 5xx and transport failures (DNS, TLS, socket).
qps5Per-endpoint rate limit. Search and fetch are limited independently.
userAgentdsh-web-tools-deepseek/<version>User-Agent header.
fallbackSearchProviderIddeepseek-officialAnother already registered provider to retry on when the tools API fails or returns nothing. Empty string disables the fallback.
fallbackOnbothWhen the fallback runs: never, error, empty, or both.
enginePath''Explicit engine binary path; overrides the bin/ lookup.
diagtrueAppend one JSON line per HTTP attempt to the diagnostics log.