fix: BYOK/custom provider models missing from WebUI model dropdown (#815)

Closes #815.

Three root causes fixed:

1. Provider aliases (z.ai/x.ai/google/grok/claude/aws-bedrock/dashscope/~25 more) not
   normalized before _PROVIDER_MODELS lookup — provider fell to empty else-branch while
   TUI worked (it normalizes at startup). Fixed via _resolve_provider_alias() + inlined
   _PROVIDER_ALIASES table in api/config.py.

2. Silent ImportError in original normalization: 'from hermes_cli.models import
   _PROVIDER_ALIASES' inside try/except silently failed without hermes-agent on sys.path
   (CI, minimal installs). The inlined table fixes this — normalization now works
   regardless of whether hermes-agent is installed.

3. /api/models/live?provider=custom now falls back to custom_providers entries from
   config.yaml when provider_model_ids() returns empty.

Also: provider_id on every group in /api/models response for deterministic JS optgroup
matching (no substring false positives). 17 targeted tests, 1725/1725 full suite.
This commit is contained in:
nesquena-hermes
2026-04-21 17:24:54 -07:00
committed by GitHub
parent a4d59b9e6c
commit 8f1f582caf
5 changed files with 496 additions and 3 deletions

View File

@@ -2028,6 +2028,14 @@ def _handle_live_models(handler, parsed):
if not provider:
return j(handler, {"error": "no_provider", "models": []})
# Normalize provider alias so 'z.ai' -> 'zai', 'x.ai' -> 'xai', etc.
# The browser sends whatever active_provider the static endpoint returned;
# without normalization, provider_model_ids() misses the alias and returns [].
# Uses the WebUI-owned table (api/config._resolve_provider_alias) which
# works even when hermes_cli is not on sys.path.
from api.config import _resolve_provider_alias
provider = _resolve_provider_alias(provider)
# Delegate to the agent's live-fetch + fallback resolver.
# provider_model_ids() tries live endpoints first and falls back to
# the static _PROVIDER_MODELS list — it never raises.
@@ -2048,7 +2056,23 @@ def _handle_live_models(handler, parsed):
ids = [m["id"] for m in _pm.get(provider, [])]
if not ids:
return j(handler, {"provider": provider, "models": [], "count": 0})
# For 'custom' provider, provider_model_ids() returns [] because
# 'custom' isn't a real endpoint. Fall back to the custom_providers
# entries from config.yaml so the live-model enrichment step can
# add any models that weren't already in the static list.
if provider == "custom":
try:
_cp_entries = cfg.get("custom_providers", [])
if isinstance(_cp_entries, list):
ids = [
_cp.get("model", "")
for _cp in _cp_entries
if isinstance(_cp, dict) and _cp.get("model", "")
]
except Exception:
pass
if not ids:
return j(handler, {"provider": provider, "models": [], "count": 0})
# Normalise to {id, label} — provider_model_ids() returns plain string IDs
def _make_label(mid):