dsh-web-search-doubao
Give DeepSeek Harness's standard web_search tool a search backend that returns
real excerpts, not just titles and links.
This is a search provider plugin for the harness's web capability seam (ctx.web).
It registers a Doubao Search (Volcengine) backend and points the seam at it, so the
model-facing tool @deepseek-ai/dsh-tool-web keeps working unchanged — only the
backend behind it changes.
English | 中文
Why this exists
The harness ships @deepseek-ai/dsh-web-search-deepseek, which performs search by
asking a model to call the server-side web_search tool over an
Anthropic-compatible Messages API. That route works, but the results it can surface
carry only a title and a URL — the response has no citation excerpts, so the
normalized sources[].snippet is always empty.
The Doubao Search API is a plain search endpoint that returns structured results,
including a per-result body. This plugin maps that body onto snippet, so every
source arrives with text the model can actually read and cite:
model-side web_search tool |
this plugin (Doubao Search API) | |
|---|---|---|
sources[].title / .url |
✅ | ✅ |
sources[].snippet |
❌ always empty | ✅ real excerpt per source |
sources[].publishedAt |
sometimes the 1970-01-01 epoch sentinel |
real date, sentinel filtered out |
| Cost per search | one extra model turn (thousands of tokens) | one API call, no model tokens |
| Typical latency | 6–14 s | ~0.2–0.6 s |
Requirements
- DeepSeek Harness
>= 0.2.0-rc.2(thectx.webseam's provider API). - Node.js
>= 22. - A Volcengine API key. An Agent Plan key works here and draws on the plan's AFP quota — the official Agent Plan docs describe Doubao Search as the plan's search service, callable with the plan's dedicated API key. A key from an unrelated Volcengine product will authenticate but is not covered by the plan.
The endpoint defaults to https://open.feedcoopapi.com/search_api/web_search, which is
the public Doubao Search URL. No configuration is needed to reach it.
Install
# straight from this repository
dsh plugin --profile web add github:gilsaia/dsh-web-search-doubao
# or from a local checkout
dsh plugin --profile web add /path/to/dsh-web-search-doubao
The package declares dsh.bundle.patch, so adding it also adds it to the profile's
bundle list, and its patch selects this provider:
- insert:
- id: web-search-doubao
name: 'dsh-web-search-doubao'
- id: web
config:
searchProvider: doubao
fetchProvider: http
fetchProvider is restated because a row patch replaces the targeted row's whole
config; omitting it would unset the fetch provider and break web_fetch.
About the
Issues with peer dependencies foundwarning: it is expected. The@deepseek-ai/dsh-*packages this plugin imports are provided by the harness itself at load time rather than installed into the profile, so pnpm reports them as missing peers. The harness resolves them when it mounts the plugin.
Restart the harness afterwards (or reload the profile), then run a search.
Configure it in the GUI
The plugin ships a client half. After one restart it adds a 联网搜索 (Web search) page to the harness settings sidebar, and everything below can be done there:
- API Key — typed into a password field and written through the harness credentials domain under the reference the form names. The literal never enters a settings file, and the page only learns whether a key is configured, not what it is.
- Every config field — endpoint, search type, result count, time range, authority filter, query rewriting, summary toggle. Fields you changed show 未保存; fields the user layer overrides show 已覆盖 with a one-click 恢复默认.
- Save applies one revision-fenced write; 撤销 discards the staged edits.
A save takes effect on the next search — no restart needed for either the config or the
key. The page mounts only while the host serves the web-search-doubao settings
namespace, so a deployment without the plugin shows no trace of it.
Provide the key without the GUI
The plugin requires a key. It resolves one in this order:
- the literal
apiKeyconfig value; - the credential named by
apiKeyEnv(defaultARK_API_KEY), read through the harness credentials service, falling back to the launching environment.
So either export it:
export ARK_API_KEY=... # then start the harness
or set it in the plugin's config in the profile's patch layer:
- id: web-search-doubao
config:
apiKey: '...' # a secret; prefer apiKeyEnv so no key enters a config file
If the profile patch is still the shipped default, it contains a bare
[]. Replace that[]with your entries — appending a- id:item after it makes the file two YAML documents and the harness refuses to parse the overlay.
A missing key fails the search with WEB_PROVIDER_CREDENTIAL_MISSING and a message
naming the reference that was consulted — it never silently returns zero results.
Configuration
| Field | Type | Default | Meaning |
|---|---|---|---|
apiKey |
string (secret) | — | Literal API key. |
apiKeyEnv |
credential ref | ARK_API_KEY |
Credential reference resolved per search. |
baseURL |
string | Doubao Search URL | Full endpoint; override for a proxy or a different region. |
searchType |
web | image |
web |
image returns image results instead of pages. |
maxResults |
1–50 | 10 |
Result count when a request carries no bound. |
timeRange |
string | — | OneDay / OneWeek / OneMonth / OneYear, or YYYY-MM-DD..YYYY-MM-DD. |
authLevel |
0–2 | 0 |
1 restricts to authoritative sources. Web searches only. |
queryRewrite |
boolean | false |
Let the service rewrite a conversational query into a search-shaped one. |
needSummary |
boolean | true |
Request the excerpt surfaced as snippet. Turning this off empties snippet. |
baseURL can also be overridden with the DOUBAO_SEARCH_BASE_URL environment variable.
Every field is read per search, so a settings change applies to the next search without restarting the harness.
Reverting
Remove the - id: web block from cordis.patch.yml (or override it in your own patch
layer, which is applied after every bundle layer) to keep the default provider. The
plugin stays registered and selectable, just unused:
- id: web
config:
searchProvider: deepseek-official
fetchProvider: http
Verify
# pure-function and plugin-contract tests: no network, no key
npm test
# live check against the real API; asserts sources carry snippets
ARK_API_KEY=... npm run test:live
To confirm the composition without booting:
dsh --profile web --dump-config | grep -A4 'id: web$'
# searchProvider: doubao
# fetchProvider: http
How it works
provider.js—DoubaoSearchProvider, the wire format, result mapping, and error translation. It uses the nativefetchclient and does not go throughctx.llm.index.js— the Cordis plugin:name,inject: ['web'], theConfigschema, andapply(), which registers the provider into the seam.client/client.js— the browser half, declared asdsh.clientand served by the host under/plugins. Hand-authored against the loader's client-bundle envelope, so it needs no build step and requires only the platformreact. Every read and write goes through the documented Remote namespaces (settings.describe/settings.mutate/credentials.describe/credentials.set).cordis.patch.yml— the bundle patch that mounts the plugin and selects it.
Two details worth knowing if you fork it:
- The API answers failures with HTTP 200 and an
Errorobject underResponseMetadata, so a successful status code is not enough — the body is checked too. - The API emits an epoch sentinel (
1970-01-01T08:00:00+08:00) for pages whose crawl time is unknown. That is filtered out rather than surfaced as a publication date.
Compatibility
@deepseek-ai/dsh-* peer dependencies are declared as open ranges (>=0.2.0-rc.2)
rather than pinned, so the plugin keeps working across harness release candidates.
DeepSeek Harness is in developer preview and may change the seam's API; if a future
version does, this plugin needs the same update as any other provider.
License
MIT
No comments yet. Be the first to write one.