DSH plugin web_search 走官方端点认证失败?自定义网关配置与替代方案排查
如果你的聊天配置指向了一个兼容网关(比如自建中转站)并且工作正常,但每次调用 web_search 都在界面上返回红色 Error: Authentication Fails, Your api key: ****eHfw is invalid,那不是你的 key 失效了——是搜索请求被发到了另一个端点。 搜索链路 dsh-web-search-deepseek 的默认端点写死为 https://api.deepseek.com/anthropic/v1,不继承聊天配置、也不读 $DEEPSEEK_BASE_URL;而两条链路默认复用同一把 DEEPSEEK_API_KEY,于是你为网关准备的 key 被静默送到了官方端点(#408)。
现象:DSH plugin 聊天正常,web_search 每次都认证失败
这个故障的迷惑性在于:它看起来像「key 坏了」,但同一把 key 在别处明明好得很。 具体表现:
- 报错 100% 复现、且从未成功过一次:GUI 里每次调用
web_search都返回上述红色错误;****eHfw只是服务端把请求里携带的DEEPSEEK_API_KEY打码后的尾号(#408)。 - 典型配置形态:
~/.dsh/settings.yaml里llm-deepseek.baseURL指向自配网关(例如https://opencode.ai/zen/go/v1),~/.dsh/.credentials.yaml里放一把只对该网关有效的 key(这类网关 key 常见 67 位混合字母数字,而官方 key 是sk-+ 32 位小写十六进制)(#408)。 - 三条验证证据把结论钉死:① 拿该 key 请求官方余额接口 → 401,错误文案与界面上完全一致;② 请求网关的
/models→ 200,说明 key 有效;③ 直接把web_search_20250305工具请求发到网关的/messages→ 200,返回web_search_tool_result与 10 条结果。第三条尤其关键:它证明只要把搜索端点指到同一网关就能正常工作(#408)。 - 这不是个案,而是同一类配置的普遍结果:有用户抓包确认「联网搜索直接把公司的密钥发到官方那里去了」,也有用户描述选用了不同 provider 但搜索供应商仍默认用官方端点;错误信息被 DeepSeek Harness 原样透传,因此界面只会显示一个让人误判的认证失败(#408)。
机制:DSH plugin 搜索端点写死官方,但凭据默认复用聊天 key
把「端点的默认值」和「凭据的默认值」分开看,就能一次看清这个缺口是怎么形成的。 逐层拆解:
- 搜索端点写死在源码里:
web-search-deepseek/src/provider.ts:35定义DEEPSEEK_DEFAULT_BASE_URL = 'https://api.deepseek.com/anthropic/v1',模块文档还明确写着 "only the API key is shared"。rc.6 的编译产物里可以看到优先级链:
baseURL: config.baseURL ?? launchEnvironmentOf(ctx).get(SEARCH_BASE_URL_ENV)?.value
?? "https://api.deepseek.com/anthropic/v1",
也就是说,只有显式设置 web-search-deepseek.baseURL 配置段,或设置 DEEPSEEK_SEARCH_BASE_URL 环境变量,才会改变搜索去向(#408)。
2. 聊天端点则是可覆盖的:llm-deepseek/src/index.ts 的 :69-70 / :115 / :210 表明聊天的 baseURL 可被 config.baseURL / $DEEPSEEK_BASE_URL 覆盖,这正是自配网关的入口。两条链路的 base 各自独立、协议还不同(聊天走 OpenAI-compatible,搜索走 Anthropic Messages),所以不复用 base 本身是有意设计(#408)。
3. 缺口出在凭据这一侧:web-search-deepseek/src/index.ts 的 DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY',即搜索默认引用聊天的 key。于是默认值组合变成了「key 是你的网关 key、端点却是官方端点」——一次静默的跨端点发送,401 只是它的表面症状(#408)。
4. 为什么「聊天成功」不能作为「搜索配置正确」的证据:这两件事共享凭据但走不同端点,聊天成功只证明 key 对聊天端点有效。判断搜索实际去向最可靠的证据是 rc.8 新增的 Session event web/deepseek-search-llm-request:它在真正 dispatch 之前记录不含密钥的最终 endpoint、model 和 body,比看当前聊天模型可靠得多(#408)。
5. 产品侧的改进建议是「联动检查 + fail-loud 告警」:当 llm-deepseek.baseURL 被显式覆盖(说明用户在用自定义网关)而 web-search-deepseek.baseURL 未显式配置(说明搜索将打官方端点)时,启动或首次 dispatch 打一条明确告警,而不是留给用户从抓包里发现。这条建议的优点是完全不影响现状下配置正确的用户(默认官方 + 官方 key 不告警),rc.7 设置页的 Endpoint 字段也可以作为同一联动的 UI 提示面(#408)。
配置与规避:DSH plugin 的 baseURL/apiKeyEnv/model 一组配齐
修法不是「只改一个 URL」,而是把搜索的端点、模型、凭据引用当成一组配置,并确认网关真的支持这套协议。 具体做法:
- 最小可用的规避:在
~/.dsh/settings.yaml增加一段(社区已实测可行),或改用环境变量DEEPSEEK_SEARCH_BASE_URL=https://opencode.ai/zen/go/v1:
web-search-deepseek:
baseURL: https://opencode.ai/zen/go/v1
注意 baseURL 不要写到以 /messages 结尾,因为 DeepSeek Harness 会自动追加(#408)。
2. 推荐的一组配置:把搜索 endpoint、model、credential reference 一起写清楚,避免只换 URL 却仍复用聊天 key:
- id: web-search-deepseek
config:
apiKeyEnv: GATEWAY_SEARCH_API_KEY
baseURL: https://gateway.example/anthropic/v1
model: gateway-search-model
在 rc.7 之后还可以走图形界面:Settings → Plugins → Web search → Endpoint,填 Anthropic Messages API 的 base URL(DeepSeek Harness 会追加 /messages)、保存、重试 web_search;搜索默认解析 DEEPSEEK_API_KEY,换 key 就通过 web-search-deepseek.apiKeyEnv 指向另一个环境变量(#408)。
3. 协议门槛必须同时满足:「OpenAI-compatible」不够。网关必须接受 Anthropic Messages 请求、把 web_search_20250305 当作服务端原生工具执行、并返回 web_search_tool_result blocks。否则 401 只会变成 reserved custom function 的 400,或变成「没有 web_search_tool_result blocks」——那都不算修复(#408)。
4. 网关不支持时的正当处理——禁用而不是硬改:把内置 provider 和工具都关掉,再挂一个自带 backend 的搜索插件:
- id: web-search-deepseek
disabled: true
- id: tool-web
disabled: true
两个可用的替代方向:零配置简化路线 dsh plugin --profile web add github:shinjiyu/dsh-plugin-search(工具名仍叫 web_search、关掉内置 web-search-deepseek、默认基于 Tavily 且 keyless);完全解耦路线 dsh plugin --profile web add [email protected] + dsh plugin --profile web add @juicesharp/rpiv-web-tools,再用 WEB_SEARCH_PROVIDER=searxng、SEARXNG_URL=https://your-searxng.example.com 把搜索交给自己的 SearXNG——主对话继续走你选的任意 provider,搜索由插件自己的 backend 负责,不会再因为聊天走自建网关而把同一把 key 错发到官方搜索端点(#408)。
5. 凭据已经发出去了要按签发方策略处理:如果确认 key 已到达非预期端点,应按密钥签发方的策略轮换,而不是只把配置改对就算完。同时建议先停止重复搜索(每次重试都在重复这个动作)。这也是为什么你在做 DSH插件 或 DeepSeek插件、并在 DSH Plugin Hub 上分发搜索类插件时,值得在自己的安装说明里明确写清「搜索端点是否独立于聊天端点」,以及它默认引用哪个环境变量(#408)。
DSH plugin 排查注意事项
先记住「key invalid」多半是误判——聊天与搜索是两条链路、两个端点,共享凭据却各走各的,所以聊天成功并不能证明搜索配置正确。 八条要点:
- 两条链路、两个端点:聊天可覆盖 baseURL,搜索默认写死官方端点,互不继承。
- 凭据默认复用:搜索默认引用
DEEPSEEK_API_KEY,这才是缺口的根源。 - 「key invalid」多为误判:那是官方端点的原样透传,同一把 key 在你的网关上通常是有效的。
- baseURL 别带
/messages:DeepSeek Harness 会自行追加。 - OpenAI-compatible 不是充分条件:必须支持 Anthropic Messages +
web_search_20250305原生工具 +web_search_tool_result。 - 配置要成组:endpoint、model、apiKeyEnv 一起配,不要只换 URL。
- 看
web/deepseek-search-llm-request:rc.8 的这条 Session event 是不含密钥的真实去向证据。 - 不支持就禁用:改 URL 硬凑不算修复;同时按签发方策略轮换已外发的 key。

常见问题
DSH plugin 的聊天链路与搜索链路是**两条独立的链路、两个独立的端点**。聊天链路 dsh-llm-deepseek 的 baseURL 可被 llm-deepseek.baseURL 或 $DEEPSEEK_BASE_URL 覆盖;而搜索链路 dsh-web-search-deepseek 的端点**写死**为 https://api.deepseek.com/anthropic/v1,**既不继承聊天配置、也不读 $DEEPSEEK_BASE_URL**。两条链路却默认用**同一个** DEEPSEEK_API_KEY,所以当你的 key 只对自配网关有效时,聊天成功、搜索把同一把 key 发到官方端点 → 官方返回 401。
DSH plugin 报的「Your api key is invalid」通常不是 key 失效,而是**端点和 key 不匹配**。这句话只是官方端点的**原样透传**:它收到的是一把不属于该平台的 key,所以回你「无效」。判断方法很简单——拿同一把 key 打官方余额接口会 401,但打你的网关 /models 会 200;把 web_search_20250305 直接发到网关的 /messages 也能正常返回 web_search_tool_result。所以真正的症结是端点与 key 不匹配,而不是 key 本身失效。
DSH plugin 只改 baseURL 不够,它必须连同 apiKeyEnv、model 作为**一组**配置,而且网关必须真的支持这套协议:接受 Anthropic Messages 请求、把 web_search_20250305 当作**服务端原生工具**、并返回 web_search_tool_result blocks。只支持 OpenAI chat-completions 的网关做不到——那样报错只会从 401 变成 reserved custom function 的 400,或者变成「没有 web_search_tool_result blocks」。另外 baseURL **不要**写到以 /messages 结尾,因为 DeepSeek Harness 会自动追加。
DSH plugin 用户遇到网关不支持这套原生搜索协议时,诚实的做法是**禁用内置 provider 和工具**,换一个自己带 backend 的搜索插件,而不是硬改 URL 让它看起来能跑:web-search-deepseek 与 tool-web 都设 disabled: true。替代方案有两类——设备端零配置路线可用 dsh plugin --profile web add github:shinjiyu/dsh-plugin-search(工具名仍是 web_search,基于 Tavily、keyless);完全解耦路线可装 pi2dsh + @juicesharp/rpiv-web-tools,用 WEB_SEARCH_PROVIDER=searxng + SEARXNG_URL 让搜索走你自己的 SearXNG。
相关术语
- search/chat endpoint split
- search/chat endpoint split 是 DeepSeek Harness 刻意保留两个独立配置面的设计:对话走 OpenAI-compatible chat 路径(llm-deepseek.baseURL / DEEPSEEK_BASE_URL),搜索走 Anthropic Messages 路径(web-search-deepseek.baseURL / DEEPSEEK_SEARCH_BASE_URL,会自动追加 /messages)。协议不同所以 base 不复用,这一点是有意设计。— https://github.com/deepseek-ai/deepseek-harness/discussions/408
- cross-endpoint credential reuse (credential containment gap)
- cross-endpoint credential reuse 是搜索凭据默认复用聊天 key 造成的缺口:搜索的 apiKeyEnv 默认为 DEEPSEEK_API_KEY,与聊天 provider 是同一把 key。缺口在于:base 各自独立、key 却默认相同,导致用户为网关放的 key 会被静默发往官方端点,401 只是表面症状。— https://github.com/deepseek-ai/deepseek-harness/discussions/408
- web_search_20250305
- web_search_20250305 是网关需要支持的服务端原生搜索工具名。它必须被网关当作服务端工具执行并返回 web_search_tool_result blocks,而不是当作普通自定义函数——同名自定义函数会被部分网关以「reserved」为由拒绝。— https://github.com/deepseek-ai/deepseek-harness/discussions/408
来源
- deepseek-harness Discussion #408:web_search 固定请求官方端点,导致自配网关的用户搜索必现认证失败· deepseek-ai(GitHub Discussions)
- dsh-plugin-search(Tavily keyless,关闭内置 provider 的替代方案)· GitHub(shinjiyu)
- pi2dsh + @juicesharp/rpiv-web-tools(搜索 provider 与聊天 provider 完全解耦)· GitHub(weijiafu14)