From 0b41259f3952f9f27a704425f97bac64ed455194 Mon Sep 17 00:00:00 2001 From: 3g0r1ch Date: Sun, 23 Aug 2026 10:01:11 +0000 Subject: [PATCH] fix(search): fall back to duckduckgo-free when no search provider is configured (#11097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated on the combined batch board over tip 8a42aeeb: static gates clean (changelog, file-size 159 frozen, complexity 2621<=2774, cognitive 1181<=1223, dead-code 408<=416), typecheck:core clean, 107 focused tests green. Zero-credential /v1/search now actually reaches the fallback-only providers (duckduckgo-free/searxng) instead of erroring before the last-resort block could run — the dead-code path is live. Also carries a one-line suppressHydrationWarning on the logo. Thank you @Egorich-print! --- src/app/api/v1/search/route.ts | 32 +++++++++++++++++++++++-- src/shared/components/OmniRouteLogo.tsx | 1 + 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/app/api/v1/search/route.ts b/src/app/api/v1/search/route.ts index 313b72dece..ceb78643ec 100644 --- a/src/app/api/v1/search/route.ts +++ b/src/app/api/v1/search/route.ts @@ -56,7 +56,7 @@ export async function OPTIONS() { * GET /v1/search — list available search providers */ export async function GET() { - const settings = await getSettings().catch(() => ({} as any)); + const settings = await getSettings().catch(() => ({}) as any); const blockedProviders = settings?.blockedProviders || []; const providers = getAllSearchProviders(blockedProviders); const timestamp = Math.floor(Date.now() / 1000); @@ -139,7 +139,7 @@ async function postHandler(request: Request, context: unknown) { const policy = await enforceApiKeyPolicy(request, "search"); if (policy.rejection) return policy.rejection; - const settings = await getSettings().catch(() => ({} as any)); + const settings = await getSettings().catch(() => ({}) as any); const blockedProviders = settings?.blockedProviders || []; // Resolve provider and credentials @@ -247,6 +247,34 @@ async function postHandler(request: Request, context: unknown) { } } + // Last resort before failing: promote a fallback-only free provider (e.g. + // duckduckgo-free) to the primary pick so out-of-the-box search works when + // no credentialed provider is configured at all. + if (!credentials) { + const fallbackProviders = Object.values(SEARCH_PROVIDERS) + .filter( + (provider) => + provider.fallbackOnly && + supportsSearchType(provider, body.search_type) && + !isProviderBlockedByIdOrAlias(provider.id, blockedProviders) + ) + .sort((a, b) => a.costPerQuery - b.costPerQuery); + + for (const fallbackProvider of fallbackProviders) { + providerConfig = fallbackProvider; + if (fallbackProvider.id === "duckduckgo-free") { + credentials = {}; + break; + } + const fallbackCreds = await resolveSearchCredentials(fallbackProvider.id); + if (isAllRateLimitedCredentials(fallbackCreds)) continue; + if (fallbackCreds) { + credentials = fallbackCreds; + break; + } + } + } + if (!credentials) { if (firstRateLimitedCredentials) { return rateLimitedProviderResponse( diff --git a/src/shared/components/OmniRouteLogo.tsx b/src/shared/components/OmniRouteLogo.tsx index 32a09cb6fa..a6520fa680 100644 --- a/src/shared/components/OmniRouteLogo.tsx +++ b/src/shared/components/OmniRouteLogo.tsx @@ -16,6 +16,7 @@ export default function OmniRouteLogo({ size = 20, className = "" }: OmniRouteLo fill="none" xmlns="http://www.w3.org/2000/svg" className={className} + suppressHydrationWarning > {/* Central node */}