ZhuLinsen/daily_stock_analysis · error · ValueError

Hermes BASE_URL path must be /v1

Error message

Hermes BASE_URL path must be /v1

What it means

Path check in canonicalize_hermes_base_url: after percent-decoding, the URL path must be exactly '/v1' or '/v1/' (trailing slash tolerated). Hermes only serves the OpenAI-compatible /v1 surface at the root; deeper prefixes ('/openai/v1', '/api/v1') or missing paths ('/' or '') do not match the bridge's route and are rejected.

Source

Thrown at src/llm/hermes.py:180

    Allowed forms are loopback HTTP(S) URLs whose path is exactly /v1 or /v1/.
    localhost is canonicalized to 127.0.0.1 to avoid DNS/hosts ambiguity.
    """

    raw = (base_url or HERMES_DEFAULT_BASE_URL).strip() or HERMES_DEFAULT_BASE_URL
    parsed = urlparse(raw)
    if parsed.scheme.lower() not in {"http", "https"}:
        raise ValueError("Hermes BASE_URL must use http or https")
    if not parsed.netloc or not parsed.hostname:
        raise ValueError("Hermes BASE_URL must include a loopback host")
    if parsed.username or parsed.password:
        raise ValueError("Hermes BASE_URL must not include userinfo")
    if parsed.params or parsed.query or parsed.fragment:
        raise ValueError("Hermes BASE_URL must not include params, query, or fragment")

    raw_path = parsed.path or ""
    decoded_path = unquote(raw_path)
    if decoded_path not in {"/v1", "/v1/"}:
        raise ValueError("Hermes BASE_URL path must be /v1")
    if quote(decoded_path, safe="/") != raw_path.rstrip("/") and raw_path not in {"/v1", "/v1/"}:
        raise ValueError("Hermes BASE_URL path must not contain encoded segments")

    hostname = parsed.hostname.strip().lower()
    if hostname == "localhost":
        hostname = "127.0.0.1"
    elif hostname not in {"127.0.0.1", "::1"}:
        raise ValueError("Hermes BASE_URL must point to 127.0.0.1, localhost, or [::1]")

    try:
        port = parsed.port
    except ValueError as exc:
        raise ValueError("Hermes BASE_URL contains an invalid port") from exc

    netloc = f"[{hostname}]" if ":" in hostname else hostname
    if port is not None:
        netloc = f"{netloc}:{port}"
    return urlunparse(parsed._replace(netloc=netloc, path="/v1", params="", query="", fragment=""))

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Set the path explicitly to /v1: 'http://127.0.0.1:8642/v1'.
  2. If Hermes truly moved off /v1, that is a Hermes-side change — align the server route or the config, but never both guessing.

Example fix

# before
BASE_URL=http://127.0.0.1:8642/api/v1

# after
BASE_URL=http://127.0.0.1:8642/v1
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse

path = urlparse(base_url).path or ""
assert path.rstrip("/") == "/v1", "BASE_URL path must be /v1"

Type guard

def has_v1_path(value: str) -> bool:
    return (urlparse(value).path or "").rstrip("/") == "/v1"

Try / catch

try:
    url = canonicalize_hermes_base_url(cfg.base_url)
except ValueError as exc:
    raise ConfigError(str(exc)) from exc

Prevention

When it happens

Trigger: BASE_URL='http://127.0.0.1:8642' (no path), 'http://127.0.0.1:8642/', 'http://127.0.0.1:8642/api/v1', or '/v2'. Percent-encoded variants like '/%76%31' decode to '/v1' and pass here (the encoding check handles abuse separately).

Common situations: Assuming the port root works like other OpenAI clients that append paths; using a reverse-proxied prefix path from another gateway's config; version drift ('/v2').

Related errors


AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15). Data as JSON: /api/errors/6197cc581b886903. Report an issue: GitHub.