diff --git a/api/config.py b/api/config.py index f7b6d30e..97cb41fc 100644 --- a/api/config.py +++ b/api/config.py @@ -3026,7 +3026,7 @@ def get_effective_default_model(config_data: dict | None = None) -> str: # 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. # Keep this WebUI-visible set aligned with hermes-agent#29248. -VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh") +VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh", "max") def parse_reasoning_effort(effort): @@ -3631,9 +3631,6 @@ 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( @@ -3649,17 +3646,15 @@ def coerce_reasoning_effort_for_model( # 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 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) + # still valid. (#3505 review) if not supported: - return "xhigh" if accepts_max_as_xhigh else raw + return raw if raw in supported: - return "xhigh" if accepts_max_as_xhigh else raw + return 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..xhigh + ladder = list(VALID_REASONING_EFFORTS) # ascending: minimal..xhigh..max try: raw_idx = ladder.index(raw) except ValueError: @@ -3712,10 +3707,8 @@ 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, - # 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.) + # Report the COERCED effort so boot/status/chip read paths agree with + # what streaming actually sends. (Codex review of the drop-max alignment.) "reasoning_effort": coerce_reasoning_effort_for_model( str(effort_raw or "").strip().lower(), resolve_model, @@ -3834,7 +3827,7 @@ def set_reasoning_effort( """Persist ``agent.reasoning_effort`` to the active profile's config.yaml. Mirrors CLI ``/reasoning ``: same key, same valid values - (``none`` | ``minimal`` | ``low`` | ``medium`` | ``high`` | ``xhigh``). + (``none`` | ``minimal`` | ``low`` | ``medium`` | ``high`` | ``xhigh`` | ``max``). Raises ``ValueError`` on an unrecognised level so callers can return 400. """ raw = str(effort or "").strip().lower() diff --git a/static/commands.js b/static/commands.js index 2c4d89ff..72f90f3d 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', subArgs:['show','hide','none','minimal','low','medium','high','xhigh'], 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:'yolo', desc:t('cmd_yolo'), fn:cmdYolo, noEcho:true}, {name:'branch', desc:t('cmd_branch'), fn:cmdBranch, arg:'[name]', noEcho:true}, ]; @@ -1799,13 +1799,13 @@ function cmdReasoning(args){ const BRAIN='\uD83E\uDDE0'; // Matches hermes_constants.VALID_REASONING_EFFORTS + 'none' (CLI parity). // Keep this WebUI effort list in sync with hermes-agent#29248. - const EFFORTS=['none','minimal','low','medium','high','xhigh']; + const EFFORTS=['none','minimal','low','medium','high','xhigh','max']; // 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'; + +' | /reasoning show|hide|none|minimal|low|medium|high|xhigh|max'; } if(!arg){ // Status — read from the same config.yaml keys the CLI uses. diff --git a/static/index.html b/static/index.html index 1d617469..076434be 100644 --- a/static/index.html +++ b/static/index.html @@ -759,6 +759,7 @@
Medium
High
Extra High
+
Max
diff --git a/static/ui.js b/static/ui.js index f552e1b5..36efe643 100644 --- a/static/ui.js +++ b/static/ui.js @@ -4481,6 +4481,7 @@ function _formatReasoningEffortLabel(effort){ if(effort==='medium') return 'Medium'; if(effort==='high') return 'High'; if(effort==='xhigh') return 'XHigh'; + if(effort==='max') return 'Max'; return effort.charAt(0).toUpperCase()+effort.slice(1); } diff --git a/tests/test_issue1103_reasoning_chip_visibility.py b/tests/test_issue1103_reasoning_chip_visibility.py index a0acc0b7..1c3660fd 100644 --- a/tests/test_issue1103_reasoning_chip_visibility.py +++ b/tests/test_issue1103_reasoning_chip_visibility.py @@ -45,8 +45,8 @@ 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" + assert 'data-effort="max"' in src, ( + "composer reasoning dropdown must include Max option" ) diff --git a/tests/test_reasoning_effort_model_capabilities.py b/tests/test_reasoning_effort_model_capabilities.py index 503ef552..1d2b569e 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", - ) == "xhigh" + ) == "max" # 'none' / unset still pass through unchanged for unknown models. assert cfg.coerce_reasoning_effort_for_model( "none", "some-unknown-model-xyz", provider_id="custom" diff --git a/tests/test_reasoning_show_hide.py b/tests/test_reasoning_show_hide.py index d1e2132f..9bf81309 100644 --- a/tests/test_reasoning_show_hide.py +++ b/tests/test_reasoning_show_hide.py @@ -312,14 +312,11 @@ class TestReasoningCommand: m = re.search(r'function cmdReasoning\(.*?\n\}', src, re.DOTALL) assert m fn = m.group(0) - for level in ('none', 'minimal', 'low', 'medium', 'high', 'xhigh'): + for level in ('none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'): assert f"'{level}'" in fn, ( 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.""" @@ -328,14 +325,11 @@ class TestReasoningCommand: assert m, "reasoning COMMANDS entry not found" entry = m.group(0) for suggestion in ( - 'show', 'hide', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh' + 'show', 'hide', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max' ): 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 ──────────────────────────────────────── @@ -365,7 +359,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' + 'minimal', 'low', 'medium', 'high', 'xhigh', 'max' ) def test_set_reasoning_effort_persists_to_config_yaml(self, tmp_path, monkeypatch):