From ec7b3c3e88c3396d2b08c0b6a1750f4f77d4522a Mon Sep 17 00:00:00 2001 From: ly-wang19 Date: Tue, 2 Jun 2026 16:07:56 +0800 Subject: [PATCH] fix(web-search): allow keyless Brave as a server-side default Brave Search is `requiresApiKey: false` and already falls back to scraping its public results page when no key is set (`searchWithBrave`). But the server-side wiring never let it activate without a key: - `loadEnvSection` for web search was not given `keylessProviders`, so Brave could only be configured via `BRAVE_API_KEY`, defeating keyless mode. - `resolveServerWebSearchProviderId` only auto-selected providers with an `apiKey`, so keyless Brave was never chosen. Net effect: in the autonomous classroom-generation flow, `resolveClassroomWebSearchConfig` returned `undefined` and web search was silently disabled for operators who wanted free, keyless Brave. This mirrors the existing keyless handling for Ollama/Lemonade: set `BRAVE_BASE_URL` (e.g. https://search.brave.com) to enable keyless Brave, which then becomes a valid server default (after any keyed providers). Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 4 ++++ lib/server/provider-config.ts | 7 ++++++- tests/server/provider-config.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index cbf7b79edb..48ab37a05c 100644 --- a/.env.example +++ b/.env.example @@ -190,6 +190,10 @@ TAVILY_API_KEY= BOCHA_API_KEY= BOCHA_BASE_URL=https://api.bocha.cn BRAVE_API_KEY= +# Brave also works without a key by scraping its public results page. Set +# BRAVE_BASE_URL (e.g. https://search.brave.com) to enable Brave as a keyless +# server-side provider — no BRAVE_API_KEY required. +BRAVE_BASE_URL= BAIDU_API_KEY= BAIDU_BASE_URL=https://qianfan.baidubce.com # Dedicated MiniMax web-search vars avoid conflicting with the LLM MINIMAX_* endpoint. diff --git a/lib/server/provider-config.ts b/lib/server/provider-config.ts index 613830d475..e388d8d4d5 100644 --- a/lib/server/provider-config.ts +++ b/lib/server/provider-config.ts @@ -308,7 +308,9 @@ function buildConfig(yamlData: YamlData): ServerConfig { pdf: loadEnvSection(PDF_ENV_MAP, yamlData.pdf, { requiresBaseUrl: true }), image, video: loadEnvSection(VIDEO_ENV_MAP, yamlData.video), - webSearch: loadEnvSection(WEB_SEARCH_ENV_MAP, yamlData['web-search']), + webSearch: loadEnvSection(WEB_SEARCH_ENV_MAP, yamlData['web-search'], { + keylessProviders: new Set(['brave']), + }), ttsDisabled: collectDisabledTTS(yamlData.tts), }; } @@ -566,5 +568,8 @@ export function resolveServerWebSearchProviderId(preferredProviderId?: string): if (webSearch.bocha?.apiKey) return 'bocha'; if (webSearch.baidu?.apiKey) return 'baidu'; if (webSearch.minimax?.apiKey) return 'minimax'; + // Brave needs no API key (it scrapes the public results page), so a configured + // keyless Brave is a valid fallback even though it has no apiKey. + if (webSearch.brave) return 'brave'; return Object.keys(webSearch)[0]; } diff --git a/tests/server/provider-config.test.ts b/tests/server/provider-config.test.ts index 25067e7e6f..b5742a545f 100644 --- a/tests/server/provider-config.test.ts +++ b/tests/server/provider-config.test.ts @@ -315,6 +315,30 @@ providers: }); }); + describe('resolveServerWebSearchProviderId', () => { + it('selects keyless Brave when only BRAVE_BASE_URL is set (no API key)', async () => { + vi.stubEnv('BRAVE_BASE_URL', 'https://search.brave.com'); + const { + resolveServerWebSearchProviderId, + getServerWebSearchProviders, + resolveWebSearchBaseUrl, + } = await import('@/lib/server/provider-config'); + // Brave is activated as a keyless server provider via base URL alone. The + // exposed map shows only presence (managed flag), not the base URL (#624). + expect(getServerWebSearchProviders().brave).toEqual({}); + expect(resolveWebSearchBaseUrl('brave')).toBe('https://search.brave.com'); + // ...and Brave is then auto-selected as the server default. + expect(resolveServerWebSearchProviderId()).toBe('brave'); + }); + + it('prefers a keyed provider over keyless Brave', async () => { + vi.stubEnv('BRAVE_BASE_URL', 'https://search.brave.com'); + vi.stubEnv('TAVILY_API_KEY', 'tvly-key'); + const { resolveServerWebSearchProviderId } = await import('@/lib/server/provider-config'); + expect(resolveServerWebSearchProviderId()).toBe('tavily'); + }); + }); + describe('baseUrl-only providers (e.g. mineru)', () => { it('includes PDF provider from YAML when only baseUrl is configured (no apiKey)', async () => { yamlOverride = `