dsh-web-search-diy
DeepSeek Harness(DSH)网页搜索提供方插件:把内置 web_search 工具接到 OpenAI 兼容 Responses API 及其原生 web_search 工具上。
- Stars
- 4
- Language
- TypeScript
- Created
- Sep 3, 2026
- Updated
- Oct 20, 2026
Introduction
dsh-web-search-diy
English · 简体中文
A web search provider plugin for the DeepSeek Harness (DSH). It supplies the built-in
web_searchtool per vendor × protocol: three in-protocol shapes (Chat Completions / Responses / Anthropic Messages) plus standalone search, covering DeepSeek, Kimi, Zhipu, Qwen, OpenAI, Anthropic, OpenRouter, and four custom targets. Results are normalized into structured citation sources for the calling model to ground its answer on.
Capabilities
- 🧭 Vendor × protocol matrix — 17 official targets (13 full + 4 degraded) and 4 custom targets. Only combinations that really search appear in the vendor list; the rest live in a collapsed section with a reason.
- 🎛️ Three-layer configuration page — search kind → protocol → vendor. Fields come from the catalog served by the host; a blank value means the combination's default.
- 🧩 Custom targets — a template picker performs a one-shot fill (selection resets, nothing is saved); endpoint, path, request field names, and response field names are all editable.
- 🧪 Test connection — runs one real search with the staged draft (nothing persisted) and grades the result by source quality.
- 🔐 Write-only credentials — API keys are stored through the credentials service, never in the configuration file; each vendor offers its reference chain.
- 🗂️ Configuration v3 — one bucket per
vendor/protocol; v1/v2 files migrate on read and the old file is backed up as.v2.bakbefore the first save. - 🪜 Thinking-effort fallback — a rejected
reasoning.effort/output_config/thinkingis dropped one at a time and retried. - 📦 Dispatcher-proof decoding — bodies are read as bytes and inflated by magic number, independent of dsh's global undici dispatcher.
Capability matrix
Legend: ✅ full (in-protocol search + citable sources); ◐ degraded (searches, but sources or model are limited); ✗ unsupported; ⚙ custom.
| Vendor | Chat Completions | Responses | Anthropic Messages | Standalone search |
|---|---|---|---|---|
| DeepSeek | ✗ | ◐ deepseek-v4-pro only; sources from open_page | ✅ | ✅ /v1/tools/search |
| Kimi | ✗ built-in $web_search retired | ✅ web_search + include | ✗ no server tool in the schema | ✅ /v1/tools/search |
| Zhipu | ✅ web_search tool | ✅ web_search + results[] | ✗ accepts but does not run it | ✅ /paas/v4/web_search |
| Qwen | ◐ enable_search, no sources | ✅ action.sources (url only) | ✅ needs the billing header | ✗ no standalone REST |
| OpenAI | ◐ gpt-5-search-api only | ✅ full filters / include | ✗ no such endpoint | ✗ |
| Anthropic | ✗ | ✗ | ✅ native web_search | ✗ |
| OpenRouter | ✅ openrouter:web_search | ✅ same server tool | ◐ alias, untested | ✗ |
| Custom | ⚙ template + 4 variants | ⚙ template + toolType | ⚙ template + toolType | ⚙ template + 8 mapping fields |
13 full, 4 degraded (OpenRouter Anthropic is pending), 4 custom. Eight
unsupportedentries exist only for the collapsed section and never create a configuration bucket.
Install
Published on npm:
dsh plugin --profile web add dsh-web-search-diy
The bundle patch performs the wiring, so no manual cordis.patch.yml edits are required:
- inserts the
web-search-diyloader entry; - overrides the shared
webrow'ssearchProvidertodiy-searchand restatesfetchProvider: http(a patch replaces the whole row config); - disables the shipped
web-search-deepseekprovider.
For local development, install the checkout as a link:
dsh plugin --profile web add link:./dsh-web-search-diy
The plugin declares the
@deepseek-ai/*hooks aspeerDependencies. Runpnpm installonce in the plugin directory before linking; the harness provides the peers at runtime.
Configuration
Configuration page
Location: Plugins → Installed plugins → the dsh-web-search-diy card (clicking the card opens this plugin's configuration page). Copy follows Settings → Language (zh / en).
- Search kind:
In-protocol searchorStandalone search. Standalone has no model turn, so model and output-budget fields are hidden. - Protocol: one of Chat Completions / Responses / Anthropic Messages in-protocol; standalone is fixed to
search. - Vendor: only vendors available under that protocol; degraded combinations show their reason on the card; unsupported ones are collapsed for the current shape.
- Fields: blank means the catalog default (shown as the placeholder). Saving a blank deletes the key.
- Endpoint override (Anthropic protocol): the launching environment variable
DEEPSEEK_SEARCH_BASE_URLoverrides the endpoint; an explicitly saved Endpoint wins over it. - Template (custom only): a one-shot picker. The selection resets immediately, the placeholder names the last template, and the template itself is never saved. If the target already has a saved configuration, switching templates first asks for confirmation and offers Reset to saved configuration.
- Save: switching vendor/protocol counts as a pending change; saving takes effect immediately (the host re-reads the file on every search).
- API key: written through the credentials service only; blank keeps the current key.
Configuration file (v3)
$DSH_HOME/dsh-web-search-diy.json, written by the configuration page:
{
"version": 3,
"vendor": "qwen",
"protocol": "responses",
"targets": {
"qwen/responses": {
"baseURL": "https://maas.qianwenaiapi.com/compatible-mode/v1",
"apiKeyEnv": "QWEN_TOKEN_PLAN_API_KEY",
"model": "qwen3.8-flash",
"maxOutputTokens": 4096,
"toolType": "web_search",
"includeSources": true
},
"custom/search": {
"baseURL": "https://search-gateway.example.com",
"path": "/v1/tools/search",
"apiKeyEnv": "SEARCH_GATEWAY_API_KEY",
"queryField": "q",
"countField": "size",
"resultsPath": "data.items",
"urlField": "link",
"titleField": "name",
"snippetField": "desc",
"count": 10
}
}
}
- The top level stores only the current
vendorandprotocol; each target bucket stores only the keys declared in itsfields. - A blank value deletes the key and restores the catalog default.
templateis a fill action, not configuration; submitting it is rejected.- API keys never appear in this file.
Migrating from 1.x
v2 mode | v3 target |
|---|---|
responses | qwen/responses |
anthropic-messages | deepseek/anthropic-messages |
zhipu-web-search | zhipu/search |
zhipu-chat-search | zhipu/chat-completions |
- A v1 flat object is first projected onto its mode, then migrated by the table above.
- Migration happens in memory on read; the first save writes v3 and backs the original up as
dsh-web-search-diy.json.v2.bak(once, for rollback). - Old key names:
responsesToolType→toolType,responsesIncludeSources→includeSources,responsesReasoningEffort→reasoningEffort,anthropicThinking→thinking,anthropicEffort→effort;apiVersionis dropped (fixed at2023-06-01). - 2.0 registers no settings section; configuration comes from the file and the credential surface only.
Field reference
| Protocol / vendor | Fields |
|---|---|
| Common | baseURL, apiKeyEnv, model, maxOutputTokens (the last two are hidden for standalone) |
| Responses · OpenAI | toolType, includeSources, reasoningEffort, searchContextSize, allowedDomains, blockedDomains, userLocation, externalWebAccess, returnTokenBudget, searchContentTypes, imageMaxResults, imageCaption |
| Responses · Kimi | toolType, includeSources, allowedDomains (blockedDomains / search_context_size return 400) |
| Responses · Zhipu / Qwen | toolType, includeSources, reasoningEffort |
| Responses · DeepSeek | toolType; the model is locked to deepseek-v4-pro and include is not sent |
| Responses · OpenRouter | toolType, includeSources, engine, mode, maxResults, maxUses, maxTotalResults, searchContextSize, maxCharacters, userLocation, allowedDomains, excludedDomains, xSearch |
| Anthropic Messages | toolType, maxUses, thinking, effort, allowedDomains, blockedDomains, userLocation, responseInclusion |
| Chat · Zhipu | searchEngine, count, searchRecencyFilter, contentSize, searchDomainFilter, searchIntent, requireSearch, searchPrompt, reasoningEffort |
| Chat · Qwen | searchStrategy, forcedSearch, freshness, assignedSiteList, searchExtension, promptIntervene |
| Chat · OpenRouter | engine, mode, maxResults, maxUses, maxTotalResults, searchContextSize, maxCharacters, userLocation, allowedDomains, excludedDomains, xSearch |
| Chat · OpenAI | no search parameters; the model is locked to gpt-5-search-api |
| Standalone | count, includeContent, searchEngine / contentSize / searchIntent / searchRecencyFilter / searchDomainFilter (Zhipu), oriQuestion / localization (DeepSeek), timeoutSeconds (Kimi), digestEntries, digestSnippetChars; custom adds path and the 8 mapping fields |
oriQuestionsends the human prompt that triggered the search as DeepSeek'sori_question: whitespace collapsed, cut to 128 characters, read from the initiating Session and omitted outside one. It shares the source used by the officialweb-deepseek-apiprovider (sendOriginalQuestion).
Result count and digest size are independent:
count/limitdrive the request and the number ofsources(1–50 for Zhipu/DeepSeek, 1–20 for Kimi);digestEntries(default 8) anddigestSnippetChars(default 200) only shape thecontentdigest returned to the caller and never trimsources.
Custom targets
- Templates: in-protocol targets list the vendors available under that protocol; standalone lists DeepSeek / Kimi / Zhipu. Picking a template performs a one-shot fill of endpoint, credential, model, and search parameters; everything remains editable.
- Chat variants: decided by the template (Zhipu answer augmentation / Qwen web search / OpenRouter server tool / OpenAI search model), not exposed as a separate field; fields are filtered per variant.
- Standalone mapping:
queryField,countField,resultsPath,urlField,titleField,snippetField,dateField,contentFieldare authoritative; the template only fills them and acts as a blank fallback. Simple dot paths (data.items) are supported; full JSONPath is not. - Search engine: free input with common candidates (Zhipu / OpenRouter / self-hosted). On the Zhipu template a blank falls back to
search_std(the API requires it). - Required: Chat needs a variant (the template fills it); Responses / Anthropic need
toolType; standalone needspath.
Target reference (default endpoint / model / credential)
| target | Support | Default endpoint | Default model | Credential reference |
|---|---|---|---|---|
deepseek/anthropic-messages | ✅ | https://api.deepseek.com/anthropic | deepseek-flash | DEEPSEEK_API_KEY |
deepseek/responses | ◐ | https://api.deepseek.com | deepseek-v4-pro | DEEPSEEK_API_KEY |
deepseek/search | ✅ | https://api.deepseek.com | — | DEEPSEEK_API_KEY |
kimi/responses | ✅ | https://api.moonshot.cn/v1 | kimi-k2.6 | MOONSHOT_API_KEY → KIMI_API_KEY |
kimi/search | ✅ | https://api.moonshot.cn | — | same |
zhipu/chat-completions | ✅ | https://open.bigmodel.cn/api/paas/v4 | glm-5.3-flash | ZAI_CODING_CN_API_KEY → ZHIPU_API_KEY |
zhipu/responses | ✅ | https://open.bigmodel.cn/api/v1 | glm-5.3 | same |
zhipu/search | ✅ | https://open.bigmodel.cn/api/paas/v4 | — | same |
qwen/chat-completions | ◐ | https://maas.qianwenaiapi.com/compatible-mode/v1 | qwen3.8-flash | QWEN_TOKEN_PLAN_API_KEY → QWEN_TOKEN_PLAN_CN_API_KEY → DASHSCOPE_API_KEY |
qwen/responses | ✅ | same | qwen3.8-flash | same |
qwen/anthropic-messages | ✅ | https://maas.qianwenaiapi.com/apps/anthropic | qwen3.8-max | same |
openai/chat-completions | ◐ | https://api.openai.com/v1 | gpt-5-search-api | OPENAI_API_KEY |
openai/responses | ✅ | https://api.openai.com/v1 | gpt-5.5 | OPENAI_API_KEY |
anthropic/anthropic-messages | ✅ | https://api.anthropic.com | claude-opus-5-5 | ANTHROPIC_API_KEY |
openrouter/chat-completions | ✅ | https://openrouter.ai/api/v1 | user-supplied | OPENROUTER_API_KEY |
openrouter/responses | ✅ | https://openrouter.ai/api/v1 | user-supplied | OPENROUTER_API_KEY |
openrouter/anthropic-messages | ◐ | https://openrouter.ai/api | user-supplied | OPENROUTER_API_KEY |
custom/chat-completions | ⚙ | template prefill | template / user | user |
custom/responses | ⚙ | template prefill | template / user | user |
custom/anthropic-messages | ⚙ | template prefill | template / user | user |
custom/search | ⚙ | template prefill | — | user |
Default models are current examples, not a long-term promise; an explicit user value always wins.
Test connection
The page's Test connection button runs one real search with the staged draft, without persisting anything:
- query fixed to
OpenAI,maxResults = 3, timeout 60 s; - in-protocol: Responses / Anthropic send
Perform a web search for the query: OpenAI; Chat sendsOpenAIdirectly; - standalone: sends
search_query/query/text_queryplus the matching count; - response:
{ ok, sourceCount, sourceQuality, degraded, latencyMs, contentPreview?, error? }; - the save-side required-field checks (for example, a standalone custom target must declare
path) run first, so a missing value returns 400 without sending a request; - with zero sources the message is graded by the combination's expected source quality: by design / may not have run / check the tool name and endpoint.
How it works
you ──> chat LLM ──> web_search tool (model-independent)
│ ctx.web seam ──> diy-search provider
▼
├─ Responses: POST {baseURL}/responses tools: [{ type, ... }]
├─ Anthropic Messages: POST {baseURL}/messages tools: [{ type: web_search_*, max_uses }]
├─ Chat Completions: POST {baseURL}/chat/completions (four variants)
└─ Standalone: POST {baseURL}{path} (no model turn)
│
▼
chat LLM answers from the sources
- Responses:
action.sources[],open_page/find_in_pageaction.url,results[]metadata, andurl_citationannotations all normalize into deduplicated sources; the answer prefersphase: "final_answer". A reply with no search evidence fails explicitly, and a 404 points out the missing/responsespath. - Anthropic Messages:
web_search_tool_result→sources[],citations[].cited_text→snippet,page_age→ ISO date;allowed_domainsandblocked_domainsare mutually exclusive (allowed wins); tool types other than20250305also sendallowed_callers: ["direct"]. - Chat Completions: Zhipu
web_searchtool, Qwenenable_search, OpenRouter server tool, OpenAI search model; sources come frommessage.web_search[]orannotations[]. - Standalone: structured results become
sources[]directly and the first few snippets formcontent; there is no model turn. - Effort fallback: drop one knob at a time in the order
reasoning.effort→output_config→thinking→reasoning_effortand retry. - Decoding: read as bytes and inflate by gzip/zlib magic, capped at 8 MiB on the wire and 16 MiB decoded.
Live verification
Real credentials, 2026-10-17 / 2026-10-18:
| target | Sources | Notes |
|---|---|---|
qwen/responses | 6 | sources carry url only |
qwen/anthropic-messages | 9 | billing header attached automatically |
deepseek/anthropic-messages | 10 | — |
deepseek/search | 3 | blank localization is not sent |
zhipu/chat-completions | 8 | — |
zhipu/responses | 5 | sources + results merged |
zhipu/search | 1 | digest cap applied |
kimi/responses | 6 | kimi-k2.6 |
kimi/search | 3 | maxResults=30 clamped to limit=20 |
custom/search | 1 | custom mapping and digest cap applied |
qwen/chat-completions | 0 | degraded as expected (no sources) |
deepseek/responses | 0 | degraded as expected (open_page only) |
custom/chat / responses / anthropic | 8 / 5 / 10 | verified on 2026-10-17 |
OpenAI, Anthropic, and OpenRouter are not verified yet; results for these endpoints are welcome under Feedback.
Known limitations
- Kimi Chat is not wired in: the in-protocol
$web_searchretires on 2026-10-20, and theapi.kimi.com/codingendpoint is not wired either. Kimi keeps Responses and standalone search (/v1/tools/search; the Pro endpoint can be reached by overridingpathon a custom target). - Zhipu's Anthropic surface accepts but never runs the tool (
web_search_requests: 0), so it is marked unsupported. - DeepSeek Responses only works with
deepseek-v4-proand returns noaction.sources; sources come fromopen_pageonly. - Qwen Chat returns no sources; Qwen is wired to the Qwen AI Platform only (
maas.qianwenaiapi.com), not Alibaba Cloud Model Studio. - OpenAI Chat only works with
gpt-5-search-api; ordinary models ignore the tool. - OpenRouter Anthropic accepts the
web_search_20250305alias per its docs, but it is not verified yet. - Kimi Responses rejects
search_context_size/blocked_domains(400). - Standalone search has no model turn; its output cap is
digestEntries/digestSnippetChars, not a model token budget. - Test connection uses a fixed query (
OpenAI) and only proves connectivity, not search quality.
Feedback
If you hit an unsupported vendor or gateway, a protocol difference, or a field-mapping problem, open an issue with the endpoint type, request shape, and error message. PRs with new live-verification results are welcome.
Credentials
Store keys in the Web Models page / credentials service ($DSH_HOME/.credentials.yaml) or export them in the launching environment. The plugin resolves them per search, never retains them on the provider, and never writes them to the configuration file.
Default chains per vendor: DeepSeek DEEPSEEK_API_KEY; Kimi MOONSHOT_API_KEY → KIMI_API_KEY → KIMI_CODING_API_KEY; Zhipu ZAI_CODING_CN_API_KEY → ZHIPU_API_KEY; Qwen QWEN_TOKEN_PLAN_API_KEY → QWEN_TOKEN_PLAN_CN_API_KEY → DASHSCOPE_API_KEY; one each for OpenAI / Anthropic / OpenRouter. Custom targets use a user-supplied reference.
A reference name must be a valid environment-variable name (letter/underscore start, then letters, digits, underscores). Saving rejects anything else, and an invalid reference left in a hand-edited file is skipped in favor of the next candidate.
Development
Host and Client both live in src/; built artifacts are committed:
src/
index.ts # apply: registers the provider and the config/test endpoints
catalog.ts # capability matrix, defaults, fields, templates
config.ts # v3 read/write, v1/v2 migration, field validation
endpoint.ts # GET/POST /api/web-search-diy/config and /test
options.ts # catalog defaults + file overrides
provider.ts # available / dispatch by protocol
transport.ts # fetch, decoding, reasoning fallback
protocols/ # responses / anthropic / chat / search
mappers/ # source normalization
client/index.tsx
pnpm install
pnpm run build # Host ESM + Client CJS
pnpm run typecheck
pnpm test # host tests (92)
pnpm run test:client # rebuild and verify the client contract
lib/index.js and lib/client.js are committed; source maps are not.
License
MIT — see LICENSE. Copyright (c) 2026 aaronlei.