mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
fix(dashboard): reject invalid MoA controls
This commit is contained in:
parent
ccdf171bcd
commit
223881e492
2 changed files with 58 additions and 9 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue