fix(model_switch): enumerate dict-format models in /model picker

list_authenticated_providers() builds /model picker rows for CLI, TUI and
gateway flows, but fails to enumerate custom provider models stored in
dict form:

- custom_providers[] entries surface only the singular `model:` field,
  hiding every other model in the `models:` dict.
- providers: dict entries with dict-format `models:` are silently dropped
  and render as `(0 models)`.

Hermes's own writer (main.py::_save_custom_provider) persists configured
models as a dict keyed by model id, and most downstream readers
(agent/models_dev.py, gateway/run.py, run_agent.py, hermes_cli/config.py)
already consume that dict format. The /model picker was the only stale
path.

Add a dict branch in both sections of list_authenticated_providers(),
preferring dict (canonical) and keeping the list branch as fallback for
hand-edited / legacy configs. Dedup against the already-added default
model so nothing duplicates when the default is also a dict key.

Six new regression tests in tests/hermes_cli/ cover: dict models with a
default, dict models without a default, and default dedup against a
matching dict key.

Fixes #11677
Fixes #9148
Related: #11017
This commit is contained in:
Jason 2026-04-19 18:03:21 +08:00 committed by Teknium
parent 13294c2d18
commit bca03eab20
3 changed files with 234 additions and 2 deletions

View file

@ -1047,9 +1047,16 @@ def list_authenticated_providers(
models_list = []
if default_model:
models_list.append(default_model)
# Also include the full models list from config
# Also include the full models list from config.
# Hermes writes ``models:`` as a dict keyed by model id
# (see hermes_cli/main.py::_save_custom_provider); older
# configs or hand-edited files may still use a list.
cfg_models = ep_cfg.get("models", [])
if isinstance(cfg_models, list):
if isinstance(cfg_models, dict):
for m in cfg_models:
if m and m not in models_list:
models_list.append(m)
elif isinstance(cfg_models, list):
for m in cfg_models:
if m and m not in models_list:
models_list.append(m)
@ -1100,10 +1107,27 @@ def list_authenticated_providers(
"api_url": api_url,
"models": [],
}
# The singular ``model:`` field only holds the currently
# active model. Hermes's own writer (main.py::_save_custom_provider)
# stores every configured model as a dict under ``models:``;
# downstream readers (agent/models_dev.py, gateway/run.py,
# run_agent.py, hermes_cli/config.py) already consume that dict.
# The /model picker previously ignored it, so multi-model
# custom providers appeared to have only the active model.
default_model = (entry.get("model") or "").strip()
if default_model and default_model not in groups[slug]["models"]:
groups[slug]["models"].append(default_model)
cfg_models = entry.get("models", {})
if isinstance(cfg_models, dict):
for m in cfg_models:
if m and m not in groups[slug]["models"]:
groups[slug]["models"].append(m)
elif isinstance(cfg_models, list):
for m in cfg_models:
if m and m not in groups[slug]["models"]:
groups[slug]["models"].append(m)
for slug, grp in groups.items():
if slug.lower() in seen_slugs:
continue