{"record":{"id":"439ccf6b60671293","repo":"ZhuLinsen/daily_stock_analysis","slug":"hermes-api-key-is-a-masked-placeholder-and-cannot","errorCode":null,"errorMessage":"Hermes API key is a masked placeholder and cannot be used for generation","messagePattern":"Hermes API key is a masked placeholder and cannot be used for generation","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"critical","filePath":"src/analyzer.py","lineNumber":2764,"sourceCode":"        self,\n        model: str,\n        call_kwargs: Dict[str, Any],\n        *,\n        config: Config,\n        use_channel_router: bool,\n        router_model_names: set[str],\n    ) -> Any:\n        \"\"\"Dispatch a LiteLLM completion through router or direct fallback.\"\"\"\n        origins = route_deployment_origins(config.llm_model_list, model)\n        if origins.is_mixed:\n            raise RuntimeError(\"Hermes/non-Hermes mixed generation route is not supported without deployment-level no-proxy client support\")\n        if origins.is_hermes_only:\n            deployment = origins.hermes_deployments[0]\n            params = dict(deployment.get(\"litellm_params\") or {})\n            api_key = str(params.get(\"api_key\") or \"\").strip()\n            base_url = str(params.get(\"api_base\") or \"\").strip()\n            if is_masked_secret_placeholder(api_key):\n                raise RuntimeError(\"Hermes API key is a masked placeholder and cannot be used for generation\")\n            timeout = float(call_kwargs.get(\"timeout\") or 30.0)\n            hermes_kwargs = dict(call_kwargs)\n            hermes_kwargs[\"model\"] = str(params.get(\"model\") or model)\n            hermes_kwargs[\"stream\"] = False\n            hermes_kwargs.pop(\"api_key\", None)\n            hermes_kwargs.pop(\"api_base\", None)\n            with open_hermes_no_proxy_client(api_key=api_key, base_url=base_url, timeout=timeout) as client:\n                hermes_kwargs[\"client\"] = client\n                return litellm.completion(**hermes_kwargs)\n\n        wire_models = resolve_fallback_litellm_wire_models(model, config.llm_model_list)\n        register_fallback_model_pricing(wire_models)\n        effective_kwargs = dict(call_kwargs)\n        if use_channel_router and self._router and model in router_model_names:\n            return self._router.completion(**effective_kwargs)\n        if self._router and model == config.litellm_model and not use_channel_router:\n            return self._router.completion(**effective_kwargs)\n","sourceCodeStart":2746,"sourceCodeEnd":2782,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/analyzer.py#L2746-L2782","documentation":"RuntimeError raised on the Hermes-only direct path when the deployment's api_key in litellm_params matches the masked-secret placeholder pattern (is_masked_secret_placeholder). Masked placeholders appear when secrets were redacted for display/logging (e.g. '****' or a masked token) and such a value is not a usable credential, so generation refuses rather than sending a guaranteed-401 request.","triggerScenarios":"LLM generation routed to a Hermes deployment whose litellm_params.api_key is a masked/redacted placeholder string — typically because a sanitized config dump (logs, UI payload, exported settings) was re-imported as the real config, or env substitution never ran and the literal mask text survived.","commonSituations":"Copying an LLM_MODEL_LIST from a web UI or log output where keys were masked; CI that serializes config with redaction and then reloads it; a secrets-injection step (devkey/env) skipped or failing silently so the placeholder remains.","solutions":["Re-enter the real Hermes API key into the deployment's litellm_params (via env var or secret store, not by pasting into chat).","Check how the config was produced: if it round-tripped through any redaction/sanitization layer, source it from the original secret store instead.","Add a startup assertion that no api_key in llm_model_list satisfies is_masked_secret_placeholder, so the failure surfaces at boot with the deployment name rather than at generation time."],"exampleFix":"# before\nlitellm_params: {model: ..., api_base: ..., api_key: \"********\"}  # masked dump re-imported\n\n# after\nlitellm_params: {model: ..., api_base: ..., api_key: \"${HERMES_API_KEY}\"}  # resolved from env/secret store at load","handlingStrategy":"validation","validationCode":"from src.utils.secrets import is_masked_secret_placeholder  # actual import path per repo\n\nfor model in config.llm_model_list:\n    for dep in model.get(\"litellm_params\", {}).get(\"api_key\", \"\"):\n        if is_masked_secret_placeholder(str(dep)):\n            raise ConfigError(\"masked API key detected; inject real secrets\")","typeGuard":null,"tryCatchPattern":"try:\n    result = analyzer._dispatch_litellm_completion(...)\nexcept RuntimeError as e:\n    if \"masked placeholder\" in str(e):\n        # credential injection failed upstream; fix secret source, never retry as-is\n        raise ConfigError(\"Hermes api_key is masked; re-inject real secret\") from e\n    raise","preventionTips":["Never re-import config dumps produced by redaction/logging layers.","Assert at startup that no api_key is a masked placeholder.","Manage keys via the secret store (devkey/env), never pasted values."],"tags":["llm","hermes","secrets","configuration","redaction"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}