dsh-web-search-plugin 0.2.0 → 0.3.0
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/CHANGELOG.md +47 -0
- package/README.md +118 -47
- package/cordis.patch.yml +1 -1
- package/lib/brave.js +1 -0
- package/lib/client.js +657 -96
- package/lib/index.js +195 -12
- package/lib/providers.js +212 -0
- package/lib/rest.js +325 -0
- package/lib/shared.js +1 -1
- package/lib/tavily.js +10 -2
- package/lib/usage.js +352 -0
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -22,6 +22,9 @@ import { MAX_RESULTS_CAP } from "./shared.js";
|
|
|
22
22
|
import { TAVILY_DEFAULT_API_KEY_ENV, TAVILY_DEFAULT_BASE_URL, TavilySearchProvider, resolveTavilyOptions } from "./tavily.js";
|
|
23
23
|
import { BRAVE_DEFAULT_API_KEY_ENV, BRAVE_DEFAULT_BASE_URL, BraveSearchProvider, resolveBraveOptions } from "./brave.js";
|
|
24
24
|
import { DEEPSEEK_DEFAULT_API_KEY_ENV, DEEPSEEK_DEFAULT_BASE_URL, DeepSeekSearchProvider, resolveDeepseekOptions } from "./deepseek.js";
|
|
25
|
+
import { TAVILY_USAGE_POLL_MS, UsageTracker } from "./usage.js";
|
|
26
|
+
import { RestSearchProvider, resolveRestProvider } from "./rest.js";
|
|
27
|
+
import { BUILTIN_REST_IDS, REST_PROVIDERS, findProvider } from "./providers.js";
|
|
25
28
|
|
|
26
29
|
/** Stable id this plugin registers under on `ctx.web`. */
|
|
27
30
|
const SEARCH_PROVIDER_ID = "dsh-web-search";
|
|
@@ -33,8 +36,8 @@ const inject = ["web"];
|
|
|
33
36
|
|
|
34
37
|
/** Plugin config (all optional — `apply` fills defaults). */
|
|
35
38
|
const Config = z.object({
|
|
36
|
-
/** Active backend: `deepseek-official
|
|
37
|
-
provider: z.string().default("
|
|
39
|
+
/** Active backend: `deepseek-official` (default), `tavily`, `brave`, `serper`, `serpapi`, `exa`, or `searxng`. */
|
|
40
|
+
provider: z.string().default("deepseek-official"),
|
|
38
41
|
/** Tavily auth mode: `keyless` (default) or `keyed`. */
|
|
39
42
|
mode: z.string().default("keyless"),
|
|
40
43
|
/** Literal Tavily API key; prefer {@link apiKeyEnv}. */
|
|
@@ -65,6 +68,38 @@ const Config = z.object({
|
|
|
65
68
|
freshness: z.string().default(""),
|
|
66
69
|
/** Optional HTTP(S) proxy URL for Brave (falls back to HTTPS_PROXY / HTTP_PROXY). */
|
|
67
70
|
proxy: z.string().default(""),
|
|
71
|
+
/** Literal Serper API key; prefer {@link serperApiKeyEnv}. */
|
|
72
|
+
serperApiKey: z.string().role("secret").default(""),
|
|
73
|
+
/** Credential reference for Serper search. */
|
|
74
|
+
serperApiKeyEnv: z.string().role("credential-ref").default("SERPER_API_KEY"),
|
|
75
|
+
/** Serper endpoint base. `/search` is appended. */
|
|
76
|
+
serperBaseURL: z.string().default(""),
|
|
77
|
+
/** Literal SerpApi API key; prefer {@link serpapiApiKeyEnv}. */
|
|
78
|
+
serpapiApiKey: z.string().role("secret").default(""),
|
|
79
|
+
/** Credential reference for SerpApi search. */
|
|
80
|
+
serpapiApiKeyEnv: z.string().role("credential-ref").default("SERPAPI_API_KEY"),
|
|
81
|
+
/** SerpApi endpoint base. `/search.json` is appended. */
|
|
82
|
+
serpapiBaseURL: z.string().default(""),
|
|
83
|
+
/** Literal Exa API key; prefer {@link exaApiKeyEnv}. */
|
|
84
|
+
exaApiKey: z.string().role("secret").default(""),
|
|
85
|
+
/** Credential reference for Exa search. */
|
|
86
|
+
exaApiKeyEnv: z.string().role("credential-ref").default("EXA_API_KEY"),
|
|
87
|
+
/** Exa endpoint base. `/search` is appended. */
|
|
88
|
+
exaBaseURL: z.string().default(""),
|
|
89
|
+
/** Optional SearXNG instance base URL (self-hosted). `/search` is appended. */
|
|
90
|
+
searxngBaseURL: z.string().default(""),
|
|
91
|
+
/** Literal Scavio API key; prefer {@link scavioApiKeyEnv}. */
|
|
92
|
+
scavioApiKey: z.string().role("secret").default(""),
|
|
93
|
+
/** Credential reference for Scavio search. */
|
|
94
|
+
scavioApiKeyEnv: z.string().role("credential-ref").default("SCAVIO_API_KEY"),
|
|
95
|
+
/** Scavio endpoint base. `/api/v2/google` is appended. */
|
|
96
|
+
scavioBaseURL: z.string().default(""),
|
|
97
|
+
/** Literal Firecrawl API key; prefer {@link firecrawlApiKeyEnv}. */
|
|
98
|
+
firecrawlApiKey: z.string().role("secret").default(""),
|
|
99
|
+
/** Credential reference for Firecrawl search. */
|
|
100
|
+
firecrawlApiKeyEnv: z.string().role("credential-ref").default("FIRECRAWL_API_KEY"),
|
|
101
|
+
/** Firecrawl endpoint base. `/v2/search` is appended. */
|
|
102
|
+
firecrawlBaseURL: z.string().default(""),
|
|
68
103
|
/** Literal DeepSeek API key; prefer {@link deepseekApiKeyEnv}. */
|
|
69
104
|
deepseekApiKey: z.string().role("secret").default(""),
|
|
70
105
|
/** Credential reference for DeepSeek search. */
|
|
@@ -94,11 +129,24 @@ const WEB_SEARCH_TAVILY_SETTINGS_NAMESPACE = WEB_SEARCH_SETTINGS_NAMESPACE;
|
|
|
94
129
|
class PluginSearchProvider {
|
|
95
130
|
id = SEARCH_PROVIDER_ID;
|
|
96
131
|
|
|
97
|
-
constructor(resolveOptions) {
|
|
132
|
+
constructor(resolveOptions, usage) {
|
|
98
133
|
this.resolveOptions = resolveOptions;
|
|
99
134
|
this.deepseek = new DeepSeekSearchProvider(() => this.resolveOptions().deepseek);
|
|
100
|
-
this.tavily = new TavilySearchProvider(() =>
|
|
101
|
-
|
|
135
|
+
this.tavily = new TavilySearchProvider(() => ({
|
|
136
|
+
...this.resolveOptions().tavily,
|
|
137
|
+
onUsage: (credits) => usage?.recordTavilyCredits(credits)
|
|
138
|
+
}));
|
|
139
|
+
this.brave = new BraveSearchProvider(() => ({
|
|
140
|
+
...this.resolveOptions().brave,
|
|
141
|
+
onRateLimit: (headers) => usage?.recordBraveHeaders(headers)
|
|
142
|
+
}));
|
|
143
|
+
this.rest = new Map();
|
|
144
|
+
for (const providerId of BUILTIN_REST_IDS) {
|
|
145
|
+
this.rest.set(providerId, new RestSearchProvider(providerId, () => this.resolveOptions().rest[providerId], {
|
|
146
|
+
onUsage: providerId === "tavily" ? (credits) => usage?.recordTavilyCredits(credits) : void 0,
|
|
147
|
+
onRateLimit: providerId === "brave" ? (headers) => usage?.recordBraveHeaders(headers) : void 0
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
102
150
|
}
|
|
103
151
|
|
|
104
152
|
available() {
|
|
@@ -110,24 +158,98 @@ class PluginSearchProvider {
|
|
|
110
158
|
}
|
|
111
159
|
|
|
112
160
|
backend() {
|
|
113
|
-
const
|
|
114
|
-
|
|
161
|
+
const options = this.resolveOptions();
|
|
162
|
+
const provider = options.provider;
|
|
163
|
+
const restInstance = this.rest.get(provider);
|
|
164
|
+
if (restInstance !== void 0) return restInstance;
|
|
115
165
|
if (provider === "deepseek-official") return this.deepseek;
|
|
116
|
-
|
|
166
|
+
// Fallback: provider points at something unknown → official default.
|
|
167
|
+
return this.deepseek;
|
|
117
168
|
}
|
|
118
169
|
}
|
|
119
170
|
|
|
120
171
|
/** Project one resolved section into options for each backend. */
|
|
121
172
|
function resolveOptions(ctx, config) {
|
|
122
|
-
const
|
|
173
|
+
const isBuiltIn = config.provider === "tavily" || config.provider === "brave" || config.provider === "deepseek-official" || BUILTIN_REST_IDS.includes(config.provider);
|
|
174
|
+
const provider = isBuiltIn ? config.provider : "deepseek-official";
|
|
175
|
+
// Per-provider literal key / optional endpoint override, used when the
|
|
176
|
+
// metadata row's default is not enough (self-hosted SearXNG, proxied
|
|
177
|
+
// providers, custom key refs).
|
|
178
|
+
const keySourceByProvider = {
|
|
179
|
+
brave: { literal: config.braveApiKey, envName: config.braveApiKeyEnv ?? BRAVE_DEFAULT_API_KEY_ENV, baseURL: config.braveBaseURL ?? "", proxy: config.proxy },
|
|
180
|
+
tavily: { literal: config.apiKey, envName: config.apiKeyEnv ?? TAVILY_DEFAULT_API_KEY_ENV, baseURL: config.baseURL ?? "" },
|
|
181
|
+
serper: { literal: config.serperApiKey, envName: config.serperApiKeyEnv ?? "SERPER_API_KEY", baseURL: config.serperBaseURL ?? "" },
|
|
182
|
+
serpapi: { literal: config.serpapiApiKey, envName: config.serpapiApiKeyEnv ?? "SERPAPI_API_KEY", baseURL: config.serpapiBaseURL ?? "" },
|
|
183
|
+
exa: { literal: config.exaApiKey, envName: config.exaApiKeyEnv ?? "EXA_API_KEY", baseURL: config.exaBaseURL ?? "" },
|
|
184
|
+
searxng: { baseURL: config.searxngBaseURL ?? "" },
|
|
185
|
+
scavio: { literal: config.scavioApiKey, envName: config.scavioApiKeyEnv ?? "SCAVIO_API_KEY", baseURL: config.scavioBaseURL ?? "" },
|
|
186
|
+
firecrawl: { literal: config.firecrawlApiKey, envName: config.firecrawlApiKeyEnv ?? "FIRECRAWL_API_KEY", baseURL: config.firecrawlBaseURL ?? "" }
|
|
187
|
+
};
|
|
188
|
+
const settings = {
|
|
189
|
+
country: config.country,
|
|
190
|
+
searchLang: config.searchLang,
|
|
191
|
+
freshness: config.freshness,
|
|
192
|
+
searchDepth: config.searchDepth,
|
|
193
|
+
topic: config.topic,
|
|
194
|
+
includeAnswer: config.includeAnswer
|
|
195
|
+
};
|
|
196
|
+
const rest = {};
|
|
197
|
+
for (const providerId of BUILTIN_REST_IDS) {
|
|
198
|
+
const row = findProvider(providerId);
|
|
199
|
+
if (row === void 0) continue;
|
|
200
|
+
const source = keySourceByProvider[providerId] ?? {};
|
|
201
|
+
const resolved = resolveRestProvider(ctx, row, {
|
|
202
|
+
config,
|
|
203
|
+
settings,
|
|
204
|
+
keyed: providerId === "tavily" ? config.mode === "keyed" : false,
|
|
205
|
+
keySource: source
|
|
206
|
+
});
|
|
207
|
+
// Endpoint override: an explicit config baseURL wins over the row default.
|
|
208
|
+
if (source.baseURL != null && source.baseURL.length > 0) resolved.baseURL = source.baseURL;
|
|
209
|
+
if (providerId === "brave" && (source.proxy ?? "").length > 0) resolved.proxy = source.proxy;
|
|
210
|
+
rest[providerId] = resolved;
|
|
211
|
+
}
|
|
123
212
|
return {
|
|
124
213
|
provider,
|
|
125
214
|
deepseek: resolveDeepseekOptions(ctx, config),
|
|
126
215
|
tavily: resolveTavilyOptions(ctx, config),
|
|
127
|
-
brave: resolveBraveOptions(ctx, config)
|
|
216
|
+
brave: resolveBraveOptions(ctx, config),
|
|
217
|
+
rest
|
|
128
218
|
};
|
|
129
219
|
}
|
|
130
220
|
|
|
221
|
+
/** JSON body helper for the usage refresh POST. */
|
|
222
|
+
function readJsonBody(req) {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
let body = "";
|
|
225
|
+
req.on("data", (chunk) => {
|
|
226
|
+
body += chunk;
|
|
227
|
+
if (body.length > 1e6) {
|
|
228
|
+
req.destroy();
|
|
229
|
+
reject(new Error("Payload too large"));
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
req.on("end", () => {
|
|
233
|
+
try {
|
|
234
|
+
resolve(body ? JSON.parse(body) : {});
|
|
235
|
+
} catch {
|
|
236
|
+
reject(new Error("Invalid JSON"));
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
req.on("error", reject);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function sendJson(res, statusCode, data) {
|
|
244
|
+
const body = JSON.stringify(data);
|
|
245
|
+
res.writeHead(statusCode, {
|
|
246
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
247
|
+
"Cache-Control": "no-store",
|
|
248
|
+
"Content-Length": Buffer.byteLength(body)
|
|
249
|
+
});
|
|
250
|
+
res.end(body);
|
|
251
|
+
}
|
|
252
|
+
|
|
131
253
|
/** Register the dispatched search provider with `ctx.web`. */
|
|
132
254
|
function apply(ctx, config) {
|
|
133
255
|
let current = () => config;
|
|
@@ -137,24 +259,85 @@ function apply(ctx, config) {
|
|
|
137
259
|
},
|
|
138
260
|
onChange: () => {}
|
|
139
261
|
});
|
|
140
|
-
|
|
262
|
+
const live = () => resolveOptions(ctx, current());
|
|
263
|
+
const usage = new UsageTracker({
|
|
264
|
+
getTavilyOptions: () => live().tavily
|
|
265
|
+
});
|
|
266
|
+
ctx.effect(() => {
|
|
267
|
+
let cancelled = false;
|
|
268
|
+
void usage.hydrate().then(() => {
|
|
269
|
+
if (cancelled) return;
|
|
270
|
+
if (live().provider === "tavily" && live().tavily.mode === "keyed") return usage.refreshTavily(undefined, true);
|
|
271
|
+
}).catch(() => {});
|
|
272
|
+
return () => {
|
|
273
|
+
cancelled = true;
|
|
274
|
+
usage.dispose();
|
|
275
|
+
};
|
|
276
|
+
}, "dsh-web-search-plugin: usage hydrate");
|
|
277
|
+
ctx.effect(() => {
|
|
278
|
+
const tick = () => {
|
|
279
|
+
if (live().provider === "tavily" && live().tavily.mode === "keyed") void usage.refreshTavily().catch(() => {});
|
|
280
|
+
};
|
|
281
|
+
const timer = setInterval(tick, TAVILY_USAGE_POLL_MS);
|
|
282
|
+
return () => clearInterval(timer);
|
|
283
|
+
}, "dsh-web-search-plugin: tavily usage poll");
|
|
284
|
+
ctx.web.registerSearchProvider(new PluginSearchProvider(live, usage));
|
|
285
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
286
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
287
|
+
kind: "exact",
|
|
288
|
+
path: "/dsh-web-search/usage",
|
|
289
|
+
async handler(req, res) {
|
|
290
|
+
if (req.method !== "GET" && req.method !== "HEAD" && req.method !== "POST") {
|
|
291
|
+
res.writeHead(405, { Allow: "GET, HEAD, POST" });
|
|
292
|
+
res.end();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
296
|
+
const force = req.method === "POST" || url.searchParams.get("force") === "1" || url.searchParams.get("force") === "true";
|
|
297
|
+
const options = live();
|
|
298
|
+
const tavilyState = usage.snapshot().tavily;
|
|
299
|
+
const missingIdentity = tavilyState.plan == null && tavilyState.limit == null;
|
|
300
|
+
if ((force || missingIdentity) && options.provider === "tavily" && options.tavily.mode === "keyed") {
|
|
301
|
+
try {
|
|
302
|
+
if (req.method === "POST") await readJsonBody(req).catch(() => ({}));
|
|
303
|
+
await usage.refreshTavily(undefined, force);
|
|
304
|
+
} catch {
|
|
305
|
+
/* serialize still returns the last cache */
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (req.method === "HEAD") {
|
|
309
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
|
|
310
|
+
res.end();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
sendJson(res, 200, usage.serialize(options.provider, options.tavily.mode));
|
|
314
|
+
}
|
|
315
|
+
}), "dsh-web-search-plugin: usage route");
|
|
316
|
+
});
|
|
141
317
|
}
|
|
142
318
|
|
|
143
319
|
export {
|
|
144
320
|
BRAVE_DEFAULT_API_KEY_ENV,
|
|
145
321
|
BRAVE_DEFAULT_BASE_URL,
|
|
322
|
+
BUILTIN_REST_IDS,
|
|
146
323
|
BraveSearchProvider,
|
|
147
324
|
Config,
|
|
148
325
|
DEEPSEEK_DEFAULT_API_KEY_ENV,
|
|
149
326
|
DEEPSEEK_DEFAULT_BASE_URL,
|
|
150
327
|
DeepSeekSearchProvider,
|
|
328
|
+
REST_PROVIDERS,
|
|
329
|
+
RestSearchProvider,
|
|
151
330
|
SEARCH_PROVIDER_ID,
|
|
152
331
|
TAVILY_DEFAULT_API_KEY_ENV,
|
|
153
332
|
TAVILY_DEFAULT_BASE_URL,
|
|
333
|
+
TAVILY_USAGE_POLL_MS,
|
|
154
334
|
TavilySearchProvider,
|
|
335
|
+
UsageTracker,
|
|
155
336
|
WEB_SEARCH_SETTINGS_NAMESPACE,
|
|
156
337
|
WEB_SEARCH_TAVILY_SETTINGS_NAMESPACE,
|
|
157
338
|
apply,
|
|
339
|
+
findProvider,
|
|
158
340
|
inject,
|
|
159
|
-
name
|
|
341
|
+
name,
|
|
342
|
+
resolveRestProvider
|
|
160
343
|
};
|
package/lib/providers.js
ADDED
|
@@ -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"];
|