Merge pull request #4134 from nesquena/stage-4130

Release NE (v0.51.392): align WebUI reasoning efforts to agent set, drop max (#3505 follow-up)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-06-13 12:02:05 -07:00
commit ebe9fff809
7 files changed
+71 -16

No files matched your search

+6
View File
@@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.392] — 2026-06-13 — Release NE (align WebUI reasoning efforts to the agent's accepted set, drop "max")
### Fixed
- **The WebUI no longer offers a "Max" reasoning effort the agent never accepted.** `VALID_REASONING_EFFORTS` is now `minimal/low/medium/high/xhigh` — matching `hermes-agent`'s own `VALID_REASONING_EFFORTS` (which has no `max`) — and the reasoning picker, `/reasoning` slash-command args, and help text drop the stale `Max` option. A previously-saved `reasoning_effort: max` is migrated to `xhigh` at resolve time (instead of becoming parser-invalid and silently dropping reasoning), so existing configs keep working. This is the "only expose what the model/agent actually supports" cleanup. (#3505 review follow-up)
## [v0.51.391] — 2026-06-13 — Release ND (Stable Assistant Turn Anchors renderer snapshot adapter, inert, #3926)
### Added
+23 -10
View File
@@ -2361,7 +2361,8 @@ def get_effective_default_model(config_data: dict | None = None) -> str:
# Mirrors hermes_constants.parse_reasoning_effort so WebUI can validate without
# importing from the agent tree (which may not be installed). Any drift here
# will show up in the shared test suite since both sides accept the same set.
VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh", "max")
# Keep this WebUI-visible set aligned with hermes-agent#29248.
VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")
def parse_reasoning_effort(effort):
@@ -2652,6 +2653,9 @@ def coerce_reasoning_effort_for_model(
return ""
if raw == "none":
return "none"
accepts_max_as_xhigh = raw == "max"
if accepts_max_as_xhigh:
raw = "xhigh"
if raw not in VALID_REASONING_EFFORTS:
return ""
supported = resolve_model_reasoning_efforts(
@@ -2663,21 +2667,21 @@ def coerce_reasoning_effort_for_model(
# both for models KNOWN not to support reasoning AND for models we simply
# don't recognize (custom providers, aggregator-rewritten ids, brand-new
# releases). Coercion exists to avoid sending a level a KNOWN-incompatible
# model rejects (e.g. openai-codex gpt-5 'max', o1/o3/o4 above 'high') —
# model rejects (e.g. openai-codex gpt-5 'max', o1/o3/o4 above 'high') -
# those paths return a NON-empty clamped set, so the degrade ladder below
# still applies. When the set is empty we can't tell "unsupported" from
# "unknown", so preserve the user's configured effort verbatim (the prior
# behavior) rather than silently disabling reasoning — the provider stays
# the final authority. Worst case is the same rejected request that master
# already produces, i.e. no regression. (#3505 review)
# "unknown", so preserve the user's configured effort verbatim where it is
# still valid. A stale 'max' value is no longer parser-valid on the WebUI
# side, so degrade that unknown-model case to xhigh instead of silently
# dropping reasoning later in parse_reasoning_effort(). (#3505 review)
if not supported:
return raw
return "xhigh" if accepts_max_as_xhigh else raw
if raw in supported:
return raw
return "xhigh" if accepts_max_as_xhigh else raw
# Degrade to the closest *lower* supported level instead of silently
# disabling reasoning. e.g. max -> xhigh -> high, or xhigh -> high when the
# target model caps below the configured effort. Never escalate.
ladder = list(VALID_REASONING_EFFORTS) # ascending: minimal..max
ladder = list(VALID_REASONING_EFFORTS) # ascending: minimal..xhigh
try:
raw_idx = ladder.index(raw)
except ValueError:
@@ -2730,7 +2734,16 @@ def get_reasoning_status(
return {
# Match CLI default (True if unset in config.yaml)
"show_reasoning": bool(show_raw) if isinstance(show_raw, bool) else True,
"reasoning_effort": str(effort_raw or "").strip().lower(),
# Report the COERCED effort (not the raw config value) so boot/status/chip
# read paths agree with what streaming actually sends — e.g. a stale
# `reasoning_effort: max` surfaces as `xhigh`, not the now-unsupported `max`.
# (Codex review of the drop-max alignment.)
"reasoning_effort": coerce_reasoning_effort_for_model(
str(effort_raw or "").strip().lower(),
resolve_model,
provider_id=resolve_provider,
base_url=resolve_base_url,
),
"supported_efforts": supported_efforts,
"supports_reasoning_effort": bool(supported_efforts),
}
+4 -3
View File
@@ -30,7 +30,7 @@ const COMMANDS=[
{name:'background',desc:t('cmd_background'),fn:cmdBackground,arg:'prompt', noEcho:true},
{name:'status', desc:t('cmd_status'), fn:cmdStatus},
{name:'voice', desc:t('cmd_voice'), fn:cmdVoice, noEcho:true},
{name:'reasoning', desc:t('cmd_reasoning'), fn:cmdReasoning, arg:'show|hide|none|minimal|low|medium|high|xhigh|max', subArgs:['show','hide','none','minimal','low','medium','high','xhigh','max'], noEcho:true},
{name:'reasoning', desc:t('cmd_reasoning'), fn:cmdReasoning, arg:'show|hide|none|minimal|low|medium|high|xhigh', subArgs:['show','hide','none','minimal','low','medium','high','xhigh'], noEcho:true},
{name:'yolo', desc:t('cmd_yolo'), fn:cmdYolo, noEcho:true},
{name:'branch', desc:t('cmd_branch'), fn:cmdBranch, arg:'[name]', noEcho:true},
];
@@ -1383,13 +1383,14 @@ function cmdReasoning(args){
const arg=(args||'').trim().toLowerCase();
const BRAIN='\uD83E\uDDE0';
// Matches hermes_constants.VALID_REASONING_EFFORTS + 'none' (CLI parity).
const EFFORTS=['none','minimal','low','medium','high','xhigh','max'];
// Keep this WebUI effort list in sync with hermes-agent#29248.
const EFFORTS=['none','minimal','low','medium','high','xhigh'];
// Shared status renderer used by the no-args branch and as a fallback.
function _fmtStatus(st){
const vis=(st && st.show_reasoning===false)?'off':'on';
const eff=(st && st.reasoning_effort)||'default';
return BRAIN+' Reasoning effort: '+eff+' \u00B7 display: '+vis
+' | /reasoning show|hide|none|minimal|low|medium|high|xhigh|max';
+' | /reasoning show|hide|none|minimal|low|medium|high|xhigh';
}
if(!arg){
// Status — read from the same config.yaml keys the CLI uses.
-1
View File
@@ -730,7 +730,6 @@
<div class="reasoning-option" data-effort="medium">Medium</div>
<div class="reasoning-option" data-effort="high">High</div>
<div class="reasoning-option" data-effort="xhigh">Extra High</div>
<div class="reasoning-option" data-effort="max">Max</div>
</div>
<div class="composer-toolsets-dropdown" id="composerToolsetsDropdown">
<div class="toolsets-dropdown-desc" id="toolsetsDropdownDesc"></div>
@@ -45,6 +45,9 @@ def test_reasoning_chip_html_starts_hidden():
src
)
assert m, "composerReasoningWrap must start with style='display:none'"
assert 'data-effort="max"' not in src, (
"composer reasoning dropdown must not include Max"
)
def test_ui_js_passes_model_context_to_reasoning_api():
@@ -83,7 +83,7 @@ def test_coerce_preserves_effort_for_unrecognized_model():
"max",
"brand-new-model-2099",
provider_id="some-custom-provider",
) == "max"
) == "xhigh"
# 'none' / unset still pass through unchanged for unknown models.
assert cfg.coerce_reasoning_effort_for_model(
"none", "some-unknown-model-xyz", provider_id="custom"
@@ -130,3 +130,30 @@ def test_get_reasoning_status_includes_supported_efforts(monkeypatch):
)
assert status["supported_efforts"] == ["low", "medium", "high"]
assert status["supports_reasoning_effort"] is True
def test_get_reasoning_status_for_reasoning_capable_model_has_no_max():
status = cfg.get_reasoning_status(
model_id="gpt-5.5",
provider_id="openai-codex",
)
assert status["supported_efforts"] == ["minimal", "low", "medium", "high", "xhigh"]
assert status["supports_reasoning_effort"] is True
assert "max" not in status["supported_efforts"]
def test_get_reasoning_status_coerces_stale_max_to_xhigh(monkeypatch):
"""A previously-saved `agent.reasoning_effort: max` (no longer a valid effort)
must be reported as the coerced `xhigh`, not the raw stale `max`, so the
boot/status/chip read paths agree with what streaming actually sends."""
monkeypatch.setattr(
cfg,
"_load_yaml_config_file",
lambda *a, **k: {"agent": {"reasoning_effort": "max"}},
)
status = cfg.get_reasoning_status(
model_id="gpt-5.5",
provider_id="openai-codex",
)
assert status["reasoning_effort"] == "xhigh"
assert status["reasoning_effort"] != "max"
+7 -1
View File
@@ -317,6 +317,9 @@ class TestReasoningCommand:
f"cmdReasoning must accept '{level}' (CLI parity with "
f"hermes_constants.parse_reasoning_effort)"
)
assert "'max'" not in fn, (
"cmdReasoning should only expose efforts up to xhigh"
)
def test_reasoning_subargs_match_cli_levels(self):
"""Autocomplete subArgs must expose every CLI effort level + show/hide."""
@@ -330,6 +333,9 @@ class TestReasoningCommand:
assert f"'{suggestion}'" in entry, (
f"reasoning subArgs must include '{suggestion}' for CLI parity"
)
assert "'max'" not in entry, (
"reasoning command subArgs must not include 'max'"
)
# ── api/config.py — reasoning helpers ────────────────────────────────────────
@@ -359,7 +365,7 @@ class TestReasoningConfigHelpers:
# Snapshot-style assertion: if hermes_constants adds a level, this
# test will fail fast so we know to update WebUI too.
assert VALID_REASONING_EFFORTS == (
'minimal', 'low', 'medium', 'high', 'xhigh', 'max'
'minimal', 'low', 'medium', 'high', 'xhigh'
)
def test_set_reasoning_effort_persists_to_config_yaml(self, tmp_path, monkeypatch):