fix(cli): honor user-defined providers via chat --provider and -m <alias>
Three related issues prevented user-defined providers in `providers:` and `model_aliases:` from being reachable through standard CLI flags. Requests silently routed to the configured `model.base_url` instead of the user- intended endpoint. * hermes_cli/model_switch.py — root cause of the silent misrouting: `_ensure_direct_aliases()` rebound `DIRECT_ALIASES` to a freshly-loaded dict, leaving every `from hermes_cli.model_switch import DIRECT_ALIASES` caller stuck on the stale empty original. Switched to `.update()` so module attribute references stay valid. * hermes_cli/main.py — chat subcommand `--provider` had `choices=[...]` hardcoded to built-in providers, rejecting valid keys from user `providers:` config. Dropped the choices list; runtime resolution validates correctly downstream. * hermes_cli/oneshot.py — `-m <alias>` only resolved the model name; the alias's base_url was never propagated. Now consults `DIRECT_ALIASES` before falling through to `detect_provider_for_model`, and threads the alias's base_url to `resolve_runtime_provider(explicit_base_url=...)`. * hermes_cli/runtime_provider.py — `_resolve_named_custom_runtime` now honors `(provider="custom", explicit_base_url=...)` so a base_url propagated from a direct-alias resolution actually builds a runtime instead of falling through to provider-registry handlers that don't know about ad-hoc local endpoints. Verified: `hermes chat --provider <user-key> -m <model> -q "..."` and `hermes -m <user-alias> -z "..."` both route to the user-intended endpoint, observable via the target server's request log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7903,32 +7903,12 @@ For more help on a command:
|
|||||||
)
|
)
|
||||||
chat_parser.add_argument(
|
chat_parser.add_argument(
|
||||||
"--provider",
|
"--provider",
|
||||||
choices=[
|
# No `choices=` here: user-defined providers from config.yaml `providers:`
|
||||||
"auto",
|
# are also valid values, and runtime resolution (resolve_runtime_provider)
|
||||||
"openrouter",
|
# handles validation/error reporting consistently with the top-level
|
||||||
"nous",
|
# `--provider` flag.
|
||||||
"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",
|
|
||||||
],
|
|
||||||
default=None,
|
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(
|
chat_parser.add_argument(
|
||||||
"-v", "--verbose", action="store_true", help="Verbose output"
|
"-v", "--verbose", action="store_true", help="Verbose output"
|
||||||
|
|||||||
@@ -213,10 +213,15 @@ def _load_direct_aliases() -> dict[str, DirectAlias]:
|
|||||||
|
|
||||||
|
|
||||||
def _ensure_direct_aliases() -> None:
|
def _ensure_direct_aliases() -> None:
|
||||||
"""Lazy-load direct aliases on first use."""
|
"""Lazy-load direct aliases on first use.
|
||||||
global DIRECT_ALIASES
|
|
||||||
|
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:
|
if not DIRECT_ALIASES:
|
||||||
DIRECT_ALIASES = _load_direct_aliases()
|
DIRECT_ALIASES.update(_load_direct_aliases())
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -128,27 +128,44 @@ def _run_agent(
|
|||||||
# the user's configured default provider, which may not host the model
|
# the user's configured default provider, which may not host the model
|
||||||
# the caller just asked for.
|
# the caller just asked for.
|
||||||
effective_provider = (provider or "").strip() or None
|
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):
|
if effective_provider is None and (model or env_model):
|
||||||
# Only auto-detect when the model was explicitly requested via arg or
|
# 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"
|
# env var (not when it came from config — that's the "use my defaults"
|
||||||
# path and the configured provider is already correct).
|
# path and the configured provider is already correct).
|
||||||
explicit_model = (model or "").strip() or env_model
|
explicit_model = (model or "").strip() or env_model
|
||||||
if explicit_model:
|
if explicit_model:
|
||||||
cfg_provider = ""
|
# First check DIRECT_ALIASES populated from config.yaml `model_aliases:`.
|
||||||
if isinstance(model_cfg, dict):
|
# These map a user-defined alias to (model, provider, base_url) for
|
||||||
cfg_provider = str(model_cfg.get("provider") or "").strip().lower()
|
# endpoints not in any catalog (local servers, custom proxies, etc.).
|
||||||
current_provider = (
|
try:
|
||||||
cfg_provider
|
from hermes_cli import model_switch as _ms
|
||||||
or os.getenv("HERMES_INFERENCE_PROVIDER", "").strip().lower()
|
_ms._ensure_direct_aliases()
|
||||||
or "auto"
|
direct = _ms.DIRECT_ALIASES.get(explicit_model.strip().lower())
|
||||||
)
|
except Exception:
|
||||||
detected = detect_provider_for_model(explicit_model, current_provider)
|
direct = None
|
||||||
if detected:
|
if direct is not None:
|
||||||
effective_provider, effective_model = detected
|
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(
|
runtime = resolve_runtime_provider(
|
||||||
requested=effective_provider,
|
requested=effective_provider,
|
||||||
target_model=effective_model or None,
|
target_model=effective_model or None,
|
||||||
|
explicit_base_url=explicit_base_url_from_alias,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pull in whatever toolsets the user has enabled for "cli".
|
# Pull in whatever toolsets the user has enabled for "cli".
|
||||||
|
|||||||
@@ -469,6 +469,30 @@ def _resolve_named_custom_runtime(
|
|||||||
explicit_api_key: Optional[str] = None,
|
explicit_api_key: Optional[str] = None,
|
||||||
explicit_base_url: Optional[str] = None,
|
explicit_base_url: Optional[str] = None,
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> 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)
|
custom_provider = _get_named_custom_provider(requested_provider)
|
||||||
if not custom_provider:
|
if not custom_provider:
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user