diff --git a/hermes_cli/main.py b/hermes_cli/main.py index c13e3dce3d..0ad6166834 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -7903,32 +7903,12 @@ For more help on a command: ) chat_parser.add_argument( "--provider", - choices=[ - "auto", - "openrouter", - "nous", - "openai-codex", - "copilot-acp", - "copilot", - "anthropic", - "gemini", - "xai", - "ollama-cloud", - "huggingface", - "zai", - "kimi-coding", - "kimi-coding-cn", - "stepfun", - "minimax", - "minimax-cn", - "kilocode", - "xiaomi", - "arcee", - "gmi", - "nvidia", - ], + # No `choices=` here: user-defined providers from config.yaml `providers:` + # are also valid values, and runtime resolution (resolve_runtime_provider) + # handles validation/error reporting consistently with the top-level + # `--provider` flag. default=None, - help="Inference provider (default: auto)", + help="Inference provider (default: auto). Built-in or a user-defined name from `providers:` in config.yaml.", ) chat_parser.add_argument( "-v", "--verbose", action="store_true", help="Verbose output" diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index d9e1b04183..aed6542804 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -213,10 +213,15 @@ def _load_direct_aliases() -> dict[str, DirectAlias]: def _ensure_direct_aliases() -> None: - """Lazy-load direct aliases on first use.""" - global DIRECT_ALIASES + """Lazy-load direct aliases on first use. + + Mutates the existing DIRECT_ALIASES dict in place rather than rebinding + the module attribute. This keeps `from hermes_cli.model_switch import + DIRECT_ALIASES` references valid in callers — rebinding would leave them + pointing at a stale empty dict. + """ if not DIRECT_ALIASES: - DIRECT_ALIASES = _load_direct_aliases() + DIRECT_ALIASES.update(_load_direct_aliases()) # --------------------------------------------------------------------------- diff --git a/hermes_cli/oneshot.py b/hermes_cli/oneshot.py index edf4526ff0..e1065b662e 100644 --- a/hermes_cli/oneshot.py +++ b/hermes_cli/oneshot.py @@ -128,27 +128,44 @@ def _run_agent( # the user's configured default provider, which may not host the model # the caller just asked for. effective_provider = (provider or "").strip() or None + explicit_base_url_from_alias: Optional[str] = None if effective_provider is None and (model or env_model): # Only auto-detect when the model was explicitly requested via arg or # env var (not when it came from config — that's the "use my defaults" # path and the configured provider is already correct). explicit_model = (model or "").strip() or env_model if explicit_model: - cfg_provider = "" - if isinstance(model_cfg, dict): - cfg_provider = str(model_cfg.get("provider") or "").strip().lower() - current_provider = ( - cfg_provider - or os.getenv("HERMES_INFERENCE_PROVIDER", "").strip().lower() - or "auto" - ) - detected = detect_provider_for_model(explicit_model, current_provider) - if detected: - effective_provider, effective_model = detected + # First check DIRECT_ALIASES populated from config.yaml `model_aliases:`. + # These map a user-defined alias to (model, provider, base_url) for + # endpoints not in any catalog (local servers, custom proxies, etc.). + try: + from hermes_cli import model_switch as _ms + _ms._ensure_direct_aliases() + direct = _ms.DIRECT_ALIASES.get(explicit_model.strip().lower()) + except Exception: + direct = None + if direct is not None: + effective_model = direct.model + effective_provider = direct.provider + if direct.base_url: + explicit_base_url_from_alias = direct.base_url.rstrip("/") + else: + cfg_provider = "" + if isinstance(model_cfg, dict): + cfg_provider = str(model_cfg.get("provider") or "").strip().lower() + current_provider = ( + cfg_provider + or os.getenv("HERMES_INFERENCE_PROVIDER", "").strip().lower() + or "auto" + ) + detected = detect_provider_for_model(explicit_model, current_provider) + if detected: + effective_provider, effective_model = detected runtime = resolve_runtime_provider( requested=effective_provider, target_model=effective_model or None, + explicit_base_url=explicit_base_url_from_alias, ) # Pull in whatever toolsets the user has enabled for "cli". diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index d625b75ba1..c87b9c42ce 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -469,6 +469,30 @@ def _resolve_named_custom_runtime( explicit_api_key: Optional[str] = None, explicit_base_url: Optional[str] = None, ) -> Optional[Dict[str, Any]]: + # Bare `provider="custom"` with an explicit base_url (e.g. propagated + # from a `model_aliases:` direct-alias resolution) — build a runtime + # directly so the alias's base_url actually takes effect. + requested_norm = (requested_provider or "").strip().lower() + if requested_norm == "custom" and explicit_base_url: + base_url = explicit_base_url.strip().rstrip("/") + api_key_candidates = [ + (explicit_api_key or "").strip(), + os.getenv("OPENAI_API_KEY", "").strip(), + os.getenv("OPENROUTER_API_KEY", "").strip(), + ] + api_key = next( + (c for c in api_key_candidates if has_usable_secret(c)), + "", + ) or "no-key-required" + return { + "provider": "custom", + "api_mode": _detect_api_mode_for_url(base_url) or "chat_completions", + "base_url": base_url, + "api_key": api_key, + "source": "direct-alias", + "requested_provider": requested_provider, + } + custom_provider = _get_named_custom_provider(requested_provider) if not custom_provider: return None