diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 30427bd257..1e52212a7d 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -2004,7 +2004,7 @@ def _normalize_custom_provider_entry( for url_key in ("base_url", "url", "api"): raw_url = entry.get(url_key) if isinstance(raw_url, str) and raw_url.strip(): - candidate = raw_url.strip() + candidate = str(_expand_env_vars(raw_url)).strip() parsed = urlparse(candidate) if parsed.scheme and parsed.netloc: base_url = candidate diff --git a/tests/hermes_cli/test_config.py b/tests/hermes_cli/test_config.py index 5c719cbc21..a3abc6b68c 100644 --- a/tests/hermes_cli/test_config.py +++ b/tests/hermes_cli/test_config.py @@ -482,7 +482,7 @@ class TestCustomProviderCompatibility: "providers": { "openai-direct": { "api": "https://api.openai.com/v1", - "api_key": "test-key", + "api_key": "***", "default_model": "gpt-5-mini", "name": "OpenAI Direct", "transport": "codex_responses", @@ -502,6 +502,38 @@ class TestCustomProviderCompatibility: assert compatible[0]["provider_key"] == "openai-direct" assert compatible[0]["api_mode"] == "codex_responses" + def test_providers_dict_expands_env_placeholders_before_url_validation(self, tmp_path): + """Runtime compat view should accept env-backed provider URLs from providers dict.""" + config_path = tmp_path / "config.yaml" + config_path.write_text( + yaml.safe_dump( + { + "_config_version": 17, + "providers": { + "provider-a": { + "name": "Provider A", + "base_url": "${PROVIDER_A_BASE_URL}", + } + }, + } + ), + encoding="utf-8", + ) + + with patch.dict( + os.environ, + {"HERMES_HOME": str(tmp_path), "PROVIDER_A_BASE_URL": "https://provider-a.example.com/v1"}, + ): + compatible = get_compatible_custom_providers() + + assert compatible == [ + { + "name": "Provider A", + "base_url": "https://provider-a.example.com/v1", + "provider_key": "provider-a", + } + ] + def test_compatible_custom_providers_prefers_base_url_then_url_then_api(self, tmp_path): """URL field precedence is base_url > url > api (PR #9332).""" config_path = tmp_path / "config.yaml" diff --git a/tests/hermes_cli/test_provider_config_validation.py b/tests/hermes_cli/test_provider_config_validation.py index 775e3284c6..56d2d18e47 100644 --- a/tests/hermes_cli/test_provider_config_validation.py +++ b/tests/hermes_cli/test_provider_config_validation.py @@ -72,12 +72,25 @@ class TestNormalizeCustomProviderEntry: entry = { "base_url": "https://correct.example.com/v1", "api": "https://wrong.example.com/v1", - "api_key": "sk-test-key", + "api_key": "***", } result = _normalize_custom_provider_entry(entry, provider_key="test") assert result is not None assert result["base_url"] == "https://correct.example.com/v1" + def test_expands_env_placeholders_before_validating_base_url(self, monkeypatch): + """base_url placeholders should be expanded before URL validation.""" + monkeypatch.setenv("PROVIDER_A_BASE_URL", "https://provider-a.example.com/v1") + entry = { + "base_url": "${PROVIDER_A_BASE_URL}", + "api_key": "***", + } + + result = _normalize_custom_provider_entry(entry, provider_key="provider-a") + + assert result is not None + assert result["base_url"] == "https://provider-a.example.com/v1" + def test_unknown_keys_logged(self, caplog): """Unknown config keys should produce a warning.""" entry = {