From e79f9eddb483bf59db1a92432e12bfa95d0933cb Mon Sep 17 00:00:00 2001 From: zie Date: Fri, 10 Jul 2026 11:27:30 +0700 Subject: [PATCH] feat(searxng): configure endpoint via SEARXNG_URL env (#2499) Add SEARXNG_URL runtime override for the built-in SearXNG web-search provider, defaulting to http://localhost:8888/search. Enables Docker and remote SearXNG deployments without changing existing behavior. Co-authored-by: Cursor --- .env.example | 3 +++ README.md | 1 + open-sse/config/runtimeConfig.js | 9 +++++++ open-sse/providers/registry/searxng.js | 4 ++- tests/unit/searxng-provider-config.test.js | 30 ++++++++++++++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/unit/searxng-provider-config.test.js diff --git a/.env.example b/.env.example index afac229a..6ed8f81e 100644 --- a/.env.example +++ b/.env.example @@ -34,5 +34,8 @@ NEXT_PUBLIC_CLOUD_URL=https://9router.com # ALL_PROXY=socks5://127.0.0.1:7890 # NO_PROXY=localhost,127.0.0.1 +# Optional SearXNG endpoint for the built-in unauthenticated web-search provider. +# SEARXNG_URL=http://searxng:8080/search + # Currently unused by application runtime (kept as reference) # INSTANCE_NAME=9router diff --git a/README.md b/README.md index 1d5eaa85..b6cd76b8 100644 --- a/README.md +++ b/README.md @@ -1220,6 +1220,7 @@ docker pull decolua/9router:latest # update to latest | `AUTH_COOKIE_SECURE` | `false` | Force `Secure` auth cookie (set `true` behind HTTPS reverse proxy) | | `REQUIRE_API_KEY` | `false` | Enforce Bearer API key on `/v1/*` routes (recommended for internet-exposed deploys) | | `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` | empty | Optional outbound proxy for upstream provider calls | +| `SEARXNG_URL` | `http://localhost:8888/search` | Endpoint for the built-in unauthenticated SearXNG web-search provider | Notes: diff --git a/open-sse/config/runtimeConfig.js b/open-sse/config/runtimeConfig.js index de199233..5cd5ba78 100644 --- a/open-sse/config/runtimeConfig.js +++ b/open-sse/config/runtimeConfig.js @@ -39,6 +39,15 @@ function envMs(name, def) { return Number.isFinite(n) && n > 0 ? n : def; } +function envUrl(name, def) { + const raw = process.env[name]?.trim(); + return raw || def; +} + +// SearXNG endpoint used by the unauthenticated web-search provider. +// Configure this for a separate Docker service or remote SearXNG instance. +export const SEARXNG_URL = envUrl("SEARXNG_URL", "http://localhost:8888/search"); + // Inter-chunk stall timeout (once tokens are flowing). Generous headroom so // slow reasoning models aren't aborted mid-stream. Env: STREAM_STALL_TIMEOUT_MS. export const STREAM_STALL_TIMEOUT_MS = envMs("STREAM_STALL_TIMEOUT_MS", 360 * 1000); diff --git a/open-sse/providers/registry/searxng.js b/open-sse/providers/registry/searxng.js index 308eabbc..bfbe4fdb 100644 --- a/open-sse/providers/registry/searxng.js +++ b/open-sse/providers/registry/searxng.js @@ -1,3 +1,5 @@ +import { SEARXNG_URL } from "../../config/runtimeConfig.js"; + export default { id: "searxng", alias: "searxng", @@ -15,7 +17,7 @@ export default { ], noAuth: true, searchConfig: { - baseUrl: "http://localhost:8888/search", + baseUrl: SEARXNG_URL, method: "GET", authType: "none", authHeader: "none", diff --git a/tests/unit/searxng-provider-config.test.js b/tests/unit/searxng-provider-config.test.js new file mode 100644 index 00000000..18ab3359 --- /dev/null +++ b/tests/unit/searxng-provider-config.test.js @@ -0,0 +1,30 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const originalSearxngUrl = process.env.SEARXNG_URL; + +async function loadProvider(url) { + if (url === undefined) delete process.env.SEARXNG_URL; + else process.env.SEARXNG_URL = url; + vi.resetModules(); + return (await import("../../open-sse/providers/registry/searxng.js")).default; +} + +afterEach(() => { + if (originalSearxngUrl === undefined) delete process.env.SEARXNG_URL; + else process.env.SEARXNG_URL = originalSearxngUrl; + vi.resetModules(); +}); + +describe("SearXNG provider configuration", () => { + it("uses SEARXNG_URL when the deployment config supplies one", async () => { + const provider = await loadProvider("http://searxng:8080/search"); + + expect(provider.searchConfig.baseUrl).toBe("http://searxng:8080/search"); + }); + + it("preserves the loopback default when SEARXNG_URL is unset", async () => { + const provider = await loadProvider(undefined); + + expect(provider.searchConfig.baseUrl).toBe("http://localhost:8888/search"); + }); +});