Skip to content

[Bug]: providers: config entries silently ignore apiKey/baseUrl and accept non-URL strings as base_url via the api field #9332

@houko

Description

@houko

Bug Description

In the providers: keyed schema for config.yaml, _normalize_provider_dict_entry (in hermes_cli/config.py) does not validate keys and does not validate that the base-URL value is actually a URL. Two problems compound:

  1. The base-URL lookup checks ("api", "url", "base_url") in that order, accepting the first non-empty string. A non-URL literal in api: (e.g. the string openai-reverse-proxy) is silently accepted as the endpoint.
  2. Only snake_case api_key is recognized. Hand-written camelCase (apiKey, baseUrl) is silently dropped.

The user sees mysterious APIConnectionError and Bearer no-key-required in outgoing requests with no hint that their config keys were misspelled or that api: must parse as a URL.

Not a duplicate of #6945 (user-defined providers unresolvable in /model picker — their api: value was a real URL so they hit a different downstream bug), #8919 (custom provider routing with model.api_base), or #9315 (custom_providers list priority conflict when two entries share a base_url).

Steps to Reproduce

  1. Write this providers: entry in ~/.hermes/config.yaml:
    providers:
      nvidia:
        baseUrl: https://integrate.api.nvidia.com/v1
        apiKey: ${NVIDIA_API_KEY}
        api: openai-reverse-proxy
  2. Add nvidia to fallback_providers and trigger a call to it (e.g. let the primary provider fail).
  3. Inspect the outgoing request via ~/.hermes/sessions/request_dump_*.json or the agent log.

Expected Behavior

Either the entry is rejected at load time with a clear error/warning naming the unknown keys (apiKey, baseUrl), or camelCase keys are accepted as aliases. In either case, api: openai-reverse-proxy should never be silently interpreted as a base URL because it does not parse as a URL (no scheme, no host).

Actual Behavior

  • base_url resolves to the literal string "openai-reverse-proxy".
  • HTTP calls go to URLs like openai-reverse-proxy/v1/models and fail with APIConnectionError: Invalid URL 'openai-reverse-proxy/v1/models': No scheme supplied.
  • Authorization header is Bearer no-key-required because neither apiKey nor api_key is found, and the runtime falls through to the local-server placeholder in _ensure_runtime_credentials.
  • No warning is logged at config load time.

Affected Component

Configuration (config.yaml, .env, hermes setup)

Messaging Platform (if gateway-related)

N/A (CLI only)

Operating System

macOS 26.4.1

Python Version

3.11.15

Hermes Version

v0.9.0 (2026.4.13)

Relevant Logs / Traceback

DEBUG agent.model_metadata: Failed to fetch model metadata from openai-reverse-proxy/models: Invalid URL 'openai-reverse-proxy/v1/models': No scheme supplied. Perhaps you meant https://openai-reverse-proxy/v1/models?
INFO  agent.model_metadata: Could not detect context length for model 'nvidia/llama-3.1-nemotron-ultra-253b-v1' at openai-reverse-proxy/ — defaulting to 128,000 tokens (probe-down).

Request dump excerpt (~/.hermes/sessions/request_dump_*.json):

{
  "method": "POST",
  "url": "openai-chat/chat/completions",
  "headers": {"Authorization": "Bearer no-key-required", "Content-Type": "application/json"}
}

Root Cause Analysis (optional)

hermes_cli/config.py, _normalize_provider_dict_entry (~line 1580):

base_url = ""
for url_key in ("api", "url", "base_url"):   # ← 'api' checked first, no URL validation
    raw_url = entry.get(url_key)
    if isinstance(raw_url, str) and raw_url.strip():
        base_url = raw_url.strip()
        break
...
api_key = entry.get("api_key")                # ← snake_case only

Unknown keys are neither read nor warned about. There is a separate api_mode field, so users reasonably assume api means "API protocol", not "base URL" — and when they misuse it, the config is accepted silently.

Proposed Fix (optional)

  1. Validate providers.<name> entries against a known field set (analogous to _VALID_CUSTOM_PROVIDER_FIELDS) and logger.warning on unknown keys, specifically calling out common camelCase mistakes (apiKey, baseUrl, apiMode).
  2. Validate that the value chosen for base_url from ("api", "url", "base_url") actually parses as a URL with a scheme and netloc (urllib.parse.urlparse(v).scheme and urlparse(v).netloc). Reject non-URL strings with a clear error at config load rather than silently accepting them. This keeps api: working for users who already supply a valid URL ([Bug]: User-defined providers from providers: config cannot be resolved via /model picker or --provider flag #6945) while catching typos and misused field names.
  3. Optional: accept apiKey/baseUrl as deprecated aliases with a one-time logger.warning to ease migration from hand-written configs.

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions