From c7c313672c6356728c6313254c08c0eda254a3d2 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sat, 13 Jun 2026 13:59:55 -0400 Subject: [PATCH 1/4] fix(reasoning): hide unsupported max effort (#4119) --- api/config.py | 10 +++++++--- static/commands.js | 7 ++++--- static/index.html | 1 - tests/test_issue1103_reasoning_chip_visibility.py | 3 +++ tests/test_reasoning_effort_model_capabilities.py | 10 ++++++++++ tests/test_reasoning_show_hide.py | 8 +++++++- 6 files changed, 31 insertions(+), 8 deletions(-) diff --git a/api/config.py b/api/config.py index 329ecd91..32330ae9 100644 --- a/api/config.py +++ b/api/config.py @@ -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( @@ -2671,9 +2675,9 @@ def coerce_reasoning_effort_for_model( # the final authority. Worst case is the same rejected request that master # already produces, i.e. no regression. (#3505 review) if not supported: - return raw + return "max" 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. diff --git a/static/commands.js b/static/commands.js index 08ddd861..831450bc 100644 --- a/static/commands.js +++ b/static/commands.js @@ -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. diff --git a/static/index.html b/static/index.html index 6191beb8..29fd5cc3 100644 --- a/static/index.html +++ b/static/index.html @@ -730,7 +730,6 @@
Medium
High
Extra High
-
Max
diff --git a/tests/test_issue1103_reasoning_chip_visibility.py b/tests/test_issue1103_reasoning_chip_visibility.py index 7bb4268f..8c01abd9 100644 --- a/tests/test_issue1103_reasoning_chip_visibility.py +++ b/tests/test_issue1103_reasoning_chip_visibility.py @@ -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(): diff --git a/tests/test_reasoning_effort_model_capabilities.py b/tests/test_reasoning_effort_model_capabilities.py index fc1a505d..88387ed0 100644 --- a/tests/test_reasoning_effort_model_capabilities.py +++ b/tests/test_reasoning_effort_model_capabilities.py @@ -130,3 +130,13 @@ 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"] diff --git a/tests/test_reasoning_show_hide.py b/tests/test_reasoning_show_hide.py index 87660586..d1e2132f 100644 --- a/tests/test_reasoning_show_hide.py +++ b/tests/test_reasoning_show_hide.py @@ -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): From 95bb9ebe66e1890ec9a7582a8efdf6ec44b06cd5 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sat, 13 Jun 2026 14:26:48 -0400 Subject: [PATCH 2/4] Keep stale max configs on the supported path --- api/config.py | 14 +++++++------- tests/test_reasoning_effort_model_capabilities.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/config.py b/api/config.py index 32330ae9..6f3c2b75 100644 --- a/api/config.py +++ b/api/config.py @@ -2667,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 "max" if accepts_max_as_xhigh else raw + return "xhigh" if accepts_max_as_xhigh else raw if raw in supported: 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: diff --git a/tests/test_reasoning_effort_model_capabilities.py b/tests/test_reasoning_effort_model_capabilities.py index 88387ed0..0657aaa6 100644 --- a/tests/test_reasoning_effort_model_capabilities.py +++ b/tests/test_reasoning_effort_model_capabilities.py @@ -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" From bf6efb1a05f9214712b34017968444d56ee7372a Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sat, 13 Jun 2026 18:44:41 +0000 Subject: [PATCH 3/4] docs(changelog): stamp #4130 reasoning-effort alignment (drop max) as v0.51.392 (Release NE) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e7e03fb..b92a7af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 30cc15975e87fb7202f5e7a0abae9ac69feff660 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sat, 13 Jun 2026 18:50:00 +0000 Subject: [PATCH 4/4] fix(#4130): coerce stale 'max' in get_reasoning_status read path too (Codex catch) get_reasoning_status() returned the raw config reasoning_effort, so a stale saved 'max' surfaced on boot/status/chip even though streaming coerces it to xhigh. Now routes the status value through coerce_reasoning_effort_for_model() with the resolved model/provider/base_url, so all read paths agree. Added a stale-max->xhigh status regression test. --- api/config.py | 11 ++++++++++- .../test_reasoning_effort_model_capabilities.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/api/config.py b/api/config.py index 6f3c2b75..f6058dd0 100644 --- a/api/config.py +++ b/api/config.py @@ -2734,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), } diff --git a/tests/test_reasoning_effort_model_capabilities.py b/tests/test_reasoning_effort_model_capabilities.py index 0657aaa6..acbac7cc 100644 --- a/tests/test_reasoning_effort_model_capabilities.py +++ b/tests/test_reasoning_effort_model_capabilities.py @@ -140,3 +140,20 @@ def test_get_reasoning_status_for_reasoning_capable_model_has_no_max(): 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"