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
This commit is contained in:
Xiangzhe
2026-08-25 19:18:25 -03:00
parent 091e2ba4da
commit 37d784b0fe

View File

@@ -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(