feat(web-search): add SearXNG as bundled web search provider plugin (#57317)

* feat(web-search): add bundled searxng plugin

* test(web-search): cover searxng config wiring

* test(web-search): include searxng in bundled provider inventory

* test(web-search): keep searxng ordering aligned

* fix(web-search): sanitize searxng result rows

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Charles Dusek
2026-04-01 05:24:33 -05:00
committed by GitHub
parent 3f67581e50
commit 32ae841098
12 changed files with 766 additions and 3 deletions

View File

@@ -7,13 +7,21 @@ vi.mock("../runtime.js", () => ({
const getScopedWebSearchCredential = (key: string) => (search?: Record<string, unknown>) =>
(search?.[key] as { apiKey?: unknown } | undefined)?.apiKey;
const getConfiguredPluginWebSearchCredential =
const getConfiguredPluginWebSearchConfig =
(pluginId: string) => (config?: Record<string, unknown>) =>
(
config?.plugins as
| { entries?: Record<string, { config?: { webSearch?: { apiKey?: unknown } } }> }
| {
entries?: Record<
string,
{ config?: { webSearch?: { apiKey?: unknown; baseUrl?: unknown } } }
>;
}
| undefined
)?.entries?.[pluginId]?.config?.webSearch?.apiKey;
)?.entries?.[pluginId]?.config?.webSearch;
const getConfiguredPluginWebSearchCredential =
(pluginId: string) => (config?: Record<string, unknown>) =>
getConfiguredPluginWebSearchConfig(pluginId)(config)?.apiKey;
const mockWebSearchProviders = [
{
@@ -58,6 +66,15 @@ const mockWebSearchProviders = [
getCredentialValue: getScopedWebSearchCredential("perplexity"),
getConfiguredCredentialValue: getConfiguredPluginWebSearchCredential("perplexity"),
},
{
id: "searxng",
envVars: ["SEARXNG_BASE_URL"],
credentialPath: "plugins.entries.searxng.config.webSearch.baseUrl",
getCredentialValue: (search?: Record<string, unknown>) =>
(search?.searxng as { baseUrl?: unknown } | undefined)?.baseUrl,
getConfiguredCredentialValue: (config?: Record<string, unknown>) =>
getConfiguredPluginWebSearchConfig("searxng")(config)?.baseUrl,
},
{
id: "tavily",
envVars: ["TAVILY_API_KEY"],
@@ -179,6 +196,24 @@ describe("web search provider config", () => {
expect(res.ok).toBe(true);
});
it("accepts searxng provider config on the plugin-owned path", () => {
const res = validateConfigObjectWithPlugins(
buildWebSearchProviderConfig({
enabled: true,
provider: "searxng",
providerConfig: {
baseUrl: {
source: "env",
provider: "default",
id: "SEARXNG_BASE_URL",
},
},
}),
);
expect(res.ok).toBe(true);
});
it("rejects legacy scoped Tavily config", () => {
const res = validateConfigObjectWithPlugins({
tools: {
@@ -261,6 +296,7 @@ describe("web search provider auto-detection", () => {
delete process.env.MOONSHOT_API_KEY;
delete process.env.PERPLEXITY_API_KEY;
delete process.env.OPENROUTER_API_KEY;
delete process.env.SEARXNG_BASE_URL;
delete process.env.TAVILY_API_KEY;
delete process.env.XAI_API_KEY;
delete process.env.KIMI_API_KEY;
@@ -296,6 +332,11 @@ describe("web search provider auto-detection", () => {
expect(resolveSearchProvider({})).toBe("firecrawl");
});
it("auto-detects searxng when only SEARXNG_BASE_URL is set", () => {
process.env.SEARXNG_BASE_URL = "http://localhost:8080";
expect(resolveSearchProvider({})).toBe("searxng");
});
it("auto-detects kimi when only KIMI_API_KEY is set", () => {
process.env.KIMI_API_KEY = "test-kimi-key"; // pragma: allowlist secret
expect(resolveSearchProvider({})).toBe("kimi");

View File

@@ -11,6 +11,7 @@ const EXPECTED_BUNDLED_WEB_SEARCH_PROVIDER_KEYS = [
"xai:grok",
"moonshot:kimi",
"perplexity:perplexity",
"searxng:searxng",
"tavily:tavily",
] as const;
const EXPECTED_BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS = [
@@ -22,6 +23,7 @@ const EXPECTED_BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS = [
"xai",
"moonshot",
"perplexity",
"searxng",
"tavily",
] as const;
const EXPECTED_BUNDLED_WEB_SEARCH_CREDENTIAL_PATHS = [
@@ -33,6 +35,7 @@ const EXPECTED_BUNDLED_WEB_SEARCH_CREDENTIAL_PATHS = [
"plugins.entries.xai.config.webSearch.apiKey",
"plugins.entries.moonshot.config.webSearch.apiKey",
"plugins.entries.perplexity.config.webSearch.apiKey",
"plugins.entries.searxng.config.webSearch.baseUrl",
"plugins.entries.tavily.config.webSearch.apiKey",
] as const;