# NousResearch/hermes-agent · Model alias resolution across CLI and runtime

## Open now

- 3 issues, 2 PRs open on GitHub.
- #18954: Model aliases not resolved for custom providers
- #62491: hermes chat --model <alias> ignores model_aliases, falls through to default provider
- #62494: hermes chat --model <alias> ignores model_aliases, routes alias verbatim to default provider
- PR #19213: fix(config): resolve direct model aliases at runtime
- PR #62534: fix(cli): resolve --model <alias> through model_aliases in interactive chat (#62491)

These issues and pull requests address failures to resolve user-defined model aliases in the CLI, runtime, config, and migration tooling, where aliases were passed verbatim instead of being looked up against the configured alias mappings. Multiple contexts are affected: interactive chat, provider routing, and the claw migrate command.

## TL;DR

- 12 member(s): 3 open issue(s), 2 closed issue(s), 2 open PR(s), 2 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #16924. Verify it addresses your case.
- **Recommended:** read PR #16924 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-16924,pr-16977`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.15`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #18954: Model aliases not resolved for custom providers
- #62491: hermes chat --model <alias> ignores model_aliases, falls through to default provider
- #62494: hermes chat --model <alias> ignores model_aliases, routes alias verbatim to default provider

## Closed / duplicate issues

- #16745: \[Bug\]: claw migrate: model alias copied verbatim instead of resolved to API ID
- #16767: CLI doesn't honor user-defined providers via chat --provider or -m <alias>

## Prior & competing PRs

- **#16924: fix(cli): honor user-defined providers via chat --provider and -m <alias> (#16767)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/main.py`, `hermes_cli/model_switch.py`, `hermes_cli/oneshot.py`, `hermes_cli/runtime_provider.py`, `scripts/release.py`, `tests/hermes_cli/test_regression_16767.py`
  - fix hunks:
  - `hermes_cli/main.py`

    ```diff
    @@ -7903,32 +7903,12 @@ def main():
         )
         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"
    ```
  - `hermes_cli/model_switch.py`

    ```diff
    @@ -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())
     
     
     # ---------------------------------------------------------------------------
    ```
  - `hermes_cli/oneshot.py`

    ```diff
    @@ -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".
    ```
  - `hermes_cli/runtime_provider.py`

    ```diff
    @@ -469,6 +469,30 @@ def _resolve_named_custom_runtime(
         explicit_api_key: [redacted:secret-assignment] = 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
    ```
  - `scripts/release.py`

    ```diff
    @@ -58,6 +58,7 @@
         "[redacted:email]": "DrStrangerUJN",
         "[redacted:email]": "dlkakbs",
         "[redacted:email]": "Mind-Dragon",
    +    "[redacted:email]": "westers",
         "[redacted:email]": "devorun",
         "[redacted:email]": "Sonoyunchu",
         "[redacted:email]": "say8hi",
    ```
  - `tests/hermes_cli/test_regression_16767.py`

    ```diff
    @@ -0,0 +1,58 @@
    +import pytest
    +import sys
    +from unittest.mock import patch
    +from pathlib import Path
    +
    +import hermes_cli.model_switch as ms
    +from hermes_cli.model_switch import DirectAlias
    +from hermes_cli.runtime_provider import _resolve_named_custom_runtime
    +
    +def test_ensure_direct_aliases_mutates_in_place(monkeypatch):
    +    """_ensure_direct_aliases mutates DIRECT_ALIASES in place (guards against rebinding regression)."""
    +    # Ensure we start with an empty but existing dict to check for mutation vs rebinding
    +    ms.DIRECT_ALIASES.clear()
    +    initial_id = id(ms.DIRECT_ALIASES)
    +    
    +    mock_data = {
    +        "my-custom-alias": DirectAlias("custom-model:v1", "custom", "https://example.com/v1")
    +    }
    +    monkeypatch.setattr(ms, "_load_direct_aliases", lambda: mock_data)
    +    
    +    ms._ensure_direct_aliases()
    +    
    +    assert id(ms.DIRECT_ALIASES) == initial_id, f"DIRECT_ALIASES was rebound (ID changed from {initial_id} to {id(ms.DIRECT_ALIASES)})"
    +    assert "my-custom-alias" in ms.DIRECT_ALIASES
    +    assert ms.DIRECT_ALIASES["my-custom-alias"].model == "custom-model:v1"
    +
    +def test_chat_provider_argparse_acceptance(monkeypatch):
    +    """chat --provider <user-defined> is accepted by argparse (guards against restrictive choices)."""
    +    recorded: dict[str, str] = {}
    +
    +    # Mock cmd_chat to record the provider passed to it
    +    def mock_cmd_chat(args):
    +        recorded["provider"] = args.provider
    +
    +    monkeypatch.setattr("hermes_cli.main.cmd_chat", mock_cmd_chat)
    +    monkeypatch.setattr(sys, "argv", ["hermes", "chat", "--provider", "my-custom-key"])
    +
    +    from hermes_cli.main import main
    +    main()
    +
    +    assert recorded["provider"] == "my-custom-key"
    +
    +def test_resolve_named_custom_runtime_honors_explicit_base_url(monkeypatch):
    +    """_resolve_named_custom_runtime honors (provider='custom', explicit_base_url=...)."""
    +    # Mock has_usable_secret to recognize our test key
    +    monkeypatch.setattr("hermes_cli.runtime_provider.has_usable_secret", lambda x: x == "test-api-key")
    +    
    +    result = _resolve_named_custom_runtime(
    +        requested_provider="custom",
    +        explicit_api_key="[redacted:secret-assignment]",
    +        explicit_base_url="http://example.test:1234/v1"
    +    )
    +    
    +    assert result is not None
    +    assert result["base_url"] == "http://example.test:1234/v1"
    +    assert result["provider"] == "custom"
    +    assert result["api_key"] == "test-api-key"
    +    assert result["source"] == "direct-alias"
    ```
- **#16977: fix(claw-migrate): resolve model aliases against real OpenClaw catalog schema (salvage #16778)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py`, `tests/skills/test_openclaw_migration.py`
- **#44667: fix(runtime): handle api_key_env alias in _get_named_custom_provider**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#16778: fix: resolve model aliases during claw migrate (#16745)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `7996c1479`, `#16977`, `#16745`
- **#16811: Fix CLI routing for user-defined providers (closes #16767)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#16924`
- **#19213: fix(config): resolve direct model aliases at runtime**. open
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#29198`, `#29198`
  - files: `cli.py`, `gateway/run.py`, `hermes_cli/model_switch.py`, `hermes_cli/oneshot.py`, `tests/cli/test_cli_provider_resolution.py`, `tests/gateway/test_session_model_override_routing.py`, `tests/hermes_cli/test_oneshot_model_aliases.py`
- **#62534: fix(cli): resolve --model <alias> through model_aliases in interactive chat (#62491)**. open
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `c1517f018`
  - files: `cli.py`, `tests/cli/test_cli_provider_resolution.py`

## Fix status

- Merged to main in PR #16924 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-28`.
- Subsystem: `hermes_cli`
