hermes-agent/tests/hermes_cli/test_provider_parity.py
Teknium c6575df927
feat(moa): expose MoA presets as selectable virtual models (#46081)
* feat(moa): expose MoA presets as selectable virtual models

Reconstructed onto current main (PR #46081's base had diverged with no common
ancestor, marking the PR dirty so CI never dispatched). MoA is now a virtual
provider: each named preset is a selectable model under provider 'moa', and the
preset's aggregator is the acting model that answers and calls tools.

Reference models fan out in parallel via a bounded ThreadPoolExecutor (the same
batch pattern delegate_task uses) — all references dispatched at once, collected
when every one finishes, then handed to the aggregator. Output order is
preserved, failures and the MoA-recursion guard stay isolated per reference.

- Removed the old mixture_of_agents model tool and moa toolset.
- Added moa as a virtual provider in the provider/model inventory.
- /moa is shortcut behavior over model selection (default preset / named preset
  / one-shot prompt).
- Dashboard + Desktop manage named presets; presets appear in model pickers.
- Parallel reference fan-out in agent/moa_loop.py with regression test.

* fix(moa): thread moa_config through _run_agent to _run_agent_inner

The reconstructed gateway MoA wiring declared moa_config on _run_agent (the
profile-scoping wrapper) and used it inside _run_agent_inner, but the wrapper
never forwarded it — _run_agent_inner had no such parameter, so the runtime hit
NameError: name 'moa_config' is not defined on the compression-failure session
sync path. Add moa_config to _run_agent_inner's signature and forward it from
both wrapper call sites (multiplex and non-multiplex). Caught by
tests/gateway/test_compression_failure_session_sync.py on CI shard test(4).

* fix(moa): classify moa as a virtual provider in the catalog

The moa virtual provider has no PROVIDER_REGISTRY/ProviderProfile entry, so
provider_catalog() fell through to the default auth_type="api_key" with no
env vars — tripping two catalog invariants:
  - test_provider_catalog: api_key providers must expose a credential env var
  - test_provider_parity: every hermes-model provider must be desktop-configurable

moa already declares auth_type="virtual" in HERMES_OVERLAYS; consult that
overlay as an auth_type fallback so the catalog reports moa as virtual (no real
credential, no network endpoint). Exempt virtual providers from the desktop
parity union check the same way 'custom' is exempt — derived from the catalog,
not a hardcoded slug, so future virtual providers are covered too.
2026-06-25 13:52:06 -07:00

97 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""End-to-end provider parity contract: the desktop Providers tabs must show
the SAME provider universe as ``hermes model`` (the CLI/TUI picker).
This is the single load-bearing invariant of the unified provider catalog:
keys(/api/env provider rows) ids(/api/providers/oauth) ⊇ CANONICAL_PROVIDERS
i.e. every provider the CLI picker offers is configurable from the desktop app,
on one of the two Providers sub-tabs (API keys or Accounts). It is asserted as
an invariant against the real FastAPI endpoints (not a snapshot / count), so it
can never silently drift again when a provider plugin is added.
"""
from fastapi.testclient import TestClient
from hermes_cli.models import CANONICAL_PROVIDERS
from hermes_cli.provider_catalog import provider_catalog
from hermes_cli.web_server import _SESSION_TOKEN, app
client = TestClient(app)
HEADERS = {"X-Hermes-Session-Token": _SESSION_TOKEN}
# `custom` is the bring-your-own-endpoint pseudo-provider configured inline via
# the model picker's local-endpoint flow, not a fixed credential card. It is in
# the CLI picker's universe but intentionally has no dedicated Providers-tab
# card. Exempt it from the union check.
#
# Virtual providers (auth_type "virtual", e.g. `moa`) are likewise in the CLI
# picker universe but have no real credential and no Providers-tab card — they
# are configured through their own feature UI (MoA presets). Exempt them too,
# derived from the catalog so any future virtual provider is covered without a
# hardcoded slug.
_VIRTUAL = {d.slug for d in provider_catalog() if d.auth_type == "virtual"}
_EXEMPT = {"custom"} | _VIRTUAL
# Providers that legitimately offer BOTH auth methods and so intentionally
# appear on both desktop tabs (an API-key card AND an account sign-in card).
# Anthropic supports a direct API key (Keys tab) and a subscription OAuth /
# Claude Code login (Accounts tab); surfacing both is correct, not a bug.
_DUAL_TAB = {"anthropic"}
def _keys_tab_providers() -> set[str]:
"""Provider slugs that have at least one card on the desktop API-keys tab."""
data = client.get("/api/env", headers=HEADERS).json()
return {
info.get("provider")
for info in data.values()
if info.get("category") == "provider" and info.get("provider")
}
def _accounts_tab_providers() -> set[str]:
"""Provider slugs offered on the desktop Accounts tab."""
data = client.get("/api/providers/oauth", headers=HEADERS).json()
return {p["id"] for p in data["providers"]}
def test_every_hermes_model_provider_is_configurable_in_desktop():
"""PARITY CONTRACT: GUI (keys accounts) ⊇ `hermes model` universe."""
gui = _keys_tab_providers() | _accounts_tab_providers()
missing = [
e.slug
for e in CANONICAL_PROVIDERS
if e.slug not in _EXEMPT and e.slug not in gui
]
assert not missing, (
"providers shown in `hermes model` but not configurable in the desktop "
f"Providers tabs: {missing}"
)
def test_each_provider_lands_on_the_tab_its_auth_type_dictates():
"""A keys-tab provider must surface under /api/env; an accounts-tab provider
under /api/providers/oauth. Cross-checks the catalog's tab routing against
where each provider actually renders.
"""
keys = _keys_tab_providers()
accounts = _accounts_tab_providers()
for d in provider_catalog():
if d.slug in _EXEMPT:
continue
if d.tab == "keys" and d.api_key_env_vars:
assert d.slug in keys, f"{d.slug} (keys tab) missing from /api/env"
elif d.tab == "accounts":
assert d.slug in accounts, f"{d.slug} (accounts tab) missing from /api/providers/oauth"
def test_no_provider_appears_on_both_tabs():
"""A provider should be configured exactly one way — not duplicated across
both tabs (which would confuse users about where to put credentials).
Exception: genuinely dual-auth providers (see ``_DUAL_TAB``) intentionally
appear on both tabs.
"""
overlap = (_keys_tab_providers() & _accounts_tab_providers()) - _EXEMPT - _DUAL_TAB
assert not overlap, f"providers appearing on BOTH desktop tabs: {sorted(overlap)}"