From f6626fccee0c426a56a4cfa2c3dc647dda70301f Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:51:13 -0700 Subject: [PATCH] =?UTF-8?q?refactor:=20remove=20provider=20tier=20system?= =?UTF-8?q?=20=E2=80=94=20flat=20picker=20in=20hermes=20model=20(#9303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the two-tier (top/extended) provider picker that hid most providers behind a 'More providers...' submenu. All providers now appear in a single flat list. - Remove tier field from ProviderEntry namedtuple - Remove tier values from all CANONICAL_PROVIDERS entries - Flatten the hermes model picker (no more 'More...' submenu) - Move 'Custom endpoint' to the bottom of the main list --- hermes_cli/main.py | 44 ++++++++------------------------------ hermes_cli/models.py | 50 ++++++++++++++++++++------------------------ 2 files changed, 32 insertions(+), 62 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 03890eaab..2712a01ea 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1044,10 +1044,8 @@ def select_provider_and_model(args=None): print(f" Active provider: {active_label}") print() - # Step 1: Provider selection — top providers shown first, rest behind "More..." - # Derived from CANONICAL_PROVIDERS (single source of truth) - top_providers = [(p.slug, p.tui_desc) for p in CANONICAL_PROVIDERS if p.tier == "top"] - extended_providers = [(p.slug, p.tui_desc) for p in CANONICAL_PROVIDERS if p.tier == "extended"] + # Step 1: Provider selection — flat list from CANONICAL_PROVIDERS + all_providers = [(p.slug, p.tui_desc) for p in CANONICAL_PROVIDERS] def _named_custom_provider_map(cfg) -> dict[str, dict[str, str]]: custom_provider_map = {} @@ -1084,29 +1082,22 @@ def select_provider_and_model(args=None): short_url = base_url.replace("https://", "").replace("http://", "").rstrip("/") saved_model = provider_info.get("model", "") model_hint = f" — {saved_model}" if saved_model else "" - top_providers.append((key, f"{name} ({short_url}){model_hint}")) + all_providers.append((key, f"{name} ({short_url}){model_hint}")) - top_keys = {k for k, _ in top_providers} - extended_keys = {k for k, _ in extended_providers} - - # If the active provider is in the extended list, promote it into top - if active and active in extended_keys: - promoted = [(k, l) for k, l in extended_providers if k == active] - extended_providers = [(k, l) for k, l in extended_providers if k != active] - top_providers = promoted + top_providers - top_keys.add(active) - - # Build the primary menu + # Build the menu ordered = [] default_idx = 0 - for key, label in top_providers: + for key, label in all_providers: if active and key == active: ordered.append((key, f"{label} ← currently active")) default_idx = len(ordered) - 1 else: ordered.append((key, label)) - ordered.append(("more", "More providers...")) + ordered.append(("custom", "Custom endpoint (enter URL manually)")) + _has_saved_custom_list = isinstance(config.get("custom_providers"), list) and bool(config.get("custom_providers")) + if _has_saved_custom_list: + ordered.append(("remove-custom", "Remove a saved custom provider")) ordered.append(("cancel", "Cancel")) provider_idx = _prompt_provider_choice( @@ -1118,23 +1109,6 @@ def select_provider_and_model(args=None): selected_provider = ordered[provider_idx][0] - # "More providers..." — show the extended list - if selected_provider == "more": - ext_ordered = list(extended_providers) - ext_ordered.append(("custom", "Custom endpoint (enter URL manually)")) - _has_saved_custom_list = isinstance(config.get("custom_providers"), list) and bool(config.get("custom_providers")) - if _has_saved_custom_list: - ext_ordered.append(("remove-custom", "Remove a saved custom provider")) - ext_ordered.append(("cancel", "Cancel")) - - ext_idx = _prompt_provider_choice( - [label for _, label in ext_ordered], default=0, - ) - if ext_idx is None or ext_ordered[ext_idx][0] == "cancel": - print("No change.") - return - selected_provider = ext_ordered[ext_idx][0] - # Step 2: Provider-specific setup + model selection if selected_provider == "openrouter": _model_flow_openrouter(config, current_model) diff --git a/hermes_cli/models.py b/hermes_cli/models.py index d8223e86c..a2a33bdd0 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -498,43 +498,39 @@ def check_nous_free_tier() -> bool: # Fields: # slug — internal provider ID (used in config.yaml, --provider flag) # label — short display name -# tier — "top" (shown first) or "extended" (behind "More...") # tui_desc — longer description for the `hermes model` interactive picker # --------------------------------------------------------------------------- class ProviderEntry(NamedTuple): slug: str label: str - tier: str # "top" or "extended" tui_desc: str # detailed description for `hermes model` TUI CANONICAL_PROVIDERS: list[ProviderEntry] = [ - # -- Top tier (shown by default) -- - ProviderEntry("nous", "Nous Portal", "top", "Nous Portal (Nous Research subscription)"), - ProviderEntry("openrouter", "OpenRouter", "top", "OpenRouter (100+ models, pay-per-use)"), - ProviderEntry("anthropic", "Anthropic", "top", "Anthropic (Claude models — API key or Claude Code)"), - ProviderEntry("openai-codex", "OpenAI Codex", "top", "OpenAI Codex"), - ProviderEntry("qwen-oauth", "Qwen OAuth (Portal)", "top", "Qwen OAuth (reuses local Qwen CLI login)"), - ProviderEntry("copilot", "GitHub Copilot", "top", "GitHub Copilot (uses GITHUB_TOKEN or gh auth token)"), - ProviderEntry("huggingface", "Hugging Face", "top", "Hugging Face Inference Providers (20+ open models)"), - # -- Extended tier (behind "More..." in hermes model) -- - ProviderEntry("copilot-acp", "GitHub Copilot ACP", "extended", "GitHub Copilot ACP (spawns `copilot --acp --stdio`)"), - ProviderEntry("gemini", "Google AI Studio", "extended", "Google AI Studio (Gemini models — OpenAI-compatible endpoint)"), - ProviderEntry("deepseek", "DeepSeek", "extended", "DeepSeek (DeepSeek-V3, R1, coder — direct API)"), - ProviderEntry("xai", "xAI", "extended", "xAI (Grok models — direct API)"), - ProviderEntry("zai", "Z.AI / GLM", "extended", "Z.AI / GLM (Zhipu AI direct API)"), - ProviderEntry("kimi-coding", "Kimi / Moonshot", "extended", "Kimi / Moonshot (Moonshot AI direct API)"), - ProviderEntry("kimi-coding-cn", "Kimi / Moonshot (China)", "extended", "Kimi / Moonshot China (Moonshot CN direct API)"), - ProviderEntry("minimax", "MiniMax", "extended", "MiniMax (global direct API)"), - ProviderEntry("minimax-cn", "MiniMax (China)", "extended", "MiniMax China (domestic direct API)"), - ProviderEntry("kilocode", "Kilo Code", "extended", "Kilo Code (Kilo Gateway API)"), - ProviderEntry("opencode-zen", "OpenCode Zen", "extended", "OpenCode Zen (35+ curated models, pay-as-you-go)"), - ProviderEntry("opencode-go", "OpenCode Go", "extended", "OpenCode Go (open models, $10/month subscription)"), - ProviderEntry("ai-gateway", "AI Gateway", "extended", "AI Gateway (Vercel — 200+ models, pay-per-use)"), - ProviderEntry("alibaba", "Alibaba Cloud (DashScope)","extended", "Alibaba Cloud / DashScope Coding (Qwen + multi-provider)"), - ProviderEntry("xiaomi", "Xiaomi MiMo", "extended", "Xiaomi MiMo (MiMo-V2 models — pro, omni, flash)"), - ProviderEntry("arcee", "Arcee AI", "extended", "Arcee AI (Trinity models — direct API)"), + ProviderEntry("nous", "Nous Portal", "Nous Portal (Nous Research subscription)"), + ProviderEntry("openrouter", "OpenRouter", "OpenRouter (100+ models, pay-per-use)"), + ProviderEntry("anthropic", "Anthropic", "Anthropic (Claude models — API key or Claude Code)"), + ProviderEntry("openai-codex", "OpenAI Codex", "OpenAI Codex"), + ProviderEntry("qwen-oauth", "Qwen OAuth (Portal)", "Qwen OAuth (reuses local Qwen CLI login)"), + ProviderEntry("copilot", "GitHub Copilot", "GitHub Copilot (uses GITHUB_TOKEN or gh auth token)"), + ProviderEntry("copilot-acp", "GitHub Copilot ACP", "GitHub Copilot ACP (spawns `copilot --acp --stdio`)"), + ProviderEntry("huggingface", "Hugging Face", "Hugging Face Inference Providers (20+ open models)"), + ProviderEntry("gemini", "Google AI Studio", "Google AI Studio (Gemini models — OpenAI-compatible endpoint)"), + ProviderEntry("deepseek", "DeepSeek", "DeepSeek (DeepSeek-V3, R1, coder — direct API)"), + ProviderEntry("xai", "xAI", "xAI (Grok models — direct API)"), + ProviderEntry("zai", "Z.AI / GLM", "Z.AI / GLM (Zhipu AI direct API)"), + ProviderEntry("kimi-coding", "Kimi / Moonshot", "Kimi / Moonshot (Moonshot AI direct API)"), + ProviderEntry("kimi-coding-cn", "Kimi / Moonshot (China)", "Kimi / Moonshot China (Moonshot CN direct API)"), + ProviderEntry("minimax", "MiniMax", "MiniMax (global direct API)"), + ProviderEntry("minimax-cn", "MiniMax (China)", "MiniMax China (domestic direct API)"), + ProviderEntry("alibaba", "Alibaba Cloud (DashScope)","Alibaba Cloud / DashScope Coding (Qwen + multi-provider)"), + ProviderEntry("xiaomi", "Xiaomi MiMo", "Xiaomi MiMo (MiMo-V2 models — pro, omni, flash)"), + ProviderEntry("arcee", "Arcee AI", "Arcee AI (Trinity models — direct API)"), + ProviderEntry("kilocode", "Kilo Code", "Kilo Code (Kilo Gateway API)"), + ProviderEntry("opencode-zen", "OpenCode Zen", "OpenCode Zen (35+ curated models, pay-as-you-go)"), + ProviderEntry("opencode-go", "OpenCode Go", "OpenCode Go (open models, $10/month subscription)"), + ProviderEntry("ai-gateway", "AI Gateway", "AI Gateway (Vercel — 200+ models, pay-per-use)"), ] # Derived dicts — used throughout the codebase