hermes - ✅(Solved) Fix [Bug]: custom_providers.base_url placeholders can be validated before expansion and get skipped as invalid URLs [3 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#14457Fetched 2026-04-24 06:17:06
View on GitHub
Comments
0
Participants
1
Timeline
7
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×3labeled ×3subscribed ×1

Root Cause

Suspected Root Cause

Fix Action

Fixed

PR fix notes

PR #14485: fix(config): expand provider base_url templates before validation

Description (problem / solution / changelog)

Summary

  • expand env-backed provider URLs before _normalize_custom_provider_entry() validates them
  • keep runtime compat views for providers entries working when base_url uses ${ENV_VAR} placeholders
  • add regression coverage for both direct normalization and the runtime compatibility path

Root cause

_normalize_custom_provider_entry() validated base_url with urlparse() before ${ENV_VAR} placeholders were expanded, so code paths that passed raw config entries could reject otherwise valid env-backed URLs as malformed.

Fix

Expand each candidate provider URL with _expand_env_vars() before running URL validation.

Regression coverage

  • tests/hermes_cli/test_provider_config_validation.py
  • tests/hermes_cli/test_config.py

Testing

  • scripts/run_tests.sh tests/hermes_cli/test_provider_config_validation.py -k expands_env_placeholders_before_validating_base_url (RED before fix)
  • scripts/run_tests.sh tests/hermes_cli/test_provider_config_validation.py
  • scripts/run_tests.sh tests/hermes_cli/test_config.py

Closes #14457

Changed files

  • hermes_cli/config.py (modified, +1/-1)
  • tests/hermes_cli/test_config.py (modified, +33/-1)
  • tests/hermes_cli/test_provider_config_validation.py (modified, +14/-1)

PR #14524: fix(config): expand ${ENV_VAR} placeholders in gateway config loading (#14457)

Description (problem / solution / changelog)

Summary

  • Gateway's _load_gateway_config() loaded config.yaml with yaml.safe_load() without calling _expand_env_vars(), so custom_providers entries with base_url: ${MY_URL} were passed to _normalize_custom_provider_entry() unexpanded
  • urlparse() then rejected them as invalid URLs ("no scheme or host") and silently skipped the provider
  • Two inline yaml.safe_load() sites in GatewayRunner (session-hygiene and /model command) had the same issue

Changes

  1. _load_gateway_config() now calls _expand_env_vars() before returning — fixes all 8+ call sites at once
  2. Two inline YAML loads in GatewayRunner (session-hygiene context-length resolution + /model command provider listing) also expand after loading
  3. _normalize_custom_provider_entry() recognises ${VAR} patterns and defers URL validation so unresolved refs are passed through instead of rejected — defense-in-depth for any remaining raw-config paths

Test plan

  • 7 new tests in tests/gateway/test_gateway_env_expansion.py:
    • _load_gateway_config() expands ${VAR} in custom_providers[].base_url
    • _load_gateway_config() expands ${VAR} in model.base_url and model.api_key
    • Unresolved ${VAR} kept verbatim (not dropped or error)
    • Literal URLs unchanged
    • Multiple providers all expanded
    • get_compatible_custom_providers() works with expanded config
    • Unexpanded ${VAR} in base_url not rejected by URL validation
  • All 80 existing config/validation/env-expansion tests pass
  • All 98 existing gateway config + runtime provider tests pass

Closes #14457

Changed files

  • gateway/run.py (modified, +13/-2)
  • hermes_cli/config.py (modified, +6/-0)
  • tests/gateway/test_gateway_env_expansion.py (added, +156/-0)

PR #14628: fix(config): defer URL validation for base_url containing env var placeholder

Description (problem / solution / changelog)

Closes #14457

Problem

Custom provider base_url values containing environment-variable placeholders like ${PROVIDER_A_BASE_URL} were being validated with urlparse() before environment-variable expansion ran in load_config(). Since urlparse('${PROVIDER_A_BASE_URL}') finds no URL scheme or netloc, the provider was silently skipped with a false-positive warning:

providers.?: 'base_url' value '${PROVIDER_A_BASE_URL}' is not a valid URL (no scheme or host) — skipped

Fix

Added a simple guard in _normalize_custom_provider_entry() in hermes_cli/config.py: if the candidate base_url contains ${, skip URL validation and accept it verbatim — load_config() will expand the placeholder and the expanded result goes through the normal path.

Changed file: hermes_cli/config.py (+8 lines)

Testing

  • All 18 tests in tests/hermes_cli/test_provider_config_validation.py pass
  • Added 2 regression tests:
    • test_env_var_placeholder_in_base_url_not_rejected
    • test_multiple_env_vars_in_base_url

Changed files

  • hermes_cli/config.py (modified, +8/-0)
  • tests/hermes_cli/test_provider_config_validation.py (modified, +23/-0)

Code Example

custom_providers:
  - name: PROVIDER_A
    base_url: ${PROVIDER_A_BASE_URL}
    key_env: PROVIDER_A_API_KEY

---

providers.?: 'base_url' value '${PROVIDER_A_BASE_URL}' is not a valid URL (no scheme or host) — skipped

---

model:
  default: MODEL_A
  provider: custom
  context_length: 200000
  base_url: ${PROVIDER_B_BASE_URL}
  api_key: ${PROVIDER_B_API_KEY}

custom_providers:
  - name: PROVIDER_A
    base_url: ${PROVIDER_A_BASE_URL}
    key_env: PROVIDER_A_API_KEY

  - name: PROVIDER_B
    base_url: ${PROVIDER_B_BASE_URL}
    key_env: PROVIDER_B_API_KEY

  - name: PROVIDER_C
    base_url: ${PROVIDER_C_BASE_URL}
    key_env: PROVIDER_C_API_KEY
    api_mode: chat_completions
    models:
      MODEL_C:
        context_length: 200000

---

providers.?: 'base_url' value '${PROVIDER_A_BASE_URL}' is not a valid URL (no scheme or host) — skipped
providers.?: 'base_url' value '${PROVIDER_B_BASE_URL}' is not a valid URL (no scheme or host) — skipped
providers.?: 'base_url' value '${PROVIDER_C_BASE_URL}' is not a valid URL (no scheme or host) — skipped

---

def _expand_env_vars(obj):
    if isinstance(obj, str):
        return re.sub(
            r"\${([^}]+)}",
            lambda m: os.environ.get(m.group(1), m.group(0)),
            obj,
        )

---

def load_config() -> Dict[str, Any]:
    ...
    normalized = _normalize_root_model_keys(_normalize_max_turns_config(config))
    expanded = _expand_env_vars(normalized)
    return expanded

---

raw_url = entry.get("base_url")
candidate = raw_url.strip()
parsed = urlparse(candidate)
if parsed.scheme and parsed.netloc:
    ...
else:
    logger.warning("... is not a valid URL ... skipped")
RAW_BUFFERClick to expand / collapse

Bug Description

In some Hermes code paths, custom_providers[].base_url values that use ${ENV_VAR} placeholders are being validated as URLs before environment-variable expansion has happened.

When that occurs, Hermes treats a valid placeholder like:

custom_providers:
  - name: PROVIDER_A
    base_url: ${PROVIDER_A_BASE_URL}
    key_env: PROVIDER_A_API_KEY

as a literal string, fails URL validation, and skips the provider with a warning like:

providers.?: 'base_url' value '${PROVIDER_A_BASE_URL}' is not a valid URL (no scheme or host) — skipped

If I replace the same value with the literal URL, the provider is immediately recognized.

So this does not look like a general ${VAR} support problem in config.yaml; it looks like a path-specific ordering bug where some code validates base_url before ${VAR} expansion has been applied.

Why I think this is a bug

The intended runtime flow appears to be:

  1. load HERMES_HOME/.env into os.environ
  2. load config.yaml
  3. expand ${VAR} placeholders
  4. validate base_url

And indeed load_config() does call _expand_env_vars() before returning.

But _normalize_custom_provider_entry() validates base_url directly with urlparse() and assumes the caller already passed in expanded values.

That means any code path that reaches _normalize_custom_provider_entry() with raw config data (instead of fully expanded config) will incorrectly reject ${ENV_VAR} placeholders as invalid URLs.

In other words, the validator is relying on an implicit ordering guarantee that is not consistently upheld across all call paths.

Reproduction

My setup is a Dockerized Hermes Gateway with:

  • HERMES_HOME=/opt/data
  • /opt/data/.env mounted and loaded
  • config.yaml using ${VAR} placeholders
  • the same data/.env also injected into the container environment via compose

Example:

model:
  default: MODEL_A
  provider: custom
  context_length: 200000
  base_url: ${PROVIDER_B_BASE_URL}
  api_key: ${PROVIDER_B_API_KEY}

custom_providers:
  - name: PROVIDER_A
    base_url: ${PROVIDER_A_BASE_URL}
    key_env: PROVIDER_A_API_KEY

  - name: PROVIDER_B
    base_url: ${PROVIDER_B_BASE_URL}
    key_env: PROVIDER_B_API_KEY

  - name: PROVIDER_C
    base_url: ${PROVIDER_C_BASE_URL}
    key_env: PROVIDER_C_API_KEY
    api_mode: chat_completions
    models:
      MODEL_C:
        context_length: 200000

And .env contains real values for all these variables.

Actual Behavior

In some runs / code paths, Hermes logs warnings like:

providers.?: 'base_url' value '${PROVIDER_A_BASE_URL}' is not a valid URL (no scheme or host) — skipped
providers.?: 'base_url' value '${PROVIDER_B_BASE_URL}' is not a valid URL (no scheme or host) — skipped
providers.?: 'base_url' value '${PROVIDER_C_BASE_URL}' is not a valid URL (no scheme or host) — skipped

and the named custom providers are not recognized properly.

If I rewrite those base_url values to literal URLs, they are recognized immediately.

Expected Behavior

${ENV_VAR} placeholders in custom_providers[].base_url should never be URL-validated before expansion.

If the env var is present, Hermes should validate the expanded URL. If the env var is missing, Hermes can keep the placeholder verbatim and report that the env var is unresolved, but it should not silently treat the placeholder as a malformed URL in a path that is supposed to support ${VAR} interpolation.

Additional Evidence

Inside the running container, Hermes runtime config loading behaves correctly:

  • load_config() returns expanded values
  • get_compatible_custom_providers() returns expanded values
  • _expand_env_vars() correctly turns ${PROVIDER_X_BASE_URL} into the real URL
  • _normalize_custom_provider_entry(_expand_env_vars(entry)) works correctly

So the failure seems specific to a path that reaches _normalize_custom_provider_entry() before expansion, not a fundamental limitation of ${VAR} support.

Relevant Code

From hermes_cli/config.py:

def _expand_env_vars(obj):
    if isinstance(obj, str):
        return re.sub(
            r"\${([^}]+)}",
            lambda m: os.environ.get(m.group(1), m.group(0)),
            obj,
        )
def load_config() -> Dict[str, Any]:
    ...
    normalized = _normalize_root_model_keys(_normalize_max_turns_config(config))
    expanded = _expand_env_vars(normalized)
    return expanded

But _normalize_custom_provider_entry() validates URLs directly:

raw_url = entry.get("base_url")
candidate = raw_url.strip()
parsed = urlparse(candidate)
if parsed.scheme and parsed.netloc:
    ...
else:
    logger.warning("... is not a valid URL ... skipped")

Suspected Root Cause

A code path is calling _normalize_custom_provider_entry() (or a compatibility layer that uses it) on raw config data instead of already-expanded config.

Proposed Fix

One of these approaches should fix it:

  1. Make _normalize_custom_provider_entry() call _expand_env_vars() on URL-like fields before validation.
  2. Guarantee that all call sites only pass already-expanded config objects.
  3. If a base_url contains ${...}, treat it as an unresolved env reference and defer validation until after config expansion, instead of immediately classifying it as an invalid URL.

Related Issues

This may be adjacent to, but is not identical to:

  • #9147 (custom:<name>:<model> triple syntax broken when currently on a custom provider)
  • #12153 (custom provider validation fails when /v1/models is unavailable)
  • #11864 (${ENV_VAR} placeholders being materialized into literal secrets during config rewrite)

Environment

  • Hermes Gateway in Docker
  • HERMES_HOME=/opt/data
  • custom providers configured in config.yaml
  • provider secrets/base URLs stored in /opt/data/.env

Additional Note

I can reliably work around the issue by replacing ${ENV_VAR} base URLs with literal URLs. That strongly suggests the failure is due to validation happening before expansion rather than a bad endpoint.

extent analysis

TL;DR

The issue can be fixed by modifying the _normalize_custom_provider_entry() function to expand environment variables in the base_url field before validating it as a URL.

Guidance

  • Identify all call sites of _normalize_custom_provider_entry() and ensure they pass already-expanded config objects.
  • Modify _normalize_custom_provider_entry() to call _expand_env_vars() on the base_url field before URL validation.
  • Consider treating base_url values containing ${...} as unresolved env references and deferring validation until after config expansion.
  • Verify the fix by checking that custom providers with ${ENV_VAR} placeholders in their base_url fields are recognized correctly.

Example

def _normalize_custom_provider_entry(entry):
    raw_url = entry.get("base_url")
    expanded_url = _expand_env_vars(raw_url)
    candidate = expanded_url.strip()
    parsed = urlparse(candidate)
    if parsed.scheme and parsed.netloc:
        # ...
    else:
        logger.warning("... is not a valid URL ... skipped")

Notes

The proposed fix assumes that the _expand_env_vars() function is correctly implemented and works as expected. Additionally, the fix may require modifications to other parts of the codebase to ensure that all call sites of _normalize_custom_provider_entry() pass already-expanded config objects.

Recommendation

Apply the workaround by modifying the _normalize_custom_provider_entry() function to expand environment variables in the base_url field before validating it as a URL. This approach is more targeted and less likely to introduce unintended side effects compared to guaranteeing that all call sites pass already-expanded config objects.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING