@x1a0f3n9/dsh-web-search-perplexity 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-perplexity/README.md
5
+ README.md: 1e0c98ed985a0d364ec44fbc939901f57d6657e0
6
+ README.zh.md: 0947fb1939a9c6bba214ec995458a189b4100700
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ ---
2
+ description: "The Perplexity-backed search provider for ctx.web: how deployments mount OpenAI-compatible Perplexity search with generated answers and citations."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @x1a0f3n9/dsh-web-search-perplexity
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ With `dsh-web-search-perplexity`, the harness searches the web through Perplexity and gets a model-generated answer plus citeable sources in one call. Choose it when a deployment has a Perplexity API key and wants a generated answer. Perplexity has no result-count control, so the returned sources are truncated to the requested bound after the fact. When Perplexity omits structured result metadata, sources fall back to URL-only citations. 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 `perplexity` search provider, so `ctx.web.search()` resolves it automatically when it is the only usable search backend — or pin it with `searchProvider: perplexity`.
29
+
30
+ ### When to choose it
31
+
32
+ Choose this backend when a deployment holds a Perplexity API key and wants a model-generated answer plus citeable sources in one search. The provider is unavailable — and every search call fails with a structured error — when the key is empty or the endpoint base does not parse.
33
+
34
+ ### Minimal configuration
35
+
36
+ Load the web service and the provider; the API key falls back to `$PERPLEXITY_API_KEY` from the launch environment, and all other settings have safe defaults.
37
+
38
+ ```yaml
39
+ - name: '@x1a0f3n9/dsh-web'
40
+ - name: '@x1a0f3n9/dsh-web-search-perplexity'
41
+ config:
42
+ apiKey: !!js process.env.PERPLEXITY_API_KEY
43
+ ```
44
+
45
+ | Field | Default | Meaning |
46
+ |---|---|---|
47
+ | `apiKey` | `$PERPLEXITY_API_KEY` | Perplexity API key; empty or absent makes the provider unavailable |
48
+ | `baseURL` | `https://api.perplexity.ai` | Endpoint base; `/chat/completions` is appended. An unparseable value makes the provider unavailable |
49
+ | `model` | `sonar` | Search model name |
50
+ | `maxTokens` | `1024` | Upper bound on generated answer tokens (`max_tokens`); must be a positive integer |
51
+ | `searchRecency` | (unset) | Recency window sent as `search_recency_filter`: `day`, `week`, `month`, or `year`. Unset sends no filter |
52
+
53
+ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-web-search-perplexity) is the exhaustive source for every accepted field and its JSDoc.
54
+
55
+ ### What a search returns
56
+
57
+ `content` carries Perplexity's generated answer. `sources[]` prefers the structured `search_results[]` (`url`, `title`, `snippet`, `publishedAt` from `date`) and falls back to the URL-only `citations[]` array only when `search_results` is absent — which is why `title`/`snippet`/`publishedAt` are optional on the service. Perplexity exposes no result-count control, so the service enforces `maxResults` by truncating and flagging.
58
+
59
+ ### Failures and recovery
60
+
61
+ Provider failures — HTTP errors, network failures, unparseable or wrong-shape bodies — surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`. HTTP redirects are rejected before the `Location` target is contacted and surface as `WEB_PROVIDER_ERROR`. Callers route on the code; the model-facing `web_search` tool surfaces failures to the model under its own error wrapper.
62
+
63
+ -----
64
+
65
+ <a id="understand-the-implementation"></a>
66
+ ## Understand the implementation
67
+
68
+ <details>
69
+ <summary>Implementation internals — click to expand</summary>
70
+
71
+ This section explains the design decisions behind the provider; the observable behavior is fully covered in [Use this package](#use-this-package).
72
+
73
+ ### Design philosophy
74
+
75
+ The provider is a thin adapter over Perplexity's chat-completions endpoint with two deliberate rules:
76
+
77
+ - **The generated answer is trusted as `content`.** Unlike the other search backends, Perplexity returns a model-generated answer, and this provider passes it through as the normalized `content` field.
78
+ - **Structured sources win; URL-only citations are the fallback.** `search_results[]` carries the portable fields; `citations[]` carries only URLs, and the service vocabulary makes those fields optional precisely for this case.
79
+
80
+ ### Source map
81
+
82
+ | File | Role |
83
+ |---|---|
84
+ | [`src/index.ts`](src/index.ts) | Plugin entry: config schema, environment fallback, provider registration |
85
+ | [`src/provider.ts`](src/provider.ts) | The `PerplexitySearchProvider`: request dispatch, abort classification, answer and source mapping |
86
+ | [`src/types.ts`](src/types.ts) | Perplexity wire types for the chat-completions response |
87
+ | — | No runtime invariant companion is published; this package exposes no independent event sequence or mutable data relation beyond contracts enforced at its owning seam. |
88
+
89
+ ### Request and mapping flow
90
+
91
+ `search()` posts the query with the model, token cap, and optional recency filter to `{baseURL}/chat/completions` with `redirect: 'error'`. The response's `content` becomes `content`; `search_results[]` becomes `sources[]` when present, otherwise each `citations[]` entry becomes a URL-only source; and the service applies the final `maxResults` bound on the way back. An abort — a `DOMException` named `AbortError` — becomes `WEB_ABORTED`; anything else becomes `WEB_PROVIDER_ERROR`.
92
+
93
+ </details>
94
+
95
+ -----
96
+
97
+ <a id="further-exploration"></a>
98
+ ## Further Exploration
99
+
100
+ 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.
101
+
102
+ - [Web subsystem](../../../docs/subsystems/web.md) — the exhaustive search request/result vocabulary and error codes.
103
+ - [Web package map](../README.md) — the seven-package family and each role.
104
+ - [dsh-web](../web/README.md) — the web service this provider registers into.
105
+ - [dsh-tool-web](../tool-web/README.md) — the model-facing `web_search` tool that renders this provider's sources.
106
+ - [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-web-search-perplexity) — every accepted config field and its source declaration.
107
+ - [Web capability seam decision](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md) — why search and fetch share one provider-selection service.
108
+
109
+ -----
110
+
111
+ <a id="model-experience"></a>
112
+ ## Model Experience
113
+
114
+ ### Auxiliary Perplexity request
115
+
116
+ #### What the model sees
117
+
118
+ A separate Perplexity model receives `<query>` verbatim as its sole user message through the chat-completions endpoint. This request is not part of the conversation model's context.
119
+
120
+ #### Token effect
121
+
122
+ Separate provider tokens are incurred per search; `maxTokens` caps the generated answer.
123
+
124
+ #### KV Cache effect
125
+
126
+ Independent of the conversation request cache. An identical query under the same model route may reuse provider cache; a changed query or route establishes a different prefix.
127
+
128
+ ### Conversation tool result, indirectly
129
+
130
+ #### What the model sees
131
+
132
+ Through `dsh-tool-web`, the conversation model sees the generated answer plus structured result metadata or URL-only citations. This provider's exact failures are `Perplexity search aborted`, `Perplexity search request failed: <error>`, and `Perplexity returned an unprocessable response body: <error>`; HTTP failures preserve the provider message. The consumer owns the error wrapper.
133
+
134
+ #### Token effect
135
+
136
+ Zero direct conversation tokens from registration. Answer and source tokens are data-dependent, source count is service-bounded, and the retained result or error is resent until compaction.
137
+
138
+ #### KV Cache effect
139
+
140
+ Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
141
+
142
+ ## Known Limitations and Deferred Work
143
+
144
+ <a id="known-limitations-and-deferred-work"></a>
145
+
146
+
147
+ These limits define when the provider is a poor fit. They are current package constraints.
148
+
149
+ - **Citation-fallback sources are URL-only** — when Perplexity omits structured `search_results[]`, sources carry no `title`/`snippet`/`publishedAt`, so the tool renders bare hostname labels.
150
+ - **Over-returned sources still cost tokens and latency** — with no result-count control on the wire, `maxResults` is enforced only post-hoc by service truncation.
151
+ - **Only `model`/`maxTokens`/`searchRecency` are exposed** — Perplexity's other search controls (domain filters, `web_search_options` context size, images) wait on provider-neutral service fields ([seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)).
152
+ - **Abort classification is error-shape-based** — only a `DOMException` named `AbortError` maps to `WEB_ABORTED`; an abort carrying a custom reason (such as `dsh-timeout`'s `TimeoutReason`) surfaces as `WEB_PROVIDER_ERROR`.
153
+
154
+ <a id="dev-note"></a>
155
+ ### Dev Note
156
+
157
+ <details>
158
+ <summary>Working context for maintainers — click to expand</summary>
159
+
160
+ 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.
161
+
162
+ #### Future: wider Perplexity control surface
163
+
164
+ Perplexity's domain filters, `web_search_options` context size, and image support stay unexposed. Exposing them needs provider-neutral service fields first, so the family adds one coordinated control rather than a vendor-specific argument.
165
+
166
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,166 @@
1
+ ---
2
+ description: "ctx.web 的 Perplexity 搜索提供方:部署方如何挂载 OpenAI 兼容的 Perplexity 搜索,获得生成答案与引用。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @x1a0f3n9/dsh-web-search-perplexity
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 有了 `dsh-web-search-perplexity`,harness 可以通过 Perplexity 搜索 web,一次调用同时获得模型生成的答案与可引用来源。当部署持有 Perplexity API 密钥、并希望获得生成答案时选择它。Perplexity 没有结果数量控制,因此返回的来源会在事后被截断到请求的上限。Perplexity 省略结构化结果元数据时,来源回退为只含 URL 的引用。面向模型的 `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 服务的组合中挂载本提供方;它以 `perplexity` 搜索提供方身份注册,因此当它是唯一可用的搜索后端时,`ctx.web.search()` 会自动解析到它——也可以用 `searchProvider: perplexity` 固定。
29
+
30
+ ### 何时选择
31
+
32
+ 当部署持有 Perplexity API 密钥、并希望一次搜索同时获得模型生成的答案与可引用来源时选择此后端。密钥为空或端点基址无法解析时,提供方不可用——每次搜索调用都会以结构化错误失败。
33
+
34
+ ### 最小配置
35
+
36
+ 加载 web 服务与本提供方;API 密钥回退到启动环境中的 `$PERPLEXITY_API_KEY`,其余设置都有安全默认值。
37
+
38
+ ```yaml
39
+ - name: '@x1a0f3n9/dsh-web'
40
+ - name: '@x1a0f3n9/dsh-web-search-perplexity'
41
+ config:
42
+ apiKey: !!js process.env.PERPLEXITY_API_KEY
43
+ ```
44
+
45
+ | 字段 | 默认值 | 含义 |
46
+ |---|---|---|
47
+ | `apiKey` | `$PERPLEXITY_API_KEY` | Perplexity API 密钥;为空或缺失时提供方不可用 |
48
+ | `baseURL` | `https://api.perplexity.ai` | 端点基址;追加 `/chat/completions`。无法解析时提供方不可用 |
49
+ | `model` | `sonar` | 搜索模型名称 |
50
+ | `maxTokens` | `1024` | 生成答案 token 上限(`max_tokens`);必须是正整数 |
51
+ | `searchRecency` | (未设置) | 以 `search_recency_filter` 发送的新近程度窗口:`day`、`week`、`month` 或 `year`。未设置时不发送过滤条件 |
52
+
53
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-web-search-perplexity)是每个受支持字段及其 JSDoc 的穷尽式真源。
54
+
55
+ ### 搜索返回什么
56
+
57
+ `content` 携带 Perplexity 的生成答案。`sources[]` 优先使用结构化 `search_results[]`(`url`、`title`、`snippet`、`publishedAt` 取自 `date`),仅当 `search_results` 缺失时才回退到只含 URL 的 `citations[]` 数组——这正是服务上 `title`/`snippet`/`publishedAt` 为可选字段的原因。Perplexity 不公开结果数量控制,因此服务通过截断并标记来强制执行 `maxResults`。
58
+
59
+ ### 失败与恢复
60
+
61
+ 提供方失败——HTTP 错误、网络失败、响应体无法解析或结构不符——以 `WebError` `WEB_PROVIDER_ERROR` 呈现;中止请求以 `WEB_ABORTED` 呈现。HTTP 重定向会在访问 `Location` 指向的目标之前被拒绝,并以 `WEB_PROVIDER_ERROR` 呈现。调用方根据错误码进行路由;面向模型的 `web_search` 工具会在自己的错误包装层内把失败呈现给模型。
62
+
63
+ -----
64
+
65
+ <a id="understand-the-implementation"></a>
66
+ ## 理解实现
67
+
68
+ <details>
69
+ <summary>实现细节——点击展开</summary>
70
+
71
+ 本节解释提供方背后的设计决策;可观察行为已在[使用本包](#use-this-package)中完整说明。
72
+
73
+ ### 设计理念
74
+
75
+ 该提供方是 Perplexity chat-completions 端点之上的薄适配器,遵循两条刻意的规则:
76
+
77
+ - **生成答案直接用作 `content`。** 与其他搜索后端不同,Perplexity 返回模型生成的答案,本提供方将其作为规范化 `content` 字段透传。
78
+ - **结构化来源优先;只含 URL 的引用是回退。** `search_results[]` 携带可移植字段;`citations[]` 只携带 URL,服务词汇把这些字段设为可选,正是为了这种情况。
79
+
80
+ ### 源码地图
81
+
82
+ | 文件 | 职责 |
83
+ |---|---|
84
+ | [`src/index.ts`](src/index.ts) | 插件入口:配置 schema、环境变量回退、提供方注册 |
85
+ | [`src/provider.ts`](src/provider.ts) | `PerplexitySearchProvider`:请求分发、中止分类、答案与来源映射 |
86
+ | [`src/types.ts`](src/types.ts) | chat-completions 响应的 Perplexity 协议类型 |
87
+ | — | 不发布运行时不变量配套入口;除所属 seam 强制执行的约定外,本包不公开独立的事件序列或可变数据关系。 |
88
+
89
+ ### 请求与映射流程
90
+
91
+ `search()` 以 `redirect: 'error'` 把查询连同模型、token 上限与可选新近程度过滤条件 POST 到 `{baseURL}/chat/completions`。响应的 `content` 变为 `content`;存在 `search_results[]` 时它变为 `sources[]`,否则每个 `citations[]` 条目变为只含 URL 的来源;服务在返回路径上应用最终的 `maxResults` 上限。中止——名为 `AbortError` 的 `DOMException`——变为 `WEB_ABORTED`;其余情况变为 `WEB_PROVIDER_ERROR`。
92
+
93
+ </details>
94
+
95
+ -----
96
+
97
+ <a id="further-exploration"></a>
98
+ ## 进一步探索
99
+
100
+ 当包级约定不够用时阅读以下页面。它们从共享词汇逐步进入服务、面向模型的工具与设计依据。
101
+
102
+ - [web 子系统](../../../docs/subsystems/web.zh.md)——穷尽式的搜索请求/结果词汇与错误码。
103
+ - [web 包映射](../README.zh.md)——七包家族与各角色。
104
+ - [dsh-web](../web/README.zh.md)——本提供方注册进入的 web 服务。
105
+ - [dsh-tool-web](../tool-web/README.zh.md)——渲染本提供方来源的面向模型 `web_search` 工具。
106
+ - [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-web-search-perplexity)——每个受支持配置字段及其源声明。
107
+ - [web 能力 seam 决策](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md)——搜索与抓取为何共用一项提供方选择服务。
108
+
109
+ -----
110
+
111
+ <a id="model-experience"></a>
112
+ ## 模型体验
113
+
114
+ ### 辅助 Perplexity 请求
115
+
116
+ #### 模型看到的内容
117
+
118
+ 独立的 Perplexity 模型通过 chat-completions 端点将 `<query>` 原样作为唯一用户消息接收。该请求不属于会话模型上下文。
119
+
120
+ #### Token 影响
121
+
122
+ 每次搜索都会产生独立的提供方 token;`maxTokens` 限制生成答案。
123
+
124
+ #### KV Cache 影响
125
+
126
+ 与会话请求缓存相互独立。同一模型路由下的相同查询可能复用提供方缓存;查询或路由改变会建立不同前缀。
127
+
128
+ ### 间接的会话工具结果
129
+
130
+ #### 模型看到的内容
131
+
132
+ 通过 `dsh-tool-web`,会话模型会看到生成答案及结构化结果元数据,或只含 URL 的引用。该提供方确切的错误消息为 `Perplexity search aborted`、`Perplexity search request failed: <error>` 和 `Perplexity returned an unprocessable response body: <error>`;HTTP 失败保留提供方消息。错误包装层属于消费方。
133
+
134
+ #### Token 影响
135
+
136
+ 注册不会直接产生会话 token。答案与来源 token 取决于数据,来源数量受服务限制;保留的结果或错误会重复发送,直到发生压缩(compaction)。
137
+
138
+ #### KV Cache 影响
139
+
140
+ 仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
141
+
142
+ ## 已知限制与延期工作
143
+
144
+ <a id="known-limitations-and-deferred-work"></a>
145
+
146
+
147
+ 这些限制说明提供方在哪些情况下不合适。它们是当前包约束。
148
+
149
+ - **引用回退来源只含 URL**——Perplexity 省略结构化 `search_results[]` 时,来源不含 `title`/`snippet`/`publishedAt`,因此工具只渲染纯主机名标签。
150
+ - **超量返回的来源仍会增加 token 消耗与延迟**——协议没有结果数量控制,`maxResults` 只能由服务在事后截断。
151
+ - **只公开 `model`/`maxTokens`/`searchRecency`**——Perplexity 的其他搜索控制项(域名过滤条件、`web_search_options` 上下文大小、图片)等待提供方无关的服务字段(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md))。
152
+ - **按错误形状分类中止**——只有名为 `AbortError` 的 `DOMException` 才映射为 `WEB_ABORTED`;携带自定义原因的中止(例如 `dsh-timeout` 的 `TimeoutReason`)呈现为 `WEB_PROVIDER_ERROR`。
153
+
154
+ <a id="dev-note"></a>
155
+ ### 开发备注
156
+
157
+ <details>
158
+ <summary>维护者的工作上下文——点击展开</summary>
159
+
160
+ 本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文和相关 Agent Note 为准。
161
+
162
+ #### 未来:更宽的 Perplexity 控制面
163
+
164
+ Perplexity 的域名过滤条件、`web_search_options` 上下文大小与图片支持仍未公开。公开它们需要先有提供方无关的服务字段,让家族以一个协调一致的控制项、而非厂商专有参数的方式新增。
165
+
166
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,152 @@
1
+ import { launchEnvironmentOf } from "@x1a0f3n9/dsh-launch-environment";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { WebError } from "@x1a0f3n9/dsh-web";
4
+ //#region lib/types/provider.js
5
+ /**
6
+ * Perplexity search over its OpenAI-compatible chat-completions endpoint. The generated answer
7
+ * becomes `content`; sources prefer structured `search_results[]` and fall back to URL-only
8
+ * `citations[]`. The wire format and native `fetch` client are provider-private and do not use
9
+ * `ctx.llm`.
10
+ * @module @x1a0f3n9/dsh-web-search-perplexity/provider
11
+ */
12
+ /** Stable id this provider registers under. */
13
+ const PERPLEXITY_PROVIDER_ID = "perplexity";
14
+ /** Default Perplexity endpoint; `/chat/completions` is the operation. */
15
+ const PERPLEXITY_DEFAULT_BASE_URL = "https://api.perplexity.ai";
16
+ /** Default search model. */
17
+ const PERPLEXITY_DEFAULT_MODEL = "sonar";
18
+ /** Default upper bound on generated answer tokens. */
19
+ const PERPLEXITY_DEFAULT_MAX_TOKENS = 1024;
20
+ /** Attribution header sent on every request. Bump with the package version. */
21
+ const USER_AGENT = "deepseek-harness/0.0.1";
22
+ /**
23
+ * Map one structured Perplexity search result to a normalized source.
24
+ *
25
+ * @param result - one entry of the response's `search_results[]`.
26
+ * @returns the normalized source; blank fields are omitted rather than set empty.
27
+ */
28
+ function mapPerplexityResult(result) {
29
+ return {
30
+ url: result.url,
31
+ ...result.title != null && result.title.length > 0 ? { title: result.title } : {},
32
+ ...result.snippet != null && result.snippet.length > 0 ? { snippet: result.snippet } : {},
33
+ ...result.date != null && result.date.length > 0 ? { publishedAt: result.date } : {}
34
+ };
35
+ }
36
+ /**
37
+ * Map a Perplexity response envelope to a normalized search result. Prefers
38
+ * structured `search_results[]`; falls back to URL-only `citations[]` (those
39
+ * sources carry just a `url`) only when `search_results` is absent.
40
+ *
41
+ * @param response - the parsed chat-completions response body.
42
+ * @returns the normalized result; `content` is omitted when the answer is empty.
43
+ */
44
+ function mapPerplexityResponse(response) {
45
+ const content = response.choices?.[0]?.message?.content;
46
+ const sources = response.search_results !== void 0 ? response.search_results.map(mapPerplexityResult) : (response.citations ?? []).map((url) => ({ url }));
47
+ return {
48
+ ...content != null && content.length > 0 ? { content } : {},
49
+ sources,
50
+ truncated: false
51
+ };
52
+ }
53
+ /** The Perplexity-backed search provider; HTTP redirects fail as `WEB_PROVIDER_ERROR`. */
54
+ var PerplexitySearchProvider = class {
55
+ options;
56
+ id = PERPLEXITY_PROVIDER_ID;
57
+ constructor(options) {
58
+ this.options = options;
59
+ }
60
+ available() {
61
+ return this.options.apiKey.length > 0 && URL.canParse(this.options.baseURL) && isPositiveInteger(this.options.maxTokens);
62
+ }
63
+ async search(request, signal) {
64
+ let response;
65
+ try {
66
+ response = await fetch(`${this.options.baseURL}/chat/completions`, {
67
+ method: "POST",
68
+ redirect: "error",
69
+ headers: {
70
+ "authorization": `Bearer ${this.options.apiKey}`,
71
+ "content-type": "application/json",
72
+ "accept": "application/json",
73
+ "user-agent": USER_AGENT
74
+ },
75
+ body: JSON.stringify({
76
+ model: this.options.model,
77
+ max_tokens: this.options.maxTokens,
78
+ messages: [{
79
+ role: "user",
80
+ content: request.query
81
+ }],
82
+ ...this.options.searchRecency !== void 0 ? { search_recency_filter: this.options.searchRecency } : {}
83
+ }),
84
+ ...signal !== void 0 ? { signal } : {}
85
+ });
86
+ } catch (error) {
87
+ if (isAbortError(error)) throw new WebError("Perplexity search aborted", "WEB_ABORTED", { cause: error });
88
+ throw new WebError(`Perplexity search request failed: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
89
+ }
90
+ if (!response.ok) {
91
+ let message = `Perplexity API error (HTTP ${response.status})`;
92
+ try {
93
+ const parsed = await response.json();
94
+ const detail = typeof parsed.error === "string" ? parsed.error : parsed.error?.message ?? parsed.message;
95
+ if (detail !== void 0 && detail.length > 0) message = detail;
96
+ } catch (error) {
97
+ if (isAbortError(error)) throw new WebError("Perplexity search aborted", "WEB_ABORTED", { cause: error });
98
+ }
99
+ throw new WebError(message, "WEB_PROVIDER_ERROR");
100
+ }
101
+ try {
102
+ return mapPerplexityResponse(await response.json());
103
+ } catch (error) {
104
+ if (isAbortError(error)) throw new WebError("Perplexity search aborted", "WEB_ABORTED", { cause: error });
105
+ throw new WebError(`Perplexity returned an unprocessable response body: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
106
+ }
107
+ }
108
+ };
109
+ /** True for a fetch/`AbortSignal` abort, surfaced as `WEB_ABORTED`. */
110
+ function isAbortError(error) {
111
+ return error instanceof DOMException && error.name === "AbortError";
112
+ }
113
+ /** True for a request limit that can be sent to Perplexity (a positive whole number). */
114
+ function isPositiveInteger(value) {
115
+ return Number.isInteger(value) && value > 0;
116
+ }
117
+ //#endregion
118
+ //#region lib/types/index.js
119
+ /**
120
+ * Perplexity-backed `WebSearchProvider` plugin. It contributes to the
121
+ * `ctx.web` registry without owning the service.
122
+ *
123
+ * @module @x1a0f3n9/dsh-web-search-perplexity
124
+ */
125
+ /** Cordis plugin name used by loader diagnostics. */
126
+ const name = "web-search-perplexity";
127
+ /** The web seam this provider registers into. */
128
+ const inject = ["web"];
129
+ const Config = z.object({
130
+ apiKey: z.string(),
131
+ baseURL: z.string(),
132
+ model: z.string(),
133
+ maxTokens: z.number().step(1).min(1),
134
+ searchRecency: z.union([
135
+ "day",
136
+ "week",
137
+ "month",
138
+ "year"
139
+ ])
140
+ });
141
+ /** Register the Perplexity search provider with `ctx.web`. */
142
+ function apply(ctx, config) {
143
+ ctx.web.registerSearchProvider(new PerplexitySearchProvider({
144
+ apiKey: config.apiKey ?? launchEnvironmentOf(ctx).get("PERPLEXITY_API_KEY")?.value ?? "",
145
+ baseURL: config.baseURL ?? "https://api.perplexity.ai",
146
+ model: config.model ?? "sonar",
147
+ maxTokens: config.maxTokens ?? 1024,
148
+ ...config.searchRecency !== void 0 ? { searchRecency: config.searchRecency } : {}
149
+ }));
150
+ }
151
+ //#endregion
152
+ export { Config, PERPLEXITY_DEFAULT_BASE_URL, PERPLEXITY_DEFAULT_MAX_TOKENS, PERPLEXITY_DEFAULT_MODEL, PERPLEXITY_PROVIDER_ID, PerplexitySearchProvider, apply, inject, name };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Perplexity-backed `WebSearchProvider` plugin. It contributes to the
3
+ * `ctx.web` registry without owning the service.
4
+ *
5
+ * @module @x1a0f3n9/dsh-web-search-perplexity
6
+ */
7
+ import type { Context } from '@deepseek-ai/cordis';
8
+ import z from '@deepseek-ai/schemastery';
9
+ export { PERPLEXITY_DEFAULT_BASE_URL, PERPLEXITY_DEFAULT_MAX_TOKENS, PERPLEXITY_DEFAULT_MODEL, PERPLEXITY_PROVIDER_ID, PerplexitySearchProvider, } from './provider.ts';
10
+ export type { PerplexityRecency, PerplexitySearchProviderOptions } from './provider.ts';
11
+ /** Cordis plugin name used by loader diagnostics. */
12
+ export declare const name = "web-search-perplexity";
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
+ /** Perplexity API key. Falls back to `$PERPLEXITY_API_KEY`. Empty → unavailable. */
18
+ apiKey?: string;
19
+ /** Endpoint base; `/chat/completions` is appended. Defaults to the public API. */
20
+ baseURL?: string;
21
+ /** Search model name. Defaults to `sonar`. */
22
+ model?: string;
23
+ /** Upper bound on generated answer tokens. Defaults to 1024. */
24
+ maxTokens?: number;
25
+ /** Recency window sent as `search_recency_filter`. Omitted = no filter. */
26
+ searchRecency?: 'day' | 'week' | 'month' | 'year';
27
+ }
28
+ export declare const Config: z<Config>;
29
+ /** Register the Perplexity search provider with `ctx.web`. */
30
+ export declare function apply(ctx: Context, config: Config): void;
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Perplexity search over its OpenAI-compatible chat-completions endpoint. The generated answer
3
+ * becomes `content`; sources prefer structured `search_results[]` and fall back to URL-only
4
+ * `citations[]`. The wire format and native `fetch` client are provider-private and do not use
5
+ * `ctx.llm`.
6
+ * @module @x1a0f3n9/dsh-web-search-perplexity/provider
7
+ */
8
+ import type { WebSearchProvider, WebSearchRequest, WebSearchResult, WebSearchSource } from '@x1a0f3n9/dsh-web';
9
+ import type { PerplexityResponse, PerplexitySearchResult } from './types.ts';
10
+ /** Stable id this provider registers under. */
11
+ export declare const PERPLEXITY_PROVIDER_ID = "perplexity";
12
+ /** Default Perplexity endpoint; `/chat/completions` is the operation. */
13
+ export declare const PERPLEXITY_DEFAULT_BASE_URL = "https://api.perplexity.ai";
14
+ /** Default search model. */
15
+ export declare const PERPLEXITY_DEFAULT_MODEL = "sonar";
16
+ /** Default upper bound on generated answer tokens. */
17
+ export declare const PERPLEXITY_DEFAULT_MAX_TOKENS = 1024;
18
+ /** Recency filter values Perplexity accepts for `search_recency_filter`. */
19
+ export type PerplexityRecency = 'day' | 'week' | 'month' | 'year';
20
+ /** Resolved provider options (the plugin's `apply` supplies env-var and constant defaults). */
21
+ export interface PerplexitySearchProviderOptions {
22
+ /** Perplexity API key. Empty/absent makes the provider unavailable. */
23
+ apiKey: string;
24
+ /** Endpoint base; `/chat/completions` is appended. */
25
+ baseURL: string;
26
+ /** Search model name. */
27
+ model: string;
28
+ /** Upper bound on generated answer tokens (`max_tokens`). */
29
+ maxTokens: number;
30
+ /** Optional recency window sent as `search_recency_filter`; omitted = no filter. */
31
+ searchRecency?: PerplexityRecency;
32
+ }
33
+ /**
34
+ * Map one structured Perplexity search result to a normalized source.
35
+ *
36
+ * @param result - one entry of the response's `search_results[]`.
37
+ * @returns the normalized source; blank fields are omitted rather than set empty.
38
+ */
39
+ export declare function mapPerplexityResult(result: PerplexitySearchResult): WebSearchSource;
40
+ /**
41
+ * Map a Perplexity response envelope to a normalized search result. Prefers
42
+ * structured `search_results[]`; falls back to URL-only `citations[]` (those
43
+ * sources carry just a `url`) only when `search_results` is absent.
44
+ *
45
+ * @param response - the parsed chat-completions response body.
46
+ * @returns the normalized result; `content` is omitted when the answer is empty.
47
+ */
48
+ export declare function mapPerplexityResponse(response: PerplexityResponse): WebSearchResult;
49
+ /** The Perplexity-backed search provider; HTTP redirects fail as `WEB_PROVIDER_ERROR`. */
50
+ export declare class PerplexitySearchProvider implements WebSearchProvider {
51
+ private readonly options;
52
+ readonly id = "perplexity";
53
+ constructor(options: PerplexitySearchProviderOptions);
54
+ available(): boolean;
55
+ search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
56
+ }
57
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Wire types for the Perplexity search API (`POST https://api.perplexity.ai/chat/completions`,
3
+ * an OpenAI-compatible chat shape). Results prefer structured `search_results` and fall back to
4
+ * URL-only `citations`; the provider-private wire shape does not depend on `ctx.llm`.
5
+ * @module @x1a0f3n9/dsh-web-search-perplexity/types
6
+ */
7
+ /** Request body sent to Perplexity's chat-completions endpoint. */
8
+ export interface PerplexityRequest {
9
+ model: string;
10
+ messages: {
11
+ role: 'user';
12
+ content: string;
13
+ }[];
14
+ }
15
+ /** One structured search result (the preferred citation shape). */
16
+ export interface PerplexitySearchResult {
17
+ url: string;
18
+ title?: string | null;
19
+ snippet?: string | null;
20
+ date?: string | null;
21
+ }
22
+ /** Perplexity's response envelope. */
23
+ export interface PerplexityResponse {
24
+ choices?: {
25
+ message?: {
26
+ content?: string | null;
27
+ };
28
+ }[];
29
+ /** Structured citation data (preferred). */
30
+ search_results?: PerplexitySearchResult[];
31
+ /** URL-only citation fallback. */
32
+ citations?: string[];
33
+ }
34
+ /** Perplexity's error response envelope (best-effort; fields vary). */
35
+ export interface PerplexityError {
36
+ error?: {
37
+ message?: string;
38
+ } | string;
39
+ message?: string;
40
+ }
41
+ //# sourceMappingURL=types.d.ts.map
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@x1a0f3n9/dsh-web-search-perplexity",
3
+ "description": "Perplexity-backed search provider 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-perplexity"
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-launch-environment": "^0.1.5-rc.2",
31
+ "@x1a0f3n9/dsh-web": "^0.1.5-rc.2",
32
+ "@deepseek-ai/cordis": "^4.0.2"
33
+ },
34
+ "dependencies": {
35
+ "@deepseek-ai/schemastery": "^3.18.2"
36
+ },
37
+ "devDependencies": {
38
+ "@x1a0f3n9/dsh-web": "^0.1.5-rc.2",
39
+ "@deepseek-ai/cordis": "^4.0.2",
40
+ "@x1a0f3n9/dsh-http-proxy": "^0.1.5-rc.2",
41
+ "@x1a0f3n9/dsh-launch-environment": "^0.1.5-rc.2"
42
+ }
43
+ }