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