From b5f96ab3e594dfffda532098fe5b11cac019b969 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:03:18 -0700 Subject: [PATCH] refactor(hermes_cli): AST-neutral layout compaction of provider/platform setup flows --- hermes_cli/main_platform_setup.py | 74 +++----- hermes_cli/main_provider_setup.py | 213 ++++++------------------ hermes_cli/model_setup_flows.py | 75 +++------ hermes_cli/model_setup_flows_azure.py | 3 +- hermes_cli/model_setup_flows_bedrock.py | 9 +- hermes_cli/model_setup_flows_common.py | 6 +- hermes_cli/model_setup_flows_custom.py | 6 +- 7 files changed, 110 insertions(+), 276 deletions(-) diff --git a/hermes_cli/main_platform_setup.py b/hermes_cli/main_platform_setup.py index 0e31a30f00..ed0dd99133 100644 --- a/hermes_cli/main_platform_setup.py +++ b/hermes_cli/main_platform_setup.py @@ -34,8 +34,7 @@ def cmd_whatsapp(args): print(" 1. Separate bot number (recommended)") print(" People message the bot's number directly — cleanest experience.") print( - " Requires a second phone number with WhatsApp installed on a device." - ) + " Requires a second phone number with WhatsApp installed on a device.") print() print(" 2. Personal number (self-chat)") print(" You message yourself to talk to the agent.") @@ -71,8 +70,7 @@ def cmd_whatsapp(args): else: wa_mode = current_mode mode_label = ( - "separate bot number" if wa_mode == "bot" else "personal number (self-chat)" - ) + "separate bot number" if wa_mode == "bot" else "personal number (self-chat)") print(f"\n✓ Mode: {mode_label}") # ── Step 2: Mode is selected, will enable WhatsApp only after pairing ── @@ -99,8 +97,7 @@ def cmd_whatsapp(args): if response.lower() in {"y", "yes"}: if wa_mode == "bot": phone = line_input( - " Phone numbers that can message the bot (comma-separated): " - ).strip() + " Phone numbers that can message the bot (comma-separated): ").strip() else: phone = line_input(" Your phone number (e.g. 15551234567): ").strip() if phone: @@ -111,8 +108,7 @@ def cmd_whatsapp(args): if wa_mode == "bot": print(" Who should be allowed to message the bot?") phone = line_input( - " Phone numbers (comma-separated, or * for anyone): " - ).strip() + " Phone numbers (comma-separated, or * for anyone): ").strip() else: phone = line_input(" Your phone number (e.g. 15551234567): ").strip() if phone: @@ -132,8 +128,7 @@ def cmd_whatsapp(args): if not (bridge_dir / "node_modules").exists(): print( - "\n→ Installing WhatsApp bridge dependencies (this can take a few minutes)..." - ) + "\n→ Installing WhatsApp bridge dependencies (this can take a few minutes)...") npm = find_node_executable("npm") if not npm: print(" ✗ npm not found on PATH — install Node.js first") @@ -147,8 +142,7 @@ def cmd_whatsapp(args): text=True, encoding="utf-8", errors="replace", - env=with_hermes_node_path(), - ) + env=with_hermes_node_path()) except KeyboardInterrupt: print("\n ✗ Install cancelled") return @@ -170,8 +164,7 @@ def cmd_whatsapp(args): print("✓ Existing WhatsApp session found") try: response = input( - "\n Re-pair? This will clear the existing session. [y/N] " - ).strip() + "\n Re-pair? This will clear the existing session. [y/N] ").strip() except (EOFError, KeyboardInterrupt): response = "n" if response.lower() in {"y", "yes"}: @@ -209,11 +202,9 @@ def cmd_whatsapp(args): str(bridge_script), "--pair-only", "--session", - str(session_dir), - ], + str(session_dir)], cwd=str(bridge_dir), - env=with_hermes_node_path(), - ) + env=with_hermes_node_path()) except KeyboardInterrupt: pass @@ -290,8 +281,7 @@ def cmd_sync(args): "\n" "Shared with your team:\n" " propose Share a skill with your organisation", - file=sys.stderr, - ) + file=sys.stderr) return 1 if sub == "device": @@ -308,8 +298,7 @@ def cmd_sync(args): print( "New commits from this device will use this label; existing " "commits keep their previous one.", - file=sys.stderr, - ) + file=sys.stderr) return 0 # No --name: print the current (creating a default on first use). print(ssc.stable_device_id()) @@ -331,8 +320,7 @@ def cmd_sync(args): print( f"Shared '{name}' with your organisation — an admin needs to " f"approve it (proposal #{result.get('proposal_id')}). It is " - f"not live for the team until then." - ) + f"not live for the team until then.") else: print(f"Added '{name}' to your organisation's shared skills.") return 0 @@ -346,8 +334,7 @@ def cmd_sync(args): f"'{skill}' is not sync-eligible (bundled, hub-installed, " f"external, or not found). Only agent-created / user-authored " f"skills under ~/.hermes/skills/ can sync.", - file=sys.stderr, - ) + file=sys.stderr) return 1 set_sync(skill, sub == "enable") print(f"sync {'enabled' if sub == 'enable' else 'disabled'} for '{skill}'.") @@ -365,41 +352,34 @@ def cmd_sync(args): f"\nOrg skills: {n} shared skill(s) from your organisation " f"(your role: {status.get('org_role')}). They load alongside " f"your own, labeled by origin, and you can edit them.", - file=sys.stderr, - ) + file=sys.stderr) if modified: print( f" {len(modified)} with local edits not yet shared: " f"{', '.join(modified)}\n" f" Share them back with `hermes sync propose `. " f"Org updates will not overwrite them.", - file=sys.stderr, - ) + file=sys.stderr) elif status.get("logged_in"): print( "\nOrg skills: not applicable — this account isn't a member " "of a shared organisation.", - file=sys.stderr, - ) + file=sys.stderr) if not status.get("logged_in"): print("\nNot logged into Nous Portal — sync is inert.", file=sys.stderr) elif not status.get("nous_admin"): print( - "\nSync is not enabled for your account yet.", - file=sys.stderr, - ) + "\nSync is not enabled for your account yet.", file=sys.stderr) elif not status.get("feature_enabled"): print( "\nSync feature is off for this instance (set HERMES_SYNC_ENABLED=1 " "or config.yaml sync.enabled: true). Sync is inert.", - file=sys.stderr, - ) + file=sys.stderr) elif not status.get("base_url"): print( "\nNo sync base URL configured (config.yaml sync.base_url or " "HERMES_SYNC_BASE_URL). Sync is inert.", - file=sys.stderr, - ) + file=sys.stderr) return 0 # pull / push / now — enforce the gate up front with a clear message. @@ -410,16 +390,13 @@ def cmd_sync(args): return 1 if not identity.get("nous_admin"): print( - "sync unavailable: not enabled for your account yet.", - file=sys.stderr, - ) + "sync unavailable: not enabled for your account yet.", file=sys.stderr) return 1 if not ssc.resolve_sync_base_url(): print( "sync inert: no sync base URL configured (config.yaml sync.base_url " "or HERMES_SYNC_BASE_URL).", - file=sys.stderr, - ) + file=sys.stderr) return 1 try: @@ -433,8 +410,7 @@ def cmd_sync(args): print( f"org: refreshed {n} shared skill(s) from your " f"organisation.", - file=sys.stderr, - ) + file=sys.stderr) clashes = org_result.get("conflicted") or [] if clashes: print( @@ -444,8 +420,7 @@ def cmd_sync(args): f" Your local version is intact. Review it, then " f"either propose it or delete the local copy and pull " f"again to take the org version.", - file=sys.stderr, - ) + file=sys.stderr) elif sub == "push": result = ssc.push_skills(identity=identity, message="hermes sync push") elif sub == "now": @@ -481,8 +456,7 @@ def cmd_slack(args): " command registered as a native slash\n" "\n" "Run `hermes slack manifest -h` for details.", - file=sys.stderr, - ) + file=sys.stderr) return 1 if sub == "manifest": diff --git a/hermes_cli/main_provider_setup.py b/hermes_cli/main_provider_setup.py index 620cdb9d9f..e2a4799a2a 100644 --- a/hermes_cli/main_provider_setup.py +++ b/hermes_cli/main_provider_setup.py @@ -32,8 +32,7 @@ _GENERIC_API_KEY_PROVIDERS = frozenset({ "openai-api", "gemini", "deepseek", "xai", "zai", "kimi-coding-cn", "minimax", "minimax-cn", "kilocode", "opencode-zen", "opencode-go", "opencode-free", "alibaba", "huggingface", "xiaomi", "arcee", "gmi", - "nvidia", "ollama-cloud", "tencent-tokenhub", "tencent-tokenplan", "lmstudio", -}) + "nvidia", "ollama-cloud", "tencent-tokenhub", "tencent-tokenplan", "lmstudio"}) def _clear_stale_openai_base_url(): @@ -62,8 +61,7 @@ def _clear_stale_openai_base_url(): print( f"Cleared stale OPENAI_BASE_URL from .env (was: {stale_url[:40]}...)" if len(stale_url) > 40 - else f"Cleared stale OPENAI_BASE_URL from .env (was: {stale_url})" - ) + else f"Cleared stale OPENAI_BASE_URL from .env (was: {stale_url})") # (task_key, display_name, short_description) @@ -80,8 +78,7 @@ _AUX_TASKS: list[tuple[str, str, str]] = [ ("triage_specifier", "Triage specifier", "kanban spec fleshing"), ("kanban_decomposer", "Kanban decomposer", "task decomposition"), ("profile_describer", "Profile describer", "auto profile descriptions"), - ("curator", "Curator", "skill-usage review pass"), -] + ("curator", "Curator", "skill-usage review pass")] # Special non-auxiliary task surfaced in the same picker: subagent delegation. @@ -154,8 +151,7 @@ def _delegation_cfg_as_task(cfg: dict) -> dict: "provider": str(d.get("provider") or "").strip(), "model": str(d.get("model") or "").strip(), "base_url": str(d.get("base_url") or "").strip(), - "api_key": str(d.get("api_key") or "").strip(), - } + "api_key": str(d.get("api_key") or "").strip()} def _aux_task_display_name(task: str) -> str: @@ -166,13 +162,7 @@ def _aux_task_display_name(task: str) -> str: def _save_aux_choice( - task: str, - *, - provider: str, - model: str = "", - base_url: str = "", - api_key: str = "", -) -> None: + task: str, *, provider: str, model: str = "", base_url: str = "", api_key: str = "") -> None: """Persist an auxiliary task's provider/model to config.yaml. Only writes the four routing fields — timeout, download_timeout, and any @@ -287,8 +277,7 @@ def _aux_config_menu() -> None: # Build the task menu with current settings inline all_tasks = _all_aux_tasks() menu_tasks = all_tasks + [ - (_DELEGATION_TASK_KEY, _DELEGATION_TASK_NAME, _DELEGATION_TASK_DESC) - ] + (_DELEGATION_TASK_KEY, _DELEGATION_TASK_NAME, _DELEGATION_TASK_DESC)] name_col = max(len(name) for _, name, _ in menu_tasks) + 2 desc_col = max(len(desc) for _, _, desc in menu_tasks) + 4 entries: list[tuple[str, str]] = [] @@ -297,20 +286,16 @@ def _aux_config_menu() -> None: task_cfg = _delegation_cfg_as_task(cfg) else: task_cfg = ( - aux.get(task_key, {}) if isinstance(aux.get(task_key), dict) else {} - ) + aux.get(task_key, {}) if isinstance(aux.get(task_key), dict) else {}) current = _format_aux_current(task_cfg) label = ( - f"{name.ljust(name_col)}{('(' + desc + ')').ljust(desc_col)}{current}" - ) + f"{name.ljust(name_col)}{('(' + desc + ')').ljust(desc_col)}{current}") entries.append((task_key, label)) entries.append(("__reset__", "Reset all to auto")) entries.append(("__back__", "Back")) idx = _prompt_provider_choice( - [label for _, label in entries], - default=0, - ) + [label for _, label in entries], default=0) if idx is None: return key = entries[idx][0] @@ -360,8 +345,7 @@ def _aux_select_for_task(task: str) -> None: providers = build_aux_picker_rows( current_provider=current_provider, current_model=current_model, - current_base_url=current_base_url, - ) + current_base_url=current_base_url) except Exception as exc: print(f"Could not detect authenticated providers: {exc}") providers = [] @@ -369,22 +353,14 @@ def _aux_select_for_task(task: str) -> None: entries: list[tuple[str, str, list[str]]] = [] # (slug, label, models) # "auto" always first auto_marker = ( - " ← current" if current_provider == "auto" and not current_base_url else "" - ) + " ← current" if current_provider == "auto" and not current_base_url else "") auto_label = ( - "auto (inherit main agent)" - if task == _DELEGATION_TASK_KEY - else "auto (recommended)" - ) + "auto (inherit main agent)" if task == _DELEGATION_TASK_KEY else "auto (recommended)") entries.append(("__auto__", f"{auto_label}{auto_marker}", [])) entries.extend( format_aux_picker_entries( - providers, - current_provider=current_provider, - current_base_url=current_base_url, - ) - ) + providers, current_provider=current_provider, current_base_url=current_base_url)) # Custom endpoint (raw base_url) custom_marker = " ← current" if current_base_url else "" @@ -417,11 +393,7 @@ def _aux_select_for_task(task: str) -> None: def _aux_flow_provider_model( - task: str, - provider_slug: str, - curated_models: list, - current_model: str = "", -) -> None: + task: str, provider_slug: str, curated_models: list, current_model: str = "") -> None: """Prompt for a model under an already-authenticated provider, save to aux.""" from hermes_cli.auth import _prompt_model_selection from hermes_cli.models import get_pricing_for_provider @@ -451,18 +423,14 @@ def _aux_flow_provider_model( selected = val or "" else: selected = _prompt_model_selection( - model_list, - current_model=current_model, - pricing=pricing, - confirm_provider=provider_slug, + model_list, current_model=current_model, pricing=pricing, confirm_provider=provider_slug ) if selected is None: print("No change.") return _save_aux_choice( - task, provider=provider_slug, model=selected or "", base_url="", api_key="" - ) + task, provider=provider_slug, model=selected or "", base_url="", api_key="") if selected: print(f"{display_name}: {provider_slug} · {selected}") else: @@ -483,8 +451,7 @@ def _aux_flow_custom_endpoint(task: str, task_cfg: dict) -> None: print() try: url_prompt = ( - f"Base URL [{current_base_url}]: " if current_base_url else "Base URL: " - ) + f"Base URL [{current_base_url}]: " if current_base_url else "Base URL: ") url = line_input(url_prompt).strip() except (KeyboardInterrupt, EOFError): print() @@ -497,8 +464,7 @@ def _aux_flow_custom_endpoint(task: str, task_cfg: dict) -> None: model_prompt = ( f"Model slug (optional) [{current_model}]: " if current_model - else "Model slug (optional): " - ) + else "Model slug (optional): ") model = line_input(model_prompt).strip() except (KeyboardInterrupt, EOFError): print() @@ -506,19 +472,13 @@ def _aux_flow_custom_endpoint(task: str, task_cfg: dict) -> None: model = model or current_model try: api_key = masked_secret_prompt( - "API key (optional, blank = use OPENAI_API_KEY): " - ).strip() + "API key (optional, blank = use OPENAI_API_KEY): ").strip() except (KeyboardInterrupt, EOFError): print() return _save_aux_choice( - task, - provider="custom", - model=model, - base_url=url, - api_key=api_key, - ) + task, provider="custom", model=model, base_url=url, api_key=api_key) short_url = url.replace("https://", "").replace("http://", "").rstrip("/") print(f"{display_name}: custom ({short_url})" + (f" · {model}" if model else "")) @@ -563,9 +523,7 @@ def _prompt_provider_choice(choices, *, default=0, title="Select provider:"): _DEFAULT_QWEN_PORTAL_MODELS = [ - "qwen3-coder-plus", - "qwen3-coder", -] + "qwen3-coder-plus", "qwen3-coder"] def _prompt_custom_api_mode_selection(base_url: str, current_api_mode: str = "") -> Optional[str]: @@ -583,24 +541,19 @@ def _prompt_custom_api_mode_selection(base_url: str, current_api_mode: str = "") ( "", "Auto-detect", - "Use Hermes URL heuristics; best for standard OpenAI-compatible endpoints.", - ), + "Use Hermes URL heuristics; best for standard OpenAI-compatible endpoints."), ( "chat_completions", "Chat Completions", - "Use /chat/completions for standard OpenAI-compatible servers.", - ), + "Use /chat/completions for standard OpenAI-compatible servers."), ( "codex_responses", "Responses / Codex", - "Use /responses for Codex-compatible tool-calling backends.", - ), + "Use /responses for Codex-compatible tool-calling backends."), ( "anthropic_messages", "Anthropic Messages", - "Use /v1/messages for Anthropic-compatible endpoints.", - ), - ] + "Use /v1/messages for Anthropic-compatible endpoints.")] print() print("Select API compatibility mode:") @@ -616,8 +569,7 @@ def _prompt_custom_api_mode_selection(base_url: str, current_api_mode: str = "") try: raw = input( - "Choice [1-4, Enter to keep current/detected]: " - ).strip().lower() + "Choice [1-4, Enter to keep current/detected]: ").strip().lower() except (KeyboardInterrupt, EOFError): print("\nCancelled.") raise @@ -681,9 +633,7 @@ def _custom_provider_base_url_config_value(provider_info, resolved_base_url=""): def _save_custom_provider( - base_url, api_key="", model="", context_length=None, name=None, api_mode=None, - key_env="" -): + base_url, api_key="", model="", context_length=None, name=None, api_mode=None, key_env=""): """Save a custom endpoint to custom_providers in config.yaml. Deduplicates by base_url — if the URL already exists, updates the @@ -781,11 +731,7 @@ def _remove_custom_provider(config): from hermes_cli.curses_ui import curses_radiolist idx = curses_radiolist( - "Select provider to remove:", - list(choices), - selected=0, - cancel_returns=-1, - ) + "Select provider to remove:", list(choices), selected=0, cancel_returns=-1) print() if idx < 0: idx = None @@ -807,18 +753,14 @@ def _remove_custom_provider(config): cfg["custom_providers"] = providers save_config(cfg) removed_name = ( - removed.get("name", "unnamed") if isinstance(removed, dict) else str(removed) - ) + removed.get("name", "unnamed") if isinstance(removed, dict) else str(removed)) print(f'✅ Removed "{removed_name}" from custom providers.') def _prompt_reasoning_effort_selection(efforts, current_effort=""): """Prompt for a reasoning effort. Returns effort, 'none', or None to keep current.""" deduped = list( - dict.fromkeys( - str(effort).strip().lower() for effort in efforts if str(effort).strip() - ) - ) + dict.fromkeys( str(effort).strip().lower() for effort in efforts if str(effort).strip() )) canonical_order = ("minimal", "low", "medium", "high", "xhigh", "max", "ultra") ordered = [effort for effort in canonical_order if effort in deduped] ordered.extend(effort for effort in deduped if effort not in canonical_order) @@ -849,11 +791,7 @@ def _prompt_reasoning_effort_selection(efforts, current_effort=""): choices.append(disable_label) choices.append(skip_label) idx = curses_radiolist( - "Select reasoning effort:", - choices, - selected=default_idx, - cancel_returns=-1, - ) + "Select reasoning effort:", choices, selected=default_idx, cancel_returns=-1) if idx < 0: return None print() @@ -893,11 +831,7 @@ def _prompt_reasoning_effort_selection(efforts, current_effort=""): def _prompt_api_key( - pconfig, - existing_key: str, - provider_id: str = "", - existing_source: str = "", -) -> tuple: + pconfig, existing_key: str, provider_id: str = "", existing_source: str = "") -> tuple: """Shared API-key entry point for ``hermes setup`` / ``hermes model``. Handles both first-time entry and the already-configured case. When a key @@ -955,8 +889,7 @@ def _prompt_api_key( menu = ( " [K]eep / [R]eplace (default K): " if pool_backed - else " [K]eep / [R]eplace / [C]lear (default K): " - ) + else " [K]eep / [R]eplace / [C]lear (default K): ") try: choice = input(menu).strip().lower() except (KeyboardInterrupt, EOFError): @@ -977,8 +910,7 @@ def _prompt_api_key( if choice.startswith("c") and not pool_backed: save_env_value(key_env, "") print( - f" API key cleared. Re-run `hermes setup` to configure {pconfig.name} again." - ) + f" API key cleared. Re-run `hermes setup` to configure {pconfig.name} again.") return "", True # Keep (default, or any other input) @@ -996,28 +928,18 @@ def _infer_stepfun_region(base_url: str) -> str: def _stepfun_base_url_for_region(region: str) -> str: from hermes_cli.auth import ( - STEPFUN_STEP_PLAN_CN_BASE_URL, - STEPFUN_STEP_PLAN_INTL_BASE_URL, - ) + STEPFUN_STEP_PLAN_CN_BASE_URL, STEPFUN_STEP_PLAN_INTL_BASE_URL) return ( - STEPFUN_STEP_PLAN_CN_BASE_URL - if region == "china" - else STEPFUN_STEP_PLAN_INTL_BASE_URL - ) + STEPFUN_STEP_PLAN_CN_BASE_URL if region == "china" else STEPFUN_STEP_PLAN_INTL_BASE_URL) def _run_anthropic_oauth_flow(save_env_value): """Run the Claude OAuth setup-token flow. Returns True if credentials were saved.""" from agent.anthropic_adapter import ( - run_oauth_setup_token, - read_claude_code_credentials, - is_claude_code_token_valid, - ) + run_oauth_setup_token, read_claude_code_credentials, is_claude_code_token_valid) from hermes_cli.config import ( - save_anthropic_oauth_token, - use_anthropic_claude_code_credentials, - ) + save_anthropic_oauth_token, use_anthropic_claude_code_credentials) def _activate_claude_code_credentials_if_available() -> bool: try: @@ -1025,8 +947,7 @@ def _run_anthropic_oauth_flow(save_env_value): except Exception: creds = None if creds and ( - is_claude_code_token_valid(creds) or bool(creds.get("refreshToken")) - ): + is_claude_code_token_valid(creds) or bool(creds.get("refreshToken"))): use_anthropic_claude_code_credentials(save_fn=save_env_value) print(" ✓ Claude Code credentials linked.") from hermes_constants import display_hermes_home as _dhh_fn @@ -1058,8 +979,7 @@ def _run_anthropic_oauth_flow(save_env_value): try: manual_token = masked_secret_prompt( - " Paste setup-token (or Enter to cancel): " - ).strip() + " Paste setup-token (or Enter to cancel): ").strip() except (KeyboardInterrupt, EOFError): print() return False @@ -1118,12 +1038,7 @@ def _named_custom_provider_map(cfg) -> dict[str, dict[str, str]]: raw_cfg = read_raw_config() def _record_raw( - name: str, - provider_key: str, - model: str, - api_key: str, - base_url: str, - ) -> None: + name: str, provider_key: str, model: str, api_key: str, base_url: str) -> None: template = str(api_key or "").strip() base_template = str(base_url or "").strip() name = str(name or "").strip() @@ -1138,8 +1053,7 @@ def _named_custom_provider_map(cfg) -> dict[str, dict[str, str]]: identities.extend(((name.lower(),), (name.lower(), model))) if provider_key: identities.extend( - ((provider_key.lower(),), (provider_key.lower(), model)) - ) + ((provider_key.lower(),), (provider_key.lower(), model))) if "${" in template: for identity in identities: raw_api_key_refs.setdefault(identity, template) @@ -1159,8 +1073,7 @@ def _named_custom_provider_map(cfg) -> dict[str, dict[str, str]]: raw_entry.get("api_key", ""), raw_entry.get("base_url", "") or raw_entry.get("url", "") - or raw_entry.get("api", ""), - ) + or raw_entry.get("api", "")) raw_providers = raw_cfg.get("providers") if isinstance(raw_providers, dict): for raw_key, raw_entry in raw_providers.items(): @@ -1173,24 +1086,15 @@ def _named_custom_provider_map(cfg) -> dict[str, dict[str, str]]: raw_entry.get("api_key", ""), raw_entry.get("base_url", "") or raw_entry.get("url", "") - or raw_entry.get("api", ""), - ) + or raw_entry.get("api", "")) def _lookup_ref( - refs: dict[tuple, str], - name: str, - provider_key: str, - model: str, - ) -> str: + refs: dict[tuple, str], name: str, provider_key: str, model: str) -> str: name_lc = str(name or "").strip().lower() pkey_lc = str(provider_key or "").strip().lower() model = str(model or "").strip() for identity in ( - (pkey_lc, model), - (pkey_lc,), - (name_lc, model), - (name_lc,), - ): + (pkey_lc, model), (pkey_lc,), (name_lc, model), (name_lc,)): if identity[0] and identity in refs: return refs[identity] return "" @@ -1218,12 +1122,9 @@ def _named_custom_provider_map(cfg) -> dict[str, dict[str, str]]: "api_mode": entry.get("api_mode", ""), "provider_key": provider_key, "api_key_ref": _lookup_ref( - raw_api_key_refs, name, provider_key, entry.get("model", "") - ), + raw_api_key_refs, name, provider_key, entry.get("model", "")), "base_url_ref": _lookup_ref( - raw_base_url_refs, name, provider_key, entry.get("model", "") - ), - } + raw_base_url_refs, name, provider_key, entry.get("model", ""))} return custom_provider_map @@ -1231,8 +1132,7 @@ def _build_provider_picker_rows( config: dict, active: str, provider_labels: dict[str, str], - custom_provider_map: dict[str, dict[str, str]], -) -> tuple[list[tuple[str, str, list[str]]], int]: + custom_provider_map: dict[str, dict[str, str]]) -> tuple[list[tuple[str, str, list[str]]], int]: """Rows for the ``hermes model`` provider picker plus the pre-selected index. Canonical providers are folded into display groups (see PROVIDER_GROUPS in @@ -1243,11 +1143,7 @@ def _build_provider_picker_rows( the CLI hides the same providers the gateway/TUI pickers do. """ from hermes_cli.models import ( - CANONICAL_PROVIDERS, - _PROVIDER_ALIASES, - group_providers, - provider_group_for_slug, - ) + CANONICAL_PROVIDERS, _PROVIDER_ALIASES, group_providers, provider_group_for_slug) _custom_provider_map = custom_provider_map canonical_descs = {p.slug: p.tui_desc for p in CANONICAL_PROVIDERS} @@ -1259,8 +1155,7 @@ def _build_provider_picker_rows( _cli_excluded = { str(p).strip().lower() for p in (config.get("model_catalog", {}) or {}).get("excluded_providers") or [] - if p - } + if p} if _cli_excluded: _alias_to_canon = _PROVIDER_ALIASES _names_for: dict[str, set[str]] = {} @@ -1270,8 +1165,7 @@ def _build_provider_picker_rows( _names_for.setdefault(_canon, {_canon.lower()}).add(_alias.lower()) _visible_slugs = [ p.slug for p in CANONICAL_PROVIDERS - if not _names_for.get(p.slug, {p.slug.lower()}) & _cli_excluded - ] + if not _names_for.get(p.slug, {p.slug.lower()}) & _cli_excluded] else: _visible_slugs = [p.slug for p in CANONICAL_PROVIDERS] grouped_rows = group_providers(_visible_slugs) @@ -1320,8 +1214,7 @@ def _build_provider_picker_rows( ordered.append(("custom", "Custom endpoint (enter URL manually)", [])) _has_saved_custom_list = isinstance(config.get("custom_providers"), list) and bool( - config.get("custom_providers") - ) + config.get("custom_providers")) if _has_saved_custom_list: ordered.append(("remove-custom", "Remove a saved custom provider", [])) ordered.append(("aux-config", "Configure auxiliary models...", [])) diff --git a/hermes_cli/model_setup_flows.py b/hermes_cli/model_setup_flows.py index 4e8b4e3786..695ba3956f 100644 --- a/hermes_cli/model_setup_flows.py +++ b/hermes_cli/model_setup_flows.py @@ -38,22 +38,18 @@ from hermes_cli.model_setup_flows_common import ( # noqa: F401 _prune_replaced_custom_model_config_credentials, _run_login, _say, - _show_curated, -) + _show_curated) from hermes_cli.model_setup_flows_custom import ( # noqa: F401 _model_flow_custom, - _model_flow_named_custom, -) + _model_flow_named_custom) from hermes_cli.model_setup_flows_azure import ( # noqa: F401 - _model_flow_azure_foundry, -) + _model_flow_azure_foundry) from hermes_cli.model_setup_flows_bedrock import ( # noqa: F401 BEDROCK_GEO_PREFIXES, bedrock_region_geo_prefix, bedrock_model_routable_from_region, _model_flow_bedrock_api_key, - _model_flow_bedrock, -) + _model_flow_bedrock) def _model_flow_openrouter(config, current_model=""): @@ -64,8 +60,7 @@ def _model_flow_openrouter(config, current_model=""): # OpenRouter isn't in PROVIDER_REGISTRY so we synthesize a minimal pconfig. pconfig = ProviderConfig(id="openrouter", name="OpenRouter", auth_type="api_key", api_key_env_vars=("OPENROUTER_API_KEY",)) existing_key, _resolved, abort = _ensure_flow_api_key( - "openrouter", pconfig, missing_hint=("Get one at: https://openrouter.ai/keys", "") - ) + "openrouter", pconfig, missing_hint=("Get one at: https://openrouter.ai/keys", "")) if abort: return @@ -76,8 +71,7 @@ def _model_flow_openrouter(config, current_model=""): pricing = get_pricing_for_provider("openrouter", force_refresh=True) selected = _prompt_model_selection( openrouter_models, current_model=current_model, pricing=pricing, confirm_provider="openrouter", - confirm_base_url=OPENROUTER_BASE_URL, confirm_api_key=_resolved or existing_key, - ) + confirm_base_url=OPENROUTER_BASE_URL, confirm_api_key=_resolved or existing_key) _finish_model(selected, "openrouter", f"Default model set to: {selected} (via OpenRouter)", base_url=OPENROUTER_BASE_URL, api_mode="chat_completions") @@ -185,8 +179,7 @@ def _nous_login_args(args) -> argparse.Namespace: no_browser=bool(getattr(args, "no_browser", False)), timeout=getattr(args, "timeout", None) or 15.0, ca_bundle=getattr(args, "ca_bundle", None), - insecure=bool(getattr(args, "insecure", False)), - ) + insecure=bool(getattr(args, "insecure", False))) def _nous_model_catalog(free_tier: bool, portal_url: str, model_ids: list, pricing: dict): @@ -197,8 +190,7 @@ def _nous_model_catalog(free_tier: bool, portal_url: str, model_ids: list, prici """ from hermes_cli.models import ( nous_policy_allowed_ids, partition_nous_models_by_tier, restrict_to_nous_policy, - union_with_portal_free_recommendations, union_with_portal_paid_recommendations, - ) + union_with_portal_free_recommendations, union_with_portal_paid_recommendations) # Free users: augment with the Portal's freeRecommendedModels (so newly launched # free models appear before this build's curated list catches up), then partition @@ -324,8 +316,7 @@ def _model_flow_nous(config, current_model="", args=None): selected = _prompt_model_selection( model_ids, current_model=current_model, pricing=pricing, unavailable_models=unavailable_models, portal_url=_nous_portal_url, unavailable_message=unavailable_message, confirm_provider="nous", - confirm_base_url=creds.get("base_url", ""), confirm_api_key=creds.get("api_key", ""), - ) + confirm_base_url=creds.get("base_url", ""), confirm_api_key=creds.get("api_key", "")) if not selected: print("No change.") return @@ -393,8 +384,7 @@ def _model_flow_openai_codex(config, current_model=""): codex_models = get_codex_model_ids(access_token=_codex_token) selected = _prompt_model_selection( codex_models, current_model=current_model, confirm_provider="openai-codex", - confirm_base_url=DEFAULT_CODEX_BASE_URL, confirm_api_key=_codex_token or "", - ) + confirm_base_url=DEFAULT_CODEX_BASE_URL, confirm_api_key=_codex_token or "") _activate_provider_model(selected, "openai-codex", DEFAULT_CODEX_BASE_URL, f"Default model set to: {selected} (via OpenAI Codex)") @@ -403,15 +393,13 @@ def _model_flow_xai_oauth(_config, current_model="", *, args=None): """xAI Grok OAuth (SuperGrok / Premium+) provider: ensure logged in, then pick model.""" from hermes_cli.auth import ( get_xai_oauth_auth_status, _prompt_model_selection, resolve_xai_oauth_runtime_credentials, _login_xai_oauth, - DEFAULT_XAI_OAUTH_BASE_URL, PROVIDER_REGISTRY, - ) + DEFAULT_XAI_OAUTH_BASE_URL, PROVIDER_REGISTRY) from hermes_cli.models import provider_model_ids login_args = argparse.Namespace(no_browser=bool(getattr(args, "no_browser", False)), timeout=getattr(args, "timeout", None)) if not _oauth_gate( bool(get_xai_oauth_auth_status().get("logged_in")), "xAI Grok OAuth (SuperGrok / Premium+)", _login_xai_oauth, - login_args, PROVIDER_REGISTRY["xai-oauth"], fresh_name="xAI OAuth", - ): + login_args, PROVIDER_REGISTRY["xai-oauth"], fresh_name="xAI OAuth"): return # ``resolve_xai_oauth_runtime_credentials`` only reads the auth.json singleton, @@ -466,8 +454,7 @@ def _model_flow_minimax_oauth(config, current_model="", args=None): """MiniMax OAuth provider: ensure logged in, then pick model.""" from hermes_cli.auth import ( get_provider_auth_state, _prompt_model_selection, resolve_minimax_oauth_runtime_credentials, AuthError, - format_auth_error, _login_minimax_oauth, PROVIDER_REGISTRY, - ) + format_auth_error, _login_minimax_oauth, PROVIDER_REGISTRY) state = get_provider_auth_state("minimax-oauth") if not state or not state.get("access_token"): @@ -475,8 +462,7 @@ def _model_flow_minimax_oauth(config, current_model="", args=None): mock_args = argparse.Namespace( region=getattr(args, "region", None) or "global", no_browser=bool(getattr(args, "no_browser", False)), - timeout=getattr(args, "timeout", None) or 15.0, - ) + timeout=getattr(args, "timeout", None) or 15.0) if not _run_login(_login_minimax_oauth, mock_args, PROVIDER_REGISTRY["minimax-oauth"]): return @@ -567,8 +553,7 @@ def _model_flow_copilot(config, current_model=""): from hermes_cli.config import load_config from hermes_cli.models import ( fetch_api_models, fetch_github_model_catalog, github_model_reasoning_efforts, copilot_model_api_mode, - normalize_copilot_model_id, - ) + normalize_copilot_model_id) provider_id = "copilot" pconfig = PROVIDER_REGISTRY[provider_id] @@ -600,8 +585,7 @@ def _model_flow_copilot(config, current_model=""): selected = _pick_model_or_prompt( _copilot_model_list(live_models), "Model name: ", current_model=_normalize(current_model), - confirm_provider=provider_id, confirm_base_url=effective_base, confirm_api_key=api_key, - ) + confirm_provider=provider_id, confirm_base_url=effective_base, confirm_api_key=api_key) if not selected: print("No change.") return @@ -631,8 +615,7 @@ def _model_flow_copilot_acp(config, current_model=""): """GitHub Copilot ACP flow using the local Copilot CLI.""" from hermes_cli.auth import ( PROVIDER_REGISTRY, get_external_process_provider_status, resolve_api_key_provider_credentials, - resolve_external_process_provider_credentials, - ) + resolve_external_process_provider_credentials) from hermes_cli.models import fetch_github_model_catalog, normalize_copilot_model_id del config @@ -666,8 +649,7 @@ def _model_flow_copilot_acp(config, current_model=""): model_list = _copilot_model_list([item.get("id", "") for item in catalog if item.get("id")] if catalog else []) selected = _pick_model_or_prompt( model_list, "Model name: ", current_model=_normalize(current_model), confirm_provider=provider_id, - confirm_base_url=effective_base, confirm_api_key=catalog_api_key, - ) + confirm_base_url=effective_base, confirm_api_key=catalog_api_key) if selected: selected = _normalize(selected) _finish_model(selected, provider_id, f"Default model set to: {selected} (via {pconfig.name})", @@ -712,8 +694,7 @@ def _model_flow_kimi(config, current_model=""): model_list = _PROVIDER_MODELS.get("kimi-coding" if is_coding_plan else "moonshot", []) selected = _pick_model_or_prompt( model_list, "Enter model name: ", current_model=current_model, confirm_provider=provider_id, - confirm_base_url=effective_base, confirm_api_key=existing_key, - ) + confirm_base_url=effective_base, confirm_api_key=existing_key) # api_mode is dropped so the runtime auto-detects it from the URL. _finish_model(selected, provider_id, f"Default model set to: {selected} (via {'Kimi Coding' if is_coding_plan else 'Moonshot'})", base_url=effective_base, drop_api_mode=True) @@ -745,8 +726,7 @@ def _model_flow_stepfun(config, current_model=""): region_choices = [ ("international", f"International ({_stepfun_base_url_for_region('international')})"), - ("china", f"China ({_stepfun_base_url_for_region('china')})"), - ] + ("china", f"China ({_stepfun_base_url_for_region('china')})")] ordered_regions = [] for region_key, label in region_choices: if region_key == current_region: @@ -773,8 +753,7 @@ def _model_flow_stepfun(config, current_model=""): selected = _pick_model_or_prompt( model_list, "Model name: ", current_model=current_model, confirm_provider=provider_id, - confirm_base_url=effective_base, confirm_api_key=existing_key, - ) + confirm_base_url=effective_base, confirm_api_key=existing_key) model = _finish_model(selected, provider_id, f"Default model set to: {selected} (via {pconfig.name})", base_url=effective_base, drop_api_mode=True) if model is not None: @@ -877,8 +856,7 @@ _GEMINI_FREE_TIER_NOTICE = ( " To use Gemini with Hermes, enable billing on your Google Cloud project and regenerate", " the key in a billing-enabled project: https://aistudio.google.com/apikey", "", " Alternatives with workable free usage: DeepSeek, OpenRouter (free models), Groq, Nous.", "", - "Not saving Gemini as the default provider.", -) + "Not saving Gemini as the default provider.") def _gemini_tier_ok(existing_key: str, pconfig, base_url_env: str) -> bool: @@ -1050,16 +1028,14 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""): pricing = {} selected = _pick_model_or_prompt( model_list, "Model name: ", current_model=current_model, pricing=pricing, confirm_provider=provider_id, - confirm_base_url=effective_base, confirm_api_key=existing_key, - ) + confirm_base_url=effective_base, confirm_api_key=existing_key) if selected and is_opencode: selected = normalize_opencode_model_id(provider_id, selected) # OpenCode pins its api_mode; everyone else drops it so the runtime auto-detects. _finish_model( selected, provider_id, f"Default model set to: {selected} (via {pconfig.name})", base_url=effective_base, api_mode=opencode_model_api_mode(provider_id, selected) if selected and is_opencode else None, - drop_api_mode=not is_opencode, - ) + drop_api_mode=not is_opencode) def _model_flow_anthropic(config, current_model=""): @@ -1137,8 +1113,7 @@ def _model_flow_anthropic(config, current_model=""): selected = _pick_model_or_prompt( _PROVIDER_MODELS.get("anthropic", []), "Model name (e.g., claude-sonnet-4-20250514): ", - current_model=current_model, confirm_provider="anthropic", - ) + current_model=current_model, confirm_provider="anthropic") # Clear base_url: resolve_runtime_provider() always hardcodes Anthropic's URL, # and a stale value can contaminate other providers on a later switch. _finish_model(selected, "anthropic", f"Default model set to: {selected} (via Anthropic)", drop_base_url=True, drop_api_mode=True) diff --git a/hermes_cli/model_setup_flows_azure.py b/hermes_cli/model_setup_flows_azure.py index 7e138c2a0a..f988be9806 100644 --- a/hermes_cli/model_setup_flows_azure.py +++ b/hermes_cli/model_setup_flows_azure.py @@ -21,8 +21,7 @@ def _azure_entra_preflight(current_entra: dict): try: from agent.azure_identity_adapter import ( EntraIdentityConfig, SCOPE_AI_AZURE_DEFAULT, build_token_provider, describe_active_credential, - has_azure_identity_installed, - ) + has_azure_identity_installed) except ImportError as exc: _say("", f"⚠ Could not import azure-identity adapter: {exc}", " Falling back to API key auth.") return False diff --git a/hermes_cli/model_setup_flows_bedrock.py b/hermes_cli/model_setup_flows_bedrock.py index 201aaf9095..424e5a76a4 100644 --- a/hermes_cli/model_setup_flows_bedrock.py +++ b/hermes_cli/model_setup_flows_bedrock.py @@ -8,8 +8,7 @@ Prompt strings and config write order are behavior. from __future__ import annotations from hermes_cli.model_setup_flows_common import ( - _ask, _ensure_dict_section, _finish_model, _pick_model_or_prompt, _say, -) + _ask, _ensure_dict_section, _finish_model, _pick_model_or_prompt, _say) # AWS cross-region inference profile prefixes. A geo-prefixed profile only routes @@ -86,8 +85,7 @@ def _model_flow_bedrock_api_key(config, region, current_model=""): print(f" Showing {len(model_list)} curated models") selected = _pick_model_or_prompt( model_list, " Model ID: ", current_model=current_model, confirm_provider="custom", - confirm_base_url=mantle_base_url, confirm_api_key=existing_key, - ) + confirm_base_url=mantle_base_url, confirm_api_key=existing_key) def _finish(cfg, _model): # The bearer token rides on a named provider entry: a bare ``provider: custom`` @@ -139,8 +137,7 @@ def _bedrock_text_model_ids(live_models: list, region: str) -> list[str]: m for m in live_models if not any(m["id"].startswith(p) for p in _BEDROCK_EXCLUDE_PREFIXES) and not any(s in m["id"].lower() for s in _BEDROCK_EXCLUDE_SUBSTRINGS) - and bedrock_model_routable_from_region(m["id"], region) - ] + and bedrock_model_routable_from_region(m["id"], region)] # Deduplicate: prefer inference profiles (geo-prefixed or global.*) over bare foundation model IDs. profile_base_ids = {_base_id(m["id"]) for m in filtered if m["id"].startswith(_BEDROCK_PROFILE_PREFIXES)} deduped = [m for m in filtered if m["id"].startswith(_BEDROCK_PROFILE_PREFIXES) or m["id"] not in profile_base_ids] diff --git a/hermes_cli/model_setup_flows_common.py b/hermes_cli/model_setup_flows_common.py index 7accc4bcdd..fc6ff89731 100644 --- a/hermes_cli/model_setup_flows_common.py +++ b/hermes_cli/model_setup_flows_common.py @@ -252,15 +252,13 @@ def _prune_replaced_custom_model_config_credentials(base_url: str, *, provider_n not isinstance(pool_key, str) or not pool_key.startswith(CUSTOM_POOL_PREFIX) or pool_key in active_pool_keys - or not isinstance(entries, list) - ): + or not isinstance(entries, list)): continue retained = [e for e in entries if not (isinstance(e, dict) and e.get("source") == "model_config")] if len(retained) != len(entries): removed_ids = [ str(e["id"]) for e in entries - if isinstance(e, dict) and e.get("source") == "model_config" and e.get("id") - ] + if isinstance(e, dict) and e.get("source") == "model_config" and e.get("id")] write_credential_pool(pool_key, retained, removed_ids=removed_ids) except Exception: return diff --git a/hermes_cli/model_setup_flows_custom.py b/hermes_cli/model_setup_flows_custom.py index b92c117a25..7aa8aa8c9a 100644 --- a/hermes_cli/model_setup_flows_custom.py +++ b/hermes_cli/model_setup_flows_custom.py @@ -15,8 +15,7 @@ from hermes_cli.cli_output import line_input from hermes_cli.providers import custom_provider_slug from hermes_cli.model_setup_flows_common import ( _HTTP, _ask, _commit_model_config, _load_config_model_section, - _prune_replaced_custom_model_config_credentials, _say, -) + _prune_replaced_custom_model_config_credentials, _say) def _parse_context_length(text: str): @@ -214,8 +213,7 @@ def _discover_named_custom_models(provider_info: dict, api_key: str, configured_ from hermes_cli.config import normalize_extra_headers from hermes_cli.models import ( fetch_api_models, fetch_ollama_local_models, _get_ollama_native_headers, _normalize_openai_base_url, - should_use_ollama_native_catalog, - ) + should_use_ollama_native_catalog) name, base_url = provider_info["name"], provider_info["base_url"] api_mode = provider_info.get("api_mode", "")