{"record":{"id":"6197cc581b886903","repo":"ZhuLinsen/daily_stock_analysis","slug":"hermes-base-url-path-must-be-v1","errorCode":null,"errorMessage":"Hermes BASE_URL path must be /v1","messagePattern":"Hermes BASE_URL path must be /v1","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/llm/hermes.py","lineNumber":180,"sourceCode":"    Allowed forms are loopback HTTP(S) URLs whose path is exactly /v1 or /v1/.\n    localhost is canonicalized to 127.0.0.1 to avoid DNS/hosts ambiguity.\n    \"\"\"\n\n    raw = (base_url or HERMES_DEFAULT_BASE_URL).strip() or HERMES_DEFAULT_BASE_URL\n    parsed = urlparse(raw)\n    if parsed.scheme.lower() not in {\"http\", \"https\"}:\n        raise ValueError(\"Hermes BASE_URL must use http or https\")\n    if not parsed.netloc or not parsed.hostname:\n        raise ValueError(\"Hermes BASE_URL must include a loopback host\")\n    if parsed.username or parsed.password:\n        raise ValueError(\"Hermes BASE_URL must not include userinfo\")\n    if parsed.params or parsed.query or parsed.fragment:\n        raise ValueError(\"Hermes BASE_URL must not include params, query, or fragment\")\n\n    raw_path = parsed.path or \"\"\n    decoded_path = unquote(raw_path)\n    if decoded_path not in {\"/v1\", \"/v1/\"}:\n        raise ValueError(\"Hermes BASE_URL path must be /v1\")\n    if quote(decoded_path, safe=\"/\") != raw_path.rstrip(\"/\") and raw_path not in {\"/v1\", \"/v1/\"}:\n        raise ValueError(\"Hermes BASE_URL path must not contain encoded segments\")\n\n    hostname = parsed.hostname.strip().lower()\n    if hostname == \"localhost\":\n        hostname = \"127.0.0.1\"\n    elif hostname not in {\"127.0.0.1\", \"::1\"}:\n        raise ValueError(\"Hermes BASE_URL must point to 127.0.0.1, localhost, or [::1]\")\n\n    try:\n        port = parsed.port\n    except ValueError as exc:\n        raise ValueError(\"Hermes BASE_URL contains an invalid port\") from exc\n\n    netloc = f\"[{hostname}]\" if \":\" in hostname else hostname\n    if port is not None:\n        netloc = f\"{netloc}:{port}\"\n    return urlunparse(parsed._replace(netloc=netloc, path=\"/v1\", params=\"\", query=\"\", fragment=\"\"))","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/llm/hermes.py#L162-L198","documentation":"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.","triggerScenarios":"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).","commonSituations":"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').","solutions":["Set the path explicitly to /v1: 'http://127.0.0.1:8642/v1'.","If Hermes truly moved off /v1, that is a Hermes-side change — align the server route or the config, but never both guessing."],"exampleFix":"# before\nBASE_URL=http://127.0.0.1:8642/api/v1\n\n# after\nBASE_URL=http://127.0.0.1:8642/v1","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\npath = urlparse(base_url).path or \"\"\nassert path.rstrip(\"/\") == \"/v1\", \"BASE_URL path must be /v1\"","typeGuard":"def has_v1_path(value: str) -> bool:\n    return (urlparse(value).path or \"\").rstrip(\"/\") == \"/v1\"","tryCatchPattern":"try:\n    url = canonicalize_hermes_base_url(cfg.base_url)\nexcept ValueError as exc:\n    raise ConfigError(str(exc)) from exc","preventionTips":["Hermes serves exactly /v1 — do not use other gateways' prefixed paths","Write the full URL including /v1 explicitly","Align server routes and config instead of guessing one side"],"tags":["hermes","llm","config","url-validation"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}