feat(moa): expose MoA presets as selectable virtual models - #46081
Conversation
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-argument-type |
15 |
unsupported-operator |
15 |
unresolved-attribute |
14 |
not-subscriptable |
1 |
invalid-assignment |
1 |
First entries
tests/run_agent/test_in_place_compaction.py:257: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
agent/moa_loop.py:294: [invalid-argument-type] invalid-argument-type: Argument to function `call_llm` is incorrect: Expected `str`, found `Any | None`
tests/hermes_cli/test_destructive_slash_confirm_gate.py:32: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
tests/gateway/test_whatsapp_reply_prefix.py:119: [unsupported-operator] unsupported-operator: Operator `>=` is not supported between objects of type `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements` and `int`
hermes_cli/mcp_startup.py:67: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
hermes_cli/config.py:5178: [unresolved-attribute] unresolved-attribute: Attribute `items` is not defined on `int`, `str`, `list[Unknown]`, `float`, `None` in union `Unknown | int | str | ... omitted 17 union elements`
tests/cron/test_suggestions.py:213: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["monitor"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
gateway/run.py:8569: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_moa_restore_override` on type `MessageEvent`
tools/browser_tool.py:1196: [invalid-argument-type] invalid-argument-type: Argument to constructor `int.__new__` is incorrect: Expected `str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc`, found `Unknown | int | str | ... omitted 17 union elements`
gateway/run.py:8578: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_moa_disable_after_turn` on type `MessageEvent`
tests/agent/test_auxiliary_config_bridge.py:284: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["provider"]` and `Unknown | int | str | ... omitted 17 union elements`
tests/cli/test_fast_command.py:484: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
tests/tools/test_browser_lightpanda.py:242: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["engine"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
tests/tools/test_browser_console.py:341: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["record_sessions"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
tests/agent/test_curator.py:1105: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["curator"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
tests/tools/test_web_providers.py:217: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
tests/hermes_cli/test_aux_config.py:47: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["session_search"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
agent/moa_loop.py:297: [invalid-argument-type] invalid-argument-type: Argument to function `call_llm` is incorrect: Expected `int`, found `Any | None`
tools/process_registry.py:514: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `Unknown | int | str | ... omitted 17 union elements`
tests/agent/test_curator.py:1110: [unsupported-operator] unsupported-operator: Operator `>` is not supported between objects of type `Unknown | int | str | ... omitted 6 union elements` and `Literal[0]`
tests/cli/test_moa_command.py:69: [not-subscriptable] not-subscriptable: Cannot subscript object of type `None` with no `__getitem__` method
tests/cli/test_resume_display.py:716: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["resume_display"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
hermes_cli/config.py:5159: [unresolved-attribute] unresolved-attribute: Attribute `items` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
tests/tools/test_refresh_agent_mcp_tools.py:257: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 35 union elements`
agent/moa_loop.py:299: [invalid-argument-type] invalid-argument-type: Argument to function `call_llm` is incorrect: Expected `dict[Unknown, Unknown]`, found `Any | None`
... and 21 more
✅ Fixed issues (37):
| Rule | Count |
|---|---|
unsupported-operator |
15 |
invalid-argument-type |
10 |
unresolved-attribute |
9 |
unresolved-import |
1 |
invalid-parameter-default |
1 |
invalid-return-type |
1 |
First entries
hermes_cli/config.py:5132: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
hermes_cli/mcp_startup.py:67: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
tests/tools/test_mixture_of_agents_tool.py:6: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tools/mixture_of_agents_tool.py:185: [invalid-parameter-default] invalid-parameter-default: Default value of type `None` is not assignable to annotated parameter type `int`
tests/gateway/test_whatsapp_reply_prefix.py:119: [unsupported-operator] unsupported-operator: Operator `>=` is not supported between objects of type `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements` and `int`
tools/mixture_of_agents_tool.py:111: [invalid-return-type] invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `tuple[str, str, bool]`
tests/hermes_cli/test_kanban_core_functionality.py:3369: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
tools/process_registry.py:514: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `Unknown | int | str | ... omitted 16 union elements`
tests/hermes_cli/test_mcp_reload_confirm_gate.py:33: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
tools/browser_tool.py:1196: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
hermes_cli/config.py:5141: [unresolved-attribute] unresolved-attribute: Attribute `items` is not defined on `int`, `str`, `list[Unknown]`, `float`, `None` in union `Unknown | int | str | ... omitted 16 union elements`
tests/run_agent/test_in_place_compaction.py:257: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
tests/tools/test_browser_console.py:341: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["record_sessions"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
tests/tools/test_refresh_agent_mcp_tools.py:257: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
tests/hermes_cli/test_destructive_slash_confirm_gate.py:32: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
tests/agent/test_auxiliary_config_bridge.py:284: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["provider"]` and `Unknown | int | str | ... omitted 16 union elements`
tests/cli/test_reasoning_command.py:552: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
gateway/run.py:16327: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent.run_conversation` is incorrect: Expected `str | None`, found `Any | str | int | float | list[dict[str, Any]]`
gateway/run.py:16327: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent.run_conversation` is incorrect: Expected `int | float | None`, found `Any | str | int | float | list[dict[str, Any]]`
tests/cli/test_resume_display.py:716: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["resume_display"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
tests/tools/test_web_providers.py:218: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["search_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
tests/tools/test_web_providers.py:217: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
gateway/run.py:16327: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent.run_conversation` is incorrect: Expected `str`, found `Any | str | int | float | list[dict[str, Any]]`
gateway/restart.py:16: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `Unknown | int | str | ... omitted 16 union elements`
tests/hermes_cli/test_aux_config.py:47: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["session_search"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
... and 12 more
Unchanged: 5910 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
71fa7e2 to
9a7c164
Compare
|
Can we call it Mable? Mixture of Fables, just to take the piss out of Anthropic? |
9a7c164 to
1860f69
Compare
1860f69 to
4c367df
Compare
|
Really like where this lands MoA — slash-command mode in the normal loop is the right call. I built a similar reference-aggregator loop in my own agent (8gent Code), where the constraint was free-and-local-by-default, so the harness was the only lever I had. The one thing that moved the needle most there was making the ensemble learn who to trust instead of asking everyone every turn. Offering that back here as a tentative, opt-in addition on top of this PR — entirely your call whether it's useful. Branch: What it addsAn opt-in
Files
Validation
What I haven't proven yetThe unit tests show it routes and learns, not that it's better. The real proof is a side-by-side on a fixed task set — static vs learned, same models — measuring quality at equal-or-lower cost once the router warms up. Happy to run that and attach numbers if you'd want to take this further. If it doesn't win or match at lower cost, it stays behind the flag. Open questions: is the aggregator verdict enough or would you want an optional second judge for high-stakes classes; worth making the task-class bucketing pluggable; and is
|
4c367df to
d90d381
Compare
d90d381 to
7e6a1fe
Compare
|
Took a thorough pass at this — particularly the prompt-caching interaction, since that's the one invariant worth being paranoid about. Verified empirically against the branch (drove the real Caching is safe ✅ — A few non-blocking observations: 1. Reference models get the main model's full system prompt + tool history. 2. MoA fans out N+1 calls before every main-model iteration. It runs over a growing history each pass, so a K-iteration turn = K×(N+1) auxiliary calls with no cross-iteration reuse. The docs say "increases model-call count" but it's per-iteration, not per-turn — worth calling out explicitly, and maybe worth gating to every-K-iterations as a future lever. 3. The per-preset Minor: the toggle + "resolve active config before Tests green (the only 3 failures are unrelated macOS PTY-WebSocket flakes), old |
7e6a1fe to
c89f437
Compare
c89f437 to
97cb4b3
Compare
|
I like this direction. One follow-up thought on the current reference-message trimming: The default of stripping system/tool messages for reference calls makes sense to me, but for power-user setups, it would be very useful to make the reference context strategy configurable per preset. Beyond raw conversation history, persona/operating-frame files such as The default can stay exactly as-is, but presets could opt into something like:
That would make the approach substantially more flexible while preserving the current safe/cheap default. I hope something like this can make it in, either in this PR or as a small follow-up. If you like the direction, I'd be happy to help implement it. |
|
This PR is exactly the direction I was hoping Hermes would take: MoA as a first-class model/provider surface, not a separate tool bolted onto the side. I've been dogfooding Hermes hard from a real Windows + Telegram gateway setup, using GPT-5.5/OpenAI and xAI lanes for local profile-fusion MoA workflows. The current virtual-provider design in this PR maps much better to how I actually want to use it remotely: A few concrete follow-up contributions I'd be happy to help with, if aligned with the maintainers' direction:
I'm here because I want to become a useful contributor to Hermes Agent, not just request features. Happy to split any of this into the smallest acceptable follow-up PR after #46081 lands, or adapt to whatever shape the maintainers prefer. |
3300dab to
85321fc
Compare
Conflicts resolved (4): - .github/workflows/claude-code-review.yml: kept fork's customized workflow (concurrency guard, bot-loop check, Hermes-specific review prompt); upstream's was the stock template. - tools/approval.py + tests/tools/test_approval.py: RECONCILED both self-termination behaviors. Fork scopes pgrep-kills to the agent's own process (_SELF_PROC_TOKEN) to avoid flagging `kill $(pgrep -f metro)`; upstream (NousResearch#33071) added pidof support + a launchctl gateway-lifecycle guard. Folded pidof into the fork's scoped patterns, kept upstream's launchctl line, and extended _SELF_PROC_TOKEN to also match the hermes_cli module entrypoint (so `kill $(pidof hermes_cli.main)` flags) while still excluding underscore lookalikes (hermes_backend/hermesd). Updated _SELF_TERM_DESCRIPTIONS to the renamed pattern descriptions so the obfuscation-collapse scan view still applies. Kept both test suites. - hermes_cli/tools_config.py: both sides removed a different default-off toolset from the same set (fork dropped `video` via "enable video by default"; upstream NousResearch#46081 dropped `moa`). Applied BOTH removals.
…rch#46081) * feat(moa): expose MoA presets as selectable virtual models Reconstructed onto current main (PR NousResearch#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.
* feat(moa): expose MoA presets as selectable virtual models (NousResearch#46081) * feat(moa): expose MoA presets as selectable virtual models Reconstructed onto current main (PR NousResearch#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. * fix(moa): keep virtual provider on MoA client * feat(moa): make /moa one-shot only; route preset switching through the model picker /moa no longer does a sticky model switch. It now always runs a single prompt through the default MoA preset and restores the prior model afterward; the whole argument is the prompt (no preset-name matching). To switch to a MoA preset for the session, select it from the model picker, where presets already surface under a virtual Mixture of Agents provider on every model-selection surface. Also fixes NousResearch#53444: the TUI one-shot only set session[model_override], which the already-built cached agent ignored, so MoA silently never ran and the turn used the original model. The TUI now does a real in-place agent.switch_model() via _apply_model_switch() when a live agent exists (with a proper restore after the turn), and falls back to a model_override for lazy/unbuilt sessions. Removes the redundant sticky-switch branch from the CLI, gateway, and TUI /moa handlers; updates the command description, usage string, and docs. * fix(moa): call reference + aggregator models through their provider's real route (NousResearch#53580) MoA was calling reference and aggregator models through a bare call_llm(provider=slot["provider"], model=slot["model"]) with a forced temperature and a forced max_tokens (the preset's hardcoded 4096). That left base_url/api_key/api_mode unresolved — so the auxiliary auto-detector guessed the API surface instead of using the provider's real runtime, and the 4096 cap truncated long aggregator syntheses. A MoA slot is just a model selection and must be called the same way any model is called elsewhere. Each slot is now resolved through resolve_runtime_provider (the canonical provider→api_mode/base_url/api_key resolver the CLI, gateway, and delegate_task all use) via a new _slot_runtime() helper, and the resolved endpoint is passed into call_llm. So a reference/aggregator gets its provider's actual API surface — MiniMax → anthropic_messages, GPT-5/o-series → max_completion_tokens, custom endpoints → their base_url — identical to how that model is handled as the acting model. MoA also no longer imposes its own output cap: max_tokens defaults to None (omitted → the model's real maximum) for references and is passed through from the caller for the aggregator. The preset's hardcoded 4096 is gone. The max_tokens preset config field is left in place (config/web/desktop unchanged); it is simply no longer applied as a forced cap. Tests: slots route through resolve_runtime_provider with resolved base_url/ api_key; resolution errors fall back to bare provider/model; neither call carries an output cap even when the preset config still contains max_tokens. * feat(moa): references see full tool state + fire on every user/tool response (NousResearch#54016) The advisory reference view stripped all tool calls and tool results, so reference models judged a task whose actions and results they never saw — and references only fired once per user turn, never re-running as the agent's state advanced through the tool loop. Two fixes: - _reference_messages() now PRESERVES the agent's tool calls and tool results, rendering them inline as text ([called tool: ...] / [tool result: ...]) so a reference gives an informed judgement on the real current state. Still emits zero tool-role messages and zero tool_calls arrays (strict providers reject those), and large tool results are previewed head+tail (4000-char budget). The required end-on-user shape is met by APPENDING a synthetic advisory user turn — not by deleting the agent's latest context (which the prior fix did). - References now re-run on every state change — each new user message AND each new tool result — instead of once per user turn. The state-sensitive advisory signature drives the cache: new tool result = miss (re-run), identical-state re-call = hit (no re-run, no re-emit). The acting aggregator still receives the full, untrimmed transcript. * fix(auxiliary): use env-only proxy policy for OpenAI SDK clients (NousResearch#53702) Auxiliary clients now inject a keepalive httpx transport with explicit HTTPS_PROXY/NO_PROXY resolution, matching the main agent. This avoids macOS system proxy settings (which omit the ExceptionsList) breaking vision and other auxiliary calls to internal provider endpoints. * fix(auxiliary): fall back to OPENROUTER_API_KEY when credential pool exhausted _try_openrouter() returned (None, None) whenever an OpenRouter credential pool existed but was exhausted (_select_pool_entry -> (True, None)), making the OPENROUTER_API_KEY env-var fallback unreachable. Auxiliary tasks (compression, vision, web_extract) silently failed even with a valid env key. Now the pool-present branch only returns early when it successfully builds a client; an exhausted pool falls through to the env-var path. The final failure (pool exhausted AND no env var) still marks the provider unhealthy. Fixes NousResearch#23452. Co-authored-by: ambition0802 <noreply@github.com> * wip(moa): 恢复自崩溃 seat — 国产 preset 适配进行中 * test(moa): verify ApexNodes relay routing end-to-end (custom:apex-nodes.com) Two tests prove the China-first managed default preset routes every MoA slot through the ApexNodes relay rather than a foreign provider: - test_apex_moa_slot_resolves_to_relay: _slot_runtime runs the REAL resolve_runtime_provider against a desktop-seeded custom_providers entry and resolves custom:apex-nodes.com -> relay base_url + user key. - test_apex_moa_turn_routes_every_slot_through_relay: a full /moa turn sends both references + the aggregator to the relay, each with its own model name on the wire (per-request routing; relay maps by hc-184). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(moa): activate ApexNodes domestic preset in cli-config.yaml.example Uncomment the moa: block so the pure-CLI/BYOK path (which copies the example as its starting config.yaml) ships the China-first managed preset live, matching the in-code default in hermes_cli/moa_config.py. The desktop managed seed needs no change: normalize_moa_config falls back to the same ApexNodes preset whenever config has no moa: block, so a freshly-seeded desktop already routes MoA through the relay (verified). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(moa): make ApexNodes relay-routing turn test order-independent References fan out across a ThreadPoolExecutor, so the two reference call_llm invocations append from worker threads in nondeterministic order. The original exact-order assertion was flaky under CPU load (18/20 in a stress loop). Assert the SET of reference calls + that the aggregator (sequential, post-fan-out) is last instead; guard the shared list with a lock. 30/30 under load now. Routing assertions (relay base_url + user key on every call) unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(apex_overlay): pilot seam package + apex-overlay plugin (hc-392 denylist) Introduce apex_overlay/ — our zero-conflict namespace (upstream never creates this dir) for behavior that would otherwise be in-place edits to hot upstream files. Pilot seam: hc-392 provider denylist moved here as a monkey-patch. - apex_overlay/provider_filter.py: patches cached_provider_model_ids (cut the copilot GitHub fetch before it fires) + list_authenticated_providers (drop disabled rows). No-op when denylist empty. - plugins/apex-overlay/: bundled plugin whose register() calls apply() during plugin discovery (runs before /model picker prewarm). - cli-config.yaml.example: enable apex-overlay via plugins.enabled (config tier). - apex_overlay/README.md: the pattern for later phases to copy. model_switch.py restore + seam-test land in follow-up commits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(model_switch): restore upstream — hc-392 denylist moved to apex_overlay Remove the +34 in-place hc-392 lines (disabled_providers set + _is_disabled_provider helper + 3 in-loop 'continue' callsites) from list_authenticated_providers. The behavior is now provided by apex_overlay.provider_filter via monkey-patch (prior commit). Diff vs upstream baseline is now MoA-only (the cherry-picked preset code), which merges cleanly on the next bump. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(apex_overlay): seam-test + behavior for hc-392 provider denylist Pins the two upstream symbols apex_overlay.provider_filter monkey-patches (cached_provider_model_ids, list_authenticated_providers) — existence + compatible signature — so an upstream rename/move fails CI loudly instead of silently disarming the guard. Behavior (run via run_tests_parallel.py, per-file interpreter isolation): - copilot denylisted + GH_TOKEN set => fetch_github_model_catalog called 0 times, no copilot row (the 'no startup network call' contract). - empty denylist control => same setup surfaces copilot (proves the denylist is the cause). - plugin.register() applies the seam; config enables it; discovery ordered before picker prewarm in cli.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(release): map upstream MoA contributors for check-attribution Cherry-picked MoA fixes (NousResearch#53580, NousResearch#53702) authored post-v0.17.0 by LeonSGP43/HexLab98; fork AUTHOR_MAP 早于他们。补邮箱使 check-attribution 过(其余 CI 已全绿)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com> Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com> Co-authored-by: HexLab98 <liruixinch@outlook.com> Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com> Co-authored-by: ambition0802 <noreply@github.com> Co-authored-by: karlligamesvc-spec <270151505+karlligamesvc-spec@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…rch#46081) * feat(moa): expose MoA presets as selectable virtual models Reconstructed onto current main (PR NousResearch#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.
…rch#46081) * feat(moa): expose MoA presets as selectable virtual models Reconstructed onto current main (PR NousResearch#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.
…rch#46081) * feat(moa): expose MoA presets as selectable virtual models Reconstructed onto current main (PR NousResearch#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.
…rch#46081) * feat(moa): expose MoA presets as selectable virtual models Reconstructed onto current main (PR NousResearch#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.
…rch#46081) * feat(moa): expose MoA presets as selectable virtual models Reconstructed onto current main (PR NousResearch#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.
…rch#46081) * feat(moa): expose MoA presets as selectable virtual models Reconstructed onto current main (PR NousResearch#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.

Summary
MoA is now a virtual provider: each named preset appears as a selectable model under provider
moa, and the preset's aggregator is the acting model that answers and calls tools.What changed
mixture_of_agentsmodel tool andmoatoolset from registry/setup/tool config/ACP/distributions/docs.moaas a virtual provider in the provider/model inventory.Mixture of Agentsprovider in model picker payloads.provider=moa,model=<preset>now builds a MoA client facade instead of a network OpenAI client./moais now shortcut behavior over model selection:/moaswitches to the default MoA preset./moa <preset>switches to that preset when it exactly matches a configured preset./moa <prompt>temporarily switches to the default MoA preset for one turn, sends the prompt, then restores the previous model.How MoA works now
provider=moa,model=<preset>.<preset>frommoa.presets.Verified behavior
build_models_payload()injects a virtualmoaprovider row with preset names as models.provider=moa,model=reviewreturns the aggregator response as the final assistant response.moa_aggregatorcall receives tools.openai-codex:gpt-5.5reference plus OpenRouter-hosted aggregator/reference models.mixture_of_agentstool andmoatoolset are gone._reference_messages()strips the system prompt +tool/tool_callsmessages; reference calls receive user/assistant text only.enabledflag is liveenabled: falseskips the reference fan-out — the aggregator acts alone (unit test).Complete live task proof from earlier in this PR
A live MoA task completed with this mixed-provider setup:
openai-codex:gpt-5.5openrouter:deepseek/deepseek-v4-proopenrouter:anthropic/claude-opus-4.8Task result:
completed=truefailed=falseapi_calls=5moa_iterations=5assistant_tool_calls=16VERIFIED_COMPLETE_TASKValidation
804 passedcd web && npm run buildpassednpm --workspace apps/desktop run typecheckpassedgit diff --checkcleanHermesBench results
A two-model MoA preset —
claude-opus-4.8aggregating over agpt-5.5reference — outscores either model run solo on HermesBench:anthropic/claude-opus-4.8openai/gpt-5.5MoA beats its strongest component (opus-4.8) by ~6 points.
Infographic