{"repo":"NousResearch/hermes-agent","public_id":"10468","display_label":"Auth API key crash in non-interactive environments","summary":"A bug where `hermes auth add api_key` crashes when a label prompt is shown in non-interactive environments, along with related CLI quality-of-life fixes. The fix involves silently using a default label when no interactive input is available.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":10599,"merged_at":"2026-04-16T00:34:15+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"stale","last_verified_at":"2026-04-16T00:34:15+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":10468,"pr_number":10599,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-16T00:34:15+00:00"}],"members":[{"kind":"issue","number":10468,"title":"Bug: hermes auth add api_key crashes on non-interactive label prompt","url":null,"state":"closed"},{"kind":"pr","number":10475,"title":"fix(auth): use default label silently in non-interactive environments","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"b750c720cdd3a0c04d5c5fe4829ddb0ba577d85a","number":0},{"kind":"pr","value":"#10468","number":10468},{"kind":"pr","value":"#10230","number":10230},{"kind":"pr","value":"#10526","number":10526},{"kind":"pr","value":"#9545","number":9545},{"kind":"pr","value":"#10599","number":10599}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":10599,"title":"fix: three CLI quality-of-life fixes","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/auth_commands.py","hermes_cli/main.py","hermes_cli/model_switch.py"],"fix_hunks":[{"file":"hermes_cli/auth_commands.py","hunks":["@@ -4,6 +4,7 @@\n \n from getpass import getpass\n import math\n+import sys\n import time\n from types import SimpleNamespace\n import uuid","@@ -160,7 +161,10 @@ def auth_add_command(args) -> None:\n         default_label = _api_key_default_label(len(pool.entries()) + 1)\n         label = (getattr(args, \"label\", None) or \"\").strip()\n         if not label:\n-            label = input(f\"Label (optional, default: {default_label}): \").strip() or default_label\n+            if sys.stdin.isatty():\n+                label = input(f\"Label (optional, default: {default_label}): \").strip() or default_label\n+            else:\n+                label = default_label\n         entry = PooledCredential(\n             provider=provider,\n             id=uuid.uuid4().hex[:6],"]},{"file":"hermes_cli/main.py","hunks":["@@ -6325,8 +6325,13 @@ def cmd_acp(args):\n             sys.stderr = _io.StringIO()\n             args = parser.parse_args(_processed_argv)\n             sys.stderr = _saved_stderr\n-        except SystemExit:\n+        except SystemExit as exc:\n             sys.stderr = _saved_stderr\n+            # Help/version flags (exit code 0) already printed output —\n+            # re-raise immediately to avoid a second parse_args printing\n+            # the same help text again (#10230).\n+            if exc.code == 0:\n+                raise\n             # Subcommand name was consumed as a flag value (e.g. -c model).\n             # Fall back to optional subparsers so argparse handles it normally.\n             subparsers.required = False"]},{"file":"hermes_cli/model_switch.py","hunks":["@@ -786,7 +786,8 @@ def list_authenticated_providers(\n     from hermes_cli.models import OPENROUTER_MODELS, _PROVIDER_MODELS\n \n     results: List[dict] = []\n-    seen_slugs: set = set()\n+    seen_slugs: set = set()  # lowercase-normalized to catch case variants (#9545)\n+    seen_mdev_ids: set = set()  # prevent duplicate entries for aliases (e.g. kimi-coding + kimi-coding-cn)\n \n     data = fetch_models_dev()\n ","@@ -799,6 +800,11 @@ def list_authenticated_providers(\n \n     # --- 1. Check Hermes-mapped providers ---\n     for hermes_id, mdev_id in PROVIDER_TO_MODELS_DEV.items():\n+        # Skip aliases that map to the same models.dev provider (e.g.\n+        # kimi-coding and kimi-coding-cn both → kimi-for-coding).\n+        # The first one with valid credentials wins (#10526).\n+        if mdev_id in seen_mdev_ids:\n+            continue\n         pdata = data.get(mdev_id)\n         if not isinstance(pdata, dict):\n             continue","@@ -837,7 +843,8 @@ def list_authenticated_providers(\n             \"total_models\": total,\n             \"source\": \"built-in\",\n         })\n-        seen_slugs.add(slug)\n+        seen_slugs.add(slug.lower())\n+        seen_mdev_ids.add(mdev_id)\n \n     # --- 2. Check Hermes-only providers (nous, openai-codex, copilot, opencode-go) ---\n     from hermes_cli.providers import HERMES_OVERLAYS","@@ -849,12 +856,12 @@ def list_authenticated_providers(\n     _mdev_to_hermes = {v: k for k, v in PROVIDER_TO_MODELS_DEV.items()}\n \n     for pid, overlay in HERMES_OVERLAYS.items():\n-        if pid in seen_slugs:\n+        if pid.lower() in seen_slugs:\n             continue\n \n         # Resolve Hermes slug — e.g. \"github-copilot\" → \"copilot\"\n         hermes_slug = _mdev_to_hermes.get(pid, pid)\n-        if hermes_slug in seen_slugs:\n+        if hermes_slug.lower() in seen_slugs:\n             continue\n \n         # Check if credentials exist","@@ -935,8 +942,8 @@ def list_authenticated_providers(\n             \"total_models\": total,\n             \"source\": \"hermes\",\n         })\n-        seen_slugs.add(pid)\n-        seen_slugs.add(hermes_slug)\n+        seen_slugs.add(pid.lower())\n+        seen_slugs.add(hermes_slug.lower())\n \n     # --- 2b. Cross-check canonical provider list ---\n     # Catches providers that are in CANONICAL_PROVIDERS but weren't found","@@ -948,7 +955,7 @@ def list_authenticated_providers(\n         _canon_provs = []\n \n     for _cp in _canon_provs:\n-        if _cp.slug in seen_slugs:\n+        if _cp.slug.lower() in seen_slugs:\n             continue\n \n         # Check credentials via PROVIDER_REGISTRY (auth.py)","@@ -995,7 +1002,7 @@ def list_authenticated_providers(\n             \"total_models\": _cp_total,\n             \"source\": \"canonical\",\n         })\n-        seen_slugs.add(_cp.slug)\n+        seen_slugs.add(_cp.slug.lower())\n \n     # --- 3. User-defined endpoints from config ---\n     if user_providers and isinstance(user_providers, dict):","@@ -1068,7 +1075,7 @@ def list_authenticated_providers(\n                 groups[slug][\"models\"].append(default_model)\n \n         for slug, grp in groups.items():\n-            if slug in seen_slugs:\n+            if slug.lower() in seen_slugs:\n                 continue\n             results.append({\n                 \"slug\": slug,","@@ -1080,7 +1087,7 @@ def list_authenticated_providers(\n                 \"source\": \"user-config\",\n                 \"api_url\": grp[\"api_url\"],\n             })\n-            seen_slugs.add(slug)\n+            seen_slugs.add(slug.lower())\n \n     # Sort: current provider first, then by model count descending\n     results.sort(key=lambda r: (not r[\"is_current\"], -r[\"total_models\"]))\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}