mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(moa): default temperatures to unset — provider default, like single-model agents (#57440)
A single-model Hermes agent never sends temperature; the provider default applies. MoA hardcoded reference_temperature=0.6 / aggregator_temperature=0.4, and the coercion float(preset.get(key, 0.6) or 0.6) made unset IMPOSSIBLE to express: absent, null, empty, and even an explicit 0 all collapsed to the baked-in default. Every MoA advisor and aggregator therefore ran at 0.6/0.4 while the same model running solo used the provider default — silently skewing solo-vs-MoA comparisons and overriding provider-tuned defaults. - moa_config normalization: temperatures coerce to None when absent/blank/ invalid (new _coerce_float_or_none); explicit values incl. 0 honored. - moa_loop: _preset_temperature() resolves preset values; None flows to call_llm, which already omits the parameter when None (same contract as max_tokens). Aggregator still inherits the acting agent's own configured temperature when the preset doesn't pin one. - conversation_loop (context-mode MoA): same resolution, no more hardcoded 0.6/0.4 at the call site. - DEFAULT_CONFIG preset + web_server payload models + docs updated: unset is the default, pinning stays available.
This commit is contained in:
parent
e1a1dac848
commit
372f8195c7
8 changed files with 74 additions and 26 deletions
|
|
@ -5902,7 +5902,7 @@ def call_llm(
|
|||
api_key: str = None,
|
||||
main_runtime: Optional[Dict[str, Any]] = None,
|
||||
messages: list,
|
||||
temperature: float = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: int = None,
|
||||
tools: list = None,
|
||||
timeout: float = None,
|
||||
|
|
@ -6533,7 +6533,7 @@ async def async_call_llm(
|
|||
api_key: str = None,
|
||||
main_runtime: Optional[Dict[str, Any]] = None,
|
||||
messages: list,
|
||||
temperature: float = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: int = None,
|
||||
tools: list = None,
|
||||
timeout: float = None,
|
||||
|
|
|
|||
|
|
@ -847,15 +847,15 @@ def run_conversation(
|
|||
|
||||
if moa_config:
|
||||
try:
|
||||
from agent.moa_loop import aggregate_moa_context
|
||||
from agent.moa_loop import _preset_temperature, aggregate_moa_context
|
||||
|
||||
_moa_context = aggregate_moa_context(
|
||||
user_prompt=original_user_message if isinstance(original_user_message, str) else str(original_user_message),
|
||||
api_messages=api_messages,
|
||||
reference_models=moa_config.get("reference_models") or [],
|
||||
aggregator=moa_config.get("aggregator") or {},
|
||||
temperature=float(moa_config.get("reference_temperature", 0.6) or 0.6),
|
||||
aggregator_temperature=float(moa_config.get("aggregator_temperature", 0.4) or 0.4),
|
||||
temperature=_preset_temperature(moa_config, "reference_temperature"),
|
||||
aggregator_temperature=_preset_temperature(moa_config, "aggregator_temperature"),
|
||||
max_tokens=moa_config.get("reference_max_tokens"),
|
||||
)
|
||||
if _moa_context:
|
||||
|
|
|
|||
|
|
@ -490,14 +490,35 @@ def _extract_text(response: Any) -> str:
|
|||
return ""
|
||||
|
||||
|
||||
def _preset_temperature(preset: dict[str, Any], key: str) -> float | None:
|
||||
"""Read an optional temperature from a preset.
|
||||
|
||||
Returns None when the key is absent, empty, or explicitly null — meaning
|
||||
"don't send temperature; let the provider default apply", exactly like a
|
||||
single-model Hermes agent (which never sends temperature unless
|
||||
configured). The old coercion ``float(preset.get(key, 0.6) or 0.6)``
|
||||
made unset impossible: absent, null, and even 0 all collapsed to the
|
||||
hardcoded default, so MoA advisors/aggregator always ran at 0.6/0.4
|
||||
while the same model running solo used the provider default.
|
||||
"""
|
||||
value = preset.get(key)
|
||||
if value is None or (isinstance(value, str) and not value.strip()):
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
logger.warning("ignoring non-numeric %s=%r in MoA preset", key, value)
|
||||
return None
|
||||
|
||||
|
||||
def aggregate_moa_context(
|
||||
*,
|
||||
user_prompt: str,
|
||||
api_messages: list[dict[str, Any]],
|
||||
reference_models: list[dict[str, str]],
|
||||
aggregator: dict[str, str],
|
||||
temperature: float = 0.6,
|
||||
aggregator_temperature: float = 0.4,
|
||||
temperature: float | None = None,
|
||||
aggregator_temperature: float | None = None,
|
||||
max_tokens: int | None = None,
|
||||
) -> str:
|
||||
"""Run configured reference models and synthesize their advice.
|
||||
|
|
@ -510,6 +531,11 @@ def aggregate_moa_context(
|
|||
the parameter entirely when it is ``None`` (see its docstring), which also
|
||||
sidesteps providers that reject ``max_tokens`` outright. A hardcoded cap
|
||||
here previously truncated long aggregator syntheses.
|
||||
|
||||
``temperature`` / ``aggregator_temperature`` are ``None`` by default:
|
||||
like max_tokens, ``call_llm`` omits temperature when None so the
|
||||
provider default applies — matching single-model agent behavior. Presets
|
||||
may still pin explicit values.
|
||||
"""
|
||||
reference_outputs: list[tuple[str, str, Any]] = []
|
||||
ref_messages = _reference_messages(api_messages)
|
||||
|
|
@ -726,8 +752,15 @@ class MoAChatCompletions:
|
|||
# The acting aggregator is never capped here (its output is the
|
||||
# user-visible answer).
|
||||
reference_max_tokens = preset.get("reference_max_tokens")
|
||||
temperature = float(preset.get("reference_temperature", 0.6) or 0.6)
|
||||
aggregator_temperature = float(preset.get("aggregator_temperature", api_kwargs.get("temperature") or 0.4) or 0.4)
|
||||
# None (the default) = don't send temperature; provider default
|
||||
# applies, matching single-model agent behavior. Presets may pin
|
||||
# explicit values. See _preset_temperature.
|
||||
temperature = _preset_temperature(preset, "reference_temperature")
|
||||
aggregator_temperature = _preset_temperature(preset, "aggregator_temperature")
|
||||
if aggregator_temperature is None and api_kwargs.get("temperature") is not None:
|
||||
# The acting agent's own configured temperature (if any) still
|
||||
# applies to the aggregator, which IS the acting model.
|
||||
aggregator_temperature = api_kwargs.get("temperature")
|
||||
|
||||
# When the preset is disabled, skip the reference fan-out and let the
|
||||
# configured aggregator act alone — it is the preset's acting model, so
|
||||
|
|
|
|||
|
|
@ -2190,8 +2190,6 @@ DEFAULT_CONFIG = {
|
|||
{"provider": "openrouter", "model": "deepseek/deepseek-v4-pro"},
|
||||
],
|
||||
"aggregator": {"provider": "openrouter", "model": "anthropic/claude-opus-4.8"},
|
||||
"reference_temperature": 0.6,
|
||||
"aggregator_temperature": 0.4,
|
||||
"max_tokens": 4096,
|
||||
"enabled": True,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,13 +21,20 @@ DEFAULT_MOA_AGGREGATOR: dict[str, str] = {
|
|||
}
|
||||
|
||||
|
||||
def _coerce_float(value: Any, default: float) -> float:
|
||||
def _coerce_float_or_none(value: Any) -> float | None:
|
||||
"""Coerce to a float, or None when unset/blank/invalid.
|
||||
|
||||
Used for optional sampling params (reference_temperature /
|
||||
aggregator_temperature) where None means 'don't send the parameter —
|
||||
provider default applies', matching how a single-model Hermes agent
|
||||
never sends temperature unless explicitly configured.
|
||||
"""
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return None
|
||||
|
||||
|
||||
def _coerce_int(value: Any, default: int) -> int:
|
||||
|
|
@ -81,8 +88,10 @@ def _default_preset() -> dict[str, Any]:
|
|||
return {
|
||||
"reference_models": deepcopy(DEFAULT_MOA_REFERENCE_MODELS),
|
||||
"aggregator": deepcopy(DEFAULT_MOA_AGGREGATOR),
|
||||
"reference_temperature": 0.6,
|
||||
"aggregator_temperature": 0.4,
|
||||
# None = temperature omitted from API calls (provider default),
|
||||
# matching single-model agent behavior.
|
||||
"reference_temperature": None,
|
||||
"aggregator_temperature": None,
|
||||
"max_tokens": 4096,
|
||||
"reference_max_tokens": None,
|
||||
"enabled": True,
|
||||
|
|
@ -110,8 +119,8 @@ def _normalize_preset(raw: Any) -> dict[str, Any]:
|
|||
"enabled": bool(raw.get("enabled", True)),
|
||||
"reference_models": refs,
|
||||
"aggregator": aggregator,
|
||||
"reference_temperature": _coerce_float(raw.get("reference_temperature"), 0.6),
|
||||
"aggregator_temperature": _coerce_float(raw.get("aggregator_temperature"), 0.4),
|
||||
"reference_temperature": _coerce_float_or_none(raw.get("reference_temperature")),
|
||||
"aggregator_temperature": _coerce_float_or_none(raw.get("aggregator_temperature")),
|
||||
"max_tokens": _coerce_int(raw.get("max_tokens"), 4096),
|
||||
# Optional cap on how much each reference ADVISOR may generate per turn.
|
||||
# None (default) = uncapped: advisors write full-length advice, matching
|
||||
|
|
|
|||
|
|
@ -940,8 +940,10 @@ class MoaModelSlot(BaseModel):
|
|||
class MoaPresetPayload(BaseModel):
|
||||
reference_models: list[MoaModelSlot] = []
|
||||
aggregator: MoaModelSlot = MoaModelSlot()
|
||||
reference_temperature: float = 0.6
|
||||
aggregator_temperature: float = 0.4
|
||||
# None = temperature omitted from API calls (provider default), matching
|
||||
# single-model agent behavior.
|
||||
reference_temperature: Optional[float] = None
|
||||
aggregator_temperature: Optional[float] = None
|
||||
max_tokens: int = 4096
|
||||
enabled: bool = True
|
||||
|
||||
|
|
@ -954,8 +956,8 @@ class MoaConfigPayload(BaseModel):
|
|||
# clients during this PR's transition window.
|
||||
reference_models: list[MoaModelSlot] = []
|
||||
aggregator: MoaModelSlot = MoaModelSlot()
|
||||
reference_temperature: float = 0.6
|
||||
aggregator_temperature: float = 0.4
|
||||
reference_temperature: Optional[float] = None
|
||||
aggregator_temperature: Optional[float] = None
|
||||
max_tokens: int = 4096
|
||||
enabled: bool = True
|
||||
profile: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -72,8 +72,11 @@ def test_normalize_moa_config_tolerates_non_numeric_values():
|
|||
|
||||
preset = cfg["presets"]["broken"]
|
||||
assert preset["max_tokens"] == 4096
|
||||
assert preset["reference_temperature"] == 0.6
|
||||
assert preset["aggregator_temperature"] == 0.4
|
||||
# Unparseable/blank temperatures degrade to None = "don't send the
|
||||
# parameter; provider default applies" (matching single-model behavior),
|
||||
# not to a hardcoded sampling value.
|
||||
assert preset["reference_temperature"] is None
|
||||
assert preset["aggregator_temperature"] is None
|
||||
|
||||
|
||||
def test_normalize_moa_config_tolerates_non_list_reference_models():
|
||||
|
|
|
|||
|
|
@ -85,8 +85,11 @@ moa:
|
|||
aggregator:
|
||||
provider: openrouter
|
||||
model: anthropic/claude-opus-4.8
|
||||
reference_temperature: 0.6
|
||||
aggregator_temperature: 0.4
|
||||
# Optional: pin sampling temperatures. When omitted (the default),
|
||||
# temperature is NOT sent and each model uses its provider default —
|
||||
# the same behavior as a single-model Hermes agent.
|
||||
# reference_temperature: 0.6
|
||||
# aggregator_temperature: 0.4
|
||||
max_tokens: 4096
|
||||
enabled: true
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue