@x1a0f3n9/dsh-web-search-deepseek 0.1.5-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/web/web-search-deepseek/README.md
5
+ README.md: c060a5563015a899391010217f40489f817c9b7f
6
+ README.zh.md: 4eac6adc3eda20475d1d99963cb8b9eb8772d571
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ ---
2
+ description: "The DeepSeek-backed search provider for ctx.web: how deployments mount native DeepSeek web search through the Anthropic-compatible Messages API, with per-search credential resolution."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @x1a0f3n9/dsh-web-search-deepseek
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ 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`.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Mount the provider in a composition that already loads the web service; it registers as the `deepseek-official` search provider, so `ctx.web.search()` resolves it automatically when it is the only usable search backend — or pin it with `searchProvider: deepseek-official`.
29
+
30
+ ### When to choose it
31
+
32
+ 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.
33
+
34
+ ### Minimal configuration
35
+
36
+ Load the web service and the provider; the key resolves from `ctx.credentials` when that service is mounted, otherwise from the process environment. The search endpoint uses the Anthropic-compatible base (`https://api.deepseek.com/anthropic/v1`), distinct from the chat-completions base the LLM adapter uses — never reuse `$DEEPSEEK_BASE_URL`.
37
+
38
+ ```yaml
39
+ - name: '@x1a0f3n9/dsh-web'
40
+ - name: '@x1a0f3n9/dsh-web-search-deepseek'
41
+ config:
42
+ apiKeyEnv: DEEPSEEK_API_KEY
43
+ baseURL: https://gateway.internal/anthropic/v1
44
+ ```
45
+
46
+ | Field | Default | Meaning |
47
+ |---|---|---|
48
+ | `apiKey` | omitted | Literal DeepSeek API key; prefer `apiKeyEnv` so no secret enters configuration. A non-empty literal wins |
49
+ | `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` |
50
+ | `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 |
51
+ | `model` | `deepseek-v4-flash` | Anthropic-format model name |
52
+ | `apiVersion` | `2023-06-01` | `anthropic-version` header value |
53
+ | `maxTokens` | `4096` | Positive-integer upper bound on generated tokens for the Messages request |
54
+ | `maxUses` | `5` | Positive-integer maximum `web_search` server-tool uses per request |
55
+
56
+ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-web-search-deepseek) is the exhaustive source for every accepted field and its JSDoc. The entry above is the base layer of the provider's Settings section; a user layer over it reaches the next search, because the provider projects the section per call rather than capturing it at registration.
57
+
58
+ ### What a search returns
59
+
60
+ `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.
61
+
62
+ ### Request logging
63
+
64
+ A search running under an initiating agent appends the log-only `web/deepseek-search-llm-request` session event immediately before dispatch. It carries the resolved endpoint, API version, and the exact secret-free JSON body sent to DeepSeek; headers and credentials are excluded. Credential failures and cancellations before dispatch create no event, while later HTTP or response failures leave the attempted request durable.
65
+
66
+ ### Failures and recovery
67
+
68
+ Failures throw `WebError` with a machine-routable code: a missing credential is `WEB_PROVIDER_CREDENTIAL_MISSING`, caller cancellation is `WEB_ABORTED`, and provider or transport failures — including a response with no `web_search_tool_result` block — are `WEB_PROVIDER_ERROR`. HTTP redirects are rejected before the `Location` target is contacted. Every failure after dispatch names the resolved search endpoint and explains that search endpoint configuration is separate from chat. If the endpoint is unintended, the message tells the conversation model to guide the user to the Endpoint field under Settings > Plugins > Plugin configuration > Web search and save the change. When that page is unavailable, it names `DEEPSEEK_SEARCH_BASE_URL` and `web-search-deepseek.baseURL` as deployment configuration alternatives. The model must not choose or change the endpoint. The model-facing `web_search` tool surfaces this text under its own error wrapper.
69
+
70
+ -----
71
+
72
+ <a id="understand-the-implementation"></a>
73
+ ## Understand the implementation
74
+
75
+ <details>
76
+ <summary>Implementation internals — click to expand</summary>
77
+
78
+ This section explains the design decisions behind the provider; the observable behavior is fully covered in [Use this package](#use-this-package).
79
+
80
+ ### Design philosophy
81
+
82
+ The provider is built on two commitments:
83
+
84
+ - **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.
85
+ - **One credential, resolved per search.** The provider reuses the `DEEPSEEK_API_KEY` reference (no new secret) but not `$DEEPSEEK_BASE_URL`, because search speaks the Anthropic-compatible Messages API. 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.
86
+
87
+ ### Source map
88
+
89
+ | File | Role |
90
+ |---|---|
91
+ | [`src/index.ts`](src/index.ts) | Plugin entry: config schema, Settings section installation, per-search option projection |
92
+ | [`src/provider.ts`](src/provider.ts) | The `DeepSeekSearchProvider`: Messages request dispatch, block parsing, citation joining, credential resolution |
93
+ | [`src/types.ts`](src/types.ts) | Anthropic wire types for the search response |
94
+ | — | 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. |
95
+
96
+ ### Request flow
97
+
98
+ Each search projects the current Settings section 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.
99
+
100
+ </details>
101
+
102
+ -----
103
+
104
+ <a id="further-exploration"></a>
105
+ ## Further Exploration
106
+
107
+ Read these pages when the package-level contract is not enough. They move from the shared vocabulary to the service, the model-facing tools, and the design rationale.
108
+
109
+ - [Web subsystem](../../../docs/subsystems/web.md) — the exhaustive search request/result vocabulary and error codes.
110
+ - [Web package map](../README.md) — the seven-package family and each role.
111
+ - [dsh-web](../web/README.md) — the web service this provider registers into.
112
+ - [dsh-tool-web](../tool-web/README.md) — the model-facing `web_search` tool that renders this provider's sources.
113
+ - [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-web-search-deepseek) — every accepted config field and its source declaration.
114
+ - [Web capability seam decision](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md) — why search and fetch share one provider-selection service.
115
+
116
+ -----
117
+
118
+ <a id="model-experience"></a>
119
+ ## Model Experience
120
+
121
+ ### Auxiliary DeepSeek search request
122
+
123
+ #### What the model sees
124
+
125
+ A separate DeepSeek model receives exactly `Perform a web search for the query: <query>` as its user text and one native `web_search` server-tool definition. This request is not part of the conversation model's context.
126
+
127
+ #### Token effect
128
+
129
+ Separate provider input and output tokens are incurred for each search; `maxTokens` caps generated output and `maxUses` caps native search uses.
130
+
131
+ #### KV Cache effect
132
+
133
+ Independent of the conversation request cache. The auxiliary instruction and native tool definition can form a stable prefix, but each changed query or model route prevents reuse from its first difference.
134
+
135
+ ### Conversation tool result, indirectly
136
+
137
+ #### What the model sees
138
+
139
+ 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.
140
+
141
+ #### Token effect
142
+
143
+ Zero direct conversation tokens from registration. Result tokens scale with returned sources and snippets, then the service enforces the requested source bound.
144
+
145
+ #### KV Cache effect
146
+
147
+ Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
148
+
149
+ ## Known Limitations and Deferred Work
150
+
151
+ <a id="known-limitations-and-deferred-work"></a>
152
+
153
+
154
+ These limits define when the provider is expensive or incomplete. They are current package constraints.
155
+
156
+ - **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.
157
+ - **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.
158
+ - **Over-returned sources still cost tokens** — with no result-count knob on the wire, `maxResults` is enforced only post-hoc by service truncation.
159
+ - **Uncited results carry no `snippet`** — a source gains one only when a text-block citation (`cited_text`) matches its URL.
160
+
161
+ <a id="dev-note"></a>
162
+ ### Dev Note
163
+
164
+ <details>
165
+ <summary>Working context for maintainers — click to expand</summary>
166
+
167
+ This Dev Note is working context for maintainers: open questions and undecided directions. It is explicitly non-authoritative — shipped behavior, limits, and rationale live in the sections above and the linked Agent Notes.
168
+
169
+ #### Future: dedicated retrieval endpoint
170
+
171
+ A native DeepSeek search endpoint that avoids the full model turn would remove the dominant cost; until DeepSeek exposes one, this provider stays a Messages-call adapter.
172
+
173
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,173 @@
1
+ ---
2
+ description: "ctx.web 的 DeepSeek 搜索提供方:部署方如何通过 Anthropic 兼容 Messages API 挂载 DeepSeek 原生 web 搜索,并逐次解析凭据。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @x1a0f3n9/dsh-web-search-deepseek
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 有了 `dsh-web-search-deepseek`,harness 可以通过 DeepSeek 原生搜索检索 web,使用部署已有的 `DEEPSEEK_API_KEY`。当部署希望使用 DeepSeek 原生搜索、并接受一次搜索在延迟与 token 上消耗一个完整模型轮次时选择它,因为 DeepSeek 不提供专用搜索端点。结果来自 DeepSeek 返回的结构化搜索块,绝不会从回复文本中抓取。凭据缺失时调用以结构化错误失败;响应缺少搜索结果块时会明确报错,而非降级。面向模型的 `web_search` 工具位于 `dsh-tool-web`。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 在已加载 web 服务的组合中挂载本提供方;它以 `deepseek-official` 搜索提供方身份注册,因此当它是唯一可用的搜索后端时,`ctx.web.search()` 会自动解析到它——也可以用 `searchProvider: deepseek-official` 固定。
29
+
30
+ ### 何时选择
31
+
32
+ 当部署希望使用 DeepSeek 原生服务端 web 搜索、且已持有 `DEEPSEEK_API_KEY` 时选择此后端——提供方复用该凭据引用。一次搜索比专用检索端点更重:DeepSeek 在完整模型轮次内执行搜索,因此每次搜索都要预期一次 Messages 调用的延迟与生成 token,每次请求最多 `maxUses` 次服务端搜索。当单次搜索的成本或延迟占主导时避免使用它。
33
+
34
+ ### 最小配置
35
+
36
+ 加载 web 服务与本提供方;密钥在已挂载 `ctx.credentials` 服务时从其解析,否则从进程环境解析。搜索端点使用 Anthropic 兼容基址(`https://api.deepseek.com/anthropic/v1`),不同于 LLM(大语言模型)适配器使用的 chat-completions 基址——绝不复用 `$DEEPSEEK_BASE_URL`。
37
+
38
+ ```yaml
39
+ - name: '@x1a0f3n9/dsh-web'
40
+ - name: '@x1a0f3n9/dsh-web-search-deepseek'
41
+ config:
42
+ apiKeyEnv: DEEPSEEK_API_KEY
43
+ baseURL: https://gateway.internal/anthropic/v1
44
+ ```
45
+
46
+ | 字段 | 默认值 | 含义 |
47
+ |---|---|---|
48
+ | `apiKey` | 未设置 | DeepSeek API 密钥字面值;优先使用 `apiKeyEnv`,避免密钥进入配置。非空字面值优先 |
49
+ | `apiKeyEnv` | `DEEPSEEK_API_KEY` | 每次搜索通过 `ctx.credentials` 解析的凭据引用;没有该服务时从进程环境解析。值缺失时调用以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败 |
50
+ | `baseURL` | `https://api.deepseek.com/anthropic/v1` | Anthropic 兼容端点基址;追加 `/messages`。缺省时回退到 `$DEEPSEEK_SEARCH_BASE_URL`;无法解析时提供方不可用 |
51
+ | `model` | `deepseek-v4-flash` | Anthropic 格式模型名称 |
52
+ | `apiVersion` | `2023-06-01` | `anthropic-version` 标头值 |
53
+ | `maxTokens` | `4096` | Messages 请求生成 token 的正整数上限 |
54
+ | `maxUses` | `5` | 每次请求使用 `web_search` 服务器工具的正整数上限 |
55
+
56
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-web-search-deepseek)是每个受支持字段及其 JSDoc 的穷尽式真源。上面的条目是提供方 Settings 段的 base 层;叠加其上的用户层会作用于下一次搜索,因为提供方是按次投影该段,而不是在注册时固化它。
57
+
58
+ ### 搜索返回什么
59
+
60
+ `content` 始终省略:DeepSeek 的提供方文本不作为答案受到信任。`sources[]` 来自 `web_search_tool_result` 块内的 `web_search_result` 条目——`url` 和 `title` 直接取自同名字段,`publishedAt` 取自 `page_age`——snippet 在存在摘录时按 URL 关联的 `cited_text` 条目拼接。结果按 URL 去重,且由于 DeepSeek 不公开结果数量旋钮,服务通过截断并标记来强制执行 `maxResults`。
61
+
62
+ ### 请求日志
63
+
64
+ 由发起 agent(智能体)运行的搜索会在发出请求前一刻,追加仅用于日志的 `web/deepseek-search-llm-request` 会话事件。其中包含已解析端点、API 版本,以及发送给 DeepSeek 且不含密钥的精确 JSON 请求体;不包含标头和凭据。发出请求前发生凭据失败或取消时不会创建事件,而发出请求后的 HTTP 或响应失败会保留本次请求尝试的持久记录。
65
+
66
+ ### 失败与恢复
67
+
68
+ 失败抛出携带可按机器路由 code 的 `WebError`:凭据缺失为 `WEB_PROVIDER_CREDENTIAL_MISSING`,调用方取消为 `WEB_ABORTED`,提供方或传输失败,包括响应中没有 `web_search_tool_result` 块,为 `WEB_PROVIDER_ERROR`。HTTP 重定向会在接触 `Location` 指向的目标之前被拒绝。请求发出后的每项失败都会指出已解析的搜索端点,并说明搜索端点配置独立于聊天端点。如果该端点不符合用户预期,错误消息会要求会话模型指导用户进入 Settings > Plugins > Plugin configuration > Web search,修改 Endpoint 字段并保存。该页面不可用时,消息会把 `DEEPSEEK_SEARCH_BASE_URL` 和 `web-search-deepseek.baseURL` 作为部署配置方式。模型不得替用户选择或修改端点。面向模型的 `web_search` 工具会在自己的错误包装层内呈现这段文本。
69
+
70
+ -----
71
+
72
+ <a id="understand-the-implementation"></a>
73
+ ## 理解实现
74
+
75
+ <details>
76
+ <summary>实现细节——点击展开</summary>
77
+
78
+ 本节解释提供方背后的设计决策;可观察行为已在[使用本包](#use-this-package)中完整说明。
79
+
80
+ ### 设计理念
81
+
82
+ 本提供方建立在两项承诺之上:
83
+
84
+ - **只取结构化块。** DeepSeek 在服务端执行搜索并返回结构化的 `web_search_tool_result` 块;提供方解析这些块,绝不从模型文本中抓取 URL。严格模式下,没有此类块的响应会抛出 `WEB_PROVIDER_ERROR`,而非降级。
85
+ - **一个凭据,逐次解析。** 提供方复用 `DEEPSEEK_API_KEY` 引用(不新增密钥),但不复用 `$DEEPSEEK_BASE_URL`,因为搜索使用 Anthropic 兼容 Messages API。已挂载的凭据服务具有权威性;没有该服务时回退到启动进程的环境。按次解析意味着在 Web 的 Models 页中存储或轮换的密钥无需重启,即可用于下一次搜索。
86
+
87
+ ### 源码地图
88
+
89
+ | 文件 | 职责 |
90
+ |---|---|
91
+ | [`src/index.ts`](src/index.ts) | 插件入口:配置 schema、Settings 段安装、逐次选项投影 |
92
+ | [`src/provider.ts`](src/provider.ts) | `DeepSeekSearchProvider`:Messages 请求分发、块解析、引用拼接、凭据解析 |
93
+ | [`src/types.ts`](src/types.ts) | 搜索响应的 Anthropic 协议类型 |
94
+ | — | 不发布运行时不变量配套入口;本包会在分发前发出日志事件,但没有后续的权威分发事件可与之关联;精确的请求包络相等性改由提供方边界保障。 |
95
+
96
+ ### 请求流程
97
+
98
+ 每次搜索先把当前 Settings 段投影为提供方选项——端点、模型、密钥引用、上限——然后通过 `ctx.credentials`(或环境)解析凭据引用,追加仅用于日志的会话事件,并以原生 `web_search` 服务器工具分发 Messages 请求。响应中的 `web_search_tool_result` 块变为 `sources[]`;文本块中的 `cited_text` 条目按其 URL 拼接为 snippet;结果按 URL 去重;服务在返回路径上强制执行请求的来源上限。
99
+
100
+ </details>
101
+
102
+ -----
103
+
104
+ <a id="further-exploration"></a>
105
+ ## 进一步探索
106
+
107
+ 当包级约定不够用时阅读以下页面。它们从共享词汇逐步进入服务、面向模型的工具与设计依据。
108
+
109
+ - [web 子系统](../../../docs/subsystems/web.zh.md)——穷尽式的搜索请求/结果词汇与错误码。
110
+ - [web 包映射](../README.zh.md)——七包家族与各角色。
111
+ - [dsh-web](../web/README.zh.md)——本提供方注册进入的 web 服务。
112
+ - [dsh-tool-web](../tool-web/README.zh.md)——渲染本提供方来源的面向模型 `web_search` 工具。
113
+ - [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-web-search-deepseek)——每个受支持配置字段及其源声明。
114
+ - [web 能力 seam 决策](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md)——搜索与抓取为何共用一项提供方选择服务。
115
+
116
+ -----
117
+
118
+ <a id="model-experience"></a>
119
+ ## 模型体验
120
+
121
+ ### 辅助 DeepSeek 搜索请求
122
+
123
+ #### 模型看到的内容
124
+
125
+ 独立的 DeepSeek 模型会原样接收 `Perform a web search for the query: <query>` 作为用户文本,并收到一个原生 `web_search` 服务器工具定义。该请求不属于会话模型上下文。
126
+
127
+ #### Token 影响
128
+
129
+ 每次搜索都会产生独立的提供方输入与输出 token;`maxTokens` 限制生成输出,`maxUses` 限制原生搜索次数。
130
+
131
+ #### KV Cache 影响
132
+
133
+ 与会话请求缓存相互独立。辅助指令与原生工具定义可以形成稳定前缀,但查询或模型路由的每次变化都会阻止从首个差异起的复用。
134
+
135
+ ### 间接的会话工具结果
136
+
137
+ #### 模型看到的内容
138
+
139
+ 通过 `dsh-tool-web`,会话模型会看到结构化搜索块中去重后的 URL、标题、日期与引用 snippet;提供方文本不会作为答案受到信任。该提供方的具体失败消息包括带有处理指引的凭据缺失消息、`DeepSeek search credential resolution failed: <error>` 和 `DeepSeek search aborted`。请求、HTTP、原生搜索和响应正文失败会追加已解析端点及前述条件式配置指引。错误包装属于消费方。
140
+
141
+ #### Token 影响
142
+
143
+ 注册不会直接产生会话 token。结果 token 随返回源与 snippet 增长,随后服务强制执行请求的来源上限。
144
+
145
+ #### KV Cache 影响
146
+
147
+ 仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
148
+
149
+ ## 已知限制与延期工作
150
+
151
+ <a id="known-limitations-and-deferred-work"></a>
152
+
153
+
154
+ 这些限制说明提供方在哪些情况下昂贵或不完整。它们是当前包约束。
155
+
156
+ - **一次搜索消耗一个完整的 Messages 模型轮次**——产生延迟与生成 token,最多执行 `maxUses` 次服务端搜索;DeepSeek 不公开专用检索端点。
157
+ - **动态凭据的可用性在操作内部解析**——同步可用性检查可以确认解析器存在,但无法查询异步凭据存储,因此选中的无密钥提供方会使搜索以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败;稳定的 `web_search` schema 仍保持注册。
158
+ - **超量返回的来源仍消耗 token**——协议没有结果数量旋钮,`maxResults` 只能由服务在事后截断。
159
+ - **未引用的结果没有 `snippet`**——只有当文本块引用(`cited_text`)匹配其 URL 时,来源才会获得 snippet。
160
+
161
+ <a id="dev-note"></a>
162
+ ### 开发备注
163
+
164
+ <details>
165
+ <summary>维护者的工作上下文——点击展开</summary>
166
+
167
+ 本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文和相关 Agent Note 为准。
168
+
169
+ #### 未来:专用检索端点
170
+
171
+ 能够避免完整模型轮次的 DeepSeek 原生搜索端点将消除主要成本;在 DeepSeek 公开此类端点之前,本提供方仍是 Messages 调用适配器。
172
+
173
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,306 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { credentialRef } from "@x1a0f3n9/dsh-credentials";
3
+ import { launchEnvironmentOf } from "@x1a0f3n9/dsh-launch-environment";
4
+ import { WebError } from "@x1a0f3n9/dsh-web";
5
+ //#region lib/types/provider.js
6
+ /**
7
+ * DeepSeek search through an Anthropic-compatible Messages model call with the native
8
+ * `web_search_20250305` server tool. Each search costs a model turn, but returns structured
9
+ * result blocks; absence of those blocks is an error rather than a prose-scraping fallback.
10
+ * The wire format and native `fetch` client are provider-private and do not use `ctx.llm`.
11
+ * @module @x1a0f3n9/dsh-web-search-deepseek/provider
12
+ */
13
+ /** Stable id this provider registers under. */
14
+ const DEEPSEEK_PROVIDER_ID = "deepseek-official";
15
+ /**
16
+ * Default endpoint: DeepSeek's Anthropic-compatible API, `/v1` included
17
+ * (`/messages` is appended). This is NOT the chat-completions base
18
+ * (`https://api.deepseek.com`) `@x1a0f3n9/dsh-llm-deepseek` uses, so this
19
+ * provider does NOT reuse `$DEEPSEEK_BASE_URL` — only the API key is shared.
20
+ */
21
+ const DEEPSEEK_DEFAULT_BASE_URL = "https://api.deepseek.com/anthropic/v1";
22
+ /** Default Anthropic-format model name (aligned with the repo's DeepSeek model vocabulary). */
23
+ const DEEPSEEK_DEFAULT_MODEL = "deepseek-v4-flash";
24
+ /** Default `anthropic-version` header value. */
25
+ const DEEPSEEK_DEFAULT_API_VERSION = "2023-06-01";
26
+ /** Default upper bound on generated tokens for the Messages request. */
27
+ const DEEPSEEK_DEFAULT_MAX_TOKENS = 4096;
28
+ /** Default maximum `web_search` server-tool uses per request. */
29
+ const DEEPSEEK_DEFAULT_MAX_USES = 5;
30
+ /** Attribution header sent on every request. Bump with the package version. */
31
+ const USER_AGENT = "deepseek-harness/0.0.1";
32
+ /**
33
+ * Build a `url → cited_text` map from every `text` block's `citations[]`. This
34
+ * is the snippet source: Anthropic `web_search_result` items carry
35
+ * `url`/`title`/`page_age` but typically NO inline snippet — the excerpt lives
36
+ * in a separate `text` block's citation, keyed by `url` (first occurrence wins).
37
+ *
38
+ * @param blocks - the response's content blocks; non-`text` blocks are skipped.
39
+ * @returns the `url → cited_text` map (empty when no citations are present).
40
+ */
41
+ function citationSnippets(blocks) {
42
+ const map = /* @__PURE__ */ new Map();
43
+ for (const block of blocks) {
44
+ if (block.type !== "text") continue;
45
+ for (const cite of block.citations ?? []) if (cite.url != null && cite.url.length > 0 && cite.cited_text != null && cite.cited_text.length > 0 && !map.has(cite.url)) map.set(cite.url, cite.cited_text);
46
+ }
47
+ return map;
48
+ }
49
+ /**
50
+ * Map a DeepSeek Anthropic Messages response to a normalized search result. Walks
51
+ * `web_search_tool_result` blocks for citeable `web_search_result` items, joins each to its
52
+ * citation excerpt as `snippet`, and dedupes by `url` (a `max_uses > 1` request can surface
53
+ * the same URL across searches). The web service owns the final `maxResults` truncation, so
54
+ * `truncated` is always `false` here.
55
+ *
56
+ * @param response - the parsed Messages response body.
57
+ * @returns the normalized result with deduped, snippet-joined sources.
58
+ * @throws {@link WebError} when native search produced no result block.
59
+ */
60
+ function mapAnthropicResponse(response) {
61
+ const blocks = response.content ?? [];
62
+ const resultBlocks = blocks.filter((block) => block.type === "web_search_tool_result");
63
+ if (resultBlocks.length === 0) throw new WebError("DeepSeek returned no web_search_tool_result blocks; the request may not have triggered native web search", "WEB_PROVIDER_ERROR");
64
+ const snippets = citationSnippets(blocks);
65
+ const seen = /* @__PURE__ */ new Set();
66
+ const sources = [];
67
+ for (const block of resultBlocks) for (const item of block.content ?? []) {
68
+ if (item.type !== "web_search_result" || item.url.length === 0 || seen.has(item.url)) continue;
69
+ seen.add(item.url);
70
+ const snippet = snippets.get(item.url);
71
+ sources.push({
72
+ url: item.url,
73
+ ...item.title != null && item.title.length > 0 ? { title: item.title } : {},
74
+ ...snippet != null && snippet.length > 0 ? { snippet } : {},
75
+ ...item.page_age != null && item.page_age.length > 0 ? { publishedAt: item.page_age } : {}
76
+ });
77
+ }
78
+ return {
79
+ sources,
80
+ truncated: false
81
+ };
82
+ }
83
+ /**
84
+ * The DeepSeek-backed search provider. HTTP redirects fail as `WEB_PROVIDER_ERROR`;
85
+ * failures after dispatch name the endpoint and tell the model how the user can configure it.
86
+ */
87
+ var DeepSeekSearchProvider = class {
88
+ resolveOptions;
89
+ id = DEEPSEEK_PROVIDER_ID;
90
+ /**
91
+ * @param resolveOptions - the options for the NEXT operation, snapshotted
92
+ * once at each operation's entry so one search never mixes two sections. A
93
+ * thunk rather than a value because the plugin's settings section can change
94
+ * between searches, and re-registering the provider to carry a new endpoint
95
+ * would make the seam's selection observable to the user as a flicker.
96
+ */
97
+ constructor(resolveOptions) {
98
+ this.resolveOptions = resolveOptions;
99
+ }
100
+ available() {
101
+ const options = this.resolveOptions();
102
+ return ((options.apiKey?.length ?? 0) > 0 || options.resolveApiKey !== void 0) && URL.canParse(options.baseURL) && isPositiveInteger(options.maxTokens) && isPositiveInteger(options.maxUses);
103
+ }
104
+ async search(request, signal) {
105
+ const options = this.resolveOptions();
106
+ const apiKey = await this.apiKey(options, signal);
107
+ throwIfSearchAborted(signal);
108
+ const endpoint = `${options.baseURL}/messages`;
109
+ const body = {
110
+ model: options.model,
111
+ max_tokens: options.maxTokens,
112
+ messages: [{
113
+ role: "user",
114
+ content: [{
115
+ type: "text",
116
+ text: `Perform a web search for the query: ${request.query}`
117
+ }]
118
+ }],
119
+ tools: [{
120
+ type: "web_search_20250305",
121
+ name: "web_search",
122
+ max_uses: options.maxUses
123
+ }]
124
+ };
125
+ options.recordRequest?.({
126
+ endpoint,
127
+ apiVersion: options.apiVersion,
128
+ body
129
+ });
130
+ throwIfSearchAborted(signal);
131
+ let response;
132
+ try {
133
+ response = await fetch(endpoint, {
134
+ method: "POST",
135
+ redirect: "error",
136
+ headers: {
137
+ "x-api-key": apiKey,
138
+ "authorization": `Bearer ${apiKey}`,
139
+ "anthropic-version": options.apiVersion,
140
+ "content-type": "application/json",
141
+ "accept": "application/json",
142
+ "user-agent": USER_AGENT
143
+ },
144
+ body: JSON.stringify(body),
145
+ ...signal !== void 0 ? { signal } : {}
146
+ });
147
+ } catch (error) {
148
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
149
+ throw searchEndpointError(endpoint, `DeepSeek search request failed: ${String(error)}`, error);
150
+ }
151
+ if (!response.ok) {
152
+ let message = `DeepSeek API error (HTTP ${response.status})`;
153
+ try {
154
+ const parsed = await response.json();
155
+ const detail = typeof parsed.error === "string" ? parsed.error : parsed.error?.message ?? parsed.message;
156
+ if (detail !== void 0 && detail.length > 0) message += `: ${detail}`;
157
+ } catch (error) {
158
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
159
+ }
160
+ throw searchEndpointError(endpoint, message);
161
+ }
162
+ try {
163
+ return mapAnthropicResponse(await response.json());
164
+ } catch (error) {
165
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
166
+ throw searchEndpointError(endpoint, error instanceof WebError ? error.message : `DeepSeek returned an unprocessable response body: ${String(error)}`, error);
167
+ }
168
+ }
169
+ /**
170
+ * Resolve one operation's credential without retaining it on the provider.
171
+ * @param options - the caller's snapshot, so the key and the endpoint it is sent to come from one section.
172
+ * @param signal - abort signal for the surrounding search.
173
+ * @returns the resolved key.
174
+ */
175
+ async apiKey(options, signal) {
176
+ throwIfSearchAborted(signal);
177
+ if (options.apiKey !== void 0 && options.apiKey.length > 0) return options.apiKey;
178
+ let resolved;
179
+ try {
180
+ resolved = await abortable(options.resolveApiKey?.() ?? Promise.resolve(void 0), signal);
181
+ } catch (error) {
182
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
183
+ throw new WebError(`DeepSeek search credential resolution failed: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
184
+ }
185
+ if (resolved !== void 0 && resolved.length > 0) return resolved;
186
+ throw new WebError(`DeepSeek search has no API key for "${options.apiKeyEnv ?? "DEEPSEEK_API_KEY"}"; 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`, "WEB_PROVIDER_CREDENTIAL_MISSING");
187
+ }
188
+ };
189
+ /** Add endpoint recovery instructions to failures that occur after request dispatch begins. */
190
+ function searchEndpointError(endpoint, message, cause) {
191
+ return new WebError(`${message}\n\nThe web search request used endpoint ${JSON.stringify(endpoint)}. Search endpoint configuration is separate from chat. If that endpoint is not intended, guide the user to Settings > Plugins > Plugin configuration > Web search, where they can change and save Endpoint. If that settings page is unavailable, the user can set DEEPSEEK_SEARCH_BASE_URL or configure web-search-deepseek.baseURL to a trusted Anthropic-compatible Messages API base. Only the user should choose or change the endpoint.`, "WEB_PROVIDER_ERROR", cause === void 0 ? void 0 : { cause });
192
+ }
193
+ /**
194
+ * Race a same-process asynchronous preflight against caller cancellation. The
195
+ * attached settlement handlers keep observing an uncooperative operation after
196
+ * abort so a later rejection cannot become unhandled.
197
+ */
198
+ function abortable(operation, signal) {
199
+ if (signal === void 0) return operation;
200
+ if (signal.aborted) return Promise.reject(searchAborted(signal));
201
+ return new Promise((resolve, reject) => {
202
+ const onAbort = () => {
203
+ reject(searchAborted(signal));
204
+ };
205
+ signal.addEventListener("abort", onAbort, { once: true });
206
+ operation.then((value) => {
207
+ signal.removeEventListener("abort", onAbort);
208
+ resolve(value);
209
+ }, (error) => {
210
+ signal.removeEventListener("abort", onAbort);
211
+ reject(new Error(String(error).replace(/^Error: /u, ""), { cause: error }));
212
+ });
213
+ });
214
+ }
215
+ /** Throw the provider's stable cancellation error when the caller already aborted. */
216
+ function throwIfSearchAborted(signal) {
217
+ if (signal?.aborted === true) throw searchAborted(signal);
218
+ }
219
+ /** Build the provider's stable cancellation error while retaining the caller's reason. */
220
+ function searchAborted(signal, fallback) {
221
+ return new WebError("DeepSeek search aborted", "WEB_ABORTED", { cause: signal?.aborted === true ? signal.reason : fallback });
222
+ }
223
+ /** True for a fetch/`AbortSignal` abort, surfaced as `WEB_ABORTED`. */
224
+ function isAbortError(error) {
225
+ return error instanceof DOMException && error.name === "AbortError";
226
+ }
227
+ /** True for DeepSeek request limits that can be sent to the Messages API. */
228
+ function isPositiveInteger(value) {
229
+ return Number.isInteger(value) && value > 0;
230
+ }
231
+ //#endregion
232
+ //#region lib/types/index.js
233
+ /**
234
+ * Register a DeepSeek-backed provider in `ctx.web`. It calls the Anthropic-compatible Messages API
235
+ * with native `web_search_20250305`. The provider reuses `DEEPSEEK_API_KEY` but not
236
+ * `DEEPSEEK_BASE_URL`, because search and chat-completions use different bases.
237
+ * @module @x1a0f3n9/dsh-web-search-deepseek
238
+ */
239
+ /** Cordis plugin name used by loader diagnostics. */
240
+ const name = "web-search-deepseek";
241
+ /** The web seam this provider registers into. */
242
+ const inject = ["web"];
243
+ const DEFAULT_API_KEY_ENV = "DEEPSEEK_API_KEY";
244
+ const Config = z.object({
245
+ apiKey: z.string().role("secret"),
246
+ apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
247
+ baseURL: z.string(),
248
+ model: z.string().default(DEEPSEEK_DEFAULT_MODEL),
249
+ apiVersion: z.string().default(DEEPSEEK_DEFAULT_API_VERSION),
250
+ maxTokens: z.number().step(1).min(1).default(DEEPSEEK_DEFAULT_MAX_TOKENS),
251
+ maxUses: z.number().step(1).min(1).default(5)
252
+ });
253
+ /**
254
+ * Environment variable naming this provider's endpoint. Deliberately distinct
255
+ * from `$DEEPSEEK_BASE_URL`, which belongs to the chat-completions adapter:
256
+ * search speaks the Anthropic-compatible Messages API, so one variable cannot
257
+ * serve both.
258
+ */
259
+ const SEARCH_BASE_URL_ENV = "DEEPSEEK_SEARCH_BASE_URL";
260
+ /** Settings namespace carrying this provider's endpoint, model, and key reference. */
261
+ const WEB_SEARCH_DEEPSEEK_SETTINGS_NAMESPACE = "web-search-deepseek";
262
+ /**
263
+ * Project one resolved section into the options the provider serves its next
264
+ * search with. Environment fallbacks stay here rather than in the provider:
265
+ * every value it reads is already fully defaulted.
266
+ * @param ctx - plugin context supplying the credential and environment planes.
267
+ * @param config - the currently authoritative section.
268
+ * @returns options for one search.
269
+ */
270
+ function resolveOptions(ctx, config) {
271
+ const apiKeyEnv = credentialRef(config.apiKeyEnv ?? DEFAULT_API_KEY_ENV);
272
+ const literalApiKey = config.apiKey !== void 0 && config.apiKey.length > 0 ? config.apiKey : void 0;
273
+ return {
274
+ ...literalApiKey === void 0 ? {} : { apiKey: literalApiKey },
275
+ resolveApiKey: async () => {
276
+ const credentials = ctx.get("credentials");
277
+ if (credentials !== void 0) return (await credentials.resolve(apiKeyEnv))?.value;
278
+ const ambient = launchEnvironmentOf(ctx).get(apiKeyEnv);
279
+ return ambient !== void 0 && ambient.value.length > 0 ? ambient.value : void 0;
280
+ },
281
+ apiKeyEnv,
282
+ baseURL: config.baseURL ?? launchEnvironmentOf(ctx).get(SEARCH_BASE_URL_ENV)?.value ?? "https://api.deepseek.com/anthropic/v1",
283
+ model: config.model ?? "deepseek-v4-flash",
284
+ apiVersion: config.apiVersion ?? "2023-06-01",
285
+ maxTokens: config.maxTokens ?? 4096,
286
+ maxUses: config.maxUses ?? 5,
287
+ recordRequest: (request) => {
288
+ ctx.get("agents")?.currentInitiator()?.session.append("web/deepseek-search-llm-request", request);
289
+ }
290
+ };
291
+ }
292
+ /** Register the DeepSeek search provider with `ctx.web`. */
293
+ function apply(ctx, config) {
294
+ let current = () => config;
295
+ ctx.inject(["settings"], (settingsCtx) => {
296
+ settingsCtx.settings.installSection(ctx, WEB_SEARCH_DEEPSEEK_SETTINGS_NAMESPACE, Config, config, {
297
+ setSource: (source) => {
298
+ current = source;
299
+ },
300
+ onChange: () => {}
301
+ });
302
+ });
303
+ ctx.web.registerSearchProvider(new DeepSeekSearchProvider(() => resolveOptions(ctx, current())));
304
+ }
305
+ //#endregion
306
+ export { Config, DEEPSEEK_DEFAULT_API_VERSION, DEEPSEEK_DEFAULT_BASE_URL, DEEPSEEK_DEFAULT_MAX_TOKENS, DEEPSEEK_DEFAULT_MAX_USES, DEEPSEEK_DEFAULT_MODEL, DEEPSEEK_PROVIDER_ID, DeepSeekSearchProvider, WEB_SEARCH_DEEPSEEK_SETTINGS_NAMESPACE, apply, inject, name };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Register a DeepSeek-backed provider in `ctx.web`. It calls the Anthropic-compatible Messages API
3
+ * with native `web_search_20250305`. The provider reuses `DEEPSEEK_API_KEY` but not
4
+ * `DEEPSEEK_BASE_URL`, because search and chat-completions use different bases.
5
+ * @module @x1a0f3n9/dsh-web-search-deepseek
6
+ */
7
+ import type { Context } from '@deepseek-ai/cordis';
8
+ import z from '@deepseek-ai/schemastery';
9
+ export { DeepSeekSearchProvider, DEEPSEEK_DEFAULT_API_VERSION, DEEPSEEK_DEFAULT_BASE_URL, DEEPSEEK_DEFAULT_MAX_TOKENS, DEEPSEEK_DEFAULT_MAX_USES, DEEPSEEK_DEFAULT_MODEL, DEEPSEEK_PROVIDER_ID, } from './provider.ts';
10
+ export type { DeepSeekSearchLlmRequest, DeepSeekSearchProviderOptions } from './provider.ts';
11
+ /** Cordis plugin name used by loader diagnostics. */
12
+ export declare const name = "web-search-deepseek";
13
+ /** The web seam this provider registers into. */
14
+ export declare const inject: string[];
15
+ /** Plugin config (all optional — `apply` fills env-var and constant defaults). */
16
+ export interface Config {
17
+ /** Literal DeepSeek API key; prefer {@link apiKeyEnv} so no secret enters configuration files. */
18
+ apiKey?: string;
19
+ /** Credential reference resolved for each search; defaults to `DEEPSEEK_API_KEY`. */
20
+ apiKeyEnv?: string;
21
+ /** Anthropic-compatible endpoint base; `/messages` is appended. */
22
+ baseURL?: string;
23
+ /** Anthropic-format model name. Defaults to `deepseek-v4-flash`. */
24
+ model?: string;
25
+ /** `anthropic-version` header value. Defaults to `2023-06-01`. */
26
+ apiVersion?: string;
27
+ /** Upper bound on generated tokens for the Messages request. Defaults to 4096. */
28
+ maxTokens?: number;
29
+ /** Maximum `web_search` server-tool uses per request. Defaults to 5. */
30
+ maxUses?: number;
31
+ }
32
+ export declare const Config: z<Config>;
33
+ /** Settings namespace carrying this provider's endpoint, model, and key reference. */
34
+ export declare const WEB_SEARCH_DEEPSEEK_SETTINGS_NAMESPACE = "web-search-deepseek";
35
+ /** Register the DeepSeek search provider with `ctx.web`. */
36
+ export declare function apply(ctx: Context, config: Config): void;
37
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,138 @@
1
+ /**
2
+ * DeepSeek search through an Anthropic-compatible Messages model call with the native
3
+ * `web_search_20250305` server tool. Each search costs a model turn, but returns structured
4
+ * result blocks; absence of those blocks is an error rather than a prose-scraping fallback.
5
+ * The wire format and native `fetch` client are provider-private and do not use `ctx.llm`.
6
+ * @module @x1a0f3n9/dsh-web-search-deepseek/provider
7
+ */
8
+ import type { WebSearchProvider, WebSearchRequest, WebSearchResult } from '@x1a0f3n9/dsh-web';
9
+ import type { CredentialRef } from '@x1a0f3n9/dsh-credentials';
10
+ import type { AnthropicResponse, ContentBlock } from './types.ts';
11
+ /** Stable id this provider registers under. */
12
+ export declare const DEEPSEEK_PROVIDER_ID = "deepseek-official";
13
+ /**
14
+ * Default endpoint: DeepSeek's Anthropic-compatible API, `/v1` included
15
+ * (`/messages` is appended). This is NOT the chat-completions base
16
+ * (`https://api.deepseek.com`) `@x1a0f3n9/dsh-llm-deepseek` uses, so this
17
+ * provider does NOT reuse `$DEEPSEEK_BASE_URL` — only the API key is shared.
18
+ */
19
+ export declare const DEEPSEEK_DEFAULT_BASE_URL = "https://api.deepseek.com/anthropic/v1";
20
+ /** Default Anthropic-format model name (aligned with the repo's DeepSeek model vocabulary). */
21
+ export declare const DEEPSEEK_DEFAULT_MODEL = "deepseek-v4-flash";
22
+ /** Default `anthropic-version` header value. */
23
+ export declare const DEEPSEEK_DEFAULT_API_VERSION = "2023-06-01";
24
+ /** Default upper bound on generated tokens for the Messages request. */
25
+ export declare const DEEPSEEK_DEFAULT_MAX_TOKENS = 4096;
26
+ /** Default maximum `web_search` server-tool uses per request. */
27
+ export declare const DEEPSEEK_DEFAULT_MAX_USES = 5;
28
+ /**
29
+ * Exact secret-free DeepSeek Messages request recorded immediately before one
30
+ * auxiliary search dispatch.
31
+ */
32
+ export interface DeepSeekSearchLlmRequest {
33
+ /** Fully resolved Messages endpoint. */
34
+ readonly endpoint: string;
35
+ /** `anthropic-version` header value. */
36
+ readonly apiVersion: string;
37
+ /** Exact JSON body sent to the provider. */
38
+ readonly body: {
39
+ readonly model: string;
40
+ readonly max_tokens: number;
41
+ readonly messages: readonly [
42
+ {
43
+ readonly role: 'user';
44
+ readonly content: readonly [
45
+ {
46
+ readonly type: 'text';
47
+ readonly text: string;
48
+ }
49
+ ];
50
+ }
51
+ ];
52
+ readonly tools: readonly [
53
+ {
54
+ readonly type: 'web_search_20250305';
55
+ readonly name: 'web_search';
56
+ readonly max_uses: number;
57
+ }
58
+ ];
59
+ };
60
+ }
61
+ declare module '@x1a0f3n9/dsh-session/types' {
62
+ interface SessionEventMap {
63
+ /** Secret-free auxiliary DeepSeek search request recorded before dispatch. */
64
+ 'web/deepseek-search-llm-request': DeepSeekSearchLlmRequest;
65
+ }
66
+ }
67
+ /** Resolved provider options (the plugin's `apply` supplies credential and constant defaults). */
68
+ export interface DeepSeekSearchProviderOptions {
69
+ /** Literal DeepSeek API key; when present it wins over {@link resolveApiKey}. */
70
+ apiKey?: string;
71
+ /** Resolve the current DeepSeek API key for one search operation. */
72
+ resolveApiKey?: () => Promise<string | undefined>;
73
+ /** Credential reference named by missing-credential diagnostics. */
74
+ apiKeyEnv?: CredentialRef;
75
+ /** Endpoint base; `/messages` is appended. */
76
+ baseURL: string;
77
+ /** Anthropic-format model name. */
78
+ model: string;
79
+ /** `anthropic-version` header value. */
80
+ apiVersion: string;
81
+ /** Upper bound on generated tokens for the Messages request. */
82
+ maxTokens: number;
83
+ /** Maximum `web_search` server-tool uses per request. */
84
+ maxUses: number;
85
+ /**
86
+ * Record the exact secret-free request immediately before dispatch. A throw
87
+ * prevents dispatch so model-visible auxiliary input cannot escape logging.
88
+ */
89
+ recordRequest?: (request: DeepSeekSearchLlmRequest) => void;
90
+ }
91
+ /**
92
+ * Build a `url → cited_text` map from every `text` block's `citations[]`. This
93
+ * is the snippet source: Anthropic `web_search_result` items carry
94
+ * `url`/`title`/`page_age` but typically NO inline snippet — the excerpt lives
95
+ * in a separate `text` block's citation, keyed by `url` (first occurrence wins).
96
+ *
97
+ * @param blocks - the response's content blocks; non-`text` blocks are skipped.
98
+ * @returns the `url → cited_text` map (empty when no citations are present).
99
+ */
100
+ export declare function citationSnippets(blocks: readonly ContentBlock[]): Map<string, string>;
101
+ /**
102
+ * Map a DeepSeek Anthropic Messages response to a normalized search result. Walks
103
+ * `web_search_tool_result` blocks for citeable `web_search_result` items, joins each to its
104
+ * citation excerpt as `snippet`, and dedupes by `url` (a `max_uses > 1` request can surface
105
+ * the same URL across searches). The web service owns the final `maxResults` truncation, so
106
+ * `truncated` is always `false` here.
107
+ *
108
+ * @param response - the parsed Messages response body.
109
+ * @returns the normalized result with deduped, snippet-joined sources.
110
+ * @throws {@link WebError} when native search produced no result block.
111
+ */
112
+ export declare function mapAnthropicResponse(response: AnthropicResponse): WebSearchResult;
113
+ /**
114
+ * The DeepSeek-backed search provider. HTTP redirects fail as `WEB_PROVIDER_ERROR`;
115
+ * failures after dispatch name the endpoint and tell the model how the user can configure it.
116
+ */
117
+ export declare class DeepSeekSearchProvider implements WebSearchProvider {
118
+ private readonly resolveOptions;
119
+ readonly id = "deepseek-official";
120
+ /**
121
+ * @param resolveOptions - the options for the NEXT operation, snapshotted
122
+ * once at each operation's entry so one search never mixes two sections. A
123
+ * thunk rather than a value because the plugin's settings section can change
124
+ * between searches, and re-registering the provider to carry a new endpoint
125
+ * would make the seam's selection observable to the user as a flicker.
126
+ */
127
+ constructor(resolveOptions: () => DeepSeekSearchProviderOptions);
128
+ available(): boolean;
129
+ search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
130
+ /**
131
+ * Resolve one operation's credential without retaining it on the provider.
132
+ * @param options - the caller's snapshot, so the key and the endpoint it is sent to come from one section.
133
+ * @param signal - abort signal for the surrounding search.
134
+ * @returns the resolved key.
135
+ */
136
+ private apiKey;
137
+ }
138
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Provider-private wire types for DeepSeek's Anthropic-compatible Messages API. Citeable
3
+ * result items and citation excerpts arrive in separate blocks; the provider joins them by
4
+ * URL. These types do not create a dependency on `ctx.llm`.
5
+ * @module @x1a0f3n9/dsh-web-search-deepseek/types
6
+ */
7
+ /** A `web_search_result` item inside a `web_search_tool_result` block. */
8
+ export interface WebSearchResultItem {
9
+ type: string;
10
+ url: string;
11
+ title?: string | null;
12
+ /** Provider-supplied page age/recency string (mapped to `publishedAt`). */
13
+ page_age?: string | null;
14
+ }
15
+ /** A `web_search_tool_result` content block: the citeable result shape. */
16
+ export interface WebSearchToolResultBlock {
17
+ type: 'web_search_tool_result';
18
+ content?: WebSearchResultItem[];
19
+ }
20
+ /** One citation location inside a `text` block (the snippet source). */
21
+ export interface CitationLocation {
22
+ type?: string;
23
+ url?: string | null;
24
+ cited_text?: string | null;
25
+ }
26
+ /** A `text` content block: the model's prose plus per-URL citations. */
27
+ export interface TextBlock {
28
+ type: 'text';
29
+ text?: string | null;
30
+ citations?: CitationLocation[];
31
+ }
32
+ /** Any content block; only `web_search_tool_result` and `text` are consumed. */
33
+ export type ContentBlock = WebSearchToolResultBlock | TextBlock | {
34
+ type: string;
35
+ };
36
+ /** DeepSeek's Anthropic Messages response envelope. */
37
+ export interface AnthropicResponse {
38
+ content?: ContentBlock[];
39
+ }
40
+ /** DeepSeek's error response envelope (best-effort; fields vary). */
41
+ export interface AnthropicError {
42
+ error?: {
43
+ message?: string;
44
+ } | string;
45
+ message?: string;
46
+ }
47
+ //# sourceMappingURL=types.d.ts.map
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@x1a0f3n9/dsh-web-search-deepseek",
3
+ "description": "DeepSeek-backed search provider (native web_search via the Anthropic-compatible API) for the DeepSeek Harness web capability seam (ctx.web)",
4
+ "version": "0.1.5-rc.2",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/web/web-search-deepseek"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./src/*": "./src/*",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "lib/index.js",
26
+ "lib/types/**/*.d.ts"
27
+ ],
28
+ "license": "MIT",
29
+ "peerDependencies": {
30
+ "@x1a0f3n9/dsh-credentials": "^0.1.5-rc.2",
31
+ "@x1a0f3n9/dsh-launch-environment": "^0.1.5-rc.2",
32
+ "@x1a0f3n9/dsh-session": "^0.1.5-rc.2",
33
+ "@x1a0f3n9/dsh-agent": "^0.1.5-rc.2",
34
+ "@x1a0f3n9/dsh-web": "^0.1.5-rc.2",
35
+ "@x1a0f3n9/dsh-settings": "^0.1.5-rc.2",
36
+ "@deepseek-ai/cordis": "^4.0.2"
37
+ },
38
+ "dependencies": {
39
+ "@deepseek-ai/schemastery": "^3.18.2"
40
+ },
41
+ "devDependencies": {
42
+ "@x1a0f3n9/dsh-agent": "^0.1.5-rc.2",
43
+ "@x1a0f3n9/dsh-credentials": "^0.1.5-rc.2",
44
+ "@x1a0f3n9/dsh-session": "^0.1.5-rc.2",
45
+ "@x1a0f3n9/dsh-credentials-local": "^0.1.5-rc.2",
46
+ "@x1a0f3n9/dsh-launch-environment": "^0.1.5-rc.2",
47
+ "@x1a0f3n9/dsh-web": "^0.1.5-rc.2",
48
+ "@deepseek-ai/cordis": "^4.0.2",
49
+ "@x1a0f3n9/dsh-settings": "^0.1.5-rc.2",
50
+ "@x1a0f3n9/dsh-http-proxy": "^0.1.5-rc.2"
51
+ }
52
+ }