mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(cli): resolve moa:<preset> model in non-interactive mode
hermes chat -Q -m moa:strategy failed with 'model moa:strategy is not supported' (HTTP 401/400): the raw model string was passed straight to the real provider. The MoA virtual provider only got wired up through the interactive /moa command and the model picker, never through the -Q one-shot startup path. resolve_runtime_provider already handles requested_provider == 'moa', and agent_init builds the MoAClient off provider == 'moa' (surface-agnostic). The only gap was mapping the moa:<preset> model string to that provider. Add _normalize_moa_model() and apply it in HermesCLI.__init__ before provider resolution: a moa:<preset> model sets requested_provider='moa' and model=<preset>, so the existing MoA path runs in non-interactive mode too. The moa: prefix wins over an explicit --provider (previously --provider deepseek -m moa:strategy silently dropped MoA). Fixes #56828
This commit is contained in:
parent
b7a05b6b6f
commit
8d72845399
2 changed files with 79 additions and 1 deletions
31
cli.py
31
cli.py
|
|
@ -3854,6 +3854,28 @@ def save_config_value(key_path: str, value: any) -> bool:
|
|||
# HermesCLI Class
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _normalize_moa_model(model: Optional[str]) -> tuple[Optional[str], Optional[str]]:
|
||||
"""Map a ``moa:<preset>`` model string to ``(provider, preset)``.
|
||||
|
||||
Returns ``("moa", "<preset>")`` when *model* selects the MoA virtual
|
||||
provider, otherwise ``(None, model)`` unchanged. This gives non-interactive
|
||||
``hermes chat -Q -m moa:<preset>`` the same routing the interactive
|
||||
``/moa`` command and the model picker already use: ``resolve_runtime_provider``
|
||||
handles ``requested_provider == "moa"`` and ``agent_init`` builds the
|
||||
MoAClient off ``provider == "moa"``. Without this the raw ``moa:<preset>``
|
||||
string is sent to the real provider and rejected with a 401/400 "model not
|
||||
supported" (#56828).
|
||||
"""
|
||||
if isinstance(model, str):
|
||||
stripped = model.strip()
|
||||
if stripped.lower().startswith("moa:"):
|
||||
preset = stripped.split(":", 1)[1].strip()
|
||||
if preset:
|
||||
return "moa", preset
|
||||
return None, model
|
||||
|
||||
|
||||
class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
"""
|
||||
Interactive CLI for the Hermes Agent.
|
||||
|
|
@ -3984,6 +4006,12 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
_config_model = (_model_config.get("default") or _model_config.get("model") or "") if isinstance(_model_config, dict) else (_model_config or "")
|
||||
_DEFAULT_CONFIG_MODEL = ""
|
||||
self.model = model or _config_model or _DEFAULT_CONFIG_MODEL
|
||||
# A ``moa:<preset>`` model string selects the MoA virtual provider in
|
||||
# one shot (parity with interactive ``/moa`` and the model picker). Do
|
||||
# this before provider resolution so ``-Q -m moa:<preset>`` routes
|
||||
# through MoA instead of hitting the real provider with an unknown
|
||||
# model (#56828). A ``moa:`` prefix wins over an explicit ``--provider``.
|
||||
_moa_provider_override, self.model = _normalize_moa_model(self.model)
|
||||
# Read max_tokens from config (env var override: HERMES_MAX_TOKENS)
|
||||
_env_mt = os.environ.get("HERMES_MAX_TOKENS")
|
||||
if _env_mt:
|
||||
|
|
@ -4019,7 +4047,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
|
||||
# Provider selection is resolved lazily at use-time via _ensure_runtime_credentials().
|
||||
self.requested_provider = (
|
||||
provider
|
||||
_moa_provider_override
|
||||
or provider
|
||||
or CLI_CONFIG["model"].get("provider")
|
||||
or os.getenv("HERMES_INFERENCE_PROVIDER")
|
||||
or "auto"
|
||||
|
|
|
|||
|
|
@ -80,3 +80,52 @@ def test_decode_legacy_encoded_moa_turn_still_works():
|
|||
prompt, cfg = decode_moa_turn(encoded)
|
||||
assert prompt == "hello"
|
||||
assert cfg["reference_models"] == [{"provider": "openrouter", "model": "deepseek/deepseek-v4-pro"}]
|
||||
|
||||
|
||||
class TestNormalizeMoaModel:
|
||||
"""#56828: `-Q -m moa:<preset>` must route through the MoA virtual provider.
|
||||
|
||||
``_normalize_moa_model`` maps the model string to (provider, preset); the
|
||||
__init__ wiring then forces ``requested_provider="moa"`` so the existing
|
||||
resolve_runtime_provider / agent_init MoA path runs in non-interactive mode.
|
||||
"""
|
||||
|
||||
def test_moa_prefix_maps_to_provider_and_preset(self):
|
||||
from cli import _normalize_moa_model
|
||||
assert _normalize_moa_model("moa:strategy") == ("moa", "strategy")
|
||||
|
||||
def test_moa_prefix_is_case_insensitive_and_trims(self):
|
||||
from cli import _normalize_moa_model
|
||||
assert _normalize_moa_model(" MOA:code-review ") == ("moa", "code-review")
|
||||
|
||||
def test_bare_moa_without_preset_is_not_treated_as_virtual(self):
|
||||
from cli import _normalize_moa_model
|
||||
# No preset after the colon → leave untouched (no provider override).
|
||||
assert _normalize_moa_model("moa:") == (None, "moa:")
|
||||
|
||||
def test_non_moa_model_unchanged(self):
|
||||
from cli import _normalize_moa_model
|
||||
assert _normalize_moa_model("anthropic/claude-opus-4.8") == (None, "anthropic/claude-opus-4.8")
|
||||
|
||||
def test_none_model_unchanged(self):
|
||||
from cli import _normalize_moa_model
|
||||
assert _normalize_moa_model(None) == (None, None)
|
||||
|
||||
def test_colon_model_that_is_not_moa_unchanged(self):
|
||||
from cli import _normalize_moa_model
|
||||
# A provider:model form for a real provider must not be hijacked.
|
||||
assert _normalize_moa_model("openrouter:deepseek/deepseek-v4") == (
|
||||
None,
|
||||
"openrouter:deepseek/deepseek-v4",
|
||||
)
|
||||
|
||||
def test_override_wins_over_explicit_provider(self):
|
||||
# __init__ resolves requested_provider as
|
||||
# ``_moa_provider_override or provider or ...``, so a moa: prefix must
|
||||
# take precedence over an explicit --provider (the #56828 deepseek case
|
||||
# where MoA was silently ignored).
|
||||
from cli import _normalize_moa_model
|
||||
override, model = _normalize_moa_model("moa:strategy")
|
||||
requested_provider = override or "deepseek" or "auto"
|
||||
assert requested_provider == "moa"
|
||||
assert model == "strategy"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue