Merge pull request #5228 from deepseek-harness/fix/web-search-account-token
fix(web-search): 账号路由会话的 DeepSeek 搜索使用账号 token 鉴权
This commit is contained in:
commit
94a9b4d700
17 files changed
+352
-68
No files matched your search
@@ -4142,7 +4142,7 @@ export interface Config {
|
||||
|
||||
- `inject`: `web`
|
||||
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
|
||||
- `source`: [`packages/web/web-search-deepseek/src/index.ts:46`](../packages/web/web-search-deepseek/src/index.ts)
|
||||
- `source`: [`packages/web/web-search-deepseek/src/index.ts:49`](../packages/web/web-search-deepseek/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config (all optional — `apply` fills env-var and constant defaults). */
|
||||
|
||||
@@ -4144,7 +4144,7 @@ export interface Config {
|
||||
|
||||
- `inject`: `web`
|
||||
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
|
||||
- `source`: [`packages/web/web-search-deepseek/src/index.ts:46`](../packages/web/web-search-deepseek/src/index.ts)
|
||||
- `source`: [`packages/web/web-search-deepseek/src/index.ts:49`](../packages/web/web-search-deepseek/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config (all optional — `apply` fills env-var and constant defaults). */
|
||||
|
||||
@@ -633,11 +633,6 @@ flowchart TD
|
||||
pkg_goal --> pkg_session
|
||||
pkg_goal --> pkg_session_projection
|
||||
pkg_goal --> pkg_typert_protocol
|
||||
pkg_web_search_deepseek --> pkg_agent
|
||||
pkg_web_search_deepseek --> pkg_credentials
|
||||
pkg_web_search_deepseek --> pkg_launch_environment
|
||||
pkg_web_search_deepseek --> pkg_session
|
||||
pkg_web_search_deepseek --> pkg_web
|
||||
pkg_file_reference --> pkg_agent
|
||||
pkg_time_context --> pkg_agent
|
||||
pkg_time_context --> pkg_invariants
|
||||
@@ -725,6 +720,12 @@ 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
|
||||
@@ -1564,7 +1565,6 @@ flowchart TD
|
||||
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
|
||||
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
|
||||
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
||||
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
|
||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
|
||||
@@ -1584,6 +1584,7 @@ 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) |
|
||||
|
||||
@@ -635,11 +635,6 @@ flowchart TD
|
||||
pkg_goal --> pkg_session
|
||||
pkg_goal --> pkg_session_projection
|
||||
pkg_goal --> pkg_typert_protocol
|
||||
pkg_web_search_deepseek --> pkg_agent
|
||||
pkg_web_search_deepseek --> pkg_credentials
|
||||
pkg_web_search_deepseek --> pkg_launch_environment
|
||||
pkg_web_search_deepseek --> pkg_session
|
||||
pkg_web_search_deepseek --> pkg_web
|
||||
pkg_file_reference --> pkg_agent
|
||||
pkg_time_context --> pkg_agent
|
||||
pkg_time_context --> pkg_invariants
|
||||
@@ -727,6 +722,12 @@ 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
|
||||
@@ -1566,7 +1567,6 @@ flowchart TD
|
||||
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
|
||||
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
|
||||
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
|
||||
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
|
||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
|
||||
@@ -1586,6 +1586,7 @@ 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) |
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
/deepseek-ai-dsh-client-ui-settings-web-search/use-this-package:
|
||||
en: b9c740855d0886d8
|
||||
zh: f6e14f6bd01ffd23
|
||||
en: 12351542785ad120
|
||||
zh: db1b53c71301d1dd
|
||||
/deepseek-ai-dsh-client-ui-settings-web-search/understand-the-implementation:
|
||||
en: 90a3d55da6722763
|
||||
zh: 8fc0463c6543f100
|
||||
|
||||
@@ -25,7 +25,7 @@ Open **Plugins** in the sidebar and select **Web search** in the Official group
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
The **Web search** card in the Official group opens the page. **API key** starts blank on every load and reports only whether a key is configured; a blank draft keeps the stored key, and the control is disabled when the credential cannot be written from here, such as a key the process environment supplies. **Endpoint** and **Max searches per request** render the effective value, carry an **Overridden** badge with **Reset to default** once overridden, and save as a reset when emptied. Nothing is written until **Save**; leaving the page drops the drafts.
|
||||
The **Web search** card in the Official group opens the page. **API key** starts blank on every load and reports only whether a key is configured; without one, it says that only conversations using a DeepSeek Account model can search, through the default endpoint, because those searches authenticate with the account sign-in; a blank draft keeps the stored key, and the control is disabled when the credential cannot be written from here, such as a key the process environment supplies. **Endpoint** and **Max searches per request** render the effective value, carry an **Overridden** badge with **Reset to default** once overridden, and save as a reset when emptied. Nothing is written until **Save**; leaving the page drops the drafts.
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ kind: "package-reference"
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
官方分组里的**网页搜索**卡片打开这一页。**API Key** 每次加载都是空的,只报告是否已配置密钥;留空保存等于保留现有密钥,而当凭据不能从这里写入时(例如由进程环境变量提供的密钥)该控件会被禁用。**接口地址**和**单次请求最多搜索次数**显示生效值,一旦被覆盖就带**已覆盖**标签和**恢复默认**,清空后保存等于重置。点击**保存**之前不会写入任何内容;离开页面即丢弃草稿。
|
||||
官方分组里的**网页搜索**卡片打开这一页。**API Key** 每次加载都是空的,只报告是否已配置密钥;未配置时提示仅使用 DeepSeek 账号模型的对话可以通过默认接口地址搜索,因为这类搜索使用账号登录鉴权;留空保存等于保留现有密钥,而当凭据不能从这里写入时(例如由进程环境变量提供的密钥)该控件会被禁用。**接口地址**和**单次请求最多搜索次数**显示生效值,一旦被覆盖就带**已覆盖**标签和**恢复默认**,清空后保存等于重置。点击**保存**之前不会写入任何内容;离开页面即丢弃草稿。
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const en: Record<WebSearchSettingsLocaleKey, string> = {
|
||||
apiKey: 'API key',
|
||||
apiKeyHint: 'Stored outside the settings file. Leave blank to keep the current key.',
|
||||
apiKeySet: 'A key is configured.',
|
||||
apiKeyUnset: 'No key is configured; search is unavailable until one is.',
|
||||
apiKeyUnset: 'No key is configured; only conversations using a DeepSeek Account model can search, through the default endpoint.',
|
||||
baseUrl: 'Endpoint',
|
||||
baseUrlHint: 'Leave blank to use the provider default.',
|
||||
maxUses: 'Max searches per request',
|
||||
@@ -39,7 +39,7 @@ export const zh: Record<WebSearchSettingsLocaleKey, string> = {
|
||||
apiKey: 'API Key',
|
||||
apiKeyHint: '不写入设置文件。留空表示保持当前密钥。',
|
||||
apiKeySet: '已配置密钥。',
|
||||
apiKeyUnset: '未配置密钥;配置之前搜索不可用。',
|
||||
apiKeyUnset: '未配置密钥;仅使用 DeepSeek 账号模型的对话可以通过默认接口地址搜索。',
|
||||
baseUrl: '接口地址',
|
||||
baseUrlHint: '留空则使用提供方默认地址。',
|
||||
maxUses: '单次请求最多搜索次数',
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: d2c50aae9d869902
|
||||
zh: ee9f3f0ff575cbf9
|
||||
/deepseek-ai-dsh-web-search-deepseek/summary:
|
||||
en: b5aa65b385b55fac
|
||||
zh: 8ca37926bec97052
|
||||
en: 1810224f58c8d39b
|
||||
zh: 2e253cb447ee49c4
|
||||
/deepseek-ai-dsh-web-search-deepseek/table-of-contents:
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
@@ -18,11 +18,14 @@
|
||||
en: e0d352d6402d9d3b
|
||||
zh: a449234557ef8bd6
|
||||
/deepseek-ai-dsh-web-search-deepseek/use-this-package/when-to-choose-it:
|
||||
en: 942798c9aa6ba90f
|
||||
zh: 800ddd760ff4ae0b
|
||||
en: 0187c8af8d2d98c5
|
||||
zh: 1c9607b3a662bcaa
|
||||
/deepseek-ai-dsh-web-search-deepseek/use-this-package/minimal-configuration:
|
||||
en: edd0d2c2f18d0277
|
||||
zh: e3921e7e080a16a5
|
||||
en: 0553b6c926c44cf4
|
||||
zh: 5cd3221d3e8eebd9
|
||||
/deepseek-ai-dsh-web-search-deepseek/use-this-package/authentication:
|
||||
en: 3e49592f15e94825
|
||||
zh: c1bcf6dbb946149a
|
||||
/deepseek-ai-dsh-web-search-deepseek/use-this-package/what-a-search-returns:
|
||||
en: e30442c90365357b
|
||||
zh: 9c9e2f1182963a8a
|
||||
@@ -36,14 +39,14 @@
|
||||
en: 9842c69ad3636bb3
|
||||
zh: 3cdc3f816cbdd0cb
|
||||
/deepseek-ai-dsh-web-search-deepseek/understand-the-implementation/design-philosophy:
|
||||
en: 39d77f6c18cc791c
|
||||
zh: d03a8d0a4e4addb2
|
||||
en: 1b1760efbf4568c1
|
||||
zh: fca06c959e51ce16
|
||||
/deepseek-ai-dsh-web-search-deepseek/understand-the-implementation/source-map:
|
||||
en: d354ae3f0f21f7c6
|
||||
zh: 95eb16d30d9c7a9c
|
||||
/deepseek-ai-dsh-web-search-deepseek/understand-the-implementation/request-flow:
|
||||
en: e2462cb33430dc4c
|
||||
zh: c31579cf224dcda2
|
||||
en: d6469ab9ac915762
|
||||
zh: 07ddf7b12c748eaf
|
||||
/deepseek-ai-dsh-web-search-deepseek/further-exploration:
|
||||
en: 59a97f197c044508
|
||||
zh: de191dd060c31801
|
||||
@@ -66,8 +69,8 @@
|
||||
en: 666f5753c99783ab
|
||||
zh: b44328e99bcc45c9
|
||||
/deepseek-ai-dsh-web-search-deepseek/model-experience/conversation-tool-result-indirectly/what-the-model-sees:
|
||||
en: d438b6d9bc0dcc3c
|
||||
zh: 93a311af5a178113
|
||||
en: 0764d19c9b82aa98
|
||||
zh: 88b0ba22ef64225b
|
||||
/deepseek-ai-dsh-web-search-deepseek/model-experience/conversation-tool-result-indirectly/token-effect:
|
||||
en: 01ec43a9b616139d
|
||||
zh: 0a5b7c28fde34bcf
|
||||
|
||||
@@ -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 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 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`.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -29,7 +29,7 @@ Mount the provider in a composition that already loads the web service; it regis
|
||||
|
||||
### When to choose it
|
||||
|
||||
Choose this backend when a deployment wants DeepSeek's native server-side web search and already holds a `DEEPSEEK_API_KEY` — the provider reuses that credential reference. One search is heavier than a dedicated retrieval endpoint: DeepSeek runs the search inside a full model turn, so expect one Messages call's latency and generated tokens per search, with up to `maxUses` server-side searches per request. Avoid it when per-search cost or latency dominates.
|
||||
Choose this backend when a deployment wants DeepSeek's native server-side web search and its users either sign in to a DeepSeek account or hold a `DEEPSEEK_API_KEY` — the provider reuses those credentials as [Authentication](#authentication) describes. One search is heavier than a dedicated retrieval endpoint: DeepSeek runs the search inside a full model turn, so expect one Messages call's latency and generated tokens per search, with up to `maxUses` server-side searches per request. Avoid it when per-search cost or latency dominates.
|
||||
|
||||
### Minimal configuration
|
||||
|
||||
@@ -45,8 +45,8 @@ Load the web service and the provider; the key resolves from `ctx.credentials` w
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `apiKey` | omitted | Literal DeepSeek API key; prefer `apiKeyEnv` so no secret enters configuration. A non-empty literal wins |
|
||||
| `apiKeyEnv` | `DEEPSEEK_API_KEY` | Credential reference resolved for each search through `ctx.credentials`, or from the process environment when that service is absent. A missing value fails the call as `WEB_PROVIDER_CREDENTIAL_MISSING` |
|
||||
| `apiKey` | omitted | Literal DeepSeek API key; prefer `apiKeyEnv` so no secret enters configuration. A non-empty literal wins over `apiKeyEnv`; an account token wins over both |
|
||||
| `apiKeyEnv` | `DEEPSEEK_API_KEY` | Credential reference resolved for each search through `ctx.credentials`, or from the process environment when that service is absent. A search that needs an API key and finds none fails as `WEB_PROVIDER_CREDENTIAL_MISSING` |
|
||||
| `baseURL` | `https://api.deepseek.com/anthropic/v1` | Anthropic-compatible endpoint base; `/messages` is appended. Falls back to `$DEEPSEEK_SEARCH_BASE_URL`; an unparseable value makes the provider unavailable |
|
||||
| `model` | `deepseek-v4-flash` | Anthropic-format model name |
|
||||
| `apiVersion` | `2023-06-01` | `anthropic-version` header value |
|
||||
@@ -55,6 +55,11 @@ Load the web service and the provider; the key resolves from `ctx.credentials` w
|
||||
|
||||
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-web-search-deepseek) lists every accepted field. Each search captures options from the live Config references.
|
||||
|
||||
<a id="authentication"></a>
|
||||
### Authentication
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
@@ -82,7 +87,7 @@ This section explains the design decisions behind the provider; the observable b
|
||||
The provider is built on two commitments:
|
||||
|
||||
- **Structured blocks only.** DeepSeek runs the search server-side and returns structured `web_search_tool_result` blocks; the provider parses those blocks and never scrapes URLs out of model prose. In strict mode, a response with no such block throws `WEB_PROVIDER_ERROR` instead of degrading.
|
||||
- **One credential, resolved per search.** The provider reuses the `DEEPSEEK_API_KEY` reference (no new secret) but keeps its auxiliary request endpoint independent through `$DEEPSEEK_SEARCH_BASE_URL`. A mounted credentials service is authoritative; without one the provider falls back to the launching process environment. Resolving per call means a key stored or rotated in the Web Models page reaches the next search without a restart.
|
||||
- **Conversation credentials, resolved per search.** The provider adds no secret: a search from a Session on the account route uses that account's token, and every other search reuses the `DEEPSEEK_API_KEY` reference. The auxiliary request endpoint stays independent through `$DEEPSEEK_SEARCH_BASE_URL`. A mounted credentials service is authoritative; without one the provider falls back to the launching process environment. Resolving per call means a key stored or rotated in the Web Models page, or an account sign-in, reaches the next search without a restart.
|
||||
|
||||
### Source map
|
||||
|
||||
@@ -95,7 +100,7 @@ The provider is built on two commitments:
|
||||
|
||||
### Request flow
|
||||
|
||||
Each search captures the current Config values into provider options — endpoint, model, key reference, limits — then 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.
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -136,7 +141,7 @@ Independent of the conversation request cache. The auxiliary instruction and nat
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Through `dsh-tool-web`, the conversation model sees deduplicated URLs, titles, dates, and citation snippets from structured search blocks; provider prose is not trusted as an answer. This provider's exact failures include the actionable missing-credential message, `DeepSeek search credential resolution failed: <error>`, and `DeepSeek search aborted`. Request, HTTP, native-search, and response-body failures append the resolved endpoint and the conditional configuration instruction described above. The consumer owns the error wrapper.
|
||||
Through `dsh-tool-web`, the conversation model sees deduplicated URLs, titles, dates, and citation snippets from structured search blocks; provider prose is not trusted as an answer. This provider's exact failures include the actionable missing-credential message, which also names DeepSeek Account sign-in, `DeepSeek search credential resolution failed: <error>`, and `DeepSeek search aborted`. An HTTP 401 to an account-authenticated search appends an instruction to guide the user to sign in to DeepSeek again. Other request, HTTP, native-search, and response-body failures append the resolved endpoint and the conditional configuration instruction described above. The consumer owns the error wrapper.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ kind: "package-reference"
|
||||
|
||||
## 概述
|
||||
|
||||
有了 `dsh-web-search-deepseek`,harness 可以通过 DeepSeek 原生搜索检索 web,使用部署已有的 `DEEPSEEK_API_KEY`。当部署希望使用 DeepSeek 原生搜索、并接受一次搜索在延迟与 token 上消耗一个完整模型轮次时选择它,因为 DeepSeek 不提供专用搜索端点。结果来自 DeepSeek 返回的结构化搜索块,绝不会从回复文本中抓取。凭据缺失时调用以结构化错误失败;响应缺少搜索结果块时会明确报错,而非降级。面向模型的 `web_search` 工具位于 `dsh-tool-web`。
|
||||
有了 `dsh-web-search-deepseek`,harness 可以通过 DeepSeek 原生搜索检索 web,使用 DeepSeek 账号登录或部署已有的 `DEEPSEEK_API_KEY`。当部署希望使用 DeepSeek 原生搜索、并接受一次搜索在延迟与 token 上消耗一个完整模型轮次时选择它,因为 DeepSeek 不提供专用搜索端点。结果来自 DeepSeek 返回的结构化搜索块,绝不会从回复文本中抓取。凭据缺失时调用以结构化错误失败;响应缺少搜索结果块时会明确报错,而非降级。面向模型的 `web_search` 工具位于 `dsh-tool-web`。
|
||||
|
||||
## 目录
|
||||
|
||||
@@ -29,7 +29,7 @@ kind: "package-reference"
|
||||
|
||||
### 何时选择
|
||||
|
||||
当部署希望使用 DeepSeek 原生服务端 web 搜索、且已持有 `DEEPSEEK_API_KEY` 时选择此后端——提供方复用该凭据引用。一次搜索比专用检索端点更重:DeepSeek 在完整模型轮次内执行搜索,因此每次搜索都要预期一次 Messages 调用的延迟与生成 token,每次请求最多 `maxUses` 次服务端搜索。当单次搜索的成本或延迟占主导时避免使用它。
|
||||
当部署希望使用 DeepSeek 原生服务端 web 搜索、且其用户登录 DeepSeek 账号或持有 `DEEPSEEK_API_KEY` 时选择此后端——提供方按[鉴权](#authentication)所述复用这些凭据。一次搜索比专用检索端点更重:DeepSeek 在完整模型轮次内执行搜索,因此每次搜索都要预期一次 Messages 调用的延迟与生成 token,每次请求最多 `maxUses` 次服务端搜索。当单次搜索的成本或延迟占主导时避免使用它。
|
||||
|
||||
### 最小配置
|
||||
|
||||
@@ -45,8 +45,8 @@ kind: "package-reference"
|
||||
|
||||
| 字段 | 默认值 | 含义 |
|
||||
|---|---|---|
|
||||
| `apiKey` | 未设置 | DeepSeek API 密钥字面值;优先使用 `apiKeyEnv`,避免密钥进入配置。非空字面值优先 |
|
||||
| `apiKeyEnv` | `DEEPSEEK_API_KEY` | 每次搜索通过 `ctx.credentials` 解析的凭据引用;没有该服务时从进程环境解析。值缺失时调用以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败 |
|
||||
| `apiKey` | 未设置 | DeepSeek API 密钥字面值;优先使用 `apiKeyEnv`,避免密钥进入配置。非空字面值优先于 `apiKeyEnv`;账号 token 优先于两者 |
|
||||
| `apiKeyEnv` | `DEEPSEEK_API_KEY` | 每次搜索通过 `ctx.credentials` 解析的凭据引用;没有该服务时从进程环境解析。需要 API 密钥却解析不到时,搜索以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败 |
|
||||
| `baseURL` | `https://api.deepseek.com/anthropic/v1` | Anthropic 兼容端点基址;追加 `/messages`。缺省时回退到 `$DEEPSEEK_SEARCH_BASE_URL`;无法解析时提供方不可用 |
|
||||
| `model` | `deepseek-v4-flash` | Anthropic 格式模型名称 |
|
||||
| `apiVersion` | `2023-06-01` | `anthropic-version` 标头值 |
|
||||
@@ -55,6 +55,11 @@ kind: "package-reference"
|
||||
|
||||
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-web-search-deepseek) 列出所有接受的字段。每次搜索从即时 Config 引用捕获选项。
|
||||
|
||||
<a id="authentication"></a>
|
||||
### 鉴权
|
||||
|
||||
当发起会话最近的 `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` 失败,附带登录指引而非端点指引,账号保持登录。
|
||||
|
||||
### 搜索返回什么
|
||||
|
||||
`content` 始终省略:DeepSeek 的提供方文本不作为答案受到信任。`sources[]` 来自 `web_search_tool_result` 块内的 `web_search_result` 条目——`url` 和 `title` 直接取自同名字段,`publishedAt` 取自 `page_age`——snippet 在存在摘录时按 URL 关联的 `cited_text` 条目拼接。结果按 URL 去重,且由于 DeepSeek 不公开结果数量旋钮,服务通过截断并标记来强制执行 `maxResults`。
|
||||
@@ -82,7 +87,7 @@ kind: "package-reference"
|
||||
本提供方建立在两项承诺之上:
|
||||
|
||||
- **只取结构化块。** DeepSeek 在服务端执行搜索并返回结构化的 `web_search_tool_result` 块;提供方解析这些块,绝不从模型文本中抓取 URL。严格模式下,没有此类块的响应会抛出 `WEB_PROVIDER_ERROR`,而非降级。
|
||||
- **一个凭据,逐次解析。** 提供方复用 `DEEPSEEK_API_KEY` 引用(不新增密钥),但通过 `$DEEPSEEK_SEARCH_BASE_URL` 保持辅助请求端点独立。已挂载的凭据服务具有权威性;没有该服务时回退到启动进程的环境。按次解析意味着在 Web 的 Models 页中存储或轮换的密钥无需重启,即可用于下一次搜索。
|
||||
- **会话凭据,逐次解析。** 提供方不新增密钥:来自账号路由会话的搜索使用该账号的 token,其余搜索复用 `DEEPSEEK_API_KEY` 引用。辅助请求端点通过 `$DEEPSEEK_SEARCH_BASE_URL` 保持独立。已挂载的凭据服务具有权威性;没有该服务时回退到启动进程的环境。按次解析意味着在 Web 的 Models 页中存储或轮换的密钥,或一次账号登录,无需重启即可用于下一次搜索。
|
||||
|
||||
### 源码地图
|
||||
|
||||
@@ -95,7 +100,7 @@ kind: "package-reference"
|
||||
|
||||
### 请求流程
|
||||
|
||||
每次搜索先把当前 Config 段投影为提供方选项——端点、模型、密钥引用、上限——然后通过 `ctx.credentials`(或环境)解析凭据引用,追加仅用于日志的会话事件,并以原生 `web_search` 服务器工具分发 Messages 请求。响应中的 `web_search_tool_result` 块变为 `sources[]`;文本块中的 `cited_text` 条目按其 URL 拼接为 snippet;结果按 URL 去重;服务在返回路径上强制执行请求的来源上限。
|
||||
每次搜索先把当前 Config 段投影为提供方选项——端点、模型、密钥引用、上限——然后在发起会话使用账号路由时向 `ctx.deepseekAccount` 请求 token,否则通过 `ctx.credentials`(或环境)解析凭据引用,追加仅用于日志的会话事件,并以原生 `web_search` 服务器工具分发 Messages 请求。响应中的 `web_search_tool_result` 块变为 `sources[]`;文本块中的 `cited_text` 条目按其 URL 拼接为 snippet;结果按 URL 去重;服务在返回路径上强制执行请求的来源上限。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -136,7 +141,7 @@ kind: "package-reference"
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
通过 `dsh-tool-web`,会话模型会看到结构化搜索块中去重后的 URL、标题、日期与引用 snippet;提供方文本不会作为答案受到信任。该提供方的具体失败消息包括带有处理指引的凭据缺失消息、`DeepSeek search credential resolution failed: <error>` 和 `DeepSeek search aborted`。请求、HTTP、原生搜索和响应正文失败会追加已解析端点及前述条件式配置指引。错误包装属于消费方。
|
||||
通过 `dsh-tool-web`,会话模型会看到结构化搜索块中去重后的 URL、标题、日期与引用 snippet;提供方文本不会作为答案受到信任。该提供方的具体失败消息包括带有处理指引的凭据缺失消息(其中也提到 DeepSeek 账号登录)、`DeepSeek search credential resolution failed: <error>` 和 `DeepSeek search aborted`。账号鉴权的搜索收到 HTTP 401 时,会追加引导用户重新登录 DeepSeek 的指引。其他请求、HTTP、原生搜索和响应正文失败会追加已解析端点及前述条件式配置指引。错误包装属于消费方。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:*",
|
||||
"@deepseek-ai/dsh-credentials": "workspace:*",
|
||||
"@deepseek-ai/dsh-deepseek-account": "workspace:*",
|
||||
"@deepseek-ai/dsh-launch-environment": "workspace:*",
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-web": "workspace:*",
|
||||
@@ -41,10 +42,16 @@
|
||||
"@deepseek-ai/dsh-agent": "workspace:*",
|
||||
"@deepseek-ai/dsh-credentials": "workspace:*",
|
||||
"@deepseek-ai/dsh-credentials-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-deepseek-account": "workspace:*",
|
||||
"@deepseek-ai/dsh-launch-environment": "workspace:*",
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-web": "workspace:*",
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/dsh-http-proxy": "workspace:*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@deepseek-ai/dsh-deepseek-account": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
/**
|
||||
* Register a DeepSeek-backed provider in `ctx.web`. It calls the Anthropic-compatible Messages API
|
||||
* with native `web_search_20250305`. The provider reuses `DEEPSEEK_API_KEY` but not
|
||||
* `DEEPSEEK_BASE_URL`; auxiliary search has its own endpoint configuration.
|
||||
* 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.
|
||||
* @module @deepseek-ai/dsh-web-search-deepseek
|
||||
*/
|
||||
import type { Volatile } from '@deepseek-ai/cordis'
|
||||
@@ -10,6 +12,7 @@ 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 {} 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-web'
|
||||
@@ -79,6 +82,9 @@ export const Config = z.object({
|
||||
*/
|
||||
const SEARCH_BASE_URL_ENV = 'DEEPSEEK_SEARCH_BASE_URL'
|
||||
|
||||
/** Provider route id `dsh-llm-deepseek-account` registers; `request/context` events record it per Session. */
|
||||
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'
|
||||
|
||||
@@ -99,6 +105,13 @@ function resolveOptions(
|
||||
: undefined
|
||||
return {
|
||||
...literalApiKey === undefined ? {} : { apiKey: literalApiKey },
|
||||
resolveAccountToken: async (endpoint) => {
|
||||
// The latest request context names the route that served the model
|
||||
// request which called this search, as account sign-out reads it.
|
||||
const provider = ctx.get('agents')?.currentInitiator()?.session.requestContext()?.provider
|
||||
if (provider !== ACCOUNT_PROVIDER) return undefined
|
||||
return await ctx.get('deepseekAccount')?.resolveToken(endpoint)
|
||||
},
|
||||
resolveApiKey: async () => {
|
||||
const credentials = ctx.get('credentials')
|
||||
if (credentials !== undefined) return (await credentials.resolve(apiKeyEnv))?.value
|
||||
|
||||
@@ -85,6 +85,12 @@ declare module '@deepseek-ai/dsh-session/types' {
|
||||
|
||||
/** Resolved provider options (the plugin's `apply` supplies credential and constant defaults). */
|
||||
export interface DeepSeekSearchProviderOptions {
|
||||
/**
|
||||
* Resolve the DeepSeek account token for one search endpoint. A token takes
|
||||
* precedence over every API key and is sent only as `x-dsh-auth-token`;
|
||||
* `undefined` selects API-key authentication.
|
||||
*/
|
||||
resolveAccountToken?: (endpoint: string) => Promise<string | undefined>
|
||||
/** Literal DeepSeek API key; when present it wins over {@link resolveApiKey}. */
|
||||
apiKey?: string
|
||||
/** Resolve the current DeepSeek API key for one search operation. */
|
||||
@@ -172,6 +178,14 @@ export function mapAnthropicResponse(response: AnthropicResponse): WebSearchResu
|
||||
return { sources, truncated: false }
|
||||
}
|
||||
|
||||
/** Authentication headers for one search, tagged by the credential that produced them. */
|
||||
interface SearchAuth {
|
||||
/** `account` for a DeepSeek account token, `api-key` for an API key. */
|
||||
readonly kind: 'account' | 'api-key'
|
||||
/** Headers carrying the credential. */
|
||||
readonly headers: Readonly<Record<string, string>>
|
||||
}
|
||||
|
||||
/**
|
||||
* The DeepSeek-backed search provider. HTTP redirects fail as `WEB_PROVIDER_ERROR`;
|
||||
* failures after dispatch name the endpoint and tell the model how the user can configure it.
|
||||
@@ -190,7 +204,7 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
||||
|
||||
available(): boolean {
|
||||
const options = this.resolveOptions()
|
||||
return ((options.apiKey?.length ?? 0) > 0 || options.resolveApiKey !== undefined)
|
||||
return ((options.apiKey?.length ?? 0) > 0 || options.resolveApiKey !== undefined || options.resolveAccountToken !== undefined)
|
||||
&& URL.canParse(options.baseURL)
|
||||
&& isPositiveInteger(options.maxTokens)
|
||||
&& isPositiveInteger(options.maxUses)
|
||||
@@ -201,9 +215,9 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
||||
// settings write landing inside that await must not send the key resolved
|
||||
// from the old section to the endpoint named by the new one.
|
||||
const options = this.resolveOptions()
|
||||
const apiKey = await this.apiKey(options, signal)
|
||||
throwIfSearchAborted(signal)
|
||||
const endpoint = `${options.baseURL}/messages`
|
||||
const auth = await this.authHeaders(options, endpoint, signal)
|
||||
throwIfSearchAborted(signal)
|
||||
const body: DeepSeekSearchLlmRequest['body'] = {
|
||||
model: options.model,
|
||||
max_tokens: options.maxTokens,
|
||||
@@ -225,10 +239,7 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
||||
method: 'POST',
|
||||
redirect: 'error',
|
||||
headers: {
|
||||
// Official DeepSeek expects `x-api-key`; an Anthropic-compatible proxy
|
||||
// may expect `Authorization: Bearer` — send both so either resolves.
|
||||
'x-api-key': apiKey,
|
||||
'authorization': `Bearer ${apiKey}`,
|
||||
...auth.headers,
|
||||
'anthropic-version': options.apiVersion,
|
||||
'content-type': 'application/json',
|
||||
'accept': 'application/json',
|
||||
@@ -262,6 +273,9 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
||||
// malformed/non-JSON error body (normal for gateway 5xx/429s) can only
|
||||
// cost a richer provider message, never the real error.
|
||||
}
|
||||
// The account service released this token only for its inference
|
||||
// origin, so the endpoint is not what the user must change.
|
||||
if (status === 401 && auth.kind === 'account') throw accountRejectedError(message)
|
||||
throw searchEndpointError(endpoint, message)
|
||||
}
|
||||
|
||||
@@ -278,7 +292,28 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one operation's credential without retaining it on the provider.
|
||||
* Resolve one operation's authentication headers without retaining a credential on the provider.
|
||||
* @param options - the caller's snapshot, so the credential and the endpoint it is sent to come from one section.
|
||||
* @param endpoint - the Messages endpoint this operation dispatches to.
|
||||
* @param signal - abort signal for the surrounding search.
|
||||
* @returns the account-token header when one resolves, otherwise the API-key headers, tagged by credential kind.
|
||||
*/
|
||||
private async authHeaders(
|
||||
options: DeepSeekSearchProviderOptions, endpoint: string, signal?: AbortSignal,
|
||||
): Promise<SearchAuth> {
|
||||
const { resolveAccountToken } = options
|
||||
const token = resolveAccountToken === undefined
|
||||
? undefined
|
||||
: await resolveCredential(() => resolveAccountToken(endpoint), signal)
|
||||
if (token !== undefined && token.length > 0) return { kind: 'account', headers: { 'x-dsh-auth-token': token } }
|
||||
const apiKey = await this.apiKey(options, signal)
|
||||
// Official DeepSeek expects `x-api-key`; an Anthropic-compatible proxy
|
||||
// may expect `Authorization: Bearer` — send both so either resolves.
|
||||
return { kind: 'api-key', headers: { 'x-api-key': apiKey, 'authorization': `Bearer ${apiKey}` } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one operation's API key without retaining it on the provider.
|
||||
* @param options - the caller's snapshot, so the key and the endpoint it is sent to come from one section.
|
||||
* @param signal - abort signal for the surrounding search.
|
||||
* @returns the resolved key.
|
||||
@@ -286,28 +321,53 @@ export class DeepSeekSearchProvider implements WebSearchProvider {
|
||||
private async apiKey(options: DeepSeekSearchProviderOptions, signal?: AbortSignal): Promise<string> {
|
||||
throwIfSearchAborted(signal)
|
||||
if (options.apiKey !== undefined && options.apiKey.length > 0) return options.apiKey
|
||||
let resolved: string | undefined
|
||||
try {
|
||||
resolved = await abortable(options.resolveApiKey?.() ?? Promise.resolve(undefined), signal)
|
||||
} catch (error: unknown) {
|
||||
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error)
|
||||
throw new WebError(
|
||||
`DeepSeek search credential resolution failed: ${String(error)}`,
|
||||
'WEB_PROVIDER_ERROR',
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
const { resolveApiKey } = options
|
||||
const resolved = resolveApiKey === undefined ? undefined : await resolveCredential(resolveApiKey, signal)
|
||||
if (resolved !== undefined && resolved.length > 0) return resolved
|
||||
const ref = options.apiKeyEnv ?? 'DEEPSEEK_API_KEY'
|
||||
throw new WebError(
|
||||
`DeepSeek search has no API key for "${ref}"; store it through the credentials service`
|
||||
+ ' (the web Models page writes it), export it in the launching environment, or set a literal'
|
||||
+ ' "apiKey" in the web-search-deepseek config',
|
||||
+ ' "apiKey" in the web-search-deepseek config; a conversation using a DeepSeek Account model'
|
||||
+ ' searches with the account sign-in instead',
|
||||
'WEB_PROVIDER_CREDENTIAL_MISSING',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one credential resolver under the search's cancellation signal. An
|
||||
* already-cancelled search never starts the resolver.
|
||||
* @param resolve - the resolver; a synchronous throw is mapped like a rejection.
|
||||
* @param signal - abort signal for the surrounding search.
|
||||
* @returns the resolved credential, or undefined when the resolver supplied none.
|
||||
* @throws {@link WebError} `WEB_ABORTED` on cancellation, `WEB_PROVIDER_ERROR` when the resolver fails.
|
||||
*/
|
||||
async function resolveCredential(
|
||||
resolve: () => Promise<string | undefined>, signal?: AbortSignal,
|
||||
): Promise<string | undefined> {
|
||||
throwIfSearchAborted(signal)
|
||||
try {
|
||||
return await abortable(resolve(), signal)
|
||||
} catch (error: unknown) {
|
||||
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error)
|
||||
throw new WebError(
|
||||
`DeepSeek search credential resolution failed: ${String(error)}`,
|
||||
'WEB_PROVIDER_ERROR',
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Replace endpoint guidance with sign-in guidance when DeepSeek rejects the account token. */
|
||||
function accountRejectedError(message: string): WebError {
|
||||
return new WebError(
|
||||
`${message}\n\nDeepSeek rejected the account sign-in used for this web search. `
|
||||
+ 'Guide the user to sign in to DeepSeek again; the search endpoint does not need changing.',
|
||||
'WEB_PROVIDER_ERROR',
|
||||
)
|
||||
}
|
||||
|
||||
/** Add endpoint recovery instructions to failures that occur after request dispatch begins. */
|
||||
function searchEndpointError(endpoint: string, message: string, cause?: unknown): WebError {
|
||||
return new WebError(
|
||||
|
||||
@@ -4,8 +4,12 @@ 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 { 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 WebRuntime, { WebError } from '@deepseek-ai/dsh-web'
|
||||
import {
|
||||
DeepSeekSearchProvider,
|
||||
@@ -166,6 +170,10 @@ describe('DeepSeekSearchProvider availability', () => {
|
||||
expect(searchProvider(options).available()).toBe(true)
|
||||
})
|
||||
|
||||
it('is available with only an account token resolver', () => {
|
||||
expect(searchProvider({ ...options, apiKey: '', resolveAccountToken: async () => 'account-token' }).available()).toBe(true)
|
||||
})
|
||||
|
||||
it('is misconfigured when the base URL is unparseable', () => {
|
||||
expect(searchProvider({ ...options, baseURL: 'not a url' }).available()).toBe(false)
|
||||
})
|
||||
@@ -216,6 +224,132 @@ describe('DeepSeekSearchProvider request mapping', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeepSeekSearchProvider account authentication', () => {
|
||||
/** Stub fetch and read back the endpoint and headers of its first call. */
|
||||
function captureFetch() {
|
||||
// The provider always passes the endpoint as a string.
|
||||
const fetchMock = vi.fn(async (_input: string, _init?: RequestInit) => jsonResponse(searchResponse()))
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
return {
|
||||
fetchMock,
|
||||
first: () => {
|
||||
const [url, init] = fetchMock.mock.calls[0] ?? []
|
||||
return { url: url ?? '', headers: (init?.headers ?? {}) as Record<string, string> }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
it('sends only the account token for the dispatched endpoint, ahead of a configured key', async () => {
|
||||
const { first } = captureFetch()
|
||||
const resolveAccountToken = vi.fn(async (_endpoint: string) => 'account-token')
|
||||
const resolveApiKey = vi.fn(async () => 'resolved-key')
|
||||
await searchProvider({ ...options, resolveAccountToken, resolveApiKey }).search({ query: 'q' })
|
||||
const { url, headers } = first()
|
||||
expect(resolveAccountToken).toHaveBeenCalledWith(url)
|
||||
expect(resolveApiKey).not.toHaveBeenCalled()
|
||||
expect(headers['x-dsh-auth-token']).toBe('account-token')
|
||||
expect(headers).not.toHaveProperty('x-api-key')
|
||||
expect(headers).not.toHaveProperty('authorization')
|
||||
})
|
||||
|
||||
it.each([undefined, ''])('falls back to the API key when the account resolves %j', async (token) => {
|
||||
const { first } = captureFetch()
|
||||
await searchProvider({ ...options, resolveAccountToken: async () => token }).search({ query: 'q' })
|
||||
const { headers } = first()
|
||||
expect(headers['x-api-key']).toBe('ds-key')
|
||||
expect(headers).not.toHaveProperty('x-dsh-auth-token')
|
||||
})
|
||||
|
||||
it('maps an account resolver rejection to WEB_PROVIDER_ERROR without dispatching', async () => {
|
||||
const { fetchMock } = captureFetch()
|
||||
await expect(searchProvider({
|
||||
...options,
|
||||
resolveAccountToken: () => Promise.reject(new Error('account storage failed')),
|
||||
}).search({ query: 'q' }))
|
||||
.rejects.toThrow(expect.objectContaining({
|
||||
code: 'WEB_PROVIDER_ERROR',
|
||||
message: 'DeepSeek search credential resolution failed: Error: account storage failed',
|
||||
}))
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('replaces endpoint guidance with sign-in guidance when DeepSeek rejects the account token', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ error: { message: 'invalid token' } }, { status: 401 })))
|
||||
const error = await rejectedWebError(searchProvider({
|
||||
...options,
|
||||
resolveAccountToken: async () => 'account-token',
|
||||
}).search({ query: 'q' }))
|
||||
expect(error).toMatchObject({
|
||||
code: 'WEB_PROVIDER_ERROR',
|
||||
message: 'DeepSeek API error (HTTP 401): invalid token\n\n'
|
||||
+ 'DeepSeek rejected the account sign-in used for this web search. '
|
||||
+ 'Guide the user to sign in to DeepSeek again; the search endpoint does not need changing.',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps endpoint guidance for other account-authenticated HTTP failures', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({}, { status: 503 })))
|
||||
const error = await rejectedWebError(searchProvider({
|
||||
...options,
|
||||
resolveAccountToken: async () => 'account-token',
|
||||
}).search({ query: 'q' }))
|
||||
expect(error.message).toContain('Search endpoint configuration is separate from chat.')
|
||||
})
|
||||
|
||||
it('maps a synchronous resolver throw to WEB_PROVIDER_ERROR', async () => {
|
||||
const { fetchMock } = captureFetch()
|
||||
await expect(searchProvider({
|
||||
...options,
|
||||
resolveAccountToken: () => { throw new Error('account service threw') },
|
||||
}).search({ query: 'q' }))
|
||||
.rejects.toThrow(expect.objectContaining({
|
||||
code: 'WEB_PROVIDER_ERROR',
|
||||
message: 'DeepSeek search credential resolution failed: Error: account service threw',
|
||||
}))
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('starts neither resolver for a pre-aborted call', async () => {
|
||||
const resolveAccountToken = vi.fn(async () => 'account-token')
|
||||
const resolveApiKey = vi.fn(async () => 'resolved-key')
|
||||
const controller = new AbortController()
|
||||
controller.abort(new Error('caller stopped'))
|
||||
await expect(searchProvider({ ...options, apiKey: '', resolveAccountToken, resolveApiKey })
|
||||
.search({ query: 'q' }, controller.signal))
|
||||
.rejects.toThrow(expect.objectContaining({ code: 'WEB_ABORTED' }))
|
||||
expect(resolveAccountToken).not.toHaveBeenCalled()
|
||||
expect(resolveApiKey).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not start API-key resolution after cancellation during account resolution', async () => {
|
||||
const controller = new AbortController()
|
||||
const resolveApiKey = vi.fn(() => Promise.reject(new Error('must not run')))
|
||||
await expect(searchProvider({
|
||||
...options,
|
||||
apiKey: '',
|
||||
resolveAccountToken: async () => {
|
||||
controller.abort(new Error('caller stopped'))
|
||||
return undefined
|
||||
},
|
||||
resolveApiKey,
|
||||
}).search({ query: 'q' }, controller.signal))
|
||||
.rejects.toThrow(expect.objectContaining({ code: 'WEB_ABORTED' }))
|
||||
expect(resolveApiKey).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('aborts while the account resolver remains pending', async () => {
|
||||
const { fetchMock } = captureFetch()
|
||||
const controller = new AbortController()
|
||||
const search = searchProvider({
|
||||
...options,
|
||||
resolveAccountToken: () => new Promise<string>(() => {}),
|
||||
}).search({ query: 'q' }, controller.signal)
|
||||
controller.abort(new Error('deadline'))
|
||||
await expect(search).rejects.toThrow(expect.objectContaining({ code: 'WEB_ABORTED' }))
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeepSeekSearchProvider settings changes mid-search', () => {
|
||||
it('serves one search from one section even when settings land during credential resolution', async () => {
|
||||
// The section the search starts on, and the one a user commits while the
|
||||
@@ -554,3 +688,52 @@ describe('web-search-deepseek plugin registration', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('web-search-deepseek account route selection', () => {
|
||||
/**
|
||||
* Mount the provider with a signed-in account and run one search inside an
|
||||
* initiator whose latest request context names `provider`.
|
||||
* @param provider - route recorded by the initiating Session's request context; undefined runs the
|
||||
* search without an initiator while a Session on the account route exists.
|
||||
* @returns the headers the search sent and the URLs the account was asked about.
|
||||
*/
|
||||
async function searchAs(provider: string | undefined): Promise<{ headers: Record<string, string>; asked: string[] }> {
|
||||
const fetchMock = vi.fn(async (_input: string, _init?: RequestInit) => jsonResponse(searchResponse()))
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
const asked: string[] = []
|
||||
const ctx = new Context()
|
||||
try {
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(WebRuntime, { searchProvider: DEEPSEEK_PROVIDER_ID })
|
||||
ctx.provide('deepseekAccount', {
|
||||
resolveToken: async (url: string) => { asked.push(url); return 'account-token' },
|
||||
} as DeepSeekAccount)
|
||||
await ctx.plugin(deepseekPlugin, { apiKey: 'ds-key' })
|
||||
const session = ctx.sessions.create(SessionId(`web-search-account-${provider ?? 'none'}`))
|
||||
session.append('turn/start', { turn: 1 })
|
||||
session.append('request/context', { provider: provider ?? 'deepseek-account', model: 'deepseek-v4-flash' })
|
||||
const agent = { session } as Agent
|
||||
const search = () => ctx.web.search({ query: 'q' })
|
||||
await (provider === undefined ? search() : ctx.agents.withInitiator(agent, search))
|
||||
const [, init] = fetchMock.mock.calls[0] ?? []
|
||||
return { headers: (init?.headers ?? {}) as Record<string, string>, asked }
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
it('authenticates with the account token when the initiating Session uses the account route', async () => {
|
||||
const { headers, asked } = await searchAs('deepseek-account')
|
||||
expect(asked).toEqual(['https://api.deepseek.com/anthropic/v1/messages'])
|
||||
expect(headers['x-dsh-auth-token']).toBe('account-token')
|
||||
expect(headers).not.toHaveProperty('x-api-key')
|
||||
})
|
||||
|
||||
it.each(['deepseek-official', undefined])('keeps API-key authentication for route %j', async (provider) => {
|
||||
const { headers, asked } = await searchAs(provider)
|
||||
expect(asked).toEqual([])
|
||||
expect(headers['x-api-key']).toBe('ds-key')
|
||||
expect(headers).not.toHaveProperty('x-dsh-auth-token')
|
||||
})
|
||||
})
|
||||
@@ -32,6 +32,9 @@
|
||||
{
|
||||
"path": "../../credentials/credentials"
|
||||
},
|
||||
{
|
||||
"path": "../../credentials/deepseek-account"
|
||||
},
|
||||
{
|
||||
"path": "../../util/http-proxy"
|
||||
}
|
||||
|
||||
Generated
+3
@@ -13570,6 +13570,9 @@ importers:
|
||||
'@deepseek-ai/dsh-credentials-local':
|
||||
specifier: workspace:*
|
||||
version: link:../../credentials/credentials-local
|
||||
'@deepseek-ai/dsh-deepseek-account':
|
||||
specifier: workspace:*
|
||||
version: link:../../credentials/deepseek-account
|
||||
'@deepseek-ai/dsh-http-proxy':
|
||||
specifier: workspace:*
|
||||
version: link:../../util/http-proxy
|
||||
|
||||
Reference in new issue
Block a user