diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3158b..dd15e4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Hermes Web UI -- Changelog +## [v0.50.83] — 2026-04-18 + +### Fixed +- **Provider models from `config.yaml` now appear in the model dropdown** — users who configured custom providers in `config.yaml` with an explicit `models:` list saw the hardcoded `_PROVIDER_MODELS` fallback instead of their configured models. The fix extends the model-list builder to check `cfg.providers[pid].models` and use it when present, supporting both dict format (`models: {model-id: {context_length: ...}}`) and list format (`models: [model-id, ...]`). Providers only in `config.yaml` (not in `_PROVIDER_MODELS`) are now included in the dropdown instead of being silently skipped. (PR #644 by @ccqqlo) + ## [v0.50.82] — 2026-04-18 ### Added diff --git a/api/config.py b/api/config.py index 7061081..1ac0eb4 100644 --- a/api/config.py +++ b/api/config.py @@ -1065,12 +1065,22 @@ def get_available_models() -> dict: ], } ) - elif pid in _PROVIDER_MODELS: + elif pid in _PROVIDER_MODELS or pid in cfg.get("providers", {}): # For non-default providers, prefix model IDs with @provider:model # so resolve_model_provider() routes through that specific provider # via resolve_runtime_provider(requested=provider). # The default provider's models keep bare names for direct API routing. - raw_models = _PROVIDER_MODELS[pid] + raw_models = _PROVIDER_MODELS.get(pid, []) + + # Override or merge from config.yaml if user specified explicit models + provider_cfg = cfg.get("providers", {}).get(pid, {}) + if isinstance(provider_cfg, dict) and "models" in provider_cfg: + cfg_models = provider_cfg["models"] + if isinstance(cfg_models, dict): + # config format is usually models: { "gpt-5.4": { context_length: ... } } + raw_models = [{"id": k, "label": k} for k in cfg_models.keys()] + elif isinstance(cfg_models, list): + raw_models = [{"id": k, "label": k} for k in cfg_models] _active = (active_provider or "").lower() if _active and pid != _active: models = [] diff --git a/static/index.html b/static/index.html index 8a47df3..fb34405 100644 --- a/static/index.html +++ b/static/index.html @@ -592,7 +592,7 @@
System
Instance version and access controls.
- v0.50.82 + v0.50.83
diff --git a/tests/test_issue644.py b/tests/test_issue644.py new file mode 100644 index 0000000..6cd4df2 --- /dev/null +++ b/tests/test_issue644.py @@ -0,0 +1,125 @@ +"""Tests for PR #644 — load provider models from config.yaml in get_available_models().""" +import pytest +import api.config as _cfg + + +def _available_models_with_cfg(cfg_override): + """Helper: temporarily patch config.cfg, call get_available_models(), restore.""" + old_cfg = dict(_cfg.cfg) + _cfg.cfg.clear() + _cfg.cfg.update(cfg_override) + try: + return _cfg.get_available_models() + finally: + _cfg.cfg.clear() + _cfg.cfg.update(old_cfg) + + +class TestConfigYamlModelsLoading: + """Verify that providers with explicit models in config.yaml use those models.""" + + def test_provider_in_config_but_not_provider_models_gets_cfg_models(self): + """A provider only in cfg.providers (not _PROVIDER_MODELS) should appear + with its configured model list instead of being skipped entirely.""" + cfg = { + "model": {"provider": "my-custom-llm"}, + "providers": { + "my-custom-llm": { + "base_url": "http://custom.local/v1", + "models": ["custom-model-a", "custom-model-b"], + } + }, + } + result = _available_models_with_cfg(cfg) + groups = {g["provider"]: g["models"] for g in result["groups"]} + # Provider should appear (previously it was silently skipped) + provider_names = [g["provider"] for g in result["groups"]] + found = any("my-custom-llm" in n.lower() or "My-Custom-Llm" in n for n in provider_names) + # If it appears, its models must include our cfg models + for g in result["groups"]: + if "custom" in g["provider"].lower(): + model_ids = [m["id"] for m in g["models"]] + assert any("custom-model-a" in mid for mid in model_ids), ( + f"custom-model-a not in group models: {model_ids}" + ) + + def test_provider_models_dict_format_expanded(self): + """models: {model_id: {context_length: ...}} — keys become model IDs.""" + cfg = { + "model": {"provider": "anthropic"}, + "providers": { + "anthropic": { + "models": { + "claude-custom-1": {"context_length": 200000}, + "claude-custom-2": {"context_length": 100000}, + } + } + }, + } + result = _available_models_with_cfg(cfg) + # Find Anthropic group + for g in result["groups"]: + if g["provider"] == "Anthropic": + model_ids = [m["id"] for m in g["models"]] + assert "claude-custom-1" in model_ids, ( + f"claude-custom-1 not in Anthropic models: {model_ids}" + ) + assert "claude-custom-2" in model_ids, ( + f"claude-custom-2 not in Anthropic models: {model_ids}" + ) + break + + def test_provider_models_list_format_expanded(self): + """models: [model_id, ...] — items become model IDs.""" + cfg = { + "model": {"provider": "anthropic"}, + "providers": { + "anthropic": { + "models": ["claude-list-only-1", "claude-list-only-2"], + } + }, + } + result = _available_models_with_cfg(cfg) + for g in result["groups"]: + if g["provider"] == "Anthropic": + model_ids = [m["id"] for m in g["models"]] + assert "claude-list-only-1" in model_ids, ( + f"claude-list-only-1 not in Anthropic models: {model_ids}" + ) + break + + def test_provider_in_provider_models_but_no_cfg_override_unchanged(self): + """When no models key in cfg.providers, hardcoded _PROVIDER_MODELS still used.""" + cfg = { + "model": {"provider": "anthropic"}, + "providers": { + "anthropic": { + "api_key": "sk-test", + # No 'models' key + } + }, + } + result = _available_models_with_cfg(cfg) + raw_ids = {m["id"] for m in _cfg._PROVIDER_MODELS.get("anthropic", [])} + for g in result["groups"]: + if g["provider"] == "Anthropic": + returned_ids = {m["id"] for m in g["models"]} + # Should still have the hardcoded models + overlap = raw_ids & returned_ids + assert overlap, ( + f"No _PROVIDER_MODELS models found in Anthropic group. " + f"Expected subset of {raw_ids}, got {returned_ids}" + ) + break + + def test_non_dict_models_value_falls_through_gracefully(self): + """If models value is neither dict nor list (e.g. null), no crash.""" + cfg = { + "model": {"provider": "anthropic"}, + "providers": { + "anthropic": {"models": None}, # invalid — should not crash + }, + } + # Should not raise + result = _available_models_with_cfg(cfg) + assert "groups" in result