diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 23d0e51ea3e0..011cf817aebd 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -1671,7 +1671,7 @@ def is_provider_explicitly_configured(provider_id: str) -> bool: except Exception: pass - # 2. Check config.yaml model.provider + # 2. Check config.yaml model.provider and other explicit provider slots. try: from hermes_cli.config import load_config cfg = load_config() @@ -1680,6 +1680,37 @@ def is_provider_explicitly_configured(provider_id: str) -> bool: cfg_provider = (model_cfg.get("provider") or "").strip().lower() if cfg_provider == normalized: return True + + # MoA presets are explicit model selections too. A user who configured + # ``provider: anthropic`` as a MoA advisor/aggregator has opted Hermes + # into using Anthropic credentials for that slot even when the main + # session model is another provider. Without this, Claude Code OAuth + # entries are pruned/ignored by credential_pool.load_pool("anthropic"), + # so MoA Anthropic advisors fail with "no ANTHROPIC_API_KEY" while the + # normal model picker says Anthropic is logged in. + def _slot_matches_provider(slot): + return ( + isinstance(slot, dict) + and (slot.get("provider") or "").strip().lower() == normalized + ) + + moa_cfg = cfg.get("moa") + if isinstance(moa_cfg, dict): + for slot in moa_cfg.get("reference_models") or []: + if _slot_matches_provider(slot): + return True + if _slot_matches_provider(moa_cfg.get("aggregator")): + return True + presets = moa_cfg.get("presets") + if isinstance(presets, dict): + for preset in presets.values(): + if not isinstance(preset, dict): + continue + for slot in preset.get("reference_models") or []: + if _slot_matches_provider(slot): + return True + if _slot_matches_provider(preset.get("aggregator")): + return True except Exception: pass diff --git a/tests/hermes_cli/test_auth_provider_gate.py b/tests/hermes_cli/test_auth_provider_gate.py index ecb602f588b1..0b559dc14977 100644 --- a/tests/hermes_cli/test_auth_provider_gate.py +++ b/tests/hermes_cli/test_auth_provider_gate.py @@ -129,6 +129,29 @@ def test_explicit_pool_source_counts_as_explicit(tmp_path, monkeypatch): assert is_provider_explicitly_configured("anthropic") is True +def test_returns_true_when_moa_reference_slot_uses_provider(tmp_path, monkeypatch): + """MoA advisor slots are explicit provider selections for auth gating.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) + _write_config(tmp_path, { + "model": {"provider": "openai-codex", "default": "gpt-5.5"}, + "moa": { + "presets": { + "default": { + "reference_models": [ + {"provider": "anthropic", "model": "claude-opus-4-8"}, + {"provider": "opencode-go", "model": "glm-5.2"}, + ], + "aggregator": {"provider": "openai-codex", "model": "gpt-5.5"}, + } + } + }, + }) + _write_auth_store(tmp_path, {"version": 1, "providers": {}, "active_provider": "openai-codex"}) + + from hermes_cli.auth import is_provider_explicitly_configured + assert is_provider_explicitly_configured("anthropic") is True + + def test_stale_env_pool_entry_does_not_count_when_var_unset(tmp_path, monkeypatch): """An env-seeded pool entry left in auth.json after the env var was removed must not mark the provider configured (#55790): the picker showed removed @@ -190,3 +213,19 @@ def test_provider_not_in_registry_but_in_models_dev(tmp_path, monkeypatch): from hermes_cli.auth import is_provider_explicitly_configured assert is_provider_explicitly_configured("openrouter") is True + + +def test_returns_true_when_moa_aggregator_uses_provider(tmp_path, monkeypatch): + """MoA aggregator slots are explicit provider selections for auth gating.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) + _write_config(tmp_path, { + "model": {"provider": "openai-codex", "default": "gpt-5.5"}, + "moa": { + "reference_models": [{"provider": "opencode-go", "model": "glm-5.2"}], + "aggregator": {"provider": "anthropic", "model": "claude-opus-4-8"}, + }, + }) + _write_auth_store(tmp_path, {"version": 1, "providers": {}, "active_provider": "openai-codex"}) + + from hermes_cli.auth import is_provider_explicitly_configured + assert is_provider_explicitly_configured("anthropic") is True