dsh-web-search-plugin 0.2.1 → 0.3.1

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/lib/index.js CHANGED
@@ -18,11 +18,14 @@
18
18
  */
19
19
  import z from "@deepseek-ai/schemastery";
20
20
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
21
+ import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
21
22
  import { MAX_RESULTS_CAP } from "./shared.js";
22
23
  import { TAVILY_DEFAULT_API_KEY_ENV, TAVILY_DEFAULT_BASE_URL, TavilySearchProvider, resolveTavilyOptions } from "./tavily.js";
23
24
  import { BRAVE_DEFAULT_API_KEY_ENV, BRAVE_DEFAULT_BASE_URL, BraveSearchProvider, resolveBraveOptions } from "./brave.js";
24
25
  import { DEEPSEEK_DEFAULT_API_KEY_ENV, DEEPSEEK_DEFAULT_BASE_URL, DeepSeekSearchProvider, resolveDeepseekOptions } from "./deepseek.js";
25
26
  import { TAVILY_USAGE_POLL_MS, UsageTracker } from "./usage.js";
27
+ import { RestSearchProvider, resolveRestProvider } from "./rest.js";
28
+ import { BUILTIN_REST_IDS, REST_PROVIDERS, findProvider } from "./providers.js";
26
29
 
27
30
  /** Stable id this plugin registers under on `ctx.web`. */
28
31
  const SEARCH_PROVIDER_ID = "dsh-web-search";
@@ -34,8 +37,8 @@ const inject = ["web"];
34
37
 
35
38
  /** Plugin config (all optional — `apply` fills defaults). */
36
39
  const Config = z.object({
37
- /** Active backend: `deepseek-official`, `tavily` (default), or `brave`. */
38
- provider: z.string().default("tavily"),
40
+ /** Active backend: `deepseek-official` (default), `tavily`, `brave`, `serper`, `serpapi`, `exa`, or `searxng`. */
41
+ provider: z.string().default("deepseek-official"),
39
42
  /** Tavily auth mode: `keyless` (default) or `keyed`. */
40
43
  mode: z.string().default("keyless"),
41
44
  /** Literal Tavily API key; prefer {@link apiKeyEnv}. */
@@ -66,6 +69,38 @@ const Config = z.object({
66
69
  freshness: z.string().default(""),
67
70
  /** Optional HTTP(S) proxy URL for Brave (falls back to HTTPS_PROXY / HTTP_PROXY). */
68
71
  proxy: z.string().default(""),
72
+ /** Literal Serper API key; prefer {@link serperApiKeyEnv}. */
73
+ serperApiKey: z.string().role("secret").default(""),
74
+ /** Credential reference for Serper search. */
75
+ serperApiKeyEnv: z.string().role("credential-ref").default("SERPER_API_KEY"),
76
+ /** Serper endpoint base. `/search` is appended. */
77
+ serperBaseURL: z.string().default("https://google.serper.dev"),
78
+ /** Literal SerpApi API key; prefer {@link serpapiApiKeyEnv}. */
79
+ serpapiApiKey: z.string().role("secret").default(""),
80
+ /** Credential reference for SerpApi search. */
81
+ serpapiApiKeyEnv: z.string().role("credential-ref").default("SERPAPI_API_KEY"),
82
+ /** SerpApi endpoint base. `/search.json` is appended. */
83
+ serpapiBaseURL: z.string().default("https://serpapi.com"),
84
+ /** Literal Exa API key; prefer {@link exaApiKeyEnv}. */
85
+ exaApiKey: z.string().role("secret").default(""),
86
+ /** Credential reference for Exa search. */
87
+ exaApiKeyEnv: z.string().role("credential-ref").default("EXA_API_KEY"),
88
+ /** Exa endpoint base. `/search` is appended. */
89
+ exaBaseURL: z.string().default("https://api.exa.ai"),
90
+ /** Optional SearXNG instance base URL (self-hosted). `/search` is appended. */
91
+ searxngBaseURL: z.string().default("https://searx.be"),
92
+ /** Literal Scavio API key; prefer {@link scavioApiKeyEnv}. */
93
+ scavioApiKey: z.string().role("secret").default(""),
94
+ /** Credential reference for Scavio search. */
95
+ scavioApiKeyEnv: z.string().role("credential-ref").default("SCAVIO_API_KEY"),
96
+ /** Scavio endpoint base. `/api/v2/google` is appended. */
97
+ scavioBaseURL: z.string().default("https://api.scavio.dev"),
98
+ /** Literal Firecrawl API key; prefer {@link firecrawlApiKeyEnv}. */
99
+ firecrawlApiKey: z.string().role("secret").default(""),
100
+ /** Credential reference for Firecrawl search. */
101
+ firecrawlApiKeyEnv: z.string().role("credential-ref").default("FIRECRAWL_API_KEY"),
102
+ /** Firecrawl endpoint base. `/v2/search` is appended. */
103
+ firecrawlBaseURL: z.string().default("https://api.firecrawl.dev"),
69
104
  /** Literal DeepSeek API key; prefer {@link deepseekApiKeyEnv}. */
70
105
  deepseekApiKey: z.string().role("secret").default(""),
71
106
  /** Credential reference for DeepSeek search. */
@@ -106,6 +141,13 @@ class PluginSearchProvider {
106
141
  ...this.resolveOptions().brave,
107
142
  onRateLimit: (headers) => usage?.recordBraveHeaders(headers)
108
143
  }));
144
+ this.rest = new Map();
145
+ for (const providerId of BUILTIN_REST_IDS) {
146
+ this.rest.set(providerId, new RestSearchProvider(providerId, () => this.resolveOptions().rest[providerId], {
147
+ onUsage: providerId === "tavily" ? (credits) => usage?.recordTavilyCredits(credits) : void 0,
148
+ onRateLimit: providerId === "brave" ? (headers) => usage?.recordBraveHeaders(headers) : void 0
149
+ }));
150
+ }
109
151
  }
110
152
 
111
153
  available() {
@@ -117,21 +159,63 @@ class PluginSearchProvider {
117
159
  }
118
160
 
119
161
  backend() {
120
- const provider = this.resolveOptions().provider;
121
- if (provider === "brave") return this.brave;
162
+ const options = this.resolveOptions();
163
+ const provider = options.provider;
164
+ const restInstance = this.rest.get(provider);
165
+ if (restInstance !== void 0) return restInstance;
122
166
  if (provider === "deepseek-official") return this.deepseek;
123
- return this.tavily;
167
+ // Fallback: provider points at something unknown → official default.
168
+ return this.deepseek;
124
169
  }
125
170
  }
126
171
 
127
172
  /** Project one resolved section into options for each backend. */
128
173
  function resolveOptions(ctx, config) {
129
- const provider = config.provider === "brave" || config.provider === "deepseek-official" ? config.provider : "tavily";
174
+ const isBuiltIn = config.provider === "tavily" || config.provider === "brave" || config.provider === "deepseek-official" || BUILTIN_REST_IDS.includes(config.provider);
175
+ const provider = isBuiltIn ? config.provider : "deepseek-official";
176
+ // Per-provider literal key / optional endpoint override, used when the
177
+ // metadata row's default is not enough (self-hosted SearXNG, proxied
178
+ // providers, custom key refs).
179
+ const keySourceByProvider = {
180
+ brave: { literal: config.braveApiKey, envName: config.braveApiKeyEnv ?? BRAVE_DEFAULT_API_KEY_ENV, baseURL: config.braveBaseURL ?? "", proxy: config.proxy || launchEnvironmentOf(ctx).get("HTTPS_PROXY")?.value || launchEnvironmentOf(ctx).get("HTTP_PROXY")?.value || "" },
181
+ tavily: { literal: config.apiKey, envName: config.apiKeyEnv ?? TAVILY_DEFAULT_API_KEY_ENV, baseURL: config.baseURL ?? "" },
182
+ serper: { literal: config.serperApiKey, envName: config.serperApiKeyEnv ?? "SERPER_API_KEY", baseURL: config.serperBaseURL ?? "" },
183
+ serpapi: { literal: config.serpapiApiKey, envName: config.serpapiApiKeyEnv ?? "SERPAPI_API_KEY", baseURL: config.serpapiBaseURL ?? "" },
184
+ exa: { literal: config.exaApiKey, envName: config.exaApiKeyEnv ?? "EXA_API_KEY", baseURL: config.exaBaseURL ?? "" },
185
+ searxng: { baseURL: config.searxngBaseURL ?? "" },
186
+ scavio: { literal: config.scavioApiKey, envName: config.scavioApiKeyEnv ?? "SCAVIO_API_KEY", baseURL: config.scavioBaseURL ?? "" },
187
+ firecrawl: { literal: config.firecrawlApiKey, envName: config.firecrawlApiKeyEnv ?? "FIRECRAWL_API_KEY", baseURL: config.firecrawlBaseURL ?? "" }
188
+ };
189
+ const settings = {
190
+ country: config.country,
191
+ searchLang: config.searchLang,
192
+ freshness: config.freshness,
193
+ searchDepth: config.searchDepth,
194
+ topic: config.topic,
195
+ includeAnswer: config.includeAnswer
196
+ };
197
+ const rest = {};
198
+ for (const providerId of BUILTIN_REST_IDS) {
199
+ const row = findProvider(providerId);
200
+ if (row === void 0) continue;
201
+ const source = keySourceByProvider[providerId] ?? {};
202
+ const resolved = resolveRestProvider(ctx, row, {
203
+ config,
204
+ settings,
205
+ keyed: providerId === "tavily" ? config.mode === "keyed" : false,
206
+ keySource: source
207
+ });
208
+ // Endpoint override: an explicit config baseURL wins over the row default.
209
+ if (source.baseURL != null && source.baseURL.length > 0) resolved.baseURL = source.baseURL;
210
+ if (providerId === "brave" && (source.proxy ?? "").length > 0) resolved.proxy = source.proxy;
211
+ rest[providerId] = resolved;
212
+ }
130
213
  return {
131
214
  provider,
132
215
  deepseek: resolveDeepseekOptions(ctx, config),
133
216
  tavily: resolveTavilyOptions(ctx, config),
134
- brave: resolveBraveOptions(ctx, config)
217
+ brave: resolveBraveOptions(ctx, config),
218
+ rest
135
219
  };
136
220
  }
137
221
 
@@ -236,11 +320,14 @@ function apply(ctx, config) {
236
320
  export {
237
321
  BRAVE_DEFAULT_API_KEY_ENV,
238
322
  BRAVE_DEFAULT_BASE_URL,
323
+ BUILTIN_REST_IDS,
239
324
  BraveSearchProvider,
240
325
  Config,
241
326
  DEEPSEEK_DEFAULT_API_KEY_ENV,
242
327
  DEEPSEEK_DEFAULT_BASE_URL,
243
328
  DeepSeekSearchProvider,
329
+ REST_PROVIDERS,
330
+ RestSearchProvider,
244
331
  SEARCH_PROVIDER_ID,
245
332
  TAVILY_DEFAULT_API_KEY_ENV,
246
333
  TAVILY_DEFAULT_BASE_URL,
@@ -250,6 +337,8 @@ export {
250
337
  WEB_SEARCH_SETTINGS_NAMESPACE,
251
338
  WEB_SEARCH_TAVILY_SETTINGS_NAMESPACE,
252
339
  apply,
340
+ findProvider,
253
341
  inject,
254
- name
342
+ name,
343
+ resolveRestProvider
255
344
  };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Static metadata table for every built-in REST web-search provider.
3
+ *
4
+ * This module is intentionally **data, not code**: adding a new REST provider
5
+ * means adding one row here (plus an optional official "get a key" link in the
6
+ * settings card), with zero per-provider execution code. The generic backend in
7
+ * `lib/rest.js` reads a row and performs `method` / `path` / `queryParam` /
8
+ * `countParam` / `params` / `auth` / `response` / `hooks`.
9
+ *
10
+ * Only **pure REST providers** ("one query → one HTTP request → one result
11
+ * array") belong here. Model-native search tool (DeepSeek's Anthropic Messages
12
+ * `web_search_20250305`, Perplexity sonar tools, OpenAI Responses) is **not**
13
+ * REST and stays on its dedicated backend (`lib/deepseek.js`).
14
+ *
15
+ * @module dsh-web-search-plugin/providers
16
+ */
17
+
18
+ /**
19
+ * Built-in REST providers.
20
+ * `auth.type` ∈ { `bearer`, `header`, `none`, `query` }:
21
+ * - `bearer` — `Authorization: Bearer <key>`
22
+ * - `header` — key in a named header (`auth.header`)
23
+ * - `none` — no key (SearXNG, keyless)
24
+ * - `query` — key as a query-string parameter (`auth.param`, e.g. SerpApi)
25
+ * `params[].when` ∈ { `always`, `nonEmpty`, `keyed`, `keyless` }.
26
+ */
27
+ export const REST_PROVIDERS = [
28
+ {
29
+ id: "brave",
30
+ name: "Brave Search",
31
+ kind: "rest",
32
+ method: "GET",
33
+ baseURL: "https://api.search.brave.com/res/v1/web/search",
34
+ path: "",
35
+ queryIn: "query",
36
+ queryParam: "q",
37
+ countParam: "count",
38
+ auth: { type: "header", header: "X-Subscription-Token" },
39
+ keyRequired: true,
40
+ apiKeyRef: "BRAVE_API_KEY",
41
+ response: "brave",
42
+ params: [
43
+ { key: "country", setting: "country", when: "nonEmpty" },
44
+ { key: "search_lang", setting: "searchLang", when: "nonEmpty" },
45
+ { key: "freshness", setting: "freshness", when: "nonEmpty" }
46
+ ],
47
+ officialUrl: "https://api-dashboard.search.brave.com/",
48
+ hooks: ["rate-limit", "proxy"]
49
+ },
50
+ {
51
+ id: "tavily",
52
+ name: "Tavily",
53
+ kind: "rest",
54
+ method: "POST",
55
+ baseURL: "https://api.tavily.com",
56
+ path: "/search",
57
+ queryIn: "body",
58
+ queryParam: "query",
59
+ countParam: "max_results",
60
+ auth: { type: "bearer" },
61
+ keyRequired: false, // keyless by default; keyed only when a key is configured
62
+ apiKeyRef: "TAVILY_API_KEY",
63
+ response: "tavily",
64
+ params: [
65
+ { key: "search_depth", setting: "searchDepth", when: "nonEmpty" },
66
+ { key: "topic", setting: "topic", when: "nonEmpty" },
67
+ { key: "include_answer", setting: "includeAnswer", when: "nonEmpty" },
68
+ { key: "include_images", value: false, when: "always" },
69
+ { key: "include_usage", value: true, when: "keyed" }
70
+ ],
71
+ keylessHeaders: { "x-tavily-access-mode": "keyless" },
72
+ keylessHeaderWhen: "keyless",
73
+ officialUrl: "https://app.tavily.com/",
74
+ hooks: ["usage"]
75
+ },
76
+ {
77
+ id: "serper",
78
+ name: "Serper",
79
+ kind: "rest",
80
+ method: "POST",
81
+ baseURL: "https://google.serper.dev",
82
+ path: "/search",
83
+ queryIn: "body",
84
+ queryParam: "q",
85
+ countParam: "num",
86
+ auth: { type: "header", header: "X-API-KEY" },
87
+ keyRequired: true,
88
+ apiKeyRef: "SERPER_API_KEY",
89
+ response: "serper",
90
+ params: [],
91
+ officialUrl: "https://serper.dev/",
92
+ hooks: []
93
+ },
94
+ {
95
+ id: "serpapi",
96
+ name: "SerpApi",
97
+ kind: "rest",
98
+ method: "GET",
99
+ baseURL: "https://serpapi.com",
100
+ path: "/search.json",
101
+ queryIn: "query",
102
+ queryParam: "q",
103
+ countParam: "num",
104
+ auth: { type: "query", param: "api_key" },
105
+ keyRequired: true,
106
+ apiKeyRef: "SERPAPI_API_KEY",
107
+ response: "serper",
108
+ params: [],
109
+ officialUrl: "https://serpapi.com/",
110
+ hooks: []
111
+ },
112
+ {
113
+ id: "exa",
114
+ name: "Exa",
115
+ kind: "rest",
116
+ method: "POST",
117
+ baseURL: "https://api.exa.ai",
118
+ path: "/search",
119
+ queryIn: "body",
120
+ queryParam: "query",
121
+ countParam: "num_results",
122
+ auth: { type: "bearer" },
123
+ keyRequired: true,
124
+ apiKeyRef: "EXA_API_KEY",
125
+ response: "exa",
126
+ params: [
127
+ { key: "contents", value: "text", when: "always" }
128
+ ],
129
+ officialUrl: "https://dashboard.exa.ai/",
130
+ hooks: []
131
+ },
132
+ {
133
+ id: "searxng",
134
+ name: "SearXNG",
135
+ kind: "rest",
136
+ method: "GET",
137
+ baseURL: "https://searx.be",
138
+ path: "/search",
139
+ queryIn: "query",
140
+ queryParam: "q",
141
+ countParam: "count",
142
+ auth: { type: "none" },
143
+ keyRequired: false,
144
+ apiKeyRef: "",
145
+ response: "searxng",
146
+ params: [
147
+ { key: "format", value: "json", when: "always" }
148
+ ],
149
+ officialUrl: "https://docs.searxng.org/",
150
+ hooks: []
151
+ },
152
+ {
153
+ id: "scavio",
154
+ name: "Scavio",
155
+ kind: "rest",
156
+ method: "POST",
157
+ baseURL: "https://api.scavio.dev",
158
+ path: "/api/v2/google",
159
+ queryIn: "body",
160
+ queryParam: "query",
161
+ countParam: "",
162
+ auth: { type: "bearer" },
163
+ keyRequired: true,
164
+ apiKeyRef: "SCAVIO_API_KEY",
165
+ response: "serper",
166
+ params: [
167
+ { key: "hl", setting: "searchLang", when: "nonEmpty" },
168
+ { key: "gl", setting: "country", when: "nonEmpty" }
169
+ ],
170
+ officialUrl: "https://scavio.dev/",
171
+ hooks: []
172
+ },
173
+ {
174
+ id: "firecrawl",
175
+ name: "Firecrawl",
176
+ kind: "rest",
177
+ method: "POST",
178
+ baseURL: "https://api.firecrawl.dev",
179
+ path: "/v2/search",
180
+ queryIn: "body",
181
+ queryParam: "query",
182
+ countParam: "limit",
183
+ auth: { type: "bearer" },
184
+ keyRequired: true,
185
+ apiKeyRef: "FIRECRAWL_API_KEY",
186
+ response: "firecrawl",
187
+ params: [
188
+ { key: "sources", value: "web", when: "always" }
189
+ ],
190
+ officialUrl: "https://firecrawl.dev/",
191
+ hooks: []
192
+ }
193
+ ];
194
+
195
+ /** Location of one provider by id (the id doubles as the `provider` value). */
196
+ export function findProvider(id) {
197
+ for (const entry of REST_PROVIDERS) if (entry.id === id) return entry;
198
+ return void 0;
199
+ }
200
+
201
+ /** Set of ids that live in the metadata table. */
202
+ export function restProviderIds() {
203
+ const ids = /* @__PURE__ */ new Set();
204
+ for (const entry of REST_PROVIDERS) ids.add(entry.id);
205
+ return ids;
206
+ }
207
+
208
+ /** The built-in REST provider ids (seam-facing `provider` values). */
209
+ export const BUILTIN_REST_IDS = REST_PROVIDERS.map((entry) => entry.id);
210
+
211
+ /** The model-native (non-REST) provider id, kept on its own backend. */
212
+ export const TOOL_PROVIDER_IDS = ["deepseek-official"];