From 37d784b0fea0f1839b149b6e152a96e2cacb2827 Mon Sep 17 00:00:00 2001 From: Xiangzhe Date: Tue, 25 Aug 2026 19:18:25 -0300 Subject: [PATCH] fix(search): prefer the configured search connection over duckduckgo-free When no explicit provider is requested and the auto-selected cheapest provider has no credentials, executeWebSearch ran the fallbackOnly loop first. duckduckgo-free (costPerQuery 0, authType none) always won there with an empty credentials object, so a configured paid connection such as serper-search was silently ignored and the caller got success:true with zero results. Move the sweep for other credentialed regular providers ahead of the fallbackOnly loop (and exclude fallbackOnly ids from it, so a free last-resort provider never outranks a configured one on cost). The fallbackOnly loop stays as the true last resort. The chat path only appeared correct because duckduckgo-free happened to fail there and handleSearch retried the alternate provider; on /v1/responses it "succeeded" with no results. Closes #11524 --- src/lib/search/executeWebSearch.ts | 42 +++++++++++++++++------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/lib/search/executeWebSearch.ts b/src/lib/search/executeWebSearch.ts index 6e2af547f0..03f895a036 100644 --- a/src/lib/search/executeWebSearch.ts +++ b/src/lib/search/executeWebSearch.ts @@ -182,6 +182,30 @@ export async function executeWebSearch( } else { credentials = await resolveSearchCredentials(providerConfig.id); + if (!credentials) { + // A CONFIGURED provider always wins over a free `fallbackOnly` one (issue #11524): + // sweep every regular provider for real credentials BEFORE considering the + // last-resort ones. Running the fallbackOnly loop first made `duckduckgo-free` + // (costPerQuery 0, no credentials required) win unconditionally, so an operator's + // paid search connection was silently ignored whenever the cheapest auto-selected + // provider happened to have no credentials. + const sortedIds = Object.values(SEARCH_PROVIDERS) + .filter((provider) => !provider.fallbackOnly && supportsSearchType(provider, searchType)) + .sort((a, b) => a.costPerQuery - b.costPerQuery) + .map((provider) => provider.id); + + for (const providerId of sortedIds) { + if (providerId === providerConfig.id) continue; + const altConfig = getSearchProvider(providerId); + const altCreds = await resolveSearchCredentials(providerId); + if (altConfig && altCreds) { + providerConfig = altConfig; + credentials = altCreds; + break; + } + } + } + if (!credentials) { const fallbackProviders = Object.values(SEARCH_PROVIDERS) .filter((provider) => provider.fallbackOnly && supportsSearchType(provider, searchType)) @@ -201,24 +225,6 @@ export async function executeWebSearch( } } - if (!credentials) { - const sortedIds = Object.values(SEARCH_PROVIDERS) - .filter((provider) => supportsSearchType(provider, searchType)) - .sort((a, b) => a.costPerQuery - b.costPerQuery) - .map((provider) => provider.id); - - for (const providerId of sortedIds) { - if (providerId === providerConfig.id) continue; - const altConfig = getSearchProvider(providerId); - const altCreds = await resolveSearchCredentials(providerId); - if (altConfig && altCreds) { - providerConfig = altConfig; - credentials = altCreds; - break; - } - } - } - if (!credentials) { throw new WebSearchExecutionError( `No credentials configured for any search provider. Add an API key for a search provider (${Object.keys(