Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gateway/slash_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,7 @@ async def _handle_model_command(self, event: MessageEvent) -> Optional[str]:
user_providers=user_provs,
custom_providers=custom_provs,
max_models=50,
include_moa=True,
)
except Exception:
providers = []
Expand Down
36 changes: 36 additions & 0 deletions hermes_cli/model_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2277,13 +2277,47 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
return results


def _prepend_moa_picker_provider(providers: List[dict], current_provider: str = "") -> List[dict]:
"""Add the virtual MoA provider row used by interactive model pickers.

``list_authenticated_providers()`` only returns real/auth-backed providers.
The CLI model inventory adds MoA separately so named presets appear next to
normal providers; gateway pickers call ``list_picker_providers()`` directly,
so they need the same virtual row here.
"""
try:
from hermes_cli.config import load_config
from hermes_cli.moa_config import normalize_moa_config

cfg = normalize_moa_config(load_config().get("moa") or {})
models = list(cfg.get("presets", {}).keys())
if not models:
return providers
moa_row = {
"slug": "moa",
"name": "Mixture of Agents",
"is_current": (current_provider or "").lower() == "moa",
"is_user_defined": False,
"models": models,
"total_models": len(models),
"source": "virtual",
"authenticated": True,
"auth_type": "virtual",
"warning": "Aggregator acts as the selected model; references provide analysis before each call.",
}
return [moa_row] + [p for p in providers if str(p.get("slug", "")).lower() != "moa"]
except Exception:
return providers


def list_picker_providers(
current_provider: str = "",
current_base_url: str = "",
user_providers: dict = None,
custom_providers: list | None = None,
max_models: int | None = None,
current_model: str = "",
include_moa: bool = False,
) -> List[dict]:
"""Interactive-picker variant of :func:`list_authenticated_providers`.

Expand Down Expand Up @@ -2314,6 +2348,8 @@ def list_picker_providers(
max_models=max_models,
current_model=current_model,
)
if include_moa:
providers = _prepend_moa_picker_provider(providers, current_provider=current_provider)

filtered: List[dict] = []
for p in providers:
Expand Down
26 changes: 26 additions & 0 deletions tests/gateway/test_model_command_async_offload.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,29 @@ def _fake_list_picker_providers(**kwargs):
"list_picker_providers must be dispatched via asyncio.to_thread "
"(it was called inline on the event loop instead)"
)


@pytest.mark.asyncio
async def test_picker_path_requests_moa_presets(_isolated_config, monkeypatch):
"""Gateway /model pickers must opt into the virtual MoA preset provider."""
captured = {}

def _fake_list_picker_providers(**kwargs):
captured.update(kwargs)
return [{"slug": "moa", "name": "Mixture of Agents", "is_current": False,
"models": ["battle", "smart"], "total_models": 2}]

monkeypatch.setattr(
"hermes_cli.model_switch.list_picker_providers",
_fake_list_picker_providers,
)

runner = _make_runner()
runner.adapters = {Platform.TELEGRAM: _FakePickerAdapter()}
monkeypatch.setattr(runner, "_thread_metadata_for_source", lambda *a, **k: None, raising=False)
monkeypatch.setattr(runner, "_reply_anchor_for_event", lambda *a, **k: None, raising=False)

result = await runner._handle_model_command(_make_event())

assert result is None
assert captured["include_moa"] is True
34 changes: 34 additions & 0 deletions tests/hermes_cli/test_list_picker_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,40 @@ def test_non_openrouter_rows_passed_through_unchanged(monkeypatch):
assert result[1]["models"] == ["gemini-3-flash-preview"]


def test_include_moa_adds_virtual_provider_with_named_presets(monkeypatch):
"""Gateway pickers opt into a virtual MoA provider so presets are tappable."""
base = [_make_provider("minimax", models=["MiniMax-M3"])]
moa_config = {
"moa": {
"default_preset": "battle",
"presets": {
"battle": {"enabled": True},
"smart": {"enabled": True},
},
}
}

monkeypatch.setattr(model_switch, "list_authenticated_providers",
lambda **kw: list(base))
monkeypatch.setattr("hermes_cli.config.load_config", lambda: moa_config)
monkeypatch.setattr("hermes_cli.models.fetch_openrouter_models",
lambda *a, **kw: pytest.fail("should not be called"))

result = model_switch.list_picker_providers(
current_provider="moa",
max_models=50,
include_moa=True,
)

assert [p["slug"] for p in result] == ["moa", "minimax"]
moa = result[0]
assert moa["name"] == "Mixture of Agents"
assert moa["is_current"] is True
assert moa["source"] == "virtual"
assert moa["models"] == ["battle", "smart"]
assert moa["total_models"] == 2


def test_empty_models_row_dropped(monkeypatch):
"""Built-in provider with an empty ``models`` list is dropped."""
base = [
Expand Down
Loading