From f0678b031e6723cf44f9fb520584120f50a33cff Mon Sep 17 00:00:00 2001 From: srojk34 <286497132+srojk34@users.noreply.github.com> Date: Fri, 26 Jun 2026 03:22:57 +0300 Subject: [PATCH] fix(moa): tolerate non-numeric values in hand-edited MoA preset config _normalize_preset uses bare float() and int() to coerce reference_temperature, aggregator_temperature, and max_tokens from config.yaml. When a user hand-edits a non-numeric value (e.g. max_tokens: "8k" or reference_temperature: "hot"), the coercion raises ValueError. Since normalize_moa_config runs on every model-selection and MoA turn (via resolve_moa_preset), the crash is unrecoverable and blocks all MoA usage until the config is manually fixed. Replace the bare casts with _coerce_float / _coerce_int helpers that fall back to the default on TypeError/ValueError instead of raising. --- hermes_cli/moa_config.py | 27 +++++++++++++++++--- tests/hermes_cli/test_moa_config.py | 39 +++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/hermes_cli/moa_config.py b/hermes_cli/moa_config.py index 4c0db3f2c35..bd74ced49f8 100644 --- a/hermes_cli/moa_config.py +++ b/hermes_cli/moa_config.py @@ -21,6 +21,27 @@ DEFAULT_MOA_AGGREGATOR: dict[str, str] = { } +def _coerce_float(value: Any, default: float) -> float: + if value is None or value == "": + return default + try: + return float(value) + except (TypeError, ValueError): + return default + + +def _coerce_int(value: Any, default: int) -> int: + if value is None or value == "": + return default + try: + return int(value) + except (TypeError, ValueError): + try: + return int(float(value)) + except (TypeError, ValueError): + return default + + def _clean_slot(slot: Any) -> dict[str, str] | None: if not isinstance(slot, dict): return None @@ -57,9 +78,9 @@ def _normalize_preset(raw: Any) -> dict[str, Any]: "enabled": bool(raw.get("enabled", True)), "reference_models": refs, "aggregator": aggregator, - "reference_temperature": float(raw.get("reference_temperature", 0.6) or 0.6), - "aggregator_temperature": float(raw.get("aggregator_temperature", 0.4) or 0.4), - "max_tokens": int(raw.get("max_tokens", 4096) or 4096), + "reference_temperature": _coerce_float(raw.get("reference_temperature"), 0.6), + "aggregator_temperature": _coerce_float(raw.get("aggregator_temperature"), 0.4), + "max_tokens": _coerce_int(raw.get("max_tokens"), 4096), } diff --git a/tests/hermes_cli/test_moa_config.py b/tests/hermes_cli/test_moa_config.py index d02b2f06459..630d183927d 100644 --- a/tests/hermes_cli/test_moa_config.py +++ b/tests/hermes_cli/test_moa_config.py @@ -55,6 +55,45 @@ def test_legacy_flat_config_becomes_default_preset(): ] +def test_normalize_moa_config_tolerates_non_numeric_values(): + """Non-numeric strings in hand-edited config.yaml must degrade to defaults + instead of crashing normalize_moa_config with ValueError.""" + cfg = normalize_moa_config( + { + "presets": { + "broken": { + "max_tokens": "notanumber", + "reference_temperature": "hot", + "aggregator_temperature": "", + } + } + } + ) + + preset = cfg["presets"]["broken"] + assert preset["max_tokens"] == 4096 + assert preset["reference_temperature"] == 0.6 + assert preset["aggregator_temperature"] == 0.4 + + +def test_normalize_moa_config_coerces_numeric_strings(): + """Valid numeric strings (e.g. from YAML round-trip) must coerce correctly.""" + cfg = normalize_moa_config({"max_tokens": "8192", "reference_temperature": "0.9"}) + + preset = cfg["presets"][DEFAULT_MOA_PRESET_NAME] + assert preset["max_tokens"] == 8192 + assert preset["reference_temperature"] == 0.9 + + +def test_normalize_moa_config_coerces_float_max_tokens(): + """max_tokens: 4096.0 (float from YAML) must coerce to int.""" + cfg = normalize_moa_config({"max_tokens": 4096.0}) + assert cfg["presets"][DEFAULT_MOA_PRESET_NAME]["max_tokens"] == 4096 + + cfg2 = normalize_moa_config({"max_tokens": "4096.5"}) + assert cfg2["presets"][DEFAULT_MOA_PRESET_NAME]["max_tokens"] == 4096 + + def test_exact_preset_matching_is_not_fuzzy(): config = {"presets": {"coding": {}, "review": {}}}