refactor(hermes_cli): AST-neutral layout compaction of provider/platform setup flows
This commit is contained in:
@@ -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 <skill> 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 <skill>`. "
|
||||
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":
|
||||
|
||||
@@ -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...", []))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
Reference in New Issue
Block a user