Custom provider models missing or incomplete in model picker
NousResearch/hermes-agent · cluster 6999 · apps/desktop
Data as of
11 open items. Fix merged in #12557. Check it covers your case.
The short answer
- Known?
- Yes. 76 related items grouped here, 28 issues already closed.
- Fix event
- A fix was merged to main in PR #12557. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PRs #12557, #21211, #61928. That is separate from the fix event. - Still open
- 7 issues and 4 PRs.
Next step: read #12557, then check whether your report is one of the 7 still open.
Read #12557Pick up a Lab taskThese issues and fixes concern custom/user-defined providers not surfacing their full model lists in the /model picker across CLI, TUI, and desktop interfaces. Work spans model discovery, normalization, deduplication, and merging of configured and live-discovered models.
Open issues7
- Issue#8758Bug: named custom provider model discovery ignores models mapping and probe fallback
- Issue#20582Model picker only shows one model for custom providers
- Issue#47704[BUG] Model picker dedup overfilters non-routing aggregators (opencode-go, huggingface, novita, etc.)
- Issue#48885[Bug]: Desktop UI model picker does not show custom/local provider models (hounddog_local / LM Studio)
- Issue#49126[Bug]: Global aggregator dedup strips overlapping models from named providers configured under providers:
- Issue#49656Desktop: custom_providers not shown in model provider selector UI
- Issue#63211Model picker hides custom base_url models until manual refresh; stale providers remain visible
Open PRs4
Fixes on record
-
#12557 fix(model_switch): enumerate dict-format models in /model picker + section-3 refinements
files:
hermes_cli/model_switch.py,scripts/release.py,tests/hermes_cli/test_model_switch_custom_providers.py,tests/hermes_cli/test_user_providers_model_switch.pyShow the diff
-
hermes_cli/model_switch.py@@ -1039,17 +1039,38 @@ def list_authenticated_providers( for ep_name, ep_cfg in user_providers.items(): if not isinstance(ep_cfg, dict): continue + # Skip if this slug was already emitted (e.g. canonical provider + # with the same name) or will be picked up by section 4. + if ep_name.lower() in seen_slugs: + continue display_name = ep_cfg.get("name", "") or ep_name - api_url = ep_cfg.get("api", "") or ep_cfg.get("url", "") or "" - default_model = ep_cfg.get("default_model", "") + # ``base_url`` is Hermes's canonical write key (matches + # custom_providers and _save_custom_provider); ``api`` / ``url`` + # remain as fallbacks for hand-edited / legacy configs. + api_url = ( + ep_cfg.get("base_url", "") + or ep_cfg.get("api", "") + or ep_cfg.get("url", "") + or "" + ) + # ``default_model`` is the legacy key; ``model`` matches what + # custom_providers entries use, so accept either. + default_model = ep_cfg.get("default_model", "") or ep_cfg.get("model", "") # Build models list from both default_model and full models array 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) @@ -1066,6 +1087,7 @@ def list_authenticated_providers( "source": "user-config", "api_url": api_url, }) + seen_slugs.add(ep_name.lower()) # --- 4. Saved custom providers from config --- # Each ``custom_providers`` entry represents one model under a named @@ -1100,10 +1122,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 -
scripts/release.py@@ -177,6 +177,7 @@ "[redacted:email]": "duerzy", "[redacted:email]": "emozilla", "[redacted:email]": "fancydirty", + "[redacted:email]": "farion1231", "[redacted:email]": "flobo3", "[redacted:email]": "pefontana", "[redacted:email]": "fxfitz", -
tests/hermes_cli/test_model_switch_custom_providers.py@@ -156,3 +156,100 @@ def test_list_deduplicates_same_model_in_group(monkeypatch): assert len(my_rows) == 1 assert my_rows[0]["models"] == ["llama3", "mistral"] assert my_rows[0]["total_models"] == 2 + + +def test_list_enumerates_dict_format_models_alongside_default(monkeypatch): + """custom_providers entry with dict-format ``models:`` plus singular + ``model:`` should surface the default and every dict key. + + Regression: Hermes's own writer stores configured models as a dict + keyed by model id, but the /model picker previously only honored the + singular ``model:`` field, so multi-model custom providers appeared + to have only the active model. + """ + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr(providers_mod, "HERMES_OVERLAYS", {}) + + providers = list_authenticated_providers( + current_provider="openai-codex", + user_providers={}, + custom_providers=[ + { + "name": "DeepSeek", + "base_url": "https://api.deepseek.com", + "api_mode": "chat_completions", + "model": "deepseek-chat", + "models": { + "deepseek-chat": {"context_length": 128000}, + "deepseek-reasoner": {"context_length": 128000}, + }, + } + ], + max_models=50, + ) + + ds_rows = [p for p in providers if p["name"] == "DeepSeek"] + assert len(ds_rows) == 1 + assert ds_rows[0]["models"] == ["deepseek-chat", "deepseek-reasoner"] + assert ds_rows[0]["total_models"] == 2 + + +def test_list_enumerates_dict_format_models_without_singular_model(monkeypatch): + """Dict-format ``models:`` with no singular ``model:`` should still + enumerate every dict key (previously the picker reported 0 models).""" + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr(providers_mod, "HERMES_OVERLAYS", {}) + + providers = list_authenticated_providers( + current_provider="openai-codex", + user_providers={}, + custom_providers=[ + { + "name": "Thor", + "base_url": "http://thor.lab:8337/v1", + "models": { + "gemma-4-26B-A4B-it-MXFP4_MOE": {"context_length": 262144}, + "Qwen3.5-35B-A3B-MXFP4_MOE": {"context_length": 262144}, + "gemma-4-31B-it-Q4_K_M": {"context_length": 262144}, + }, + } + ], + max_models=50, + ) + + thor_rows = [p for p in providers if p["name"] == "Thor"] + assert len(thor_rows) == 1 + assert set(thor_rows[0]["models"]) == { + "gemma-4-26B-A4B-it-MXFP4_MOE", + "Qwen3.5-35B-A3B-MXFP4_MOE", + "gemma-4-31B-it-Q4_K_M", + } + assert thor_rows[0]["total_models"] == 3 + + +def test_list_dedupes_dict_model_matching_singular_default(monkeypatch): + """When the singular ``model:`` is also a key in the ``models:`` dict, + it must appear exactly once in the picker.""" + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr(providers_mod, "HERMES_OVERLAYS", {}) + + providers = list_authenticated_providers( + current_provider="openai-codex", + user_providers={}, + custom_providers=[ + { + "name": "DeepSeek", + "base_url": "https://api.deepseek.com", + "model": "deepseek-chat", + "models": { + "deepseek-chat": {"context_length": 128000}, + "deepseek-reasoner": {"context_length": 128000}, + }, + } + ], + max_models=50, + ) + + ds_rows = [p for p in providers if p["name"] == "DeepSeek"] + assert ds_rows[0]["models"].count("deepseek-chat") == 1 + assert ds_rows[0]["models"] == ["deepseek-chat", "deepseek-reasoner"] -
tests/hermes_cli/test_user_providers_model_switch.py@@ -86,6 +86,117 @@ def test_list_authenticated_providers_dedupes_models_when_default_in_list(monkey assert user_prov["models"].count("model-a") == 1, "model-a should not be duplicated" +def test_list_authenticated_providers_enumerates_dict_format_models(monkeypatch): + """providers: dict entries with ``models:`` as a dict keyed by model id + (canonical Hermes write format) should surface every key in the picker. + + Regression: the ``providers:`` dict path previously only accepted + list-format ``models:`` and silently dropped dict-format entries, + even though Hermes's own writer and downstream readers use dict format. + """ + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) + + user_providers = { + "local-ollama": { + "name": "Local Ollama", + "api": "http://localhost:11434/v1", + "default_model": "minimax-m2.7:cloud", + "models": { + "minimax-m2.7:cloud": {"context_length": 196608}, + "kimi-k2.5:cloud": {"context_length": 200000}, + "glm-5.1:cloud": {"context_length": 202752}, + }, + } + } + + providers = list_authenticated_providers( + current_provider="local-ollama", + user_providers=user_providers, + custom_providers=[], + max_models=50, + ) + + user_prov = next( + (p for p in providers if p.get("is_user_defined") and p["slug"] == "local-ollama"), + None, + ) + + assert user_prov is not None + assert user_prov["total_models"] == 3 + assert user_prov["models"] == [ + "minimax-m2.7:cloud", + "kimi-k2.5:cloud", + "glm-5.1:cloud", + ] + + +def test_list_authenticated_providers_dict_models_without_default_model(monkeypatch): + """Dict-format ``models:`` without a ``default_model`` must still expose + every dict key, not collapse to an empty list.""" + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) + + user_providers = { + "multimodel": { + "api": "http://example.com/v1", + "models": { + "alpha": {"context_length": 8192}, + "beta": {"context_length": 16384}, + }, + } + } + + providers = list_authenticated_providers( + current_provider="", + user_providers=user_providers, + custom_providers=[], + ) + + user_prov = next( + (p for p in providers if p.get("is_user_defined") and p["slug"] == "multimodel"), + None, + ) + + assert user_prov is not None + assert user_prov["total_models"] == 2 + assert set(user_prov["models"]) == {"alpha", "beta"} + + +def test_list_authenticated_providers_dict_models_dedupe_with_default(monkeypatch): + """When ``default_model`` is also a key in the ``models:`` dict, it must + appear exactly once (list already had this for list-format models).""" + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) + + user_providers = { + "my-provider": { + "api": "http://example.com/v1", + "default_model": "model-a", + "models": { + "model-a": {"context_length": 8192}, + "model-b": {"context_length": 16384}, + "model-c": {"context_length": 32768}, + }, + } + } + + providers = list_authenticated_providers( + current_provider="my-provider", + user_providers=user_providers, + custom_providers=[], + ) + + user_prov = next( + (p for p in providers if p.get("is_user_defined")), + None, + ) + + assert user_prov is not None + assert user_prov["total_models"] == 3 + assert user_prov["models"].count("model-a") == 1 + + def test_list_authenticated_providers_fallback_to_default_only(monkeypatch): """When no models array is provided, should fall back to default_model.""" monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) @@ -116,6 +227,83 @@ def test_list_authenticated_providers_fallback_to_default_only(monkeypatch): assert user_prov["models"] == ["single-model"] +def test_list_authenticated_providers_accepts_base_url_and_singular_model(monkeypatch): + """providers: dict entries written in canonical Hermes shape + (``base_url`` + singular ``model``) should resolve the same as the + legacy ``api`` + ``default_model`` shape. + + Regression: section 3 previously only read ``api``/``url`` and + ``default_model``, so new-shape entries written by Hermes's own writer + surfaced with empty ``api_url`` and no default. + """ + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) + + user_providers = { + "custom": { + "base_url": "http://example.com/v1", + "model": "gpt-5.4", + "models": { + "gpt-5.4": {}, + "grok-4.20-beta": {}, + "minimax-m2.7": {}, + }, + } + } + + providers = list_authenticated_providers( + current_provider="custom", + user_providers=user_providers, + custom_providers=[], + max_models=50, + ) + + custom = next((p for p in providers if p["slug"] == "custom"), None) + assert custom is not None + assert custom["api_url"] == "http://example.com/v1" + assert custom["models"] == ["gpt-5.4", "grok-4.20-beta", "minimax-m2.7"] + assert custom["total_models"] == 3 + + +def test_list_authenticated_providers_dedupes_when_user_and_custom_overlap(monkeypatch): + """When the same slug appears in both ``providers:`` dict and + ``custom_providers:`` list, emit exactly one row (providers: dict wins + since it is processed first). + + Regression: section 3 previously had no ``seen_slugs`` check, so + overlapping entries produced two picker rows for the same provider. + """ + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) + + providers = list_authenticated_providers( + current_provider="custom", + user_providers={ + "custom": { + "base_url": "http://example.com/v1", + "model": "gpt-5.4", + "models": { + "gpt-5.4": {}, + "grok-4.20-beta": {}, + }, + } + }, + custom_providers=[ + { + "name": "custom", + "base_url": "http://example.com/v1", + "model": "legacy-only-model", + } + ], + max_models=50, + ) + + matches = [p for p in providers if p["slug"] == "custom"] + assert len(matches) == 1 + # providers: dict wins — legacy-only-model is suppressed. + assert matches[0]["models"] == ["gpt-5.4", "grok-4.20-beta"] + + # ============================================================================= # Tests for _get_named_custom_provider with providers: dict # =============================================================================
-
-
#14495 fix(config): preserve list-format models in custom_providers normalize (#13509)
files:
hermes_cli/config.py,scripts/release.py,tests/hermes_cli/test_provider_config_validation.pyShow the diff
-
hermes_cli/config.py@@ -2055,6 +2055,14 @@ def _normalize_custom_provider_entry( models = entry.get("models") if isinstance(models, dict) and models: normalized["models"] = models + elif isinstance(models, list) and models: + # Hand-edited configs (and older Hermes versions) write ``models`` as + # a plain list of model ids. Preserve them by converting to the dict + # shape downstream code expects; otherwise normalize silently drops + # the list and /model shows the provider with (0) models. + normalized["models"] = { + str(m): {} for m in models if isinstance(m, str) and m.strip() + } context_length = entry.get("context_length") if isinstance(context_length, int) and context_length > 0: -
scripts/release.py@@ -43,6 +43,7 @@ "[redacted:email]": "teknium1", "[redacted:email]": "teknium1", "[redacted:email]": "teknium1", + "[redacted:email]": "DrStrangerUJN", # contributors (from noreply pattern) "[redacted:email]": "xiaoqiang243", "[redacted:email]": "snreynolds", -
tests/hermes_cli/test_provider_config_validation.py@@ -135,3 +135,48 @@ def test_no_name_returns_none(self): } result = _normalize_custom_provider_entry(entry, provider_key="") assert result is None + + def test_models_list_converted_to_dict(self): + """List-format models should be preserved as an empty-value dict so + /model picks them up instead of showing the provider with (0) models.""" + entry = { + "name": "tencent-coding-plan", + "base_url": "https://api.lkeap.cloud.tencent.com/coding/v3", + "models": ["glm-5", "kimi-k2.5", "minimax-m2.5"], + } + result = _normalize_custom_provider_entry(entry) + assert result is not None + assert result["models"] == {"glm-5": {}, "kimi-k2.5": {}, "minimax-m2.5": {}} + + def test_models_dict_preserved(self): + """Dict-format models should pass through unchanged.""" + entry = { + "name": "acme", + "base_url": "https://api.example.com/v1", + "models": {"gpt-foo": {"context_length": 32000}}, + } + result = _normalize_custom_provider_entry(entry) + assert result is not None + assert result["models"] == {"gpt-foo": {"context_length": 32000}} + + def test_models_list_filters_empty_and_non_string(self): + """List entries that are empty strings or non-strings are skipped.""" + entry = { + "name": "acme", + "base_url": "https://api.example.com/v1", + "models": ["valid", "", None, 42, " ", "also-valid"], + } + result = _normalize_custom_provider_entry(entry) + assert result is not None + assert result["models"] == {"valid": {}, "also-valid": {}} + + def test_models_empty_list_omitted(self): + """Empty list (falsy) should not produce a models key.""" + entry = { + "name": "acme", + "base_url": "https://api.example.com/v1", + "models": [], + } + result = _normalize_custom_provider_entry(entry) + assert result is not None + assert "models" not in result
-
-
#17023 fix(model): prefer live /v1/models for user-defined providers (salvage #16747)
files:
hermes_cli/model_switch.py,scripts/release.py,tests/hermes_cli/test_user_providers_model_switch.pyShow the diff
-
hermes_cli/model_switch.py@@ -1346,8 +1346,23 @@ def list_authenticated_providers( if fb: models_list = list(fb) - # Try to probe /v1/models if URL is set (but don't block on it) - # For now just show what we know from config + # Prefer the endpoint's live /models list when credentials are + # available. This keeps OpenAI-compatible relays (for example CRS) + # in sync when the server catalog changes without requiring the + # user to mirror every model into config.yaml. + api_key = [redacted:secret-assignment]"api_key", "") or "").strip() + if not api_key: + key_env = str(ep_cfg.get("key_env", "") or "").strip() + api_key = [redacted:secret-assignment] "").strip() if key_env else "" + if api_url and api_key: + try: + from hermes_cli.models import fetch_api_models + live_models = fetch_api_models(api_key, api_url) + if live_models: + models_list = live_models + except Exception: + pass + results.append({ "slug": ep_name, "name": display_name, -
scripts/release.py@@ -53,6 +53,7 @@ "[redacted:email]": "cyclingwithelephants", "[redacted:email]": "liizfq", "[redacted:email]": "dhabibi", + "[redacted:email]": "JayGwod", "[redacted:email]": "johnncenae", "[redacted:email]": "ThomassJonax", "[redacted:email]": "yes999zc", -
tests/hermes_cli/test_user_providers_model_switch.py@@ -131,6 +131,55 @@ def test_list_authenticated_providers_enumerates_dict_format_models(monkeypatch) ] +def test_list_authenticated_providers_uses_live_models_for_user_provider(monkeypatch): + """User-defined OpenAI-compatible providers should prefer live /models. + + Regression: CRS-style providers with a stale config ``models:`` dict kept + showing only the configured subset in the /model picker, even though their + /v1/models endpoint exposed newly added models. + """ + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) + monkeypatch.setenv("CRS_TEST_KEY", "sk-test") + + calls = [] + + def fake_fetch_api_models(api_key, base_url): + calls.append((api_key, base_url)) + return ["old-configured-model", "new-live-model"] + + monkeypatch.setattr("hermes_cli.models.fetch_api_models", fake_fetch_api_models) + + user_providers = { + "crs-henkee": { + "name": "CRS Henkee", + "base_url": "http://127.0.0.1:3000/api/v1", + "key_env": "CRS_TEST_KEY", + "model": "old-configured-model", + "models": { + "old-configured-model": {"context_length": 200000}, + }, + } + } + + providers = list_authenticated_providers( + current_provider="crs-henkee", + user_providers=user_providers, + custom_providers=[], + max_models=50, + ) + + user_prov = next( + (p for p in providers if p.get("is_user_defined") and p["slug"] == "crs-henkee"), + None, + ) + + assert user_prov is not None + assert calls == [("sk-test", "http://127.0.0.1:3000/api/v1")] + assert user_prov["models"] == ["old-configured-model", "new-live-model"] + assert user_prov["total_models"] == 2 + + def test_list_authenticated_providers_dict_models_without_default_model(monkeypatch): """Dict-format ``models:`` without a ``default_model`` must still expose every dict key, not collapse to an empty list."""
-
-
#21211 fix(model_switch): live model discovery for custom_providers in /model picker (salvage #19072)
files:
hermes_cli/model_switch.py,tests/hermes_cli/test_model_switch_custom_providers.pyShow the diff
-
hermes_cli/model_switch.py@@ -1637,7 +1637,8 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool: groups[group_key]["models"].append(m) _section4_emitted_slugs: set = set() - for grp in groups.values(): + for grp_key, grp in groups.items(): + api_url, api_key = [redacted:secret-assignment] slug = grp["slug"] # If the slug is already claimed by a built-in / overlay / # user-provider row (sections 1-3), skip this custom group @@ -1675,6 +1676,18 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool: _grp_url_norm = _pair_key[1] if _grp_url_norm and _grp_url_norm in _builtin_endpoints: continue + # Live model discovery from custom provider endpoints (matches + # Section 3 behavior for user ``providers:`` entries). + if api_url and api_key: + try: + from hermes_cli.models import fetch_api_models + + live_models = fetch_api_models(api_key, api_url) + if live_models: + grp["models"] = live_models + grp["total_models"] = len(live_models) + except Exception: + pass results.append({ "slug": slug, "name": grp["name"], -
tests/hermes_cli/test_model_switch_custom_providers.py@@ -506,3 +506,64 @@ def _fake_fetch(api_key=None, base_url=None, timeout=5.0): ) assert "base_url" not in captured + + +def test_custom_providers_uses_live_models_for_multi_model_endpoint(monkeypatch): + """Custom providers with api_key + base_url should prefer live /models. + + Custom providers (section 4 of list_authenticated_providers) point at + gateways like Bifrost that expose hundreds of models. Reading only the + static ``models:`` dict from config.yaml leaves the /model picker with + a stale subset. Live discovery fills the picker with all available + models from the endpoint. + """ + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) + + calls = [] + + def fake_fetch_api_models(api_key, base_url): + calls.append((api_key, base_url)) + return ["gateway-model-a", "gateway-model-b", "gateway-model-c"] + + monkeypatch.setattr("hermes_cli.models.fetch_api_models", fake_fetch_api_models) + + custom_providers = [ + { + "name": "my-gateway", + "api_key": "sk-gateway-key", + "base_url": "https://gateway.example.com/v1", + "model": "gateway-model-a", + "models": { + "gateway-model-a": {"context_length": 128000}, + "gateway-model-b": {"context_length": 128000}, + }, + } + ] + + providers = list_authenticated_providers( + current_provider="openrouter", + current_base_url="https://openrouter.ai/api/v1", + custom_providers=custom_providers, + max_models=50, + ) + + gateway_prov = next( + ( + p + for p in providers + if p.get("api_url") == "https://gateway.example.com/v1" + ), + None, + ) + + assert gateway_prov is not None, "Custom provider group not found in results" + assert calls == [("sk-gateway-key", "https://gateway.example.com/v1")], ( + "fetch_api_models must be called with the custom provider's credentials" + ) + assert gateway_prov["models"] == [ + "gateway-model-a", + "gateway-model-b", + "gateway-model-c", + ], "Live models must replace the static subset" + assert gateway_prov["total_models"] == 3
-
-
#45597 fix(model): show bare custom endpoints in gateway picker
files:
hermes_cli/model_switch.py,tests/hermes_cli/test_model_switch_custom_providers.py -
#61928 fix(model): discover no-key custom provider catalogs
files:
hermes_cli/model_switch.py,tests/hermes_cli/test_model_switch_custom_providers.py
31 PRs closed without merging
From the close comments: done elsewhere 12 · duplicate 5 · superseded by another PR 5 · merged via another change 4 · closed without comment 2 · stale or abandoned 2 · other 1
-
#7132 feat: auto-discover models for custom/user-defined providers via /v1/models · done elsewhere →
#21211,#17023 - #8223 fix(cli): include custom provider model maps in /model picker · done elsewhere
-
#8770 fix(models): list named custom provider models · merged via another change →
#21211,#19072 - #9134 fix(model): pass api_mode through validation chain and support custom:name:model triple syntax · closed without comment
- #9152 fix(model): api_mode propagation, triple syntax, and custom provider model listing · closed without comment
Show 26 more
-
#10326 fix(config): support models list in custom_providers picker (#8216) · superseded by another PR →
#12557 -
#11403 fix(model_switch): read models dict keys from custom_providers entries · superseded by another PR →
#12557 -
#11505 fix(cli): support custom provider model dictionaries in model picker · done elsewhere →
#11499 -
#11534 fix(model): support providers.*.models dict in Telegram /model picker · superseded by another PR →
#12557,#12505 -
#11681 fix(model-picker): expose full custom provider model maps (#11677) · done elsewhere →
#11677 -
#11968 fix: include all models from custom_providers[].models in model picker · superseded by another PR →
#12557 -
#12505 fix(model_switch): enumerate dict-format models in /model picker · merged via another change →
#12557 -
#15081 fix: support list format for custom_providers[].models in _normalize_custom_provider_entry · done elsewhere →
#14495,#15081 - #16460 fix(cli): include custom_providers in list_available_providers for TUI /model picker · stale or abandoned
- #19072 fix(model_switch): live model discovery for custom_providers in /model picker · other
-
#20751 fix: live model discovery for custom providers in /model picker · duplicate →
#19072,#20763,#20582 - #20760 fix(cli): probe /v1/models for custom providers in model picker · superseded by another PR
- #20763 fix: live model discovery for custom providers in /model picker · done elsewhere
- #20919 fix(models): show configured custom provider model in /model picker · done elsewhere
-
#33601 fix(model): probe keyless user provider catalogs · done elsewhere →
1039e90 -
#33966 fix(cli): probe live /v1/models for keyless providers in model picker · duplicate →
#33601,#33595 -
#40529 fix(cli): surface inline custom provider model in the picker (#40480) · done elsewhere →
74c5158,#45597,#40480 -
#40554 fix(cli): probe /v1/models live when custom provider has no api_key · merged via another change →
#61928,#55158,#40542 -
#47050 fix(inventory): skip self-dedup when custom provider is also classified as aggregator · done elsewhere →
b7fa62c -
#47133 fix(picker): skip user-defined providers in aggregator dedup (#47042) · duplicate →
#46921,#47050,#47042,#45954,#46829 -
#49150 fix(cli): preserve named provider aggregator models (#49126) · done elsewhere →
a6ce9b2,#49273 -
#49273 fix(cli): restrict aggregator dedup to custom proxy providers (#49126) · duplicate →
#49150,#49126 -
#55158 fix(model): probe no-key custom provider catalogs · merged via another change →
#61928 - #59912 fix(model-switch): merge configured models with live /models discovery instead of replacing · stale or abandoned
-
#60320 fix(model-switch): merge configured custom_providers models with live discovery · duplicate →
#36834,#59912,#59560,#35145 -
#60503 fix(model-switch): merge configured custom_providers.models · done elsewhere →
#59560,#36834
Closed issues · 28
- Closed#6999Bug: /model shows only default_model for user-defined providers (providers: config)
- Closed#7103Support dynamic model discovery via /v1/models for user-defined providers
- Closed#8216Bug: /model picker ignores custom_providers.models and only shows one model
- Closed#9140fix(model): /model picker shows 0 models for custom_providers with models: field
- Closed#9148Bug: /model picker shows 0 models for custom_providers with models: dict
- Closed#10011Auto-discover models from custom provider endpoints in /model picker
- Closed#11499[Bug]: Telegram model picker does not list models from providers.custom.models dict
- Closed#11677[Bug]: Custom Model Provider is not Enumerating Models for Model Picker, only the Default Model
- Closed#16438[Bug]: TUI /model picker does not show custom_providers defined in config.yaml
- Closed#19071custom_providers: /model picker ignores live /v1/models endpoint, shows stale static subset
- Closed#30202Feature: /model command should enumerate custom/local provider models
- Closed#33595[Bug]: Telegram model picker skips live /v1/models discovery for providers: entries without api_key
- Closed#40480Desktop App: Custom provider models not shown in model selection dropdown
- Closed#40542[Bug]: /model picker only shows 1 model for local custom providers without api_key (while hermes model CLI shows all)
- Closed#43409feat: /model interactive picker should list custom_providers models
- Closed#44283feat: Show custom_providers in model/provider selector (desktop + CLI)
- Closed#44434feat: Show custom_providers in model/provider selector (desktop + CLI)
- Closed#44435feat: Show custom_providers in model/provider selector (desktop + CLI)
- Closed#47042[BUG] Desktop model picker hides custom providers due to is_aggregator() false positive in dedup logic
- Closed#47124[Bug]: Custom endpoints do not appear in the Agent because their models are wiped by the aggregator deduplication logic
- Closed#47132bug(inventory): named custom providers lose all models in GUI picker after 60cc42e38
- Closed#47155Regression: /model hides custom:* provider after deduplication empties its model list
- Closed#47258UI model picker shows 0 models for custom_providers entries
- Closed#47386Custom providers with models: list show 0 models in Desktop/TUI model picker
- Closed#47402Model picker shows 0 models for custom: providers due to is_aggregator() treating all custom: slugs as aggregators
- Closed#48990Bug: custom_providers models silently stripped from /model picker
- Closed#55573[Feature]: Support multi-model configuration/selection for custom OpenAI-compatible providers
- Closed#59560[Bug]: custom_providers.models list is ignored — combo/9router models are not detected/selectable in Hermes Desktop
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.03: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab