mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
fix(moa): block the moa virtual provider as a reference or aggregator slot (#53281)
A MoA preset whose reference or aggregator slot points at the moa virtual provider creates a recursive MoA tree. The runtime guards in moa_loop.py only surface this mid-turn (references silently skipped, aggregator raises). Reject it at the config chokepoint (_clean_slot) so it can never be saved, and hide it from the desktop/dashboard slot pickers so it isn't offered as a dead choice.
This commit is contained in:
parent
515192c4b9
commit
7e101e553b
4 changed files with 72 additions and 2 deletions
|
|
@ -182,6 +182,13 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
|
|||
|
||||
const providerOptions = providers.length ? providers : NO_PROVIDERS
|
||||
|
||||
// MoA reference/aggregator slots must never be the moa virtual provider —
|
||||
// that would create a recursive MoA tree (the backend rejects it on save).
|
||||
// Hide it from the slot selectors so it isn't offered as a dead choice.
|
||||
const moaSlotProviderOptions = providerOptions.filter(
|
||||
provider => (provider.slug || '').toLowerCase() !== 'moa'
|
||||
)
|
||||
|
||||
const selectedProviderRow = useMemo(
|
||||
() => providers.find(provider => provider.slug === selectedProvider),
|
||||
[providers, selectedProvider]
|
||||
|
|
@ -851,7 +858,7 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
|
|||
<SelectValue placeholder={m.provider} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{providerOptions.map(provider => (
|
||||
{moaSlotProviderOptions.map(provider => (
|
||||
<SelectItem key={provider.slug || 'none'} value={provider.slug || 'none'}>
|
||||
{provider.name}
|
||||
</SelectItem>
|
||||
|
|
@ -930,7 +937,7 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
|
|||
<SelectValue placeholder={m.provider} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{providerOptions.map(provider => (
|
||||
{moaSlotProviderOptions.map(provider => (
|
||||
<SelectItem key={provider.slug || 'none'} value={provider.slug || 'none'}>
|
||||
{provider.name}
|
||||
</SelectItem>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,13 @@ def _clean_slot(slot: Any) -> dict[str, str] | None:
|
|||
model = str(slot.get("model") or "").strip()
|
||||
if not provider or not model:
|
||||
return None
|
||||
# MoA is a virtual provider whose presets are themselves MoA runs. Allowing
|
||||
# one as a reference or aggregator slot would create a recursive MoA tree
|
||||
# (the runtime guards in moa_loop.py skip references / raise on aggregators,
|
||||
# but that surfaces only mid-turn). Reject it here so it can never be saved:
|
||||
# an invalid slot is dropped, falling back to the preset's defaults.
|
||||
if provider.lower() == "moa":
|
||||
return None
|
||||
return {"provider": provider, "model": model}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -134,3 +134,54 @@ def test_build_moa_turn_prompt_encodes_one_shot_default_preset():
|
|||
assert decoded_prompt == "write a file then inspect it"
|
||||
assert cfg is not None
|
||||
assert cfg["reference_models"] == DEFAULT_MOA_REFERENCE_MODELS
|
||||
|
||||
|
||||
def test_moa_provider_rejected_as_reference_slot():
|
||||
"""A reference slot pointing at the moa virtual provider is dropped, so a
|
||||
preset cannot recursively reference another MoA run."""
|
||||
cfg = normalize_moa_config(
|
||||
{
|
||||
"presets": {
|
||||
"p": {
|
||||
"reference_models": [
|
||||
{"provider": "moa", "model": "default"},
|
||||
{"provider": "openrouter", "model": "deepseek/deepseek-v4-pro"},
|
||||
],
|
||||
"aggregator": {"provider": "openrouter", "model": "anthropic/claude-opus-4.8"},
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
refs = cfg["presets"]["p"]["reference_models"]
|
||||
assert {"provider": "moa", "model": "default"} not in refs
|
||||
assert refs == [{"provider": "openrouter", "model": "deepseek/deepseek-v4-pro"}]
|
||||
|
||||
|
||||
def test_moa_provider_rejected_as_aggregator_slot():
|
||||
"""An aggregator slot pointing at the moa virtual provider is dropped and
|
||||
falls back to the default aggregator, never a recursive MoA aggregator."""
|
||||
cfg = normalize_moa_config(
|
||||
{
|
||||
"presets": {
|
||||
"p": {
|
||||
"reference_models": [{"provider": "openrouter", "model": "deepseek/deepseek-v4-pro"}],
|
||||
"aggregator": {"provider": "moa", "model": "default"},
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
agg = cfg["presets"]["p"]["aggregator"]
|
||||
assert agg["provider"] != "moa"
|
||||
assert agg == DEFAULT_MOA_AGGREGATOR
|
||||
|
||||
|
||||
def test_moa_provider_rejected_case_insensitive():
|
||||
"""Case variants like ``MoA`` are also blocked."""
|
||||
cfg = normalize_moa_config(
|
||||
{"presets": {"p": {"aggregator": {"provider": "MoA", "model": "default"}}}}
|
||||
)
|
||||
|
||||
assert cfg["presets"]["p"]["aggregator"]["provider"] != "moa"
|
||||
assert cfg["presets"]["p"]["aggregator"] == DEFAULT_MOA_AGGREGATOR
|
||||
|
|
|
|||
|
|
@ -846,6 +846,11 @@ function MoaModelsModal({
|
|||
alwaysGlobal
|
||||
title="Select MoA Model"
|
||||
onApply={async ({ provider, model }) => {
|
||||
if ((provider || "").toLowerCase() === "moa") {
|
||||
setError("MoA presets can't reference or aggregate the Mixture of Agents provider (no recursive MoA).");
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
updateSelectedPreset((prev) => {
|
||||
if (picker.kind === "aggregator") return { ...prev, aggregator: { provider, model } };
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue