diff --git a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml
index 490d5c5d24..05e275e67d 100644
--- a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml
@@ -9,8 +9,8 @@
en: c8b932b02a4e1c80
zh: 96c466be1a521f3f
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/decision:
- en: b57eb0f268033ff1
- zh: 4143382a50181414
+ en: 05ae26970169c2e2
+ zh: c520ea65e92095ae
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/package-topology:
en: 5ea3ddfae82df181
zh: 2d05d95e51081696
diff --git a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md
index 8c6c088ea5..632fa2454b 100644
--- a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md
+++ b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md
@@ -32,7 +32,7 @@ Search and fetch are separate tools but one web-access seam. `ctx.web` owns prov
- A tool is never unregistered merely because its selected provider is missing, misconfigured, missing credentials, ambiguous, or temporarily unavailable.
- The provider is resolved at execution time, and a structured `WebError` is returned when the selected capability cannot run.
-This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. If web search is enabled but no usable search provider exists, `web_search` remains visible and execution fails with a structured `WebError` such as `WEB_PROVIDER_UNAVAILABLE` or `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. If a provider appears after `dsh-tool-web`, the next execution can use it without changing the schema. If a provider disappears mid-call, execution fails with a structured `WebError` instead of silently choosing another provider or falling through to `UNKNOWN_TOOL`.
+This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. If web search is enabled but no usable search provider exists, `web_search` remains visible and execution fails with a structured `WebError` such as `WEB_PROVIDER_UNAVAILABLE` or `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. The shipped DeepSeek provider narrows this for its own credentials: it [withholds `web_search` from a model request whose search would have no credential](2026-09-27-deepseek-search-withholds-web-search-without-credentials.md). If a provider appears after `dsh-tool-web`, the next execution can use it without changing the schema. If a provider disappears mid-call, execution fails with a structured `WebError` instead of silently choosing another provider or falling through to `UNKNOWN_TOOL`.
The seam deliberately exposes no observation surface — no registry-change event and no aggregated capability-status query. Unavailability is a fact a caller observes by executing: `search()`/`fetch()` resolve the provider at call time and throw the structured `WebError` that names what failed. [The observation-surface Agent Note](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) records that judgment: derived-on-call selection and enablement-based registration leave no consumer that needs a change signal or an availability probe distinct from executing and routing the error, and a future provider-status panel reintroduces the smallest signal or query it actually consumes.
diff --git a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md
index 4921c4a464..146371bbac 100644
--- a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md
@@ -32,7 +32,7 @@ Web 访问是一个一等能力 seam,遵循[能力 seam Agent Note](2026-06-13
- 工具绝不会仅仅因为其选定的提供方缺失、配置错误、缺少凭证、存在歧义或暂时不可用就被注销。
- 提供方在执行时解析,当选定的能力无法运行时返回结构化的 `WebError`。
-这使模型 schema 保持稳定,而不将插件加载顺序、凭证状态或 HMR(热模块替换)时序纳入面向模型的约定。如果 web 搜索已启用但不存在可用的搜索提供方,`web_search` 仍然可见,执行时以结构化的 `WebError`(如 `WEB_PROVIDER_UNAVAILABLE` 或 `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`)失败。如果某个提供方在 `dsh-tool-web` 之后出现,下一次执行即可使用它而无需更改 schema。如果某个提供方在调用过程中消失,执行以结构化的 `WebError` 失败,而不是静默选择另一个提供方或回退到 `UNKNOWN_TOOL`。
+这使模型 schema 保持稳定,而不将插件加载顺序、凭证状态或 HMR(热模块替换)时序纳入面向模型的约定。如果 web 搜索已启用但不存在可用的搜索提供方,`web_search` 仍然可见,执行时以结构化的 `WebError`(如 `WEB_PROVIDER_UNAVAILABLE` 或 `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`)失败。发行版的 DeepSeek 提供方针对自身凭据收窄了这一点:它会[从搜索没有可用凭据的模型请求中撤下 `web_search`](2026-09-27-deepseek-search-withholds-web-search-without-credentials.zh.md)。如果某个提供方在 `dsh-tool-web` 之后出现,下一次执行即可使用它而无需更改 schema。如果某个提供方在调用过程中消失,执行以结构化的 `WebError` 失败,而不是静默选择另一个提供方或回退到 `UNKNOWN_TOOL`。
该 seam 刻意不暴露任何观察面——没有注册表变更事件,也没有聚合的能力状态查询。不可用性是调用方通过执行观察到的事实:`search()`/`fetch()` 在调用时解析提供方,并抛出命名了失败原因的结构化 `WebError`。[观察面 Agent Note](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) 记录了这一判断:基于调用的派生选择与基于启用的注册使得没有消费方需要变更信号或独立于执行和错误路由的可用性探测;未来的提供方状态面板会重新引入它实际消费的最小信号或查询。
diff --git a/.agents/notes/implemented/architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.i18n.yaml
new file mode 100644
index 0000000000..eb9188454a
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.i18n.yaml
@@ -0,0 +1,19 @@
+# Bilingual-pair consistency record for 2026-09-27-deepseek-search-withholds-web-search-without-credentials.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.md
+/agent-note-deepseek-search-withholds-web-search-from-requests-without-credentials:
+ en: 1b566b013361a55c
+ zh: 42c1d079a0777069
+/agent-note-deepseek-search-withholds-web-search-from-requests-without-credentials/problem:
+ en: 5cac7bee675f0abf
+ zh: 9ed6c916e7a0ad59
+/agent-note-deepseek-search-withholds-web-search-from-requests-without-credentials/decision:
+ en: 1237062ad39fd815
+ zh: 357837769fee8636
+/agent-note-deepseek-search-withholds-web-search-from-requests-without-credentials/alternatives-considered:
+ en: 55597c99c640dabc
+ zh: 4539f62d052873d4
+/agent-note-deepseek-search-withholds-web-search-from-requests-without-credentials/consequences:
+ en: 26d36d8a35306d49
+ zh: fa501a9c783eb480
diff --git a/.agents/notes/implemented/architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.md b/.agents/notes/implemented/architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.md
new file mode 100644
index 0000000000..dedb97f8c2
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.md
@@ -0,0 +1,39 @@
+# Agent Note: DeepSeek search withholds web_search from requests without credentials
+
+Status: implemented
+
+English | [中文](2026-09-27-deepseek-search-withholds-web-search-without-credentials.zh.md)
+
+## Problem
+
+`dsh-tool-web` registers `web_search` whenever search is enabled, and the [web capability seam decision](2026-06-24-web-capability-seam.md) keeps it visible while the selected provider cannot run. The shipped search provider, `dsh-web-search-deepseek`, authenticates a search with the DeepSeek account token only for a model request on the `deepseek-account` route; every other route needs a configured API key. A Session on an API-key route without a search key was therefore offered a tool whose every call failed with `WEB_PROVIDER_CREDENTIAL_MISSING`. Sessions in one Host can use different routes, a Session can switch routes between requests, and the Web app registers `web_search` once per agent preset, so no registration decision matches every request.
+
+## Decision
+
+`dsh-web-search-deepseek` adds a prepended `system-prompt/assemble` listener. For each assembly that offers `web_search`, it removes the tool and the `tool:web_search` prompt section unless the search that request would make has a credential:
+
+- the request's `provider` variable is `deepseek-account` and `ctx.deepseekAccount.resolveToken()` returns a token for the configured search endpoint;
+- the section carries a literal `apiKey`; or
+- `ctx.credentials.describe()` reports the `apiKeyEnv` reference configured, or, without that service, the launching environment carries it.
+
+Only presence decides. The listener reads no key value and contacts no endpoint, so a wrong key or an endpoint that does not serve the native `web_search_20250305` server tool keeps the tool offered. A failed account-token or credential-store read counts as no credential.
+
+The listener is prepended so that it runs after model selection has written the route of this request into the assembly variables. An assembly that contains the PTC `run_code` transport is returned unchanged, because the generated SDK declares `web_search` apart from the tool list. Withholding changes only the offered tools: a call that still arrives resolves credentials when it runs and fails with the existing structured error.
+
+The request header records the assembled tools, so the agent loop announces each change through tool-registry updates. A route switch, a stored key, or a removed key changes the next request; DeepSeek routes load a returning tool as a deferred addition, and routes without tool-update support receive the complete tool list.
+
+## Alternatives considered
+
+**Register or unregister `web_search` in `dsh-tool-web`.** Registration is shared by every Session of an agent preset, while credentials depend on each request's route. Unregistering would also make `toolOrder` entries and subagent `toolFilter` restrictions that name `web_search` fail.
+
+**Add a readiness query to `ctx.web` and a per-request visibility predicate to `dsh-tools`.** This keeps the decision with the tool owner and also covers the PTC SDK, but it adds public API to two core packages and a route accessor to `dsh-agent` for one provider's credential rule.
+
+**Keep the tool offered and fail at execution.** The model spends a call on a search that cannot authenticate, and the failure repeats on every attempt.
+
+**Probe the key or endpoint before offering the tool.** A network request per model request adds latency and cost; the user controls configuration, not its validity at that moment.
+
+## Consequences
+
+The provider edits a tool list owned by `dsh-tool-web` and names that package's tool and prompt section. The check assumes DeepSeek serves search: a composition that loads this provider but selects another one still loses `web_search` whenever DeepSeek has no credential. PTC presentations keep offering `web_search` without credentials. An endpoint that cannot serve the native server tool keeps the tool offered and fails each call. Removing the tool on an unchanged route changes the declared tools and misses the prompt cache once; a model switch already invalidates it.
+
+Unit tests in `packages/web/web-search-deepseek/tests/deepseek.spec.ts` assemble through the real prompt, tool, and model-selection services. The shipped headless keyless smoke test and the `web-search-unconfigured` recorded-session snapshot pin the withheld tool in real compositions. Snapshot replays supply a placeholder DeepSeek key, so their tool lists do not depend on the runner's environment.
diff --git a/.agents/notes/implemented/architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.zh.md b/.agents/notes/implemented/architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.zh.md
new file mode 100644
index 0000000000..b3f07e1d3c
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.zh.md
@@ -0,0 +1,39 @@
+# Agent Note: DeepSeek 搜索从没有凭据的请求中撤下 web_search
+
+Status: implemented
+
+[English](2026-09-27-deepseek-search-withholds-web-search-without-credentials.md) | 中文
+
+## Problem
+
+`dsh-tool-web` 在启用搜索时总会注册 `web_search`,而 [web 能力 seam 决策](2026-06-24-web-capability-seam.zh.md)让它在选中的提供方无法运行时仍然可见。发行版的搜索提供方 `dsh-web-search-deepseek` 只对 `deepseek-account` 路由上的模型请求使用 DeepSeek 账号 token 鉴权;其他路由都需要已配置的 API 密钥。因此,API 密钥路由上没有搜索密钥的会话会获得一个每次调用都以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败的工具。同一 Host 中的会话可以使用不同路由,一个会话可以在请求之间切换路由,而 Web 应用按 agent preset 只注册一次 `web_search`,因此任何注册层面的决定都无法与每个请求相符。
+
+## Decision
+
+`dsh-web-search-deepseek` 添加一个前置的 `system-prompt/assemble` 监听器。对于每个提供 `web_search` 的组装,除非该请求将发起的搜索具有凭据,否则它会移除该工具与 `tool:web_search` 提示词段落:
+
+- 请求的 `provider` 变量为 `deepseek-account`,且 `ctx.deepseekAccount.resolveToken()` 为配置的搜索端点返回 token;
+- 配置段带有字面 `apiKey`;或
+- `ctx.credentials.describe()` 报告 `apiKeyEnv` 引用已配置;没有该服务时,启动环境中存在该变量。
+
+只看凭据是否存在。监听器不读取密钥的值,也不访问任何端点,因此错误的密钥,或不提供原生 `web_search_20250305` 服务器工具的端点,仍会让该工具保持提供。读取账号 token 或凭据存储失败时按没有凭据处理。
+
+监听器是前置的,因此它在模型选择把本请求的路由写入组装变量之后运行。包含 PTC `run_code` 传输工具的组装原样返回,因为生成的 SDK 独立于工具列表声明 `web_search`。撤下只改变提供给模型的工具:仍然到达的调用会在运行时解析凭据,并以现有的结构化错误失败。
+
+请求头记录组装后的工具,因此 agent loop(智能体循环)通过工具注册表更新宣告每次变化。路由切换、存储密钥或删除密钥都会改变下一个请求;DeepSeek 路由以延迟加载的新增方式载入重新提供的工具,不支持工具更新的路由则接收完整工具列表。
+
+## Alternatives considered
+
+**在 `dsh-tool-web` 中注册或注销 `web_search`。** 注册由同一 agent preset 的所有会话共享,而凭据取决于每个请求的路由。注销还会使点名 `web_search` 的 `toolOrder` 条目和 subagent `toolFilter` 限制失败。
+
+**为 `ctx.web` 增加就绪查询,并为 `dsh-tools` 增加按请求的可见性谓词。** 这样决定留在工具所有者处,也能覆盖 PTC SDK,但会为一个提供方的凭据规则给两个核心包增加公共 API,并给 `dsh-agent` 增加路由访问器。
+
+**保持工具可见,在执行时失败。** 模型会把一次调用花在无法鉴权的搜索上,并且每次尝试都会重复失败。
+
+**提供工具前探测密钥或端点。** 每个模型请求都发起一次网络请求会增加延迟与成本;用户控制的是配置本身,而不是它在那一刻是否有效。
+
+## Consequences
+
+提供方修改了 `dsh-tool-web` 拥有的工具列表,并点名该包的工具与提示词段落。该检查假定由 DeepSeek 负责搜索:加载本提供方但选择其他提供方的组合,只要 DeepSeek 没有凭据,仍会失去 `web_search`。PTC 呈现在没有凭据时仍然提供 `web_search`。无法提供原生服务器工具的端点仍会让工具保持提供,并使每次调用失败。在路由不变时移除工具会改变声明的工具,并使提示词缓存失效一次;模型切换本身已经使缓存失效。
+
+`packages/web/web-search-deepseek/tests/deepseek.spec.ts` 中的单元测试通过真实的提示词、工具与模型选择服务进行组装。发行版 headless 无密钥冒烟测试与 `web-search-unconfigured` 录制会话快照在真实组合中固定了撤下行为。快照回放提供占位 DeepSeek 密钥,因此其工具列表不依赖运行环境。
diff --git a/.agents/notes/implemented/feature/2026-07-31-web-default-search.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-web-default-search.i18n.yaml
index f398e72a4e..8dede0b777 100644
--- a/.agents/notes/implemented/feature/2026-07-31-web-default-search.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-31-web-default-search.i18n.yaml
@@ -9,8 +9,8 @@
en: cd5af0c73d578874
zh: 29e14c7ff1c77391
/agent-note-default-web-search-in-shipped-compositions/decision:
- en: 7062647c8e227743
- zh: 26bdd8dc4b0cd7ad
+ en: 72c0d2ecb024a4ce
+ zh: 6d79e27c405b6f1c
/agent-note-default-web-search-in-shipped-compositions/alternatives-considered:
en: f28e0f1183e755ce
zh: 2cbf256a33d99686
diff --git a/.agents/notes/implemented/feature/2026-07-31-web-default-search.md b/.agents/notes/implemented/feature/2026-07-31-web-default-search.md
index fed162cf25..df7678fd57 100644
--- a/.agents/notes/implemented/feature/2026-07-31-web-default-search.md
+++ b/.agents/notes/implemented/feature/2026-07-31-web-default-search.md
@@ -14,7 +14,7 @@ The harness had a complete Web capability family—provider registry, DeepSeek/E
`packages/bundle/base/cordis.patch.yml` explicitly mounts `dsh-web` with `searchProvider: deepseek-official` and `fetchProvider: http`, `dsh-web-search-deepseek`, `dsh-web-fetch-http`, and `dsh-tool-web` with `searchTimeoutMs: 60000`. The [shared-base Web fetch default](../../archived/feature/2026-09-01-shared-base-web-fetch-default.md) owns the current `fetch: true`; this record continues to own provider selection, search credentials, and timeout. Explicit provider ids keep selection independent of registration order and leave personal or `--patch` overlays able to replace or disable the rows. The one-minute shipped budget covers an auxiliary DeepSeek Messages request plus server-side retrieval while leaving `dsh-tool-web`'s provider-neutral 30-second default unchanged for custom compositions. The [Web capability seam decision](../architecture/2026-06-24-web-capability-seam.md) owns the public-fetch security policy.
-DeepSeek search uses the same `DEEPSEEK_API_KEY` credential reference as the official conversation adapter. The provider resolves that reference inside every search through the optional `ctx.credentials` service; only a composition without the seam falls back to the launching process environment, and a non-empty literal `apiKey` remains the programmatic last resort. A stored or rotated Web Models key therefore reaches the next search without restarting or retaining the value on the provider. Because `WebSearchProvider.available()` is synchronous, it treats an installed resolver as locally usable and missing dynamic credentials fail the operation with the provider-specific `WEB_PROVIDER_CREDENTIAL_MISSING` code while the stable tool schema stays registered.
+DeepSeek search uses the same `DEEPSEEK_API_KEY` credential reference as the official conversation adapter. The provider resolves that reference inside every search through the optional `ctx.credentials` service; only a composition without the seam falls back to the launching process environment, and a non-empty literal `apiKey` remains the programmatic last resort. A stored or rotated Web Models key therefore reaches the next search without restarting or retaining the value on the provider. Because `WebSearchProvider.available()` is synchronous, it treats an installed resolver as locally usable and missing dynamic credentials fail the operation with the provider-specific `WEB_PROVIDER_CREDENTIAL_MISSING` code while the stable tool schema stays registered. A model request whose search would have no credential is not offered the tool; [credential-based withholding](../architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.md) owns that decision.
Search keeps its endpoint distinct from chat completions: `DEEPSEEK_SEARCH_BASE_URL` overrides the Anthropic-compatible base, while `DEEPSEEK_BASE_URL` continues to configure conversation requests. Each `web_search` performs an auxiliary DeepSeek Messages call with the native search server tool. Immediately before dispatch, the provider appends a log-only `web/deepseek-search-llm-request` event to the initiating Agent session with the resolved endpoint, API version, and exact secret-free JSON body. A failure after dispatch names that endpoint and tells the conversation model to guide the user to the Web search Endpoint field in Settings when the endpoint is unintended. The message names `DEEPSEEK_SEARCH_BASE_URL` and `web-search-deepseek.baseURL` when that settings page is unavailable; the model does not select or change the credential destination. Credential preflight remains provider-local and races caller cancellation; neither concern expands the generic Web or credentials seams.
diff --git a/.agents/notes/implemented/feature/2026-07-31-web-default-search.zh.md b/.agents/notes/implemented/feature/2026-07-31-web-default-search.zh.md
index 3f39ef7d9f..f1a83b325c 100644
--- a/.agents/notes/implemented/feature/2026-07-31-web-default-search.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-31-web-default-search.zh.md
@@ -14,7 +14,7 @@ Status: implemented
`packages/bundle/base/cordis.patch.yml` 明确挂载 `dsh-web`,配置 `searchProvider: deepseek-official` 与 `fetchProvider: http`,同时挂载 `dsh-web-search-deepseek`、`dsh-web-fetch-http`,并以 `searchTimeoutMs: 60000` 挂载 `dsh-tool-web`。[共享 base 的 Web 抓取默认值](../../archived/feature/2026-09-01-shared-base-web-fetch-default.md)负责当前的 `fetch: true`;本文继续负责提供方选择、搜索凭据与超时。显式提供方 id 使选择不受注册顺序影响,同时个人覆盖层或 `--patch` 覆盖层仍可替换或禁用这些配置项。已交付的一分钟预算用于覆盖一次辅助 DeepSeek Messages 请求及服务端检索,同时保持 `dsh-tool-web` 提供方无关的 30 秒默认值不变,以供自定义组合使用。[Web 能力 seam 决策](../architecture/2026-06-24-web-capability-seam.zh.md)负责公开抓取安全策略。
-DeepSeek 搜索使用与官方会话适配器相同的 `DEEPSEEK_API_KEY` 凭据引用。提供方在每次搜索内部通过可选的 `ctx.credentials` 服务解析该引用;只有未挂载该 seam 的组合才会回退到启动进程的环境变量,非空的 `apiKey` 字面值仍作为程序化配置的最后兜底。因此,由 Web 的 Models 页存储或轮换的密钥无需重启即可用于下一次搜索,提供方也无需保留该值。由于 `WebSearchProvider.available()` 是同步方法,它会将已安装解析器视为本地可用;若动态凭据缺失,操作会以提供方专属错误码 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败,而稳定的工具 schema 仍保持注册。
+DeepSeek 搜索使用与官方会话适配器相同的 `DEEPSEEK_API_KEY` 凭据引用。提供方在每次搜索内部通过可选的 `ctx.credentials` 服务解析该引用;只有未挂载该 seam 的组合才会回退到启动进程的环境变量,非空的 `apiKey` 字面值仍作为程序化配置的最后兜底。因此,由 Web 的 Models 页存储或轮换的密钥无需重启即可用于下一次搜索,提供方也无需保留该值。由于 `WebSearchProvider.available()` 是同步方法,它会将已安装解析器视为本地可用;若动态凭据缺失,操作会以提供方专属错误码 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败,而稳定的工具 schema 仍保持注册。搜索没有可用凭据的模型请求不会获得该工具;[按凭据撤下工具](../architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.zh.md)的决策负责这一点。
搜索端点与 chat completions 保持独立:`DEEPSEEK_SEARCH_BASE_URL` 覆盖 Anthropic 兼容基址,`DEEPSEEK_BASE_URL` 则继续配置会话请求。每次 `web_search` 都会发起一次辅助 DeepSeek Messages 调用,并携带原生搜索服务器工具。发出请求前一刻,提供方会向发起请求的 agent(智能体)会话追加仅用于日志的 LLM(大语言模型)请求事件 `web/deepseek-search-llm-request`,其中包含已解析端点、API 版本,以及不含密钥的精确 JSON 请求体。请求发出后的失败会指出该端点;当端点不符合用户预期时,错误消息会要求会话模型指导用户在 Settings 中修改网页搜索的 Endpoint 字段。该设置页面不可用时,消息会说明 `DEEPSEEK_SEARCH_BASE_URL` 和 `web-search-deepseek.baseURL`;模型不得替用户选择或修改凭据发送目的地。凭据预检仍留在提供方内部,并与调用方取消存在竞态;这两项关注点都不会扩展通用 Web seam 或凭据 seam。
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index cf43f07cb8..fd0ca7bba3 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -4142,7 +4142,7 @@ export interface Config {
- `inject`: `web`
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
-- `source`: [`packages/web/web-search-deepseek/src/index.ts:49`](../packages/web/web-search-deepseek/src/index.ts)
+- `source`: [`packages/web/web-search-deepseek/src/index.ts:53`](../packages/web/web-search-deepseek/src/index.ts)
```ts config-catalog
/** Plugin config (all optional — `apply` fills env-var and constant defaults). */
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index e9e22b4c7f..372babf6d2 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -4144,7 +4144,7 @@ export interface Config {
- `inject`: `web`
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
-- `source`: [`packages/web/web-search-deepseek/src/index.ts:49`](../packages/web/web-search-deepseek/src/index.ts)
+- `source`: [`packages/web/web-search-deepseek/src/index.ts:53`](../packages/web/web-search-deepseek/src/index.ts)
```ts config-catalog
/** Plugin config (all optional — `apply` fills env-var and constant defaults). */
diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md
index 4385771ec8..1fc8a422b8 100644
--- a/docs/event-producer-consumer.md
+++ b/docs/event-producer-consumer.md
@@ -71,7 +71,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:154`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:165`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
-| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) |
+| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt), [`web-search-deepseek`](../packages/web/web-search-deepseek) |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:208`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:164`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md
index b3edadf12e..48c6127f13 100644
--- a/docs/event-producer-consumer.zh.md
+++ b/docs/event-producer-consumer.zh.md
@@ -73,7 +73,7 @@
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:154`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:165`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
-| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) |
+| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt), [`web-search-deepseek`](../packages/web/web-search-deepseek) |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:208`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:164`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
diff --git a/docs/module-graph.md b/docs/module-graph.md
index 0da940458a..4fb3c31e72 100644
--- a/docs/module-graph.md
+++ b/docs/module-graph.md
@@ -720,12 +720,6 @@ flowchart TD
pkg_fs_sandbox --> pkg_fs_local
pkg_fs_sandbox --> pkg_sandbox
pkg_fs_sandbox --> pkg_sandbox_policy
- pkg_web_search_deepseek --> pkg_agent
- pkg_web_search_deepseek --> pkg_credentials
- pkg_web_search_deepseek --> pkg_deepseek_account
- pkg_web_search_deepseek --> pkg_launch_environment
- pkg_web_search_deepseek --> pkg_session
- pkg_web_search_deepseek --> pkg_web
pkg_api_account_controller --> pkg_agent
pkg_api_account_controller --> pkg_deepseek_account
pkg_api_account_controller --> pkg_typert_protocol
@@ -829,6 +823,14 @@ flowchart TD
pkg_tool_web --> pkg_system_prompt
pkg_tool_web --> pkg_tools
pkg_tool_web --> pkg_web
+ pkg_web_search_deepseek --> pkg_agent
+ pkg_web_search_deepseek --> pkg_credentials
+ pkg_web_search_deepseek --> pkg_deepseek_account
+ pkg_web_search_deepseek --> pkg_launch_environment
+ pkg_web_search_deepseek --> pkg_session
+ pkg_web_search_deepseek --> pkg_system_prompt
+ pkg_web_search_deepseek --> pkg_tools
+ pkg_web_search_deepseek --> pkg_web
pkg_tool_todo --> pkg_agent
pkg_tool_todo --> pkg_invariants
pkg_tool_todo --> pkg_session
@@ -1584,7 +1586,6 @@ flowchart TD
| [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) |
| [`goal-round-driver`](../packages/goal/goal-round-driver) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
-| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
| [`api-account-controller`](../packages/api/account-controller) | `api` | [`agent`](../packages/core/agent), [`deepseek-account`](../packages/credentials/deepseek-account), [`typert-protocol`](../packages/typert/protocol) |
| [`compaction`](../packages/compaction/compaction) | `compaction` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) | `credentials` | [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`host-webserver`](../packages/host/webserver) |
@@ -1605,6 +1606,7 @@ flowchart TD
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`tool-workspace-dependencies`](../packages/skill/tool-workspace-dependencies) | `skill` | [`tools`](../packages/core/tools) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
+| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
| [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) |
| [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md
index e023865c6b..b4867d8ddd 100644
--- a/docs/module-graph.zh.md
+++ b/docs/module-graph.zh.md
@@ -722,12 +722,6 @@ flowchart TD
pkg_fs_sandbox --> pkg_fs_local
pkg_fs_sandbox --> pkg_sandbox
pkg_fs_sandbox --> pkg_sandbox_policy
- pkg_web_search_deepseek --> pkg_agent
- pkg_web_search_deepseek --> pkg_credentials
- pkg_web_search_deepseek --> pkg_deepseek_account
- pkg_web_search_deepseek --> pkg_launch_environment
- pkg_web_search_deepseek --> pkg_session
- pkg_web_search_deepseek --> pkg_web
pkg_api_account_controller --> pkg_agent
pkg_api_account_controller --> pkg_deepseek_account
pkg_api_account_controller --> pkg_typert_protocol
@@ -831,6 +825,14 @@ flowchart TD
pkg_tool_web --> pkg_system_prompt
pkg_tool_web --> pkg_tools
pkg_tool_web --> pkg_web
+ pkg_web_search_deepseek --> pkg_agent
+ pkg_web_search_deepseek --> pkg_credentials
+ pkg_web_search_deepseek --> pkg_deepseek_account
+ pkg_web_search_deepseek --> pkg_launch_environment
+ pkg_web_search_deepseek --> pkg_session
+ pkg_web_search_deepseek --> pkg_system_prompt
+ pkg_web_search_deepseek --> pkg_tools
+ pkg_web_search_deepseek --> pkg_web
pkg_tool_todo --> pkg_agent
pkg_tool_todo --> pkg_invariants
pkg_tool_todo --> pkg_session
@@ -1586,7 +1588,6 @@ flowchart TD
| [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) |
| [`goal-round-driver`](../packages/goal/goal-round-driver) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
-| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
| [`api-account-controller`](../packages/api/account-controller) | `api` | [`agent`](../packages/core/agent), [`deepseek-account`](../packages/credentials/deepseek-account), [`typert-protocol`](../packages/typert/protocol) |
| [`compaction`](../packages/compaction/compaction) | `compaction` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) | `credentials` | [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`host-webserver`](../packages/host/webserver) |
@@ -1607,6 +1608,7 @@ flowchart TD
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`tool-workspace-dependencies`](../packages/skill/tool-workspace-dependencies) | `skill` | [`tools`](../packages/core/tools) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
+| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
| [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) |
| [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
diff --git a/packages/web/tool-web/README.i18n.yaml b/packages/web/tool-web/README.i18n.yaml
index 45a3635e9c..53f7543165 100644
--- a/packages/web/tool-web/README.i18n.yaml
+++ b/packages/web/tool-web/README.i18n.yaml
@@ -30,8 +30,8 @@
en: 402540da7e990147
zh: 1adbbcc261777b30
/deepseek-ai-dsh-tool-web/use-this-package/stable-registration:
- en: 7b87e7f2208e354c
- zh: 6709ce199e4be8f3
+ en: bac61990604ab5cb
+ zh: b3044262a43dafae
/deepseek-ai-dsh-tool-web/use-this-package/failures-and-recovery:
en: 19581f8f8947611b
zh: 8eca83f619382659
diff --git a/packages/web/tool-web/README.md b/packages/web/tool-web/README.md
index c102023e23..78267f53b4 100644
--- a/packages/web/tool-web/README.md
+++ b/packages/web/tool-web/README.md
@@ -73,7 +73,7 @@ web_fetch({ url: 'https://example.com' })
### Stable registration
-Tool registration follows product enablement, not backend availability: a tool stays visible even when its selected provider is missing, misconfigured, ambiguous, or temporarily unavailable. Execution then fails with a structured `WebError` — for example `WEB_PROVIDER_UNAVAILABLE` or `WEB_PROVIDER_AMBIGUOUS` — which becomes an error tool result the model can read and hooks or UI can route on. To remove a web tool, disable it here in config.
+Tool registration follows product enablement, not backend availability: a tool stays visible even when its selected provider is missing, misconfigured, ambiguous, or temporarily unavailable. Execution then fails with a structured `WebError` — for example `WEB_PROVIDER_UNAVAILABLE` or `WEB_PROVIDER_AMBIGUOUS` — which becomes an error tool result the model can read and hooks or UI can route on. To remove a web tool, disable it here in config. A provider package may still withhold `web_search` from individual model requests: [`dsh-web-search-deepseek`](../web-search-deepseek/README.md#authentication) does so when a request's search would have no credential.
### Failures and recovery
diff --git a/packages/web/tool-web/README.zh.md b/packages/web/tool-web/README.zh.md
index eecc47485e..46149c4855 100644
--- a/packages/web/tool-web/README.zh.md
+++ b/packages/web/tool-web/README.zh.md
@@ -73,7 +73,7 @@ web_fetch({ url: 'https://example.com' })
### 稳定注册
-工具注册遵循产品启用状态,而非后端可用性:即使选中的提供方缺失、错误配置、存在歧义或暂时不可用,工具仍保持可见。执行随后以结构化 `WebError` 失败——例如 `WEB_PROVIDER_UNAVAILABLE` 或 `WEB_PROVIDER_AMBIGUOUS`——它变成模型可读、钩子或 UI 可路由的错误工具结果。要移除 web 工具,请在此处通过配置将其禁用。
+工具注册遵循产品启用状态,而非后端可用性:即使选中的提供方缺失、错误配置、存在歧义或暂时不可用,工具仍保持可见。执行随后以结构化 `WebError` 失败——例如 `WEB_PROVIDER_UNAVAILABLE` 或 `WEB_PROVIDER_AMBIGUOUS`——它变成模型可读、钩子或 UI 可路由的错误工具结果。要移除 web 工具,请在此处通过配置将其禁用。提供方包仍可能从单个模型请求中撤下 `web_search`:[`dsh-web-search-deepseek`](../web-search-deepseek/README.zh.md#authentication) 会在请求的搜索没有可用凭据时这样做。
### 失败与恢复
diff --git a/packages/web/web-search-deepseek/README.i18n.yaml b/packages/web/web-search-deepseek/README.i18n.yaml
index c6c339e567..3044be2ff1 100644
--- a/packages/web/web-search-deepseek/README.i18n.yaml
+++ b/packages/web/web-search-deepseek/README.i18n.yaml
@@ -9,8 +9,8 @@
en: d2c50aae9d869902
zh: ee9f3f0ff575cbf9
/deepseek-ai-dsh-web-search-deepseek/summary:
- en: 1810224f58c8d39b
- zh: 2e253cb447ee49c4
+ en: c5e051ff0ec10e64
+ zh: 2ccb9ddbb27e1d9a
/deepseek-ai-dsh-web-search-deepseek/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
@@ -24,8 +24,8 @@
en: 0553b6c926c44cf4
zh: 5cd3221d3e8eebd9
/deepseek-ai-dsh-web-search-deepseek/use-this-package/authentication:
- en: 3e49592f15e94825
- zh: c1bcf6dbb946149a
+ en: 7aa164bfd685acd0
+ zh: 7f64a25a27b8228c
/deepseek-ai-dsh-web-search-deepseek/use-this-package/what-a-search-returns:
en: e30442c90365357b
zh: 9c9e2f1182963a8a
@@ -42,14 +42,14 @@
en: 1b1760efbf4568c1
zh: fca06c959e51ce16
/deepseek-ai-dsh-web-search-deepseek/understand-the-implementation/source-map:
- en: d354ae3f0f21f7c6
- zh: 95eb16d30d9c7a9c
+ en: 931d4b8deeb35193
+ zh: 29ba4609c2dfa6e3
/deepseek-ai-dsh-web-search-deepseek/understand-the-implementation/request-flow:
- en: d6469ab9ac915762
- zh: 07ddf7b12c748eaf
+ en: 2a010d8f723d3e08
+ zh: e9567d701196f3d8
/deepseek-ai-dsh-web-search-deepseek/further-exploration:
- en: 59a97f197c044508
- zh: de191dd060c31801
+ en: d82a5364c3038ef2
+ zh: 7aef74f0d2593efb
/deepseek-ai-dsh-web-search-deepseek/model-experience:
en: 215e7ba838619b7b
zh: a311da3843709f90
@@ -77,9 +77,21 @@
/deepseek-ai-dsh-web-search-deepseek/model-experience/conversation-tool-result-indirectly/kv-cache-effect:
en: f5d5f97a153f7c70
zh: f681f89596f500f5
+/deepseek-ai-dsh-web-search-deepseek/model-experience/conversation-tool-list:
+ en: 2ff809f92438ff14
+ zh: 1455137cc3b04889
+/deepseek-ai-dsh-web-search-deepseek/model-experience/conversation-tool-list/what-the-model-sees:
+ en: 8442688bb84b6080
+ zh: f8ca9bcf90c363eb
+/deepseek-ai-dsh-web-search-deepseek/model-experience/conversation-tool-list/token-effect:
+ en: fc3904d8f90cc055
+ zh: 60f82b8a3b894e7a
+/deepseek-ai-dsh-web-search-deepseek/model-experience/conversation-tool-list/kv-cache-effect:
+ en: 28eaa6cb84269c07
+ zh: d4253f1a470eda4f
/deepseek-ai-dsh-web-search-deepseek/known-limitations-and-deferred-work:
- en: c9fa522bcfa7bfef
- zh: f95aeb879603bb62
+ en: c11af65479cf66d7
+ zh: d56f599875768a3e
/deepseek-ai-dsh-web-search-deepseek/known-limitations-and-deferred-work/dev-note:
en: 7f775cc79dbce94b
zh: 12b8f68f9e7c48ca
diff --git a/packages/web/web-search-deepseek/README.md b/packages/web/web-search-deepseek/README.md
index 6feddeec26..cb9571b85c 100644
--- a/packages/web/web-search-deepseek/README.md
+++ b/packages/web/web-search-deepseek/README.md
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
-With `dsh-web-search-deepseek`, the harness searches the web through DeepSeek's native search using the DeepSeek account sign-in or an existing `DEEPSEEK_API_KEY`. Choose it when a deployment wants DeepSeek native search and accepts that one search costs a full model turn in latency and tokens, because DeepSeek exposes no dedicated search endpoint. Results come from the structured search blocks DeepSeek returns, never from scraping text out of a reply. A missing credential fails the call with a structured error; a response without a search-result block fails loudly rather than degrading. The model-facing `web_search` tool lives in `dsh-tool-web`.
+With `dsh-web-search-deepseek`, the harness searches the web through DeepSeek's native search using the DeepSeek account sign-in or an existing `DEEPSEEK_API_KEY`. Choose it when a deployment accepts that one search costs a full model turn in latency and tokens, because DeepSeek exposes no dedicated search endpoint. Results come from the structured search blocks DeepSeek returns, never from reply text. A model request whose search has no credential is not offered `web_search`, which `dsh-tool-web` owns; a call that still arrives, or a response without a search-result block, fails with a structured error.
## Table of Contents
@@ -60,6 +60,8 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
A search authenticates with the DeepSeek account when the latest `request/context` event of the initiating Session names the `deepseek-account` provider route and `ctx.deepseekAccount` resolves a token for the search endpoint. The account service resolves one only while signed in and only for its deployment-configured inference origin, `https://api.deepseek.com` by default. That search sends only `x-dsh-auth-token`, even when an API key is configured. Every other search, including a call without an initiating Session and a search whose endpoint has another origin, sends the API key as both `x-api-key` and `Authorization: Bearer`. An HTTP 401 response to an account-authenticated search fails as `WEB_PROVIDER_ERROR` with sign-in guidance instead of endpoint guidance, and leaves the account signed in.
+Each model request is offered `dsh-tool-web`'s `web_search` only when the search it would make has a credential: the route that model selection assigns to the request is `deepseek-account` and `ctx.deepseekAccount` resolves a token for the configured search endpoint, a literal `apiKey` is set, or `ctx.credentials.describe` reports the `apiKeyEnv` reference configured (without that service, the process environment must carry it). Otherwise the provider removes `web_search` and its prompt guidance from that request. Only presence is checked: a wrong key, or an endpoint that does not serve the native `web_search_20250305` server tool, keeps the tool offered and fails each call. The check runs for every request, so a model switch or a newly stored key applies to the next request. A request presented through PTC keeps the tool, and a call that still arrives resolves credentials when it runs.
+
### What a search returns
`content` is always omitted: DeepSeek's provider prose is not trusted as an answer. `sources[]` comes from `web_search_result` items inside `web_search_tool_result` blocks — `url` and `title` directly, and `publishedAt` from `page_age` — with snippets joined from URL-keyed `cited_text` entries where an excerpt exists. Results are deduplicated by URL, and because DeepSeek exposes no result-count knob, the service enforces `maxResults` by truncating and flagging.
@@ -93,7 +95,7 @@ The provider is built on two commitments:
| File | Role |
|---|---|
-| [`src/index.ts`](src/index.ts) | Config schema and per-search option capture |
+| [`src/index.ts`](src/index.ts) | Config schema, per-search option capture, and per-request `web_search` withholding |
| [`src/provider.ts`](src/provider.ts) | The `DeepSeekSearchProvider`: Messages request dispatch, block parsing, citation joining, credential resolution |
| [`src/types.ts`](src/types.ts) | Anthropic wire types for the search response |
| — | No runtime invariant companion is published; the package emits a pre-dispatch log event but owns no later authoritative dispatch event to relate it to. Exact envelope equality is pinned at the provider boundary instead. |
@@ -102,6 +104,8 @@ The provider is built on two commitments:
Each search captures the current Config values into provider options — endpoint, model, key reference, limits — then asks `ctx.deepseekAccount` for a token when the initiating Session uses the account route, otherwise resolves the credential reference through `ctx.credentials` (or the environment), appends the log-only session event, and dispatches the Messages request with the native `web_search` server tool. The response's `web_search_tool_result` blocks become `sources[]`; `cited_text` entries from text blocks are joined to their URLs as snippets; results are deduplicated by URL; and the service enforces the requested source bound on the way back.
+Separately, a prepended `system-prompt/assemble` listener reads the route that model selection writes into the assembly variables and removes `web_search` and its `tool:web_search` section when that route's search would have no credential. An assembly that contains the PTC `run_code` transport is left unchanged.
+
-----
@@ -117,6 +121,7 @@ Read these pages when the package-level contract is not enough. They move from t
- [dsh-tool-web](../tool-web/README.md) — the model-facing `web_search` tool that renders this provider's sources.
- [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-web-search-deepseek) — every accepted config field and its source declaration.
- [Web capability seam decision](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md) — why search and fetch share one provider-selection service.
+- [Credential-based `web_search` withholding decision](../../../.agents/notes/implemented/architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.md) — why this provider removes `web_search` from requests it cannot authenticate.
-----
@@ -151,6 +156,20 @@ Zero direct conversation tokens from registration. Result tokens scale with retu
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
+### Conversation tool list
+
+#### What the model sees
+
+A conversation request whose search would have no credential carries neither the `web_search` schema nor its prompt guidance; `web_fetch` and the other tools are unchanged. The agent loop records each difference from the previous request as a tool-registry update, so a route switch, or a key being stored or removed, appears as a `web_search` addition or removal on the next request.
+
+#### Token effect
+
+Withholding removes the `web_search` schema and its prompt guidance from the request.
+
+#### KV Cache effect
+
+On a DeepSeek route, a returning `web_search` is loaded as a deferred addition that keeps the cached prefix; removing it while the route stays the same changes the declared tools and prevents reuse from there. Routes without tool-update support send the complete tool list, so either change prevents reuse from the tool declarations. A model switch already invalidates the cache.
+
## Known Limitations and Deferred Work
@@ -159,7 +178,10 @@ Append-only; newly visible content follows the reusable request prefix and does
These limits define when the provider is expensive or incomplete. They are current package constraints.
- **One search costs a full Messages model turn** — latency plus generated tokens, with up to `maxUses` server-side searches; DeepSeek exposes no dedicated retrieval endpoint.
-- **Dynamic credential availability resolves inside the operation** — the synchronous availability check can establish that a resolver exists but cannot query an asynchronous credential store, so a selected keyless provider fails the search with `WEB_PROVIDER_CREDENTIAL_MISSING`; the stable `web_search` schema stays registered.
+- **Provider selection cannot see a missing key** — `available()` is synchronous and cannot query the asynchronous credential store, so `ctx.web` still selects a keyless provider and a direct search fails with `WEB_PROVIDER_CREDENTIAL_MISSING`; only model requests withhold `web_search`.
+- **Withholding checks presence, not validity** — a wrong key, or an endpoint without the native `web_search_20250305` server tool, keeps `web_search` offered, and each call fails with a structured error.
+- **Withholding assumes this provider serves search** — a composition that loads this package but selects another search provider still loses `web_search` whenever DeepSeek has no credential.
+- **PTC presentations keep `web_search`** — their generated SDK declares tools apart from the request's tool list, so a keyless call there fails with `WEB_PROVIDER_CREDENTIAL_MISSING`.
- **Over-returned sources still cost tokens** — with no result-count knob on the wire, `maxResults` is enforced only post-hoc by service truncation.
- **Uncited results carry no `snippet`** — a source gains one only when a text-block citation (`cited_text`) matches its URL.
diff --git a/packages/web/web-search-deepseek/README.zh.md b/packages/web/web-search-deepseek/README.zh.md
index 40579e7bb9..5ea851af7c 100644
--- a/packages/web/web-search-deepseek/README.zh.md
+++ b/packages/web/web-search-deepseek/README.zh.md
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
-有了 `dsh-web-search-deepseek`,harness 可以通过 DeepSeek 原生搜索检索 web,使用 DeepSeek 账号登录或部署已有的 `DEEPSEEK_API_KEY`。当部署希望使用 DeepSeek 原生搜索、并接受一次搜索在延迟与 token 上消耗一个完整模型轮次时选择它,因为 DeepSeek 不提供专用搜索端点。结果来自 DeepSeek 返回的结构化搜索块,绝不会从回复文本中抓取。凭据缺失时调用以结构化错误失败;响应缺少搜索结果块时会明确报错,而非降级。面向模型的 `web_search` 工具位于 `dsh-tool-web`。
+有了 `dsh-web-search-deepseek`,harness 可以通过 DeepSeek 原生搜索检索 web,使用 DeepSeek 账号登录或部署已有的 `DEEPSEEK_API_KEY`。当部署接受一次搜索在延迟与 token 上消耗一个完整模型轮次时选择它,因为 DeepSeek 不提供专用搜索端点。结果来自 DeepSeek 返回的结构化搜索块,绝不取自回复文本。搜索没有可用凭据的模型请求不会获得 `web_search`(该工具属于 `dsh-tool-web`);仍然到达的调用,或缺少搜索结果块的响应,都以结构化错误失败。
## 目录
@@ -60,6 +60,8 @@ kind: "package-reference"
当发起会话最近的 `request/context` 事件指明 `deepseek-account` 提供方路由,且 `ctx.deepseekAccount` 为搜索端点解析出 token 时,搜索使用 DeepSeek 账号鉴权。账号服务仅在已登录时、且仅对部署配置的推理 origin(默认 `https://api.deepseek.com`)解析 token。这类搜索只发送 `x-dsh-auth-token`,即使配置了 API 密钥也是如此。其余所有搜索,包括没有发起会话的调用以及端点属于其他 origin 的搜索,都把 API 密钥同时作为 `x-api-key` 与 `Authorization: Bearer` 发送。账号鉴权的搜索收到 HTTP 401 时以 `WEB_PROVIDER_ERROR` 失败,附带登录指引而非端点指引,账号保持登录。
+每个模型请求仅在其将发起的搜索有可用凭据时才会获得 `dsh-tool-web` 的 `web_search`:模型选择为该请求指定的路由是 `deepseek-account` 且 `ctx.deepseekAccount` 为配置的搜索端点解析出 token,或设置了字面 `apiKey`,或 `ctx.credentials.describe` 报告 `apiKeyEnv` 引用已配置(没有该服务时,进程环境中必须存在该变量)。否则提供方会从该请求中移除 `web_search` 及其提示词说明。这里只检查凭据是否存在:错误的密钥,或不提供原生 `web_search_20250305` 服务器工具的端点,都会让该工具继续提供并使每次调用失败。检查在每个请求上执行,因此模型切换或新存储的密钥会在下一个请求生效。通过 PTC 呈现的请求保留该工具,仍然到达的调用会在运行时解析凭据。
+
### 搜索返回什么
`content` 始终省略:DeepSeek 的提供方文本不作为答案受到信任。`sources[]` 来自 `web_search_tool_result` 块内的 `web_search_result` 条目——`url` 和 `title` 直接取自同名字段,`publishedAt` 取自 `page_age`——snippet 在存在摘录时按 URL 关联的 `cited_text` 条目拼接。结果按 URL 去重,且由于 DeepSeek 不公开结果数量旋钮,服务通过截断并标记来强制执行 `maxResults`。
@@ -93,7 +95,7 @@ kind: "package-reference"
| 文件 | 职责 |
|---|---|
-| [`src/index.ts`](src/index.ts) | Config schema 与每次搜索的选项捕获 |
+| [`src/index.ts`](src/index.ts) | Config schema、每次搜索的选项捕获,以及按请求撤下 `web_search` |
| [`src/provider.ts`](src/provider.ts) | `DeepSeekSearchProvider`:Messages 请求分发、块解析、引用拼接、凭据解析 |
| [`src/types.ts`](src/types.ts) | 搜索响应的 Anthropic 协议类型 |
| — | 不发布运行时不变量配套入口;本包会在分发前发出日志事件,但没有后续的权威分发事件可与之关联;精确的请求包络相等性改由提供方边界保障。 |
@@ -102,6 +104,8 @@ kind: "package-reference"
每次搜索先把当前 Config 段投影为提供方选项——端点、模型、密钥引用、上限——然后在发起会话使用账号路由时向 `ctx.deepseekAccount` 请求 token,否则通过 `ctx.credentials`(或环境)解析凭据引用,追加仅用于日志的会话事件,并以原生 `web_search` 服务器工具分发 Messages 请求。响应中的 `web_search_tool_result` 块变为 `sources[]`;文本块中的 `cited_text` 条目按其 URL 拼接为 snippet;结果按 URL 去重;服务在返回路径上强制执行请求的来源上限。
+另外,一个前置的 `system-prompt/assemble` 监听器读取模型选择写入组装变量的路由,当该路由的搜索没有可用凭据时,移除 `web_search` 及其 `tool:web_search` 段落。包含 PTC `run_code` 传输工具的组装保持不变。
+
-----
@@ -117,6 +121,7 @@ kind: "package-reference"
- [dsh-tool-web](../tool-web/README.zh.md)——渲染本提供方来源的面向模型 `web_search` 工具。
- [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-web-search-deepseek)——每个受支持配置字段及其源声明。
- [web 能力 seam 决策](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md)——搜索与抓取为何共用一项提供方选择服务。
+- [按凭据撤下 `web_search` 的决策](../../../.agents/notes/implemented/architecture/2026-09-27-deepseek-search-withholds-web-search-without-credentials.zh.md)——本提供方为何从无法鉴权的请求中移除 `web_search`。
-----
@@ -151,6 +156,20 @@ kind: "package-reference"
仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
+### 会话工具列表
+
+#### 模型看到的内容
+
+搜索没有可用凭据的会话请求既不包含 `web_search` 的 schema,也不包含它的提示词说明;`web_fetch` 与其他工具不变。agent loop(智能体循环)把与上一个请求的每处差异记录为工具注册表更新,因此路由切换,或密钥的存储与删除,会在下一个请求中表现为 `web_search` 的新增或移除。
+
+#### Token 影响
+
+撤下时,请求中不再包含 `web_search` 的 schema 及其提示词说明。
+
+#### KV Cache 影响
+
+在 DeepSeek 路由上,重新提供的 `web_search` 以延迟加载的新增方式进入,保留已缓存的前缀;在路由不变时移除它会改变声明的工具,从该处起无法复用。不支持工具更新的路由发送完整工具列表,因此两种变化都会使复用从工具声明处中断。模型切换本身已经使缓存失效。
+
## 已知限制与延期工作
@@ -159,7 +178,10 @@ kind: "package-reference"
这些限制说明提供方在哪些情况下昂贵或不完整。它们是当前包约束。
- **一次搜索消耗一个完整的 Messages 模型轮次**——产生延迟与生成 token,最多执行 `maxUses` 次服务端搜索;DeepSeek 不公开专用检索端点。
-- **动态凭据的可用性在操作内部解析**——同步可用性检查可以确认解析器存在,但无法查询异步凭据存储,因此选中的无密钥提供方会使搜索以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败;稳定的 `web_search` schema 仍保持注册。
+- **提供方选择看不到缺失的密钥**——`available()` 是同步的,无法查询异步凭据存储,因此 `ctx.web` 仍会选中没有密钥的提供方,直接搜索会以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败;只有模型请求会撤下 `web_search`。
+- **撤下只检查凭据是否存在,不检查是否有效**——错误的密钥,或不提供原生 `web_search_20250305` 服务器工具的端点,会让 `web_search` 继续提供,每次调用都以结构化错误失败。
+- **撤下假定由本提供方负责搜索**——加载本包但选择其他搜索提供方的组合,只要 DeepSeek 没有凭据,仍会失去 `web_search`。
+- **PTC 呈现保留 `web_search`**——生成的 SDK 独立于请求的工具列表声明工具,因此在 PTC 中没有密钥的调用会以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败。
- **超量返回的来源仍消耗 token**——协议没有结果数量旋钮,`maxResults` 只能由服务在事后截断。
- **未引用的结果没有 `snippet`**——只有当文本块引用(`cited_text`)匹配其 URL 时,来源才会获得 snippet。
diff --git a/packages/web/web-search-deepseek/package.json b/packages/web/web-search-deepseek/package.json
index 6123f01e50..4b2348a201 100644
--- a/packages/web/web-search-deepseek/package.json
+++ b/packages/web/web-search-deepseek/package.json
@@ -32,6 +32,8 @@
"@deepseek-ai/dsh-deepseek-account": "workspace:*",
"@deepseek-ai/dsh-launch-environment": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
+ "@deepseek-ai/dsh-system-prompt": "workspace:*",
+ "@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-web": "workspace:*",
"@deepseek-ai/cordis": "workspace:~"
},
@@ -45,6 +47,9 @@
"@deepseek-ai/dsh-deepseek-account": "workspace:*",
"@deepseek-ai/dsh-launch-environment": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
+ "@deepseek-ai/dsh-system-prompt": "workspace:*",
+ "@deepseek-ai/dsh-tool-web": "workspace:*",
+ "@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-web": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-http-proxy": "workspace:*"
diff --git a/packages/web/web-search-deepseek/src/index.ts b/packages/web/web-search-deepseek/src/index.ts
index 8f5012fd49..5d14ce038a 100644
--- a/packages/web/web-search-deepseek/src/index.ts
+++ b/packages/web/web-search-deepseek/src/index.ts
@@ -3,7 +3,8 @@
* with native `web_search_20250305`. A search initiated by a Session on the DeepSeek account route
* authenticates with the account token when the account service allows the search endpoint;
* every other search reuses `DEEPSEEK_API_KEY`. The provider does not reuse `DEEPSEEK_BASE_URL`;
- * auxiliary search has its own endpoint configuration.
+ * auxiliary search has its own endpoint configuration. A model request whose search would have
+ * neither credential is not offered `dsh-tool-web`'s `web_search`.
* @module @deepseek-ai/dsh-web-search-deepseek
*/
import type { Volatile } from '@deepseek-ai/cordis'
@@ -12,9 +13,12 @@ import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-agent'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
+import type { CredentialRef } from '@deepseek-ai/dsh-credentials'
import type {} from '@deepseek-ai/dsh-deepseek-account'
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
import type {} from '@deepseek-ai/dsh-session'
+import type {} from '@deepseek-ai/dsh-system-prompt'
+import { RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-web'
import {
DeepSeekSearchProvider,
@@ -88,6 +92,10 @@ const ACCOUNT_PROVIDER = 'deepseek-account'
/** Settings namespace carrying this provider's endpoint, model, and key reference. */
export const WEB_SEARCH_DEEPSEEK_SETTINGS_NAMESPACE = 'web-search-deepseek'
+/** `dsh-tool-web`'s model-facing search tool and the prompt section that guides it. */
+const WEB_SEARCH_TOOL = 'web_search'
+const WEB_SEARCH_SECTION = 'tool:web_search'
+
/**
* Project one resolved section into the options the provider serves its next
* search with. Environment fallbacks stay here rather than in the provider:
@@ -98,7 +106,7 @@ export const WEB_SEARCH_DEEPSEEK_SETTINGS_NAMESPACE = 'web-search-deepseek'
*/
function resolveOptions(
ctx: Context, config: { [K in keyof Config]: ReturnType },
-): DeepSeekSearchProviderOptions {
+): DeepSeekSearchProviderOptions & { readonly apiKeyEnv: CredentialRef } {
const apiKeyEnv = credentialRef(config.apiKeyEnv)
const literalApiKey = config.apiKey !== undefined && config.apiKey.length > 0
? config.apiKey
@@ -136,10 +144,56 @@ function resolveOptions(
}
}
-/** Register the DeepSeek search provider with `ctx.web`. */
+/**
+ * Whether the search a model request on `route` would make has a credential to send. Only
+ * presence is checked: the key's value is not read and the endpoint is not contacted, so a
+ * wrong key or an endpoint without native search still counts as configured.
+ * @param ctx - plugin context supplying the account, credential, and environment planes.
+ * @param options - options the next search would use.
+ * @param route - provider route of the model request being assembled.
+ * @returns whether the account token or an API key is available to that search.
+ */
+async function searchConfigured(
+ ctx: Context, options: ReturnType, route: string | undefined,
+): Promise {
+ if (options.apiKey !== undefined) return true
+ const account = ctx.get('deepseekAccount')
+ if (route === ACCOUNT_PROVIDER && account !== undefined) {
+ let token: string | undefined
+ try { token = await account.resolveToken(`${options.baseURL}/messages`) }
+ catch (_unreadableGrant) { /* An unreadable grant cannot authenticate the search, so the API key decides. */ }
+ if (token !== undefined && token.length > 0) return true
+ }
+ const credentials = ctx.get('credentials')
+ if (credentials === undefined) {
+ const ambient = launchEnvironmentOf(ctx).get(options.apiKeyEnv)
+ return ambient !== undefined && ambient.value.length > 0
+ }
+ try { return (await credentials.describe(options.apiKeyEnv)).configured }
+ catch (_unreadableStore) { return false /* A store that cannot be read supplies no key to the search either. */ }
+}
+
+/**
+ * Register the DeepSeek search provider with `ctx.web`, and withhold `dsh-tool-web`'s
+ * `web_search` from each model request whose search would have no credential to send.
+ */
export function apply(ctx: Context, config: Config): void {
- ctx.web.registerSearchProvider(new DeepSeekSearchProvider(() => resolveOptions(ctx, {
+ const options = () => resolveOptions(ctx, {
apiKey: config.apiKey.get(), apiKeyEnv: config.apiKeyEnv.get(), baseURL: config.baseURL.get(), model: config.model.get(),
apiVersion: config.apiVersion.get(), maxTokens: config.maxTokens.get(), maxUses: config.maxUses.get(),
- })))
+ })
+ ctx.web.registerSearchProvider(new DeepSeekSearchProvider(options))
+ // Prepended so the route model selection writes for this request is already in the
+ // variables. A PTC presentation keeps the tool: its SDK declares web_search on its own.
+ ctx.on('system-prompt/assemble', async (_assembly, _context, next) => {
+ const assembly = await next()
+ const names = assembly.tools.map(tool => tool.name)
+ if (!names.includes(WEB_SEARCH_TOOL) || names.includes(RUN_CODE_NAME)) return assembly
+ if (await searchConfigured(ctx, options(), assembly.variables.provider)) return assembly
+ return {
+ ...assembly,
+ tools: assembly.tools.filter(tool => tool.name !== WEB_SEARCH_TOOL),
+ sections: assembly.sections.filter(section => section.name !== WEB_SEARCH_SECTION),
+ }
+ }, { prepend: true })
}
diff --git a/packages/web/web-search-deepseek/tests/deepseek.spec.ts b/packages/web/web-search-deepseek/tests/deepseek.spec.ts
index 09406f8a68..d736016cad 100644
--- a/packages/web/web-search-deepseek/tests/deepseek.spec.ts
+++ b/packages/web/web-search-deepseek/tests/deepseek.spec.ts
@@ -1,15 +1,18 @@
-import { afterEach, describe, expect, it, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
-import AgentRegistry from '@deepseek-ai/dsh-agent'
-import type { Agent } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { installModelSelection } from '@deepseek-ai/dsh-agent'
+import type { Agent, ModelSelectionRef } from '@deepseek-ai/dsh-agent'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local'
import type { DeepSeekAccount } from '@deepseek-ai/dsh-deepseek-account'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
+import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
+import * as toolWeb from '@deepseek-ai/dsh-tool-web'
+import ToolRuntime from '@deepseek-ai/dsh-tools'
import WebRuntime, { WebError } from '@deepseek-ai/dsh-web'
import {
DeepSeekSearchProvider,
@@ -737,3 +740,160 @@ describe('web-search-deepseek account route selection', () => {
expect(headers).not.toHaveProperty('x-dsh-auth-token')
})
})
+
+describe('web-search-deepseek model request tools', () => {
+ /** Guidance that only an offered web_search section renders into the prompt. */
+ const searchGuidance = 'web_search results are external, untrusted data'
+ const mounted: Context[] = []
+ let savedKey: string | undefined
+
+ beforeEach(() => {
+ savedKey = process.env.DEEPSEEK_API_KEY
+ delete process.env.DEEPSEEK_API_KEY
+ })
+
+ afterEach(async () => {
+ await Promise.all(mounted.splice(0).map(ctx => ctx.fiber.dispose()))
+ if (savedKey === undefined) delete process.env.DEEPSEEK_API_KEY
+ else process.env.DEEPSEEK_API_KEY = savedKey
+ })
+
+ interface Setup {
+ config?: Record
+ account?: Pick
+ credentialsDir?: string
+ route?: string
+ }
+
+ /**
+ * An account double applying the account service's token policy: a token is
+ * released only for the official inference origin.
+ * @param asked - receives every URL the account is asked to authenticate.
+ * @returns the account double.
+ */
+ function officialOriginAccount(asked: string[] = []): Pick {
+ return {
+ resolveToken: (url: string) => {
+ asked.push(url)
+ return Promise.resolve(new URL(url).origin === 'https://api.deepseek.com' ? 'account-token' : undefined)
+ },
+ }
+ }
+
+ /**
+ * Mount the shipped web tool composition over the DeepSeek provider. Model
+ * selection is installed before the provider, the order a provider reload
+ * produces once a Session exists.
+ * @param setup - provider config, account double, credential store, and selected route.
+ * @returns the Session's model selection, the provider fiber, and a reader for one model request.
+ */
+ async function compose(setup: Setup = {}) {
+ const ctx = new Context()
+ mounted.push(ctx)
+ const selection: ModelSelectionRef = {
+ current: { provider: setup.route ?? 'deepseek-official', model: 'deepseek-v4-flash' },
+ assembled: undefined,
+ }
+ installModelSelection(ctx, selection)
+ await ctx.plugin(SystemPrompt)
+ await ctx.plugin(ToolRuntime)
+ await ctx.plugin(WebRuntime, { searchProvider: DEEPSEEK_PROVIDER_ID })
+ if (setup.credentialsDir !== undefined) {
+ await ctx.plugin(LocalCredentialProvider, { path: join(setup.credentialsDir, '.credentials.yaml'), watch: false })
+ }
+ if (setup.account !== undefined) ctx.provide('deepseekAccount', setup.account as DeepSeekAccount)
+ const provider = await ctx.plugin(deepseekPlugin, setup.config ?? {})
+ await ctx.plugin(toolWeb, {})
+ const request = async (): Promise<{ tools: string[]; prompt: string }> => {
+ const assembly = await ctx.systemPrompt.assemble({})
+ return { tools: assembly.tools.map(tool => tool.name), prompt: renderPrompt(assembly) }
+ }
+ return { ctx, selection, provider, request }
+ }
+
+ it('withholds web_search and its guidance on an API-key route without a search key', async () => {
+ const { request } = await compose()
+ const { tools, prompt } = await request()
+ expect(tools).toContain('web_fetch')
+ expect(tools).not.toContain('web_search')
+ expect(prompt).not.toContain(searchGuidance)
+ })
+
+ it('offers web_search from the next request after a search key is stored', async () => {
+ const dir = await mkdtemp(join(tmpdir(), 'dsh-web-search-offer-'))
+ try {
+ const { ctx, request } = await compose({ credentialsDir: dir })
+ expect((await request()).tools).not.toContain('web_search')
+ await ctx.credentials.set(credentialRef('DEEPSEEK_API_KEY'), 'stored-key')
+ const { tools, prompt } = await request()
+ expect(tools).toContain('web_search')
+ expect(prompt).toContain(searchGuidance)
+ } finally {
+ await rm(dir, { recursive: true, force: true })
+ }
+ })
+
+ it('offers web_search when the provider config carries a literal key', async () => {
+ const { request } = await compose({ config: { apiKey: 'ds-key' } })
+ expect((await request()).tools).toContain('web_search')
+ })
+
+ it('offers web_search when the launch environment supplies the key', async () => {
+ process.env.DEEPSEEK_API_KEY = 'env-key'
+ const { request } = await compose()
+ expect((await request()).tools).toContain('web_search')
+ })
+
+ it('offers web_search on the account route when the account authenticates the search endpoint', async () => {
+ const asked: string[] = []
+ const { request } = await compose({ route: 'deepseek-account', account: officialOriginAccount(asked) })
+ expect((await request()).tools).toContain('web_search')
+ expect(asked).toEqual(['https://api.deepseek.com/anthropic/v1/messages'])
+ })
+
+ it.each<[string, Setup]>([
+ ['a search endpoint outside the account origin', { config: { baseURL: 'https://gateway.test/anthropic/v1' }, account: officialOriginAccount() }],
+ ['a signed-out account', { account: { resolveToken: () => Promise.resolve(undefined) } }],
+ ['an empty account token', { account: { resolveToken: () => Promise.resolve('') } }],
+ ['an unreadable account grant', { account: { resolveToken: () => Promise.reject(new Error('unreadable grant')) } }],
+ ['no account service', {}],
+ ])('withholds web_search on the account route with %s and no search key', async (_label, setup) => {
+ const { request } = await compose({ ...setup, route: 'deepseek-account' })
+ expect((await request()).tools).not.toContain('web_search')
+ })
+
+ it('withholds web_search when the credential store cannot be read', async () => {
+ const dir = await mkdtemp(join(tmpdir(), 'dsh-web-search-store-'))
+ try {
+ const { ctx, request } = await compose({ credentialsDir: dir })
+ await ctx.credentials.set(credentialRef('DEEPSEEK_API_KEY'), 'stored-key')
+ vi.spyOn(ctx.credentials, 'describe').mockRejectedValue(new Error('credential store unreadable'))
+ expect((await request()).tools).not.toContain('web_search')
+ } finally {
+ await rm(dir, { recursive: true, force: true })
+ }
+ })
+
+ it('follows the model selected for each request', async () => {
+ const { selection, request } = await compose({ route: 'deepseek-account', account: officialOriginAccount() })
+ expect((await request()).tools).toContain('web_search')
+ selection.current = { provider: 'deepseek-official', model: 'deepseek-v4-flash' }
+ expect((await request()).tools).not.toContain('web_search')
+ selection.current = { provider: 'deepseek-account', model: 'deepseek-v4-flash' }
+ expect((await request()).tools).toContain('web_search')
+ })
+
+ it('leaves a PTC presentation unchanged, whose SDK declares web_search on its own', async () => {
+ const { ctx, request } = await compose()
+ ctx.systemPrompt.tools(() => ({
+ schemas: [{ name: 'run_code', description: 'Run one program.', parameters: { type: 'object', properties: {} } }],
+ }))
+ expect((await request()).tools).toEqual(expect.arrayContaining(['run_code', 'web_search']))
+ })
+
+ it('stops withholding web_search once the provider plugin is disposed', async () => {
+ const { provider, request } = await compose()
+ await provider.dispose()
+ expect((await request()).tools).toContain('web_search')
+ })
+})
diff --git a/packages/web/web-search-deepseek/tsconfig.json b/packages/web/web-search-deepseek/tsconfig.json
index 423c1fc570..365360c9ab 100644
--- a/packages/web/web-search-deepseek/tsconfig.json
+++ b/packages/web/web-search-deepseek/tsconfig.json
@@ -37,6 +37,12 @@
},
{
"path": "../../util/http-proxy"
+ },
+ {
+ "path": "../../core/system-prompt"
+ },
+ {
+ "path": "../../core/tools"
}
]
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 244692dda7..fd36de44bc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13582,6 +13582,15 @@ importers:
'@deepseek-ai/dsh-session':
specifier: workspace:*
version: link:../../core/session
+ '@deepseek-ai/dsh-system-prompt':
+ specifier: workspace:*
+ version: link:../../core/system-prompt
+ '@deepseek-ai/dsh-tool-web':
+ specifier: workspace:*
+ version: link:../tool-web
+ '@deepseek-ai/dsh-tools':
+ specifier: workspace:*
+ version: link:../../core/tools
'@deepseek-ai/dsh-web':
specifier: workspace:*
version: link:../web