diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 30427bd25..91232dc0d 100644 --- a/hermes_cli/config.py +++ b/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: diff --git a/tests/hermes_cli/test_provider_config_validation.py b/tests/hermes_cli/test_provider_config_validation.py index 775e3284c..ffc036b31 100644 --- a/tests/hermes_cli/test_provider_config_validation.py +++ b/tests/hermes_cli/test_provider_config_validation.py @@ -135,3 +135,48 @@ class TestNormalizeCustomProviderEntry: } 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