fix(auth): look up keyed custom providers by durable pool slug

hermes auth add stores providers.<key> credentials under the config
slug, but runtime only tried custom:<display-name> and then sent the
no-key-required placeholder. Try the slug first, keep the legacy
namespace as fallback, and thread provider_key/key_env through named
custom resolution.
This commit is contained in:
xxxigm
2026-09-02 00:04:40 +09:00
committed by kshitij
parent 43470980bf
commit 0bee5ff408
5 changed files with 227 additions and 77 deletions
+61 -33
View File
@@ -15,6 +15,7 @@ from agent.credential_pool import (
CredentialPool,
PooledCredential,
credential_pool_matches_provider,
custom_provider_pool_key_candidates,
get_custom_provider_pool_key,
load_pool,
)
@@ -670,42 +671,59 @@ def _try_resolve_from_custom_pool(
provider_name: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
"""Check if a credential pool exists for a custom endpoint and return a runtime dict if so."""
pool_key = get_custom_provider_pool_key(base_url, provider_name=provider_name)
if not pool_key:
return None
candidates: list[str] = []
seen = set()
def _add(key: Optional[str]) -> None:
normalized = str(key or "").strip().lower()
if normalized and normalized not in seen:
seen.add(normalized)
candidates.append(normalized)
try:
pool = load_pool(pool_key)
if not pool.has_credentials():
return None
entry = pool.select()
if entry is None:
return None
pool_api_key = getattr(entry, "runtime_api_key", None) or getattr(entry, "access_token", "")
if not pool_api_key:
return None
if not has_usable_secret(pool_api_key) and _loopback_hostname(base_url_hostname(base_url)):
# Legacy configs commonly used short/placeholder keys ('123',
# 'm', ...) for local no-auth services like Ollama -- fine for
# the endpoint itself, but has_usable_secret's 4-char floor
# (added after these configs were written) now rejects them
# here with no migration path. Every OTHER resolution path in
# this file already substitutes "no-key-required" for a
# loopback endpoint with no usable secret (the config-based
# custom_providers fallback a few hundred lines below, and the
# "actual" provider's local-offline exemption further down) --
# this pool path was the one gap (issue #86864).
pool_api_key = "no-key-required"
return {
"provider": provider_label,
"api_mode": api_mode_override or _detect_api_mode_for_url(base_url) or "chat_completions",
"base_url": base_url,
"api_key": pool_api_key,
"source": f"pool:{pool_key}",
"credential_pool": pool,
}
for key in custom_provider_pool_key_candidates(base_url, provider_name):
_add(key)
except Exception:
pass
_add(get_custom_provider_pool_key(base_url, provider_name=provider_name))
if not candidates:
return None
for pool_key in candidates:
try:
pool = load_pool(pool_key)
if not pool.has_credentials():
continue
entry = pool.select()
if entry is None:
continue
pool_api_key = getattr(entry, "runtime_api_key", None) or getattr(entry, "access_token", "")
if not pool_api_key:
continue
if not has_usable_secret(pool_api_key) and _loopback_hostname(base_url_hostname(base_url)):
# Legacy configs commonly used short/placeholder keys ('123',
# 'm', ...) for local no-auth services like Ollama -- fine for
# the endpoint itself, but has_usable_secret's 4-char floor
# (added after these configs were written) now rejects them
# here with no migration path. Every OTHER resolution path in
# this file already substitutes "no-key-required" for a
# loopback endpoint with no usable secret (the config-based
# custom_providers fallback a few hundred lines below, and the
# "actual" provider's local-offline exemption further down) --
# this pool path was the one gap (issue #86864).
pool_api_key = "no-key-required"
return {
"provider": provider_label,
"api_mode": api_mode_override or _detect_api_mode_for_url(base_url) or "chat_completions",
"base_url": base_url,
"api_key": pool_api_key,
"source": f"pool:{pool_key}",
"credential_pool": pool,
}
except Exception:
continue
return None
def _filter_capabilities(value: Any) -> Dict[str, bool]:
"""Return the string-keyed boolean capabilities accepted at runtime."""
@@ -834,6 +852,11 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
"api_key": resolved_api_key,
"model": entry.get("default_model", ""),
}
provider_key = str(ep_name or "").strip()
if provider_key:
result["provider_key"] = provider_key
if key_env:
result["key_env"] = key_env
extra_body = entry.get("extra_body")
if isinstance(extra_body, dict):
result["extra_body"] = dict(extra_body)
@@ -1267,7 +1290,12 @@ def _resolve_named_custom_runtime(
return None
# Check if a credential pool exists for this custom endpoint
pool_result = _try_resolve_from_custom_pool(base_url, "custom", custom_provider.get("api_mode"), provider_name=custom_provider.get("name"))
pool_result = _try_resolve_from_custom_pool(
base_url,
"custom",
custom_provider.get("api_mode"),
provider_name=custom_provider.get("provider_key") or custom_provider.get("name"),
)
if pool_result:
# Propagate the model name even when using pooled credentials —
# the pool doesn't know about the custom_providers model field.