@herbertgao/pi-extensions 2026.9.10 → 2026.9.11
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/README.md +1 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -2
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -2
- package/node_modules/@herbertgao/resume-from/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/README.md +3 -3
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
- package/node_modules/@herbertgao/sol-pi/agents-install.md +4 -4
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +6 -6
- package/node_modules/@herbertgao/sol-pi/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/dist/index.ts +39 -89
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -87
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +8 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +40 -37
- package/node_modules/@narumitw/pi-caffeinate/README.md +21 -66
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +10 -41
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +50 -51
- package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +637 -663
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +114 -120
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +29 -29
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +108 -126
- package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +124 -128
- package/node_modules/pi-multi-account/CHANGELOG.md +1209 -0
- package/node_modules/pi-multi-account/CONTRIBUTING.md +61 -0
- package/node_modules/pi-multi-account/LICENSE +21 -0
- package/node_modules/pi-multi-account/README.md +197 -0
- package/node_modules/pi-multi-account/SECURITY.md +27 -0
- package/node_modules/pi-multi-account/auth-file-transaction.ts +56 -0
- package/node_modules/pi-multi-account/child-usability.ts +233 -0
- package/node_modules/pi-multi-account/compaction-summary.ts +32 -0
- package/node_modules/pi-multi-account/completion-route-planner.ts +224 -0
- package/node_modules/pi-multi-account/context-guard.ts +420 -0
- package/node_modules/pi-multi-account/cursor/LICENSE +21 -0
- package/node_modules/pi-multi-account/cursor/NOTICE +2 -0
- package/node_modules/pi-multi-account/cursor/auth.ts +165 -0
- package/node_modules/pi-multi-account/cursor/bridge-handle.ts +155 -0
- package/node_modules/pi-multi-account/cursor/conversation-registry.ts +104 -0
- package/node_modules/pi-multi-account/cursor/cursor-models-raw.json +611 -0
- package/node_modules/pi-multi-account/cursor/cursor-shared.ts +192 -0
- package/node_modules/pi-multi-account/cursor/h2-bridge.mjs +175 -0
- package/node_modules/pi-multi-account/cursor/index.ts +572 -0
- package/node_modules/pi-multi-account/cursor/message-parsing.ts +323 -0
- package/node_modules/pi-multi-account/cursor/prompt-usage.ts +53 -0
- package/node_modules/pi-multi-account/cursor/proto/agent_pb.ts +15294 -0
- package/node_modules/pi-multi-account/cursor/proxy.ts +2510 -0
- package/node_modules/pi-multi-account/cursor/session-lifecycle.ts +40 -0
- package/node_modules/pi-multi-account/cursor/sse-keepalive.ts +24 -0
- package/node_modules/pi-multi-account/cursor/stream-lifecycle.ts +193 -0
- package/node_modules/pi-multi-account/cursor/upstream-watchdog.ts +88 -0
- package/node_modules/pi-multi-account/cursor-bridge.ts +240 -0
- package/node_modules/pi-multi-account/cursor-model-name.ts +12 -0
- package/node_modules/pi-multi-account/index.ts +11825 -0
- package/node_modules/pi-multi-account/model-catalog.ts +354 -0
- package/node_modules/pi-multi-account/package.json +101 -0
- package/node_modules/pi-multi-account/pi-contract.ts +281 -0
- package/node_modules/pi-multi-account/provider-payload-stream.ts +44 -0
- package/node_modules/pi-multi-account/provider-priority.ts +189 -0
- package/node_modules/pi-multi-account/slot-proxy-auth.ts +167 -0
- package/node_modules/pi-multi-account/slot-proxy.ts +344 -0
- package/node_modules/pi-multi-account/state-file-transaction.ts +67 -0
- package/node_modules/pi-multi-account/usage.ts +1099 -0
- package/node_modules/pi-typesafe/README.md +6 -2
- package/node_modules/pi-typesafe/dist/client.d.ts +11 -0
- package/node_modules/pi-typesafe/dist/client.js +45 -10
- package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/package.json +2 -2
- package/node_modules/pi-web-access/CHANGELOG.md +36 -0
- package/node_modules/pi-web-access/README.md +75 -18
- package/node_modules/pi-web-access/anysearch.ts +4 -15
- package/node_modules/pi-web-access/bocha.ts +3 -22
- package/node_modules/pi-web-access/brave.ts +3 -21
- package/node_modules/pi-web-access/brightdata.ts +5 -32
- package/node_modules/pi-web-access/content-find.ts +168 -53
- package/node_modules/pi-web-access/curator-page.ts +4 -1
- package/node_modules/pi-web-access/curator-run.ts +2 -1
- package/node_modules/pi-web-access/curator-server.ts +1 -0
- package/node_modules/pi-web-access/dist/index.js +24620 -0
- package/node_modules/pi-web-access/domain-filter-normalization.ts +14 -0
- package/node_modules/pi-web-access/duckduckgo.ts +3 -21
- package/node_modules/pi-web-access/extract.ts +3 -1
- package/node_modules/pi-web-access/firecrawl.ts +5 -29
- package/node_modules/pi-web-access/gemini-search.ts +81 -32
- package/node_modules/pi-web-access/index.ts +149 -148
- package/node_modules/pi-web-access/jina-search.ts +4 -15
- package/node_modules/pi-web-access/kagi.ts +4 -13
- package/node_modules/pi-web-access/kimi-search.ts +5 -30
- package/node_modules/pi-web-access/mistral-search.ts +1 -15
- package/node_modules/pi-web-access/ollama.ts +2 -7
- package/node_modules/pi-web-access/openai-search.ts +174 -36
- package/node_modules/pi-web-access/opencode-session-headers.ts +24 -0
- package/node_modules/pi-web-access/package.json +10 -4
- package/node_modules/pi-web-access/page-query.ts +10 -2
- package/node_modules/pi-web-access/parallel.ts +1 -15
- package/node_modules/pi-web-access/pdf-extract.ts +3 -0
- package/node_modules/pi-web-access/querit.ts +5 -29
- package/node_modules/pi-web-access/search-answer-formatting.ts +11 -0
- package/node_modules/pi-web-access/search-result-count-normalization.ts +4 -0
- package/node_modules/pi-web-access/search1api.ts +5 -29
- package/node_modules/pi-web-access/searchinfinity.ts +5 -29
- package/node_modules/pi-web-access/searxng.ts +3 -21
- package/node_modules/pi-web-access/serpapi.ts +5 -28
- package/node_modules/pi-web-access/serpbase.ts +3 -22
- package/node_modules/pi-web-access/serpdive.ts +3 -21
- package/node_modules/pi-web-access/serper.ts +5 -28
- package/node_modules/pi-web-access/serply.ts +197 -0
- package/node_modules/pi-web-access/source-check.ts +11 -47
- package/node_modules/pi-web-access/summary-review.ts +7 -3
- package/node_modules/pi-web-access/tavily.ts +3 -21
- package/node_modules/pi-web-access/tinyfish.ts +5 -29
- package/node_modules/pi-web-access/utils.ts +9 -1
- package/node_modules/pi-web-access/valyu.ts +5 -28
- package/node_modules/pi-web-access/xai-search.ts +1 -15
- package/node_modules/pi-web-access/xcrawl.ts +5 -32
- package/package.json +17 -11
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function normalizeDomain(value: string): string | null {
|
|
2
|
+
let input = value.trim().toLowerCase();
|
|
3
|
+
if (!input) return null;
|
|
4
|
+
if (input.startsWith("-")) input = input.slice(1).trim();
|
|
5
|
+
if (!input) return null;
|
|
6
|
+
try {
|
|
7
|
+
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
8
|
+
input = parsed.hostname;
|
|
9
|
+
} catch {
|
|
10
|
+
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
11
|
+
}
|
|
12
|
+
input = input.replace(/^\.+|\.+$/g, "");
|
|
13
|
+
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
14
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { activityMonitor } from "./activity.ts";
|
|
2
|
+
import { normalizeDomain } from "./domain-filter-normalization.ts";
|
|
2
3
|
import type { SearchOptions, SearchResult, SearchResponse } from "./perplexity.ts";
|
|
4
|
+
import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
|
|
3
5
|
|
|
4
6
|
const SEARCH_URL = "https://html.duckduckgo.com/html/";
|
|
5
7
|
const SEARCH_TIMEOUT_MS = 30_000;
|
|
@@ -9,26 +11,6 @@ interface NormalizedDomainFilters {
|
|
|
9
11
|
blocked: string[];
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
function normalizeCount(value: number | undefined): number {
|
|
13
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 5;
|
|
14
|
-
return Math.max(1, Math.min(Math.floor(value), 20));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizeDomain(value: string): string | null {
|
|
18
|
-
let input = value.trim().toLowerCase();
|
|
19
|
-
if (!input) return null;
|
|
20
|
-
if (input.startsWith("-")) input = input.slice(1).trim();
|
|
21
|
-
if (!input) return null;
|
|
22
|
-
try {
|
|
23
|
-
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
24
|
-
input = parsed.hostname;
|
|
25
|
-
} catch {
|
|
26
|
-
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
27
|
-
}
|
|
28
|
-
input = input.replace(/^\.+|\.+$/g, "");
|
|
29
|
-
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
14
|
function normalizeDomainFilters(domainFilter: string[] | undefined): NormalizedDomainFilters {
|
|
33
15
|
const filters: NormalizedDomainFilters = { allowed: [], blocked: [] };
|
|
34
16
|
for (const raw of domainFilter ?? []) {
|
|
@@ -103,7 +85,7 @@ export async function searchWithDuckDuckGo(query: string, options: SearchOptions
|
|
|
103
85
|
if (!matchesDomainFilters(resultUrl, filters)) continue;
|
|
104
86
|
const snippet = container.querySelector(".result__snippet")?.textContent?.trim() ?? "";
|
|
105
87
|
results.push({ title, url: resultUrl, snippet });
|
|
106
|
-
if (results.length >=
|
|
88
|
+
if (results.length >= normalizeSearchResultCount(options.numResults)) break;
|
|
107
89
|
}
|
|
108
90
|
if (parseableResults === 0) {
|
|
109
91
|
throw new Error("DuckDuckGo returned no parseable results (invalid response)");
|
|
@@ -1275,7 +1275,9 @@ async function extractViaHttp(
|
|
|
1275
1275
|
return {
|
|
1276
1276
|
url,
|
|
1277
1277
|
title: result.title,
|
|
1278
|
-
content:
|
|
1278
|
+
content: options?.mode === "answer"
|
|
1279
|
+
? result.content
|
|
1280
|
+
: `PDF extracted and saved to: ${result.outputPath}\n\nPages: ${result.pages}\nCharacters: ${result.chars}`,
|
|
1279
1281
|
error: null,
|
|
1280
1282
|
};
|
|
1281
1283
|
} catch (err) {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import net from "node:net";
|
|
3
3
|
import { activityMonitor } from "./activity.ts";
|
|
4
|
+
import { normalizeDomain } from "./domain-filter-normalization.ts";
|
|
4
5
|
import { redactCredential, resolveCredential } from "./credential-source.ts";
|
|
5
6
|
import type { ExtractedContent, ExtractOptions } from "./extract.ts";
|
|
6
7
|
import type { SearchOptions, SearchResponse } from "./perplexity.ts";
|
|
8
|
+
import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
|
|
9
|
+
import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
|
|
7
10
|
import { loadSsrfConfig, validateRemoteUrl, type Lookup } from "./ssrf-protection.ts";
|
|
8
11
|
import { getWebSearchConfigPath } from "./utils.ts";
|
|
9
12
|
|
|
@@ -242,26 +245,6 @@ function scrapeBody(url: string): Record<string, unknown> {
|
|
|
242
245
|
};
|
|
243
246
|
}
|
|
244
247
|
|
|
245
|
-
function normalizeCount(value: number | undefined): number {
|
|
246
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 5;
|
|
247
|
-
return Math.max(1, Math.min(Math.floor(value), 20));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function normalizeDomain(value: string): string | null {
|
|
251
|
-
let input = value.trim().toLowerCase();
|
|
252
|
-
if (!input) return null;
|
|
253
|
-
if (input.startsWith("-")) input = input.slice(1).trim();
|
|
254
|
-
if (!input) return null;
|
|
255
|
-
try {
|
|
256
|
-
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
257
|
-
input = parsed.hostname;
|
|
258
|
-
} catch {
|
|
259
|
-
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
260
|
-
}
|
|
261
|
-
input = input.replace(/^\.+|\.+$/g, "");
|
|
262
|
-
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
248
|
function parseDomainFilter(domainFilter: string[] | undefined): DomainFilters {
|
|
266
249
|
const filters: DomainFilters = { include: [], exclude: [] };
|
|
267
250
|
for (const raw of domainFilter ?? []) {
|
|
@@ -321,13 +304,6 @@ function firstString(...values: unknown[]): string | null {
|
|
|
321
304
|
return null;
|
|
322
305
|
}
|
|
323
306
|
|
|
324
|
-
function buildAnswer(results: SearchResponse["results"]): string {
|
|
325
|
-
return results.map((result) => {
|
|
326
|
-
if (result.snippet) return `${result.snippet}\nSource: ${result.title} (${result.url})`;
|
|
327
|
-
return `Source: ${result.title} (${result.url})`;
|
|
328
|
-
}).join("\n\n");
|
|
329
|
-
}
|
|
330
|
-
|
|
331
307
|
function mapSearchResults(data: unknown, numResults: number, filters: DomainFilters): {
|
|
332
308
|
results: SearchResponse["results"];
|
|
333
309
|
inlineContent: ExtractedContent[];
|
|
@@ -413,7 +389,7 @@ export function isFirecrawlAvailable(): boolean {
|
|
|
413
389
|
|
|
414
390
|
export async function searchWithFirecrawl(query: string, options: FirecrawlSearchOptions = {}): Promise<SearchResponse> {
|
|
415
391
|
requireBaseUrl();
|
|
416
|
-
const numResults =
|
|
392
|
+
const numResults = normalizeSearchResultCount(options.numResults);
|
|
417
393
|
const filters = parseDomainFilter(options.domainFilter);
|
|
418
394
|
const envelope = await firecrawlFetch(
|
|
419
395
|
"search",
|
|
@@ -424,7 +400,7 @@ export async function searchWithFirecrawl(query: string, options: FirecrawlSearc
|
|
|
424
400
|
{ type: "api", query },
|
|
425
401
|
);
|
|
426
402
|
const mapped = mapSearchResults(envelope.data, numResults, filters);
|
|
427
|
-
const response: SearchResponse = { answer:
|
|
403
|
+
const response: SearchResponse = { answer: formatSearchResultsAsAnswer(mapped.results), results: mapped.results };
|
|
428
404
|
if (options.includeContent && mapped.inlineContent.length > 0) response.inlineContent = mapped.inlineContent;
|
|
429
405
|
return response;
|
|
430
406
|
}
|
|
@@ -11,6 +11,7 @@ import { isBraveAvailable, searchWithBrave } from "./brave.ts";
|
|
|
11
11
|
import {
|
|
12
12
|
isCurrentModelHostedSearchEligible,
|
|
13
13
|
isOpenAISearchAvailable,
|
|
14
|
+
OpenAIAlphaSearchUnsupportedError,
|
|
14
15
|
searchWithCurrentModelOpenAI,
|
|
15
16
|
searchWithOpenAI,
|
|
16
17
|
} from "./openai-search.ts";
|
|
@@ -36,12 +37,13 @@ import { isBrightDataAvailable, searchWithBrightData } from "./brightdata.ts";
|
|
|
36
37
|
import { isSerpBaseAvailable, searchWithSerpBase } from "./serpbase.ts";
|
|
37
38
|
import { isSerpApiAvailable, searchWithSerpApi } from "./serpapi.ts";
|
|
38
39
|
import { isSerperAvailable, searchWithSerper } from "./serper.ts";
|
|
40
|
+
import { isSerplyAvailable, searchWithSerply } from "./serply.ts";
|
|
39
41
|
import { isValyuAvailable, searchWithValyu } from "./valyu.ts";
|
|
40
42
|
import { isKimiSearchAvailable, searchWithKimi } from "./kimi-search.ts";
|
|
41
43
|
import { isMistralAvailable, searchWithMistral } from "./mistral-search.ts";
|
|
42
44
|
import { getWebSearchConfigPath } from "./utils.ts";
|
|
43
45
|
|
|
44
|
-
export const RESOLVED_SEARCH_PROVIDERS = ["openai", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "exa", "serpdive", "kagi", "ollama", "anysearch", "xai", "mistral", "brightdata", "serpbase", "serpapi", "serper", "valyu", "bocha", "xcrawl"] as const;
|
|
46
|
+
export const RESOLVED_SEARCH_PROVIDERS = ["openai", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "exa", "serpdive", "kagi", "ollama", "anysearch", "xai", "mistral", "brightdata", "serpbase", "serpapi", "serper", "serply", "valyu", "bocha", "xcrawl"] as const;
|
|
45
47
|
export const SEARCH_PROVIDERS = ["auto", "all", ...RESOLVED_SEARCH_PROVIDERS] as const;
|
|
46
48
|
|
|
47
49
|
export type ResolvedSearchProvider = typeof RESOLVED_SEARCH_PROVIDERS[number];
|
|
@@ -106,7 +108,7 @@ export interface AttributedSearchResponse extends SearchResponse {
|
|
|
106
108
|
|
|
107
109
|
const CONFIG_PATH = getWebSearchConfigPath();
|
|
108
110
|
const DEFAULT_SEARCH_MODEL = "gemini-3.6-flash";
|
|
109
|
-
// Explicit-only providers (Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, Valyu) are deliberately absent:
|
|
111
|
+
// Explicit-only providers (Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, Serply, Valyu) are deliberately absent:
|
|
110
112
|
// `all` must never fan out to an opt-in or paid provider without the user asking for it.
|
|
111
113
|
export const ALL_SEARCH_PROVIDERS: ResolvedSearchProvider[] = ["searxng", "openai", "exa", "brave", "parallel", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "serpdive", "kagi", "ollama", "perplexity", "gemini", "bocha"];
|
|
112
114
|
const VALID_ROUTING_KINDS = ["transient", "quota", "network", "invalid-response", "unsupported"] as const;
|
|
@@ -116,6 +118,7 @@ type SearchConfig = {
|
|
|
116
118
|
searchProviderConfigured: boolean;
|
|
117
119
|
searchRouting?: SearchRoutingConfig;
|
|
118
120
|
searchModel?: string;
|
|
121
|
+
allowedProviders?: ResolvedSearchProvider[];
|
|
119
122
|
};
|
|
120
123
|
|
|
121
124
|
let cachedSearchConfig: SearchConfig | null = null;
|
|
@@ -141,12 +144,31 @@ function getSearchConfig(): SearchConfig {
|
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
const searchModel = normalizeSearchModel(raw.searchModel);
|
|
147
|
+
const webSearch = raw.webSearch;
|
|
148
|
+
if (webSearch !== undefined && (!webSearch || typeof webSearch !== "object" || Array.isArray(webSearch))) {
|
|
149
|
+
throw new Error(`webSearch in ${CONFIG_PATH} must be an object`);
|
|
150
|
+
}
|
|
151
|
+
const allowedProviders = webSearch && Object.hasOwn(webSearch, "allowedProviders")
|
|
152
|
+
? normalizeResolvedProviderList((webSearch as Record<string, unknown>).allowedProviders, `webSearch.allowedProviders in ${CONFIG_PATH}`)
|
|
153
|
+
: undefined;
|
|
144
154
|
const searchProviderConfigured = Object.hasOwn(raw, "searchProvider") || Object.hasOwn(raw, "provider");
|
|
155
|
+
const searchProvider = normalizeSearchProviderSelection(raw.searchProvider ?? raw.provider, `provider in ${CONFIG_PATH}`);
|
|
156
|
+
const searchRouting = Object.hasOwn(raw, "searchRouting") ? normalizeSearchRouting(raw.searchRouting) : undefined;
|
|
157
|
+
if (allowedProviders) {
|
|
158
|
+
if (Object.hasOwn(raw, "searchProvider")) {
|
|
159
|
+
assertSearchProviderSelectionAllowed(normalizeSearchProviderSelection(raw.searchProvider), `searchProvider in ${CONFIG_PATH}`, allowedProviders);
|
|
160
|
+
}
|
|
161
|
+
if (Object.hasOwn(raw, "provider")) {
|
|
162
|
+
assertSearchProviderSelectionAllowed(normalizeSearchProviderSelection(raw.provider), `provider in ${CONFIG_PATH}`, allowedProviders);
|
|
163
|
+
}
|
|
164
|
+
if (searchRouting) assertSearchProviderSelectionAllowed(searchRouting.providers, `searchRouting.providers in ${CONFIG_PATH}`, allowedProviders);
|
|
165
|
+
}
|
|
145
166
|
cachedSearchConfig = {
|
|
146
|
-
searchProvider
|
|
167
|
+
searchProvider,
|
|
147
168
|
searchProviderConfigured,
|
|
148
|
-
...(
|
|
169
|
+
...(searchRouting ? { searchRouting } : {}),
|
|
149
170
|
...(searchModel ? { searchModel } : {}),
|
|
171
|
+
...(allowedProviders ? { allowedProviders } : {}),
|
|
150
172
|
};
|
|
151
173
|
return cachedSearchConfig;
|
|
152
174
|
}
|
|
@@ -209,6 +231,25 @@ function normalizeResolvedProviderList(value: unknown, label: string): ResolvedS
|
|
|
209
231
|
return providers;
|
|
210
232
|
}
|
|
211
233
|
|
|
234
|
+
export function assertSearchProviderSelectionAllowed(
|
|
235
|
+
selection: SearchProviderSelection,
|
|
236
|
+
label = "provider",
|
|
237
|
+
allowedProviders = getSearchConfig().allowedProviders,
|
|
238
|
+
): void {
|
|
239
|
+
if (!allowedProviders) return;
|
|
240
|
+
const requested = Array.isArray(selection)
|
|
241
|
+
? selection
|
|
242
|
+
: selection === "auto" || selection === "all" ? [] : [selection];
|
|
243
|
+
const disabled = requested.filter(provider => !allowedProviders.includes(provider));
|
|
244
|
+
if (disabled.length > 0) {
|
|
245
|
+
throw new Error(`${label} ${disabled.length === 1 ? `references disabled provider "${disabled[0]}"` : `references disabled providers: ${disabled.join(", ")}`}; allowed by webSearch.allowedProviders: ${allowedProviders.join(", ")}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function getAllowedSearchProviders(): readonly ResolvedSearchProvider[] {
|
|
250
|
+
return getSearchConfig().allowedProviders ?? RESOLVED_SEARCH_PROVIDERS;
|
|
251
|
+
}
|
|
252
|
+
|
|
212
253
|
export function normalizeSearchProviderSelection(value: unknown, label = "provider"): SearchProviderSelection {
|
|
213
254
|
if (Array.isArray(value)) return normalizeResolvedProviderList(value, label);
|
|
214
255
|
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
@@ -304,6 +345,8 @@ function classifyProviderError(provider: ResolvedSearchProvider, err: unknown):
|
|
|
304
345
|
kind = "credential";
|
|
305
346
|
} else if (isAbortError(err)) {
|
|
306
347
|
kind = "aborted";
|
|
348
|
+
} else if (err instanceof OpenAIAlphaSearchUnsupportedError) {
|
|
349
|
+
kind = "unsupported";
|
|
307
350
|
} else if (provider === "xai" && status === 403 && /spending[- ]limit|(?:no|out of) credits?|insufficient quota|quota (?:exceeded|exhausted)|credits? (?:exhausted|depleted|used up)/.test(lower)) {
|
|
308
351
|
kind = "quota";
|
|
309
352
|
} else if (status === 401 || status === 403) {
|
|
@@ -339,7 +382,7 @@ async function searchWithResolvedProvider(
|
|
|
339
382
|
query: string,
|
|
340
383
|
options: FullSearchOptions,
|
|
341
384
|
useCurrentModel = false,
|
|
342
|
-
): Promise<
|
|
385
|
+
): Promise<ProviderSearchResponse> {
|
|
343
386
|
if (provider === "openai") {
|
|
344
387
|
const result = useCurrentModel
|
|
345
388
|
? await searchWithCurrentModelOpenAI(query, options, options.extensionContext)
|
|
@@ -367,6 +410,7 @@ async function searchWithResolvedProvider(
|
|
|
367
410
|
if (provider === "serpbase") return { ...(await searchWithSerpBase(query, options)), provider };
|
|
368
411
|
if (provider === "serpapi") return { ...(await searchWithSerpApi(query, options)), provider };
|
|
369
412
|
if (provider === "serper") return { ...(await searchWithSerper(query, options)), provider };
|
|
413
|
+
if (provider === "serply") return { ...(await searchWithSerply(query, options)), provider };
|
|
370
414
|
if (provider === "valyu") return { ...(await searchWithValyu(query, options)), provider };
|
|
371
415
|
if (provider === "xcrawl") return { ...(await searchWithXCrawl(query, options)), provider };
|
|
372
416
|
if (provider === "perplexity") return { ...(await searchWithPerplexity(query, options)), provider };
|
|
@@ -416,6 +460,7 @@ async function isResolvedProviderAvailable(provider: ResolvedSearchProvider, opt
|
|
|
416
460
|
if (provider === "serpbase") return isSerpBaseAvailable();
|
|
417
461
|
if (provider === "serpapi") return isSerpApiAvailable();
|
|
418
462
|
if (provider === "serper") return isSerperAvailable();
|
|
463
|
+
if (provider === "serply") return isSerplyAvailable();
|
|
419
464
|
if (provider === "valyu") return isValyuAvailable();
|
|
420
465
|
if (provider === "xcrawl") return isXcrawlAvailable();
|
|
421
466
|
if (provider === "perplexity") return isPerplexityAvailable();
|
|
@@ -434,7 +479,7 @@ async function isGeminiWebOptionallyAvailable(): Promise<boolean> {
|
|
|
434
479
|
}
|
|
435
480
|
}
|
|
436
481
|
|
|
437
|
-
function providerLabel(provider: ResolvedSearchProvider): string {
|
|
482
|
+
export function providerLabel(provider: ResolvedSearchProvider): string {
|
|
438
483
|
if (provider === "openai") return "OpenAI";
|
|
439
484
|
if (provider === "parallel-mcp") return "Parallel MCP";
|
|
440
485
|
if (provider === "tinyfish") return "TinyFish";
|
|
@@ -456,6 +501,7 @@ function providerLabel(provider: ResolvedSearchProvider): string {
|
|
|
456
501
|
if (provider === "serpbase") return "SerpBase";
|
|
457
502
|
if (provider === "serpapi") return "SerpApi";
|
|
458
503
|
if (provider === "serper") return "Serper";
|
|
504
|
+
if (provider === "serply") return "Serply";
|
|
459
505
|
if (provider === "valyu") return "Valyu";
|
|
460
506
|
return provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
461
507
|
}
|
|
@@ -464,7 +510,7 @@ async function searchWithAllProvider(
|
|
|
464
510
|
provider: ResolvedSearchProvider,
|
|
465
511
|
query: string,
|
|
466
512
|
options: FullSearchOptions,
|
|
467
|
-
): Promise<
|
|
513
|
+
): Promise<ProviderSearchResponse> {
|
|
468
514
|
if (provider !== "gemini") return searchWithResolvedProvider(provider, query, options);
|
|
469
515
|
const result = await searchWithGeminiApi(query, options);
|
|
470
516
|
if (result) return { ...result, provider };
|
|
@@ -476,14 +522,15 @@ async function searchWithProviders(
|
|
|
476
522
|
options: FullSearchOptions,
|
|
477
523
|
selectedProviders?: ResolvedSearchProvider[],
|
|
478
524
|
): Promise<AttributedSearchResponse> {
|
|
479
|
-
const
|
|
525
|
+
const allowed = getAllowedSearchProviders();
|
|
526
|
+
const providers = selectedProviders ?? (await Promise.all(ALL_SEARCH_PROVIDERS.filter(provider => allowed.includes(provider)).map(async (provider) => ({
|
|
480
527
|
provider,
|
|
481
528
|
available: provider === "gemini"
|
|
482
529
|
? isGeminiApiAvailable()
|
|
483
530
|
: await isResolvedProviderAvailable(provider, options),
|
|
484
531
|
})))).filter((entry) => entry.available).map((entry) => entry.provider);
|
|
485
532
|
if (providers.length === 0) {
|
|
486
|
-
throw new Error("No configured search provider available for provider \"all\". Parallel MCP, DuckDuckGo, Kimi, AnySearch, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, Valyu, and XCrawl are excluded.");
|
|
533
|
+
throw new Error("No configured search provider available for provider \"all\". Parallel MCP, DuckDuckGo, Kimi, AnySearch, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, Serply, Valyu, and XCrawl are excluded.");
|
|
487
534
|
}
|
|
488
535
|
|
|
489
536
|
const settled = await Promise.allSettled(
|
|
@@ -493,7 +540,7 @@ async function searchWithProviders(
|
|
|
493
540
|
);
|
|
494
541
|
if (options.signal?.aborted) throw new Error("Aborted");
|
|
495
542
|
|
|
496
|
-
const successes:
|
|
543
|
+
const successes: ProviderSearchResponse[] = [];
|
|
497
544
|
const failures: Array<{ provider: ResolvedSearchProvider; error: string }> = [];
|
|
498
545
|
for (let index = 0; index < settled.length; index++) {
|
|
499
546
|
const outcome = settled[index];
|
|
@@ -526,7 +573,7 @@ async function searchWithProviders(
|
|
|
526
573
|
}
|
|
527
574
|
|
|
528
575
|
const answerSections = successes.map((response) =>
|
|
529
|
-
`## ${providerLabel(response.provider
|
|
576
|
+
`## ${providerLabel(response.provider)}\n\n${response.answer || "(No answer text returned.)"}`
|
|
530
577
|
);
|
|
531
578
|
if (failures.length > 0) {
|
|
532
579
|
answerSections.push(
|
|
@@ -538,7 +585,7 @@ async function searchWithProviders(
|
|
|
538
585
|
provider: "all",
|
|
539
586
|
answer: answerSections.join("\n\n"),
|
|
540
587
|
results,
|
|
541
|
-
providerResponses: successes
|
|
588
|
+
providerResponses: successes,
|
|
542
589
|
...(failures.length > 0 ? { providerErrors: failures } : {}),
|
|
543
590
|
...(inlineContent.length > 0 ? { inlineContent } : {}),
|
|
544
591
|
};
|
|
@@ -574,6 +621,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
574
621
|
const provider = options.provider === undefined || options.provider === "auto"
|
|
575
622
|
? config.searchProvider
|
|
576
623
|
: options.provider;
|
|
624
|
+
assertSearchProviderSelectionAllowed(provider, "Requested provider");
|
|
577
625
|
if (Array.isArray(provider)) {
|
|
578
626
|
return searchWithProviders(query, options, normalizeResolvedProviderList(provider, "provider"));
|
|
579
627
|
}
|
|
@@ -584,8 +632,9 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
584
632
|
}
|
|
585
633
|
|
|
586
634
|
const fallbackErrors: string[] = [];
|
|
635
|
+
const allowed = new Set(config.allowedProviders ?? RESOLVED_SEARCH_PROVIDERS);
|
|
587
636
|
|
|
588
|
-
if (isSearXNGAvailable()) {
|
|
637
|
+
if (allowed.has("searxng") && isSearXNGAvailable()) {
|
|
589
638
|
try {
|
|
590
639
|
const result = await searchWithSearXNG(query, options);
|
|
591
640
|
return { ...result, provider: "searxng" };
|
|
@@ -596,13 +645,13 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
596
645
|
}
|
|
597
646
|
|
|
598
647
|
let triedOpenAI = false;
|
|
599
|
-
if (!options.extensionContext || isOpenAICodexSelected(options.extensionContext)) {
|
|
648
|
+
if (allowed.has("openai") && (!options.extensionContext || isOpenAICodexSelected(options.extensionContext))) {
|
|
600
649
|
triedOpenAI = true;
|
|
601
650
|
const result = await tryOpenAIInAuto(query, options, fallbackErrors);
|
|
602
651
|
if (result) return result;
|
|
603
652
|
}
|
|
604
653
|
|
|
605
|
-
if (isExaAvailable()) {
|
|
654
|
+
if (allowed.has("exa") && isExaAvailable()) {
|
|
606
655
|
try {
|
|
607
656
|
const result = await searchWithExa(query, options);
|
|
608
657
|
if (result) return { ...result, provider: "exa" };
|
|
@@ -612,12 +661,12 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
612
661
|
}
|
|
613
662
|
}
|
|
614
663
|
|
|
615
|
-
if (!triedOpenAI) {
|
|
664
|
+
if (allowed.has("openai") && !triedOpenAI) {
|
|
616
665
|
const result = await tryOpenAIInAuto(query, options, fallbackErrors);
|
|
617
666
|
if (result) return result;
|
|
618
667
|
}
|
|
619
668
|
|
|
620
|
-
if (isBraveAvailable()) {
|
|
669
|
+
if (allowed.has("brave") && isBraveAvailable()) {
|
|
621
670
|
try {
|
|
622
671
|
const result = await searchWithBrave(query, options);
|
|
623
672
|
return { ...result, provider: "brave" };
|
|
@@ -627,7 +676,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
627
676
|
}
|
|
628
677
|
}
|
|
629
678
|
|
|
630
|
-
if (isParallelAvailable()) {
|
|
679
|
+
if (allowed.has("parallel") && isParallelAvailable()) {
|
|
631
680
|
try {
|
|
632
681
|
const result = await searchWithParallel(query, options);
|
|
633
682
|
return { ...result, provider: "parallel" };
|
|
@@ -637,7 +686,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
637
686
|
}
|
|
638
687
|
}
|
|
639
688
|
|
|
640
|
-
if (isTinyFishAvailable()) {
|
|
689
|
+
if (allowed.has("tinyfish") && isTinyFishAvailable()) {
|
|
641
690
|
try {
|
|
642
691
|
const result = await searchWithTinyFish(query, options);
|
|
643
692
|
return { ...result, provider: "tinyfish" };
|
|
@@ -647,7 +696,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
647
696
|
}
|
|
648
697
|
}
|
|
649
698
|
|
|
650
|
-
if (isSearch1APIAvailable()) {
|
|
699
|
+
if (allowed.has("search1api") && isSearch1APIAvailable()) {
|
|
651
700
|
try {
|
|
652
701
|
const result = await searchWithSearch1API(query, options);
|
|
653
702
|
return { ...result, provider: "search1api" };
|
|
@@ -657,7 +706,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
657
706
|
}
|
|
658
707
|
}
|
|
659
708
|
|
|
660
|
-
if (isSearchinfinityAvailable()) {
|
|
709
|
+
if (allowed.has("searchinfinity") && isSearchinfinityAvailable()) {
|
|
661
710
|
try {
|
|
662
711
|
const result = await searchWithSearchinfinity(query, options);
|
|
663
712
|
return { ...result, provider: "searchinfinity" };
|
|
@@ -667,7 +716,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
667
716
|
}
|
|
668
717
|
}
|
|
669
718
|
|
|
670
|
-
if (isQueritAvailable()) {
|
|
719
|
+
if (allowed.has("querit") && isQueritAvailable()) {
|
|
671
720
|
try {
|
|
672
721
|
const result = await searchWithQuerit(query, options);
|
|
673
722
|
return { ...result, provider: "querit" };
|
|
@@ -677,7 +726,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
677
726
|
}
|
|
678
727
|
}
|
|
679
728
|
|
|
680
|
-
if (isTavilyAvailable()) {
|
|
729
|
+
if (allowed.has("tavily") && isTavilyAvailable()) {
|
|
681
730
|
try {
|
|
682
731
|
const result = await searchWithTavily(query, options);
|
|
683
732
|
return { ...result, provider: "tavily" };
|
|
@@ -687,7 +736,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
687
736
|
}
|
|
688
737
|
}
|
|
689
738
|
|
|
690
|
-
if (isFirecrawlAvailable()) {
|
|
739
|
+
if (allowed.has("firecrawl") && isFirecrawlAvailable()) {
|
|
691
740
|
try {
|
|
692
741
|
const result = await searchWithFirecrawl(query, options);
|
|
693
742
|
return { ...result, provider: "firecrawl" };
|
|
@@ -697,7 +746,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
697
746
|
}
|
|
698
747
|
}
|
|
699
748
|
|
|
700
|
-
if (isJinaSearchAvailable()) {
|
|
749
|
+
if (allowed.has("jina") && isJinaSearchAvailable()) {
|
|
701
750
|
try {
|
|
702
751
|
const result = await searchWithJina(query, options);
|
|
703
752
|
return { ...result, provider: "jina" };
|
|
@@ -707,7 +756,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
707
756
|
}
|
|
708
757
|
}
|
|
709
758
|
|
|
710
|
-
if (isSerpdiveAvailable()) {
|
|
759
|
+
if (allowed.has("serpdive") && isSerpdiveAvailable()) {
|
|
711
760
|
try {
|
|
712
761
|
const result = await searchWithSerpdive(query, options);
|
|
713
762
|
return { ...result, provider: "serpdive" };
|
|
@@ -717,7 +766,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
717
766
|
}
|
|
718
767
|
}
|
|
719
768
|
|
|
720
|
-
if (isKagiAvailable()) {
|
|
769
|
+
if (allowed.has("kagi") && isKagiAvailable()) {
|
|
721
770
|
try {
|
|
722
771
|
const result = await searchWithKagi(query, options);
|
|
723
772
|
return { ...result, provider: "kagi" };
|
|
@@ -727,7 +776,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
727
776
|
}
|
|
728
777
|
}
|
|
729
778
|
|
|
730
|
-
if (isBochaAvailable()) {
|
|
779
|
+
if (allowed.has("bocha") && isBochaAvailable()) {
|
|
731
780
|
try {
|
|
732
781
|
const result = await searchWithBocha(query, options);
|
|
733
782
|
return { ...result, provider: "bocha" };
|
|
@@ -737,7 +786,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
737
786
|
}
|
|
738
787
|
}
|
|
739
788
|
|
|
740
|
-
if (isOllamaAvailable()) {
|
|
789
|
+
if (allowed.has("ollama") && isOllamaAvailable()) {
|
|
741
790
|
try {
|
|
742
791
|
const result = await searchWithOllama(query, options);
|
|
743
792
|
return { ...result, provider: "ollama" };
|
|
@@ -747,7 +796,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
747
796
|
}
|
|
748
797
|
}
|
|
749
798
|
|
|
750
|
-
if (isPerplexityAvailable()) {
|
|
799
|
+
if (allowed.has("perplexity") && isPerplexityAvailable()) {
|
|
751
800
|
try {
|
|
752
801
|
const result = await searchWithPerplexity(query, options);
|
|
753
802
|
return { ...result, provider: "perplexity" };
|
|
@@ -757,7 +806,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
757
806
|
}
|
|
758
807
|
}
|
|
759
808
|
|
|
760
|
-
try {
|
|
809
|
+
if (allowed.has("gemini")) try {
|
|
761
810
|
const geminiResult = await searchWithGemini(query, options, false);
|
|
762
811
|
if (geminiResult) return { ...geminiResult, provider: "gemini" };
|
|
763
812
|
} catch (err) {
|
|
@@ -776,7 +825,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
776
825
|
" 3. Set OPENAI_API_KEY, BRAVE_API_KEY, PARALLEL_API_KEY, TINYFISH_API_KEY, SEARCH1API_KEY, SEARCHINFINITY_API_KEY, QUERIT_API_KEY, TAVILY_API_KEY, FIRECRAWL_BASE_URL, JINA_API_KEY, SERPDIVE_API_KEY, KAGI_API_KEY, BOCHA_API_KEY, OLLAMA_API_KEY, SEARXNG_BASE_URL, EXA_API_KEY, PERPLEXITY_API_KEY, GEMINI_API_KEY, or CLOUDFLARE_API_KEY env vars\n" +
|
|
777
826
|
" 4. Set GOOGLE_GEMINI_BASE_URL with CLOUDFLARE_API_KEY for Cloudflare AI Gateway routing\n" +
|
|
778
827
|
" 5. Sign into gemini.google.com in a supported Chromium-based browser\n" +
|
|
779
|
-
" 6. Explicitly select provider: \"anysearch\" for anonymous AnySearch, \"xcrawl\" for XCrawl, \"xai\" for Grok, \"mistral\" for Mistral Conversations web search, \"brightdata\" with brightdataSerpZone for paid Bright Data SERP, \"serpbase\", \"serpapi\", or \"
|
|
828
|
+
" 6. Explicitly select provider: \"anysearch\" for anonymous AnySearch, \"xcrawl\" for XCrawl, \"xai\" for Grok, \"mistral\" for Mistral Conversations web search, \"brightdata\" with brightdataSerpZone for paid Bright Data SERP, \"serpbase\", \"serpapi\", \"serper\", or \"serply\" for Google SERP, or \"valyu\" for research search"
|
|
780
829
|
);
|
|
781
830
|
}
|
|
782
831
|
|