mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: re-order providers,Quick Install, subscription polling
This commit is contained in:
parent
6f1cb46df9
commit
79aeaa97e6
4 changed files with 628 additions and 580 deletions
|
|
@ -908,7 +908,7 @@ def select_provider_and_model(args=None):
|
|||
try:
|
||||
active = resolve_provider("auto")
|
||||
except AuthError:
|
||||
active = "openrouter" # no provider yet; show full picker
|
||||
active = None # no provider yet; default to first in list
|
||||
|
||||
# Detect custom endpoint
|
||||
if active == "openrouter" and get_env_value("OPENAI_BASE_URL"):
|
||||
|
|
@ -933,21 +933,25 @@ def select_provider_and_model(args=None):
|
|||
"huggingface": "Hugging Face",
|
||||
"custom": "Custom endpoint",
|
||||
}
|
||||
active_label = provider_labels.get(active, active)
|
||||
active_label = provider_labels.get(active, active) if active else "none"
|
||||
|
||||
print()
|
||||
print(f" Current model: {current_model}")
|
||||
print(f" Active provider: {active_label}")
|
||||
print()
|
||||
|
||||
# Step 1: Provider selection — put active provider first with marker
|
||||
providers = [
|
||||
("openrouter", "OpenRouter (100+ models, pay-per-use)"),
|
||||
# Step 1: Provider selection — top providers shown first, rest behind "More..."
|
||||
top_providers = [
|
||||
("nous", "Nous Portal (Nous Research subscription)"),
|
||||
("openai-codex", "OpenAI Codex"),
|
||||
("copilot-acp", "GitHub Copilot ACP (spawns `copilot --acp --stdio`)"),
|
||||
("copilot", "GitHub Copilot (uses GITHUB_TOKEN or gh auth token)"),
|
||||
("openrouter", "OpenRouter (100+ models, pay-per-use)"),
|
||||
("anthropic", "Anthropic (Claude models — API key or Claude Code)"),
|
||||
("openai-codex", "OpenAI Codex"),
|
||||
("copilot", "GitHub Copilot (uses GITHUB_TOKEN or gh auth token)"),
|
||||
("huggingface", "Hugging Face Inference Providers (20+ open models)"),
|
||||
]
|
||||
|
||||
extended_providers = [
|
||||
("copilot-acp", "GitHub Copilot ACP (spawns `copilot --acp --stdio`)"),
|
||||
("zai", "Z.AI / GLM (Zhipu AI direct API)"),
|
||||
("kimi-coding", "Kimi / Moonshot (Moonshot AI direct API)"),
|
||||
("minimax", "MiniMax (global direct API)"),
|
||||
|
|
@ -957,7 +961,6 @@ def select_provider_and_model(args=None):
|
|||
("opencode-go", "OpenCode Go (open models, $10/month subscription)"),
|
||||
("ai-gateway", "AI Gateway (Vercel — 200+ models, pay-per-use)"),
|
||||
("alibaba", "Alibaba Cloud / DashScope Coding (Qwen + multi-provider)"),
|
||||
("huggingface", "Hugging Face Inference Providers (20+ open models)"),
|
||||
]
|
||||
|
||||
# Add user-defined custom providers from config.yaml
|
||||
|
|
@ -971,12 +974,11 @@ def select_provider_and_model(args=None):
|
|||
base_url = (entry.get("base_url") or "").strip()
|
||||
if not name or not base_url:
|
||||
continue
|
||||
# Generate a stable key from the name
|
||||
key = "custom:" + name.lower().replace(" ", "-")
|
||||
short_url = base_url.replace("https://", "").replace("http://", "").rstrip("/")
|
||||
saved_model = entry.get("model", "")
|
||||
model_hint = f" — {saved_model}" if saved_model else ""
|
||||
providers.append((key, f"{name} ({short_url}){model_hint}"))
|
||||
top_providers.append((key, f"{name} ({short_url}){model_hint}"))
|
||||
_custom_provider_map[key] = {
|
||||
"name": name,
|
||||
"base_url": base_url,
|
||||
|
|
@ -984,31 +986,54 @@ def select_provider_and_model(args=None):
|
|||
"model": saved_model,
|
||||
}
|
||||
|
||||
# Always add the manual custom endpoint option last
|
||||
providers.append(("custom", "Custom endpoint (enter URL manually)"))
|
||||
top_keys = {k for k, _ in top_providers}
|
||||
extended_keys = {k for k, _ in extended_providers}
|
||||
|
||||
# Add removal option if there are saved custom providers
|
||||
if _custom_provider_map:
|
||||
providers.append(("remove-custom", "Remove a saved custom provider"))
|
||||
# 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)
|
||||
|
||||
# Reorder so the active provider is at the top
|
||||
known_keys = {k for k, _ in providers}
|
||||
active_key = active if active in known_keys else "custom"
|
||||
# Build the primary menu
|
||||
ordered = []
|
||||
for key, label in providers:
|
||||
if key == active_key:
|
||||
ordered.insert(0, (key, f"{label} ← currently active"))
|
||||
default_idx = 0
|
||||
for key, label in top_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(("cancel", "Cancel"))
|
||||
|
||||
provider_idx = _prompt_provider_choice([label for _, label in ordered])
|
||||
provider_idx = _prompt_provider_choice(
|
||||
[label for _, label in ordered], default=default_idx,
|
||||
)
|
||||
if provider_idx is None or ordered[provider_idx][0] == "cancel":
|
||||
print("No change.")
|
||||
return
|
||||
|
||||
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)"))
|
||||
if _custom_provider_map:
|
||||
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)
|
||||
|
|
@ -1034,34 +1059,33 @@ def select_provider_and_model(args=None):
|
|||
_model_flow_api_key_provider(config, selected_provider, current_model)
|
||||
|
||||
|
||||
def _prompt_provider_choice(choices):
|
||||
"""Show provider selection menu. Returns index or None."""
|
||||
def _prompt_provider_choice(choices, *, default=0):
|
||||
"""Show provider selection menu with curses arrow-key navigation.
|
||||
|
||||
Falls back to a numbered list when curses is unavailable (e.g. piped
|
||||
stdin, non-TTY environments). Returns the selected index, or None
|
||||
if the user cancels.
|
||||
"""
|
||||
try:
|
||||
from simple_term_menu import TerminalMenu
|
||||
menu_items = [f" {c}" for c in choices]
|
||||
menu = TerminalMenu(
|
||||
menu_items, cursor_index=0,
|
||||
menu_cursor="-> ", menu_cursor_style=("fg_green", "bold"),
|
||||
menu_highlight_style=("fg_green",),
|
||||
cycle_cursor=True, clear_screen=False,
|
||||
title="Select provider:",
|
||||
)
|
||||
idx = menu.show()
|
||||
print()
|
||||
return idx
|
||||
except (ImportError, NotImplementedError):
|
||||
from hermes_cli.setup import _curses_prompt_choice
|
||||
idx = _curses_prompt_choice("Select provider:", choices, default)
|
||||
if idx >= 0:
|
||||
print()
|
||||
return idx
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: numbered list
|
||||
print("Select provider:")
|
||||
for i, c in enumerate(choices, 1):
|
||||
print(f" {i}. {c}")
|
||||
marker = "→" if i - 1 == default else " "
|
||||
print(f" {marker} {i}. {c}")
|
||||
print()
|
||||
while True:
|
||||
try:
|
||||
val = input(f"Choice [1-{len(choices)}]: ").strip()
|
||||
val = input(f"Choice [1-{len(choices)}] ({default + 1}): ").strip()
|
||||
if not val:
|
||||
return None
|
||||
return default
|
||||
idx = int(val) - 1
|
||||
if 0 <= idx < len(choices):
|
||||
return idx
|
||||
|
|
@ -1084,7 +1108,8 @@ def _model_flow_openrouter(config, current_model=""):
|
|||
print("Get one at: https://openrouter.ai/keys")
|
||||
print()
|
||||
try:
|
||||
key = input("OpenRouter API key (or Enter to cancel): ").strip()
|
||||
import getpass
|
||||
key = getpass.getpass("OpenRouter API key (or Enter to cancel): ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
return
|
||||
|
|
@ -1307,7 +1332,8 @@ def _model_flow_custom(config):
|
|||
|
||||
try:
|
||||
base_url = input(f"API base URL [{current_url or 'e.g. https://api.example.com/v1'}]: ").strip()
|
||||
api_key = input(f"API key [{current_key[:8] + '...' if current_key else 'optional'}]: ").strip()
|
||||
import getpass
|
||||
api_key = getpass.getpass(f"API key [{current_key[:8] + '...' if current_key else 'optional'}]: ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print("\nCancelled.")
|
||||
return
|
||||
|
|
@ -1816,7 +1842,8 @@ def _model_flow_copilot(config, current_model=""):
|
|||
return
|
||||
elif choice == "2":
|
||||
try:
|
||||
new_key = input(" Token (COPILOT_GITHUB_TOKEN): ").strip()
|
||||
import getpass
|
||||
new_key = getpass.getpass(" Token (COPILOT_GITHUB_TOKEN): ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
return
|
||||
|
|
@ -2057,7 +2084,8 @@ def _model_flow_kimi(config, current_model=""):
|
|||
print(f"No {pconfig.name} API key configured.")
|
||||
if key_env:
|
||||
try:
|
||||
new_key = input(f"{key_env} (or Enter to cancel): ").strip()
|
||||
import getpass
|
||||
new_key = getpass.getpass(f"{key_env} (or Enter to cancel): ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
return
|
||||
|
|
@ -2151,7 +2179,8 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
|
|||
print(f"No {pconfig.name} API key configured.")
|
||||
if key_env:
|
||||
try:
|
||||
new_key = input(f"{key_env} (or Enter to cancel): ").strip()
|
||||
import getpass
|
||||
new_key = getpass.getpass(f"{key_env} (or Enter to cancel): ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
return
|
||||
|
|
@ -2285,7 +2314,8 @@ def _run_anthropic_oauth_flow(save_env_value):
|
|||
print(" If the setup-token was displayed above, paste it here:")
|
||||
print()
|
||||
try:
|
||||
manual_token = input(" Paste setup-token (or Enter to cancel): ").strip()
|
||||
import getpass
|
||||
manual_token = getpass.getpass(" Paste setup-token (or Enter to cancel): ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
return False
|
||||
|
|
@ -2312,7 +2342,8 @@ def _run_anthropic_oauth_flow(save_env_value):
|
|||
print(" Or paste an existing setup-token now (sk-ant-oat-...):")
|
||||
print()
|
||||
try:
|
||||
token = input(" Setup-token (or Enter to cancel): ").strip()
|
||||
import getpass
|
||||
token = getpass.getpass(" Setup-token (or Enter to cancel): ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
return False
|
||||
|
|
@ -2405,7 +2436,8 @@ def _model_flow_anthropic(config, current_model=""):
|
|||
print(" Get an API key at: https://console.anthropic.com/settings/keys")
|
||||
print()
|
||||
try:
|
||||
api_key = input(" API key (sk-ant-...): ").strip()
|
||||
import getpass
|
||||
api_key = getpass.getpass(" API key (sk-ant-...): ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue