feat: add max reasoning effort level (salvage of #4627)
Salvaged from #4627 by @perejaslav. Core Hermes already supports reasoning_effort=max; this closes the WebUI parity gap (Anchor-A) — adds max to VALID_REASONING_EFFORTS + dropdown/chip/reasoning-command, keeps the degrade ladder for capped models. Co-authored-by: perejaslav <perejaslav@users.noreply.github.com>
This commit is contained in:
parent
91804b30fc
commit
d56123f796
7 changed files with 19 additions and 30 deletions
|
|
@ -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 <level>``: 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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -759,6 +759,7 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue