{"record":{"id":"3f4f857c56f573bd","repo":"ZhuLinsen/daily_stock_analysis","slug":"hermes-only-supports-protocol-openai","errorCode":null,"errorMessage":"Hermes only supports PROTOCOL=openai","messagePattern":"Hermes only supports PROTOCOL=openai","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/llm/hermes.py","lineNumber":155,"sourceCode":"    for secret in sorted(values, key=len, reverse=True):\n        if secret:\n            sanitized = sanitized.replace(secret, \"[REDACTED]\")\n    patterns = [\n        (r\"(?i)(authorization\\s*[:=]\\s*)(bearer\\s+)?([^\\s,;]+)\", r\"\\1[REDACTED]\"),\n        (r\"(?i)(api[_-]?key\\s*[:=]\\s*)([^\\s,;]+)\", r\"\\1[REDACTED]\"),\n        (r\"(?i)(cookie\\s*[:=]\\s*)([^\\s,;]+)\", r\"\\1[REDACTED]\"),\n        (r\"(?i)bearer\\s+[a-z0-9._\\-]+\", \"Bearer [REDACTED]\"),\n        (r\"(?i)sk-[a-z0-9_\\-]+\", \"[REDACTED]\"),\n    ]\n    for pattern, replacement in patterns:\n        sanitized = re.sub(pattern, replacement, sanitized)\n    return \" \".join(sanitized.split())[:300]\n\n\ndef canonicalize_hermes_protocol(protocol: str) -> str:\n    normalized = (protocol or HERMES_DEFAULT_PROTOCOL).strip().lower() or HERMES_DEFAULT_PROTOCOL\n    if normalized != HERMES_DEFAULT_PROTOCOL:\n        raise ValueError(\"Hermes only supports PROTOCOL=openai\")\n    return HERMES_DEFAULT_PROTOCOL\n\n\ndef canonicalize_hermes_base_url(base_url: str) -> str:\n    \"\"\"Return canonical Hermes base URL or raise ValueError.\n\n    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\")","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/llm/hermes.py#L137-L173","documentation":"Hermes LLM gateway config guard (src/llm/hermes.py): canonicalize_hermes_protocol normalizes PROTOCOL (strip+lower, defaulting HERMES_DEFAULT_PROTOCOL='openai') and requires exactly 'openai'. The Hermes bridge only implements the OpenAI-compatible wire protocol; any other value (anthropic, gemini, etc.) is a misconfiguration and fails fast.","triggerScenarios":"Setting PROTOCOL to anything other than 'openai' (case-insensitive) in Hermes-related config — 'anthropic', 'openai-compatible', 'OpenAI ' trailing space is fine (normalized) but 'oai' is not. Raised during Hermes settings canonicalization at startup.","commonSituations":"Copy-pasting a multi-provider LLM config template into the Hermes section; assuming Hermes proxies arbitrary protocols like the main LiteLLM channels do.","solutions":["Set PROTOCOL=openai (or omit it — 'openai' is the default when empty).","Route non-OpenAI-protocol providers through the regular LLM channel config, not the Hermes bridge."],"exampleFix":"# before\nPROTOCOL=anthropic\n\n# after\nPROTOCOL=openai","handlingStrategy":"validation","validationCode":"from src.llm.hermes import canonicalize_hermes_protocol\n\ncanonicalize_hermes_protocol(protocol)  # raises on anything non-openai","typeGuard":"def is_openai_protocol(value: str) -> bool:\n    return (value or \"openai\").strip().lower() in {\"\", \"openai\"}","tryCatchPattern":"try:\n    proto = canonicalize_hermes_protocol(cfg_protocol)\nexcept ValueError:\n    proto = \"openai\"  # or fail config load","preventionTips":["Omit PROTOCOL for Hermes (defaults to openai)","Do not reuse multi-provider channel templates for the Hermes section"],"tags":["hermes","llm","config","validation"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}