fix(search): scope failure locks so search cannot take chat offline

Two problems on the credentialFallback path, where a search provider
borrows a chat provider's connection:

- the lock was attributed to the search provider id, but the connection
  belongs to the chat provider, so markAccountUnavailable looked it up
  under the wrong provider and read a stale backoffLevel
- with no model argument the lock key is `modelLock___all`, which
  isModelLockActive treats as blocking every model — one failing search
  would have taken the shared glm key offline for chat as well

Attribute the lock to the provider that owns the connection, and scope
it to `websearch:<provider>`, passed to getProviderCredentials too so
the lock is read back under the same key.
This commit is contained in:
decolua
2026-08-28 16:18:46 +07:00
parent e5a13c3ab7
commit ec6692808b

View File

@@ -154,14 +154,24 @@ async function handleSingleProviderSearch(body, providerInput, request, apiKey,
// fall back to the linked provider's credentials.
const fallbackProviderId = resolvedProvider.credentialFallback;
// Lock scope for this handler. Without it markAccountUnavailable would write
// an account-wide `__all` lock, which on the credentialFallback path takes
// the shared chat key (e.g. glm) offline for chat as well. Must be passed to
// getProviderCredentials too, so the lock is read back under the same key.
const searchLockKey = `websearch:${providerId}`;
while (true) {
let credentials = await getProviderCredentials(providerId, excludeConnectionIds);
// Provider that actually owns the connection in use — differs from
// providerId once we fall back, and error locks must be attributed to it.
let credentialProviderId = providerId;
let credentials = await getProviderCredentials(providerId, excludeConnectionIds, searchLockKey);
// Fall back to the related chat provider's credentials when this search
// provider has none of its own (one key, chat + search).
if (!credentials && fallbackProviderId) {
credentials = await getProviderCredentials(fallbackProviderId, excludeConnectionIds);
credentials = await getProviderCredentials(fallbackProviderId, excludeConnectionIds, searchLockKey);
if (credentials) {
credentialProviderId = fallbackProviderId;
log.info("AUTH", `\x1b[32m${providerId} reusing ${fallbackProviderId} credentials\x1b[0m`);
}
}
@@ -206,7 +216,7 @@ async function handleSingleProviderSearch(body, providerInput, request, apiKey,
if (result.success) return result.response;
const { shouldFallback } = await markAccountUnavailable(credentials.connectionId, result.status, result.error, providerId);
const { shouldFallback } = await markAccountUnavailable(credentials.connectionId, result.status, result.error, credentialProviderId, searchLockKey);
if (shouldFallback) {
log.warn("AUTH", `Account ${credentials.connectionName} unavailable (${result.status}), trying fallback`);