DSH plugin web_search ignores your gateway: fix baseURL

TroubleshootingPublished 2026-09-12Author: DeepSeek Plugin Market
DeepSeek HarnessDSH pluginweb_searchbaseURLcustom gateway
Chat works via your gateway but web_search returns Authentication Fails? The endpoint is hardcoded to the official base and the key defaults to the chat key.

If your chat configuration points at a compatible gateway — a self-hosted relay, say — and works fine, but every web_search call returns a red Error: Authentication Fails, Your api key: ****eHfw is invalid, your key is not dead: the search request is going to a different endpoint. The search path dsh-web-search-deepseek has a hardcoded default endpoint of https://api.deepseek.com/anthropic/v1, and it neither inherits the chat config nor reads $DEEPSEEK_BASE_URL; meanwhile both paths default to the same DEEPSEEK_API_KEY, so the key you provisioned for your gateway is silently sent to the official endpoint (#408).

Symptom: DSH plugin chat is fine, but web_search fails authentication every time

What makes this so misleading is that it looks like a broken key, while the same key works perfectly elsewhere. Concretely:

  1. The error reproduces 100% of the time and never once succeeds: every web_search call in the GUI returns the red error above; ****eHfw is only the trailing fragment of the request's DEEPSEEK_API_KEY after the server masks it (#408).
  2. The canonical configuration shape: ~/.dsh/settings.yaml points llm-deepseek.baseURL at a custom gateway (for example https://opencode.ai/zen/go/v1), and ~/.dsh/.credentials.yaml holds a key valid only for that gateway (such gateway keys are often 67 mixed alphanumeric characters, whereas an official key is sk- plus 32 lowercase hex digits) (#408).
  3. Three pieces of evidence nail the conclusion: ① the key against the official balance endpoint returns 401, with text identical to the UI; ② the gateway's /models returns 200, so the key is valid; ③ posting the web_search_20250305 tool request straight to the gateway's /messages returns 200 with web_search_tool_result and 10 results. The third matters most: it proves that pointing the search endpoint at the same gateway simply works (#408).
  4. This is not a one-off but the universal outcome of that config shape: one user packet-captured it and confirmed "the web search sent the company's key straight to the official side"; another reported picking a different provider while search still defaulted to the official one; and because DeepSeek Harness passes the message through verbatim, the UI only ever shows a misleading authentication failure (#408).

Mechanism: a DSH plugin search endpoint is hardcoded official while the credential defaults to the chat key

Look at "the endpoint's default" and "the credential's default" separately, and the gap becomes clear in one pass. Layer by layer:

  1. The search endpoint is hardcoded in source: web-search-deepseek/src/provider.ts:35 defines DEEPSEEK_DEFAULT_BASE_URL = 'https://api.deepseek.com/anthropic/v1', and the module doc even states "only the API key is shared". In the rc.6 compiled output the precedence chain is visible:
js
baseURL: config.baseURL ?? launchEnvironmentOf(ctx).get(SEARCH_BASE_URL_ENV)?.value
         ?? "https://api.deepseek.com/anthropic/v1",

In other words, only setting the web-search-deepseek.baseURL config block explicitly, or setting the DEEPSEEK_SEARCH_BASE_URL environment variable, redirects search (#408). 2. The chat endpoint, by contrast, is overridable: llm-deepseek/src/index.ts at :69-70 / :115 / :210 shows the chat baseURL can be overridden by config.baseURL / $DEEPSEEK_BASE_URL, which is precisely the custom-gateway entry point. The two paths keep independent bases and different protocols (chat speaks OpenAI-compatible, search speaks Anthropic Messages), so not sharing the base is deliberate design (#408). 3. The gap sits on the credential side: web-search-deepseek/src/index.ts has DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY', meaning search references the chat key by default. The default combination therefore becomes "your key is a gateway key while the endpoint is the official one" — a silent cross-endpoint send of which the 401 is merely the surface symptom (#408). 4. Why "chat works" cannot serve as proof that search is configured correctly: the two share a credential but use different endpoints, so chat succeeding only proves the key is valid for the chat endpoint. The most reliable evidence of where search actually goes is the rc.8 Session event web/deepseek-search-llm-request, recorded before dispatch, which carries the final endpoint, model, and body without the key — far more trustworthy than looking at the current chat model (#408). 5. The product-side proposal is "linkage check + fail-loud alarm": when llm-deepseek.baseURL is explicitly overridden (meaning a custom gateway is in use) while web-search-deepseek.baseURL is not explicitly set (meaning search will hit the official endpoint), emit one clear startup or first-dispatch warning instead of leaving users to discover it from a packet capture. The appeal of this is that it does not affect users who are correctly configured today (official default plus an official key stays silent), and the Endpoint field on the rc.7 settings page can serve as the UI surface for the same linkage (#408).

Configuration and workarounds: DSH plugin baseURL/apiKeyEnv/model as one group

The fix is not "change one URL" but treating the search endpoint, model, and credential reference as a set — and confirming the gateway truly supports the protocol. Specifically:

  1. The minimal workaround: add a block to ~/.dsh/settings.yaml (community-verified), or use the environment variable DEEPSEEK_SEARCH_BASE_URL=https://opencode.ai/zen/go/v1:
yaml
web-search-deepseek:
  baseURL: https://opencode.ai/zen/go/v1

Note that baseURL must not end with /messages, because DeepSeek Harness appends it (#408). 2. The recommended grouped configuration: spell out the search endpoint, model, and credential reference together, so you do not swap the URL while still reusing the chat key:

yaml
- id: web-search-deepseek
  config:
    apiKeyEnv: GATEWAY_SEARCH_API_KEY
    baseURL: https://gateway.example/anthropic/v1
    model: gateway-search-model

From rc.7 onward you can also use the GUI: Settings → Plugins → Web search → Endpoint, enter an Anthropic Messages API base URL (DeepSeek Harness appends /messages), save, and retry web_search. Search resolves DEEPSEEK_API_KEY by default; a different key needs web-search-deepseek.apiKeyEnv pointing at another environment variable (#408). 3. The protocol bar must be met as well: "OpenAI-compatible" is not enough. The gateway must accept Anthropic Messages requests, execute web_search_20250305 as a server-side native tool, and return web_search_tool_result blocks. Otherwise the 401 merely becomes a 400 about a reserved custom function, or becomes "no web_search_tool_result blocks" — neither of which counts as a fix (#408). 4. When the gateway cannot do it, disable rather than bend: turn off both the built-in provider and the tool, then mount a search plugin that brings its own backend:

yaml
- id: web-search-deepseek
  disabled: true
- id: tool-web
  disabled: true

Two viable replacement routes: the zero-config simplified route, dsh plugin --profile web add github:shinjiyu/dsh-plugin-search (tool name stays web_search, the built-in web-search-deepseek is turned off, built on Tavily and keyless); and the fully decoupled route, dsh plugin --profile web add [email protected] plus dsh plugin --profile web add @juicesharp/rpiv-web-tools, then WEB_SEARCH_PROVIDER=searxng and SEARXNG_URL=https://your-searxng.example.com to hand search to your own SearXNG — the main conversation keeps using whichever provider you selected, while search is the plugin's own backend's responsibility, so it can no longer misroute the same key to the official search endpoint just because chat goes through a self-hosted gateway (#408). 5. If the credential already left, follow the issuer's policy: once you confirm the key reached an unintended endpoint, rotate it per the key issuer's policy rather than just fixing the config. It is also worth stopping repeated searches first, since every retry repeats the exposure. This is exactly why, when distributing search plugins through DSH Plugin Hub, your install notes should state clearly whether the search endpoint is independent from the chat endpoint and which environment variable it references by default (#408).

DSH plugin troubleshooting notes

Remember first that "key invalid" is usually a misread — chat and search are two paths with two endpoints that share one credential, so chat working does not prove the search config is right. Eight points to keep in mind when a DeepSeek Harness plugin sends web searches to the wrong endpoint:

  1. Two paths, two endpoints: chat's baseURL is overridable; search's default is hardcoded official, and neither inherits the other.
  2. The credential is reused by default: search references DEEPSEEK_API_KEY, and that is the root of the gap.
  3. "Key invalid" is usually a misread: it is the official endpoint's verbatim passthrough; the same key is typically valid on your gateway.
  4. Do not end baseURL with /messages: DeepSeek Harness appends it itself.
  5. OpenAI-compatible is not sufficient: it must support Anthropic Messages + web_search_20250305 as a native tool + web_search_tool_result.
  6. Configure as a set: endpoint, model, and apiKeyEnv together, not just the URL.
  7. Watch web/deepseek-search-llm-request: the rc.8 Session event is key-free and shows the true destination.
  8. Disable what you cannot support: bending the URL is not a fix; also rotate any key that already went out.
DSH Plugin Hub plugin market: find and install search plugins that bring their own backend

Sources: Discussion #408, dsh-plugin-search, pi2dsh.

FAQ

Chat in my DSH plugin works perfectly, so why does web_search fail authentication every time?

In a DSH plugin, chat and search are **two separate paths with two separate endpoints**. The chat path dsh-llm-deepseek takes its baseURL from llm-deepseek.baseURL or $DEEPSEEK_BASE_URL, while the search path dsh-web-search-deepseek has a **hardcoded** endpoint, https://api.deepseek.com/anthropic/v1, and **neither inherits the chat config nor reads $DEEPSEEK_BASE_URL**. Yet both default to the **same** DEEPSEEK_API_KEY, so when your key is only valid for your gateway, chat succeeds while search sends that same key to the official endpoint and gets a 401.

The DSH plugin error says "Your api key is invalid" — is my key actually dead?

In a DSH plugin that message usually means an **endpoint/key mismatch**, not a dead key. That sentence is just the official endpoint's **verbatim passthrough**: it received a key that does not belong to that platform, so it answers "invalid". The check is simple — the same key returns 401 against the official balance endpoint but 200 against your gateway's /models, and posting web_search_20250305 straight to the gateway's /messages returns proper web_search_tool_result blocks.

Is changing baseURL enough in a DSH plugin, and why does it still error afterwards?

In a DSH plugin, changing baseURL alone is not enough: it has to travel with apiKeyEnv and model as **one set**, and the gateway must genuinely support this protocol. It must accept Anthropic Messages requests, treat web_search_20250305 as a **server-side native tool**, and return web_search_tool_result blocks. A gateway that only speaks OpenAI chat-completions cannot — the error merely shifts from a 401 to a 400 about a reserved custom function, or to "no web_search_tool_result blocks". Also, do **not** end baseURL with /messages, because DeepSeek Harness appends it.

My gateway does not support that native search protocol — what can a DSH plugin user do?

A DSH plugin user whose gateway does not support the native search protocol should **disable the built-in provider and tool** and switch to a search plugin that brings its own backend, instead of bending the URL until it looks like it works: set both web-search-deepseek and tool-web to disabled: true. Two replacement routes exist — the zero-config simplified one, dsh plugin --profile web add github:shinjiyu/dsh-plugin-search (tool name stays web_search, built on Tavily and keyless), and the fully decoupled one, pi2dsh + @juicesharp/rpiv-web-tools with WEB_SEARCH_PROVIDER=searxng and SEARXNG_URL pointing at your own SearXNG.

Related Terms

separate search and chat endpoints
DeepSeek Harness deliberately keeps two independent configuration surfaces: chat uses an OpenAI-compatible path (llm-deepseek.baseURL / DEEPSEEK_BASE_URL), search uses an Anthropic Messages path (web-search-deepseek.baseURL / DEEPSEEK_SEARCH_BASE_URL, with /messages appended). Different protocols mean the base is intentionally not shared.https://github.com/deepseek-ai/deepseek-harness/discussions/408
cross-endpoint credential reuse (credential containment gap)
The search apiKeyEnv defaults to DEEPSEEK_API_KEY, the same key the chat provider uses. The gap is that the bases are independent while the key defaults to identical, so a key the user provisioned for a gateway gets sent silently to the official endpoint; the 401 is only the surface symptom.https://github.com/deepseek-ai/deepseek-harness/discussions/408
web_search_20250305
The server-side native search tool name a gateway must support. It has to be executed by the gateway as a server tool returning web_search_tool_result blocks, not treated as an ordinary custom function — a same-named custom function is rejected by some gateways as reserved.https://github.com/deepseek-ai/deepseek-harness/discussions/408

Sources