From 563ae6b7c1a87fd22816f49480da91f330dae45a Mon Sep 17 00:00:00 2001 From: AIalliAI <285906080+AIalliAI@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:47:20 +0000 Subject: [PATCH] fix: merge custom_providers.models with live-discovered models instead of replacing (fixes #59560) When live /models discovery returns a list from the API, the previous code replaced the configured models_list entirely with the live_models list. This caused custom_providers.models entries to be ignored in the Desktop picker when the endpoint supported /models discovery. Fix: merge the two lists by keeping all configured models and appending only live-discovered models whose slug/id is not already present. This preserves user-configured models while enriching the list with any additional models the live endpoint reports. Fixes #59560 --- hermes_cli/model_switch.py | 18 ++- tests/test_custom_providers_model_merge.py | 127 +++++++++++++++++++++ 2 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 tests/test_custom_providers_model_merge.py diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index b9a50f6df63d..6637e0348f6c 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -2006,7 +2006,13 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool: headers=_extra_headers_from_config(ep_cfg) or None, ) if live_models: - models_list = live_models + # Merge: keep configured models, add live models that aren't already present + existing_slugs = {m.get("slug", m.get("id", "")) for m in models_list} + for lm in live_models: + lm_slug = lm.get("slug", lm.get("id", "")) + if lm_slug and lm_slug not in existing_slugs: + models_list.append(lm) + existing_slugs.add(lm_slug) except Exception: pass @@ -2269,8 +2275,14 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool: headers=grp.get("extra_headers") or None, ) if live_models: - grp["models"] = live_models - grp["total_models"] = len(live_models) + # Merge: keep configured models, add live models that aren't already present + existing_slugs = {m.get("slug", m.get("id", "")) for m in grp["models"]} + for lm in live_models: + lm_slug = lm.get("slug", lm.get("id", "")) + if lm_slug and lm_slug not in existing_slugs: + grp["models"].append(lm) + existing_slugs.add(lm_slug) + grp["total_models"] = len(grp["models"]) except Exception: pass results.append({ diff --git a/tests/test_custom_providers_model_merge.py b/tests/test_custom_providers_model_merge.py new file mode 100644 index 000000000000..0bb08e1a1024 --- /dev/null +++ b/tests/test_custom_providers_model_merge.py @@ -0,0 +1,127 @@ +"""Test for issue #59560: custom_providers.models list should not be ignored when live /models discovery returns a different list.""" + +import pytest +from unittest.mock import patch, MagicMock + +def test_configured_models_merged_with_live_models(): + """ + When custom_providers have configured models AND live discovery returns models, + the result should MERGE them (not replace configured with live). + """ + from hermes_cli.model_switch import list_authenticated_providers + + configured_models = [ + {"slug": "configured-model-1", "name": "Configured Model 1"}, + {"slug": "configured-model-2", "name": "Configured Model 2"} + ] + live_models = [ + {"slug": "live-model-1", "name": "Live Model 1"}, + {"slug": "live-model-2", "name": "Live Model 2"} + ] + + # Patch fetch_api_models in hermes_cli.model_switch (where it's imported locally) + with patch('hermes_cli.model_switch.fetch_api_models', return_value=live_models): + providers = list_authenticated_providers( + custom_providers=[ + { + "name": "Test Provider", + "api": "http://localhost:9999/v1", + "models": configured_models, + "discover_models": True + } + ] + ) + + # Find our test provider + test_provider = None + for p in providers: + if p.get("name") == "Test Provider": + test_provider = p + break + + assert test_provider is not None, "Test provider not found in results" + + result_models = test_provider.get("models", []) + result_slugs = {m.get("slug") for m in result_models} + + # Should contain BOTH configured and live models + assert "configured-model-1" in result_slugs, "Configured model 1 missing" + assert "configured-model-2" in result_slugs, "Configured model 2 missing" + assert "live-model-1" in result_slugs, "Live model 1 missing" + assert "live-model-2" in result_slugs, "Live model 2 missing" + assert len(result_models) == 4, f"Expected 4 merged models, got {len(result_models)}" + + +def test_configured_models_preserved_when_live_discovery_fails(): + """When live discovery fails, configured models should still be present.""" + from hermes_cli.model_switch import list_authenticated_providers + + configured_models = [ + {"slug": "configured-only", "name": "Configured Only"} + ] + + # Mock fetch_api_models to raise an exception + with patch('hermes_cli.model_switch.fetch_api_models', side_effect=Exception("Network error")): + providers = list_authenticated_providers( + custom_providers=[ + { + "name": "Test Provider 2", + "api": "http://localhost:9998/v1", + "models": configured_models, + "discover_models": True + } + ] + ) + + test_provider = None + for p in providers: + if p.get("name") == "Test Provider 2": + test_provider = p + break + + assert test_provider is not None + result_models = test_provider.get("models", []) + assert len(result_models) == 1, "Configured models should be preserved when discovery fails" + assert result_models[0].get("slug") == "configured-only" + + +def test_no_duplicates_when_live_returns_same_model(): + """ + If a model exists in both configured and live, it should not be duplicated. + """ + from hermes_cli.model_switch import list_authenticated_providers + + configured_models = [ + {"slug": "shared-model", "name": "Shared Model"}, + {"slug": "only-configured", "name": "Only Configured"} + ] + live_models = [ + {"slug": "shared-model", "name": "Shared Model Live"}, + {"slug": "only-live", "name": "Only Live"} + ] + + with patch('hermes_cli.model_switch.fetch_api_models', return_value=live_models): + providers = list_authenticated_providers( + custom_providers=[ + { + "name": "Test Provider 3", + "api": "http://localhost:9997/v1", + "models": configured_models, + "discover_models": True + } + ] + ) + + test_provider = None + for p in providers: + if p.get("name") == "Test Provider 3": + test_provider = p + break + + assert test_provider is not None + result_models = test_provider.get("models", []) + result_slugs = [m.get("slug") for m in result_models] + + # shared-model should appear only once + assert result_slugs.count("shared-model") == 1, "Duplicate model found" + assert len(result_models) == 3, f"Expected 3 unique models, got {len(result_models)}: {result_slugs}"