fix(dashboard): reject invalid MoA controls

This commit is contained in:
robbyczgw-cla 2026-07-15 08:33:24 +00:00 committed by Teknium
parent ccdf171bcd
commit 223881e492
2 changed files with 58 additions and 9 deletions

View file

@ -27,6 +27,7 @@ import inspect
import importlib.util
import json
import logging
import math
import mimetypes
import os
import queue
@ -47,7 +48,7 @@ import zipfile
from hermes_cli._subprocess_compat import windows_detach_flags, windows_hide_flags
import urllib.request
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Literal, Optional, Tuple
import yaml
@ -105,7 +106,7 @@ try:
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, SecretStr
from pydantic import BaseModel, SecretStr, field_validator
from starlette.concurrency import run_in_threadpool
except ImportError:
# First try lazy-installing the dashboard extras. Only the user actually
@ -121,7 +122,7 @@ except ImportError:
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, SecretStr
from pydantic import BaseModel, SecretStr, field_validator
from starlette.concurrency import run_in_threadpool
except Exception:
raise SystemExit(
@ -1363,15 +1364,34 @@ class MoaModelSlot(BaseModel):
enabled: bool = True
class MoaPresetPayload(BaseModel):
class _MoaReferenceControls(BaseModel):
reference_timeout: float = 30.0
degraded_reference_policy: Literal["loud", "silent"] = "loud"
@field_validator("reference_timeout", mode="before")
@classmethod
def _validate_reference_timeout(cls, value: Any) -> float:
"""Reject JSON booleans/non-finite values before float coercion."""
if isinstance(value, bool):
raise ValueError("reference_timeout must be a finite positive number")
try:
timeout = float(value)
except (TypeError, ValueError) as exc:
raise ValueError(
"reference_timeout must be a finite positive number"
) from exc
if not math.isfinite(timeout) or timeout <= 0:
raise ValueError("reference_timeout must be a finite positive number")
return timeout
class MoaPresetPayload(_MoaReferenceControls):
reference_models: list[MoaModelSlot] = []
aggregator: MoaModelSlot = MoaModelSlot()
# None = temperature omitted from API calls (provider default), matching
# single-model agent behavior.
reference_temperature: Optional[float] = None
aggregator_temperature: Optional[float] = None
reference_timeout: float = 30.0
degraded_reference_policy: str = "loud"
max_tokens: int = 4096
# Newer per-preset knobs (see moa_config._normalize_preset). Optional so
# older clients that never send them keep working; declared so clients
@ -1381,7 +1401,7 @@ class MoaPresetPayload(BaseModel):
enabled: bool = True
class MoaConfigPayload(BaseModel):
class MoaConfigPayload(_MoaReferenceControls):
default_preset: str = "default"
active_preset: str = ""
presets: dict[str, MoaPresetPayload] = {}
@ -1391,8 +1411,6 @@ class MoaConfigPayload(BaseModel):
aggregator: MoaModelSlot = MoaModelSlot()
reference_temperature: Optional[float] = None
aggregator_temperature: Optional[float] = None
reference_timeout: float = 30.0
degraded_reference_policy: str = "loud"
max_tokens: int = 4096
reference_max_tokens: Optional[int] = None
fanout: Optional[str] = None

View file

@ -901,6 +901,9 @@ class TestWebServerEndpoints:
assert {"provider": agg["provider"], "model": agg["model"]} == payload["aggregator"]
assert cfg["moa"]["reference_timeout"] == 44.5
assert cfg["moa"]["degraded_reference_policy"] == "silent"
returned = self.client.get("/api/model/moa").json()
assert returned["reference_timeout"] == 44.5
assert returned["degraded_reference_policy"] == "silent"
def test_put_moa_models_persists_reference_failure_controls_per_preset(self):
from hermes_cli.config import load_config
@ -1325,6 +1328,34 @@ class TestWebServerEndpoints:
json={"values": {"userPeerAliases": "{not json"}},
).status_code == 400
@pytest.mark.parametrize(
"payload",
[
{"reference_timeout": True},
{"reference_timeout": False},
{"reference_timeout": "nan"},
{"reference_timeout": "inf"},
{"presets": {"review": {"reference_timeout": True}}},
{"presets": {"review": {"reference_timeout": "-inf"}}},
],
)
def test_put_moa_models_rejects_invalid_reference_timeout(self, payload):
resp = self.client.put("/api/model/moa", json=payload)
assert resp.status_code == 422
@pytest.mark.parametrize(
"payload",
[
{"degraded_reference_policy": "verbose"},
{"presets": {"review": {"degraded_reference_policy": "verbose"}}},
],
)
def test_put_moa_models_rejects_invalid_degraded_reference_policy(self, payload):
resp = self.client.put("/api/model/moa", json=payload)
assert resp.status_code == 422
# ── GET /api/media (remote image display) ───────────────────────────
def test_get_media_serves_image_in_root(self):