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:
7 files changed
+71
-16
No files matched your search
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user