Merge pull request #5922 from nesquena/release/stage-5910

Release exp-v0.52.33: max reasoning-effort level, availability-gated (#5910)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-07-10 22:01:59 -07:00
commit 6abb447b89
8 files changed
+256 -30

No files matched your search

+2
View File
@@ -9,6 +9,8 @@
### Added
- **"Max" reasoning-effort level (opt-in, availability-gated).** The reasoning selector now offers `Max` for models that actually support it (Claude adaptive-thinking 4.6+, DeepSeek), matching the level Hermes core already accepts. `Max` is shown *only* when the active model genuinely supports it — it's hidden for GPT-5 / o-series / Gemini / legacy Claude and stripped anywhere it would be rejected, and a stored/CLI `max` on a model that can't take it degrades to the next supported level (`xhigh`) instead of erroring. (One known edge: an aggregator provider like OpenRouter/Copilot that returns no per-model capability data can't confirm support, so `max` stays selectable there and would fall back at the provider if the specific model doesn't support it — same behavior as the CLI.) Thanks @perejaslav. (#5910, #4627)
- **Opt-in: start a new chat when switching workspace.** A new default-off setting (Settings → Preferences → "Start a new chat when switching workspace"). When enabled, selecting a *different* workspace starts a fresh conversation bound to that workspace and leaves your current chat on its original workspace — avoiding stale cross-workspace context carrying into the conversation. Selecting the same workspace stays an in-place refresh, and when the setting is off (the default) switching workspace changes the current conversation's workspace in place exactly as before, so nothing changes for anyone who doesn't opt in. Thanks @ruizanthony. (#5473)
- **Public read-only conversation sharing.** You can now opt in to share a single conversation via a public link (session action menu → Share). The link serves a redacted, read-only snapshot at `/share/<token>` that anyone can view without logging in — nothing is public unless you explicitly share it, and you can revoke the link at any time (Stop sharing). The public page renders the transcript through the same sanitized markdown path as the app, is theme- and skin-aware (light/dark + all skins), and looks clean on desktop and mobile. The share payload is hardened at a dedicated always-on boundary: credentials, local workspace/worktree/home paths, system/tool messages, and any non-text structured content are stripped regardless of your API-redaction setting; only the conversation title and user/assistant text reach the public snapshot. Share tokens are 144-bit CSPRNG; create/revoke stay authenticated + CSRF-protected. Thanks @MicoRobot. (#5337)
+154 -16
View File
@@ -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):
@@ -3246,14 +3246,91 @@ def _filter_reasoning_efforts_for_provider(
normalized = list(dict.fromkeys(normalized))
provider = _resolve_provider_alias(str(provider_id or "").strip().lower())
bare = _strip_provider_hint_for_reasoning(model_id).lower().rsplit("/", 1)[-1]
if provider == "openai-codex":
# OpenAI-family lanes (Codex, direct OpenAI, Azure Foundry) cap GPT-5 at xhigh
# and o-series at high — 'max' is a WebUI-only level none of them accept.
if provider in {"openai-codex", "openai", "openai-api", "azure-foundry", "azure-openai", "azure"}:
if bare.startswith(("o1", "o3", "o4")):
return [eff for eff in normalized if eff in {"low", "medium", "high"}]
if bare.startswith("gpt-5"):
return [eff for eff in normalized if eff != "max"]
# 'max' is a WebUI-level ceiling; providers whose native ladder tops out lower
# must NOT advertise it, otherwise a stored/CLI 'max' degrades WORSE than the
# prior max->xhigh coercion (Gemini's adapter treats unknown 'max' as medium;
# pre-adaptive Anthropic manual-thinking lacks a 'max' budget and falls to 8k).
# Dropping 'max' here lets the existing downgrade ladder land on xhigh/high.
if provider in {"gemini", "google", "google-gemini", "google-vertex", "vertex"}:
return [eff for eff in normalized if eff != "max"]
# Legacy Claude is pre-adaptive whether served natively OR via Azure Foundry /
# Bedrock / Vertex — the ceiling follows the MODEL, not just the provider name.
_anthropic_lanes = {
"anthropic", "claude", "anthropic-claude",
"azure-foundry", "azure-openai", "azure", "bedrock", "aws-bedrock",
"vertex", "google-vertex",
}
if provider in _anthropic_lanes and "claude" in bare and _is_pre_adaptive_anthropic(bare):
return [eff for eff in normalized if eff != "max"]
return normalized
_KNOWN_REASONING_PROVIDERS = frozenset({
"anthropic", "claude", "anthropic-claude",
"openai", "openai-api", "openai-codex",
"azure", "azure-openai", "azure-foundry",
"bedrock", "aws-bedrock", "vertex", "google-vertex",
"gemini", "google", "google-gemini",
"deepseek", "x-ai", "xai", "grok",
"copilot", "github-copilot", "openrouter",
})
def _provider_known_reasoning_capable(provider_id) -> bool:
"""True if the provider is one we recognize as reasoning-capable.
Used to gate the 'max' default-deny: for a RECOGNIZED provider whose specific
model we couldn't resolve (empty capability list), preserve 'max' since those
providers genuinely support it; for a truly unknown/custom provider, degrade
'max' -> 'xhigh' so we never send a supra-ceiling level that would 400.
"""
prov = _resolve_provider_alias(str(provider_id or "").strip().lower())
return prov in _KNOWN_REASONING_PROVIDERS
def _is_pre_adaptive_anthropic(bare_model: str) -> bool:
"""True for Claude models that predate the adaptive-thinking (4.6+) generation.
Adaptive models (Opus/Sonnet 4.6+, 4.7, …) accept 'max'; earlier manual-thinking
Claudes (3.x and 4.0–4.5) do not and must degrade 'max' to xhigh rather than
fall through the manual-thinking budget table to its 8k default.
Handles the ID shapes the Anthropic adapter uses:
- claude-3-opus / claude-3-5-sonnet / claude-3-7-sonnet → pre-adaptive
- claude-sonnet-4-5 / claude-haiku-4-5 → pre-adaptive (4.5)
- claude-sonnet-4-20250514 (date-stamped 4.0 build) → pre-adaptive
- claude-opus-4.6 / claude-sonnet-4.6 / claude-opus-4.7 → adaptive
- claude-opus-latest / unversioned → adaptive (flagship)
"""
import re as _re
m = (bare_model or "").lower()
if "claude" not in m:
return False
# Claude 3.x family is always pre-adaptive.
if _re.search(r"claude-3\b", m) or _re.search(r"claude-3[.\-]", m):
return True
# Find a major[.-]minor version. A date-stamp (>=6 digits) is NOT a minor
# version — treat "4-20250514" as major 4 with no real minor (a 4.0 build).
match = _re.search(r"(\d+)[.\-](\d{1,2})(?!\d)", m)
if not match:
# A bare major like "claude-sonnet-4" or "...-4-20250514" (date stamp
# consumed no minor): major-4 with no minor is a pre-adaptive 4.0 build.
major_only = _re.search(r"[-.](\d+)(?:[-.]\d{6,})?(?:\b|-)", m)
if major_only:
return int(major_only.group(1)) < 5 # 4.x (no minor) → pre-adaptive; ≥5 → adaptive
# Unversioned / *-latest → treat as current flagship (adaptive).
return False
major, minor = int(match.group(1)), int(match.group(2))
return (major, minor) < (4, 6)
def _heuristic_reasoning_efforts(model_id: str, provider_id: str) -> list[str]:
"""Fallback when hermes_cli is unavailable."""
model = _strip_provider_hint_for_reasoning(model_id).lower()
@@ -3512,6 +3589,35 @@ def resolve_model_reasoning_efforts(
model_id: str | None = None,
provider_id: str | None = None,
base_url: str | None = None,
) -> list[str]:
"""Return supported reasoning-effort levels for *model_id*, or [] if none.
Always passes the sourced list through _filter_reasoning_efforts_for_provider
so the hard provider ceilings (openai-codex/openai/azure GPT-5 cap at xhigh,
Gemini + pre-adaptive/cloud-hosted Claude cap at xhigh) are applied uniformly
— the UI dropdown (which gates options on this list) and coercion therefore
agree: 'max' is offered ONLY for models whose native ladder genuinely includes
it, and is stripped everywhere it would be rejected/mishandled.
"""
raw = _resolve_model_reasoning_efforts_impl(model_id, provider_id, base_url)
if not raw:
return raw
# Preserve any explicit 'none' sentinel (valid UI option = "no reasoning");
# the ceiling filter only knows the reasoning LEVELS.
had_none = "none" in raw
filtered = _filter_reasoning_efforts_for_provider(
[e for e in raw if e != "none"], str(model_id or ""), str(provider_id or "")
)
if had_none:
# Keep 'none' in its original leading position if it was there.
return ["none", *filtered] if raw and raw[0] == "none" else [*filtered, "none"]
return filtered
def _resolve_model_reasoning_efforts_impl(
model_id: str | None = None,
provider_id: str | None = None,
base_url: str | None = None,
) -> list[str]:
"""Return supported reasoning-effort levels for *model_id*, or [] if none."""
model = str(model_id or "").strip()
@@ -3631,9 +3737,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(
@@ -3641,6 +3744,32 @@ def coerce_reasoning_effort_for_model(
provider_id=provider_id,
base_url=base_url,
)
# Hard provider ceilings must win regardless of what the sourced capability
# list says. resolve_model_reasoning_efforts() draws from hermes_cli /
# models.dev / heuristics, and those can (a) return [] for an unrecognized
# model or (b) wrongly advertise a WebUI-only level like 'max' for a provider
# whose native ladder tops out lower. _filter_reasoning_efforts_for_provider
# encodes the known ceilings (openai-codex gpt-5, Gemini, pre-adaptive
# Anthropic all cap below 'max'); if it actively EXCLUDES the requested level,
# honor that ceiling and degrade down the ladder even when the sourced list is
# empty or (mistakenly) includes the level. This keeps a stored/CLI 'max' from
# reaching an adapter that would silently downgrade it worse than xhigh/high
# (Gemini→medium, legacy Claude manual-thinking→8k). For providers with NO
# ceiling rule the filter returns the full list unchanged, so genuinely
# unknown models still preserve the configured effort (#3505 behavior).
ceiling = _filter_reasoning_efforts_for_provider(
list(VALID_REASONING_EFFORTS), str(model_id or ""), str(provider_id or "")
)
if ceiling and raw not in ceiling:
ladder = list(VALID_REASONING_EFFORTS) # ascending: minimal..xhigh..max
try:
raw_idx = ladder.index(raw)
except ValueError:
raw_idx = None
if raw_idx is not None:
for level in reversed(ladder[:raw_idx]): # strictly lower, highest first
if level in ceiling:
return level
# An empty list is ambiguous: resolve_model_reasoning_efforts() returns []
# both for models KNOWN not to support reasoning AND for models we simply
# don't recognize (custom providers, aggregator-rewritten ids, brand-new
@@ -3649,17 +3778,28 @@ 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)
#
# EXCEPTION for 'max' (the #3505 default-deny refinement, maintainer call
# 2026-07-11): 'max' is a WebUI-only level ABOVE the universally-safe ceiling
# 'xhigh'. A genuinely unknown/custom provider will 400 on it. So when the
# capability list is empty AND the provider is not one we recognize as
# reasoning-capable, degrade 'max' -> 'xhigh' rather than send an unsupported
# supra-ceiling level. But do NOT degrade for a RECOGNIZED reasoning provider
# whose specific model id we simply couldn't resolve (e.g. claude-opus-latest,
# a brand-new adaptive id) — those genuinely support 'max', and the ceiling
# filter above already stripped it for any KNOWN-capped model. All other
# levels (minimal..xhigh) keep the conservative preserve-verbatim behavior.
if not supported:
return "xhigh" if accepts_max_as_xhigh else raw
if raw == "max" and not _provider_known_reasoning_capable(provider_id):
return "xhigh"
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 +3852,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 +3972,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()
+3 -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', 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.
+1
View File
@@ -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>
+1
View File
@@ -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"
)
@@ -79,6 +79,10 @@ def test_coerce_preserves_effort_for_unrecognized_model():
"some-unknown-model-xyz",
provider_id="some-custom-provider",
) == "high"
# #3505 default-deny refinement (maintainer 2026-07-11): 'max' is a supra-
# ceiling WebUI-only level, so on an UNRECOGNIZED provider it degrades to
# xhigh (a truly-unknown provider would 400 on max). All OTHER levels still
# preserve verbatim below.
assert cfg.coerce_reasoning_effort_for_model(
"max",
"brand-new-model-2099",
@@ -327,3 +331,89 @@ def test_get_reasoning_status_coerces_stale_max_to_xhigh(monkeypatch):
)
assert status["reasoning_effort"] == "xhigh"
assert status["reasoning_effort"] != "max"
def test_max_effort_degrades_to_xhigh_for_gemini():
# Gemini's native ladder tops out below 'max'; its adapter would silently
# treat an unknown 'max' as medium. A stored/CLI 'max' must degrade to xhigh
# (the highest supported), not fall through to a worse level. (#4627 gate)
for model in ("gemini-3-pro", "gemini-3-flash"):
assert cfg.coerce_reasoning_effort_for_model(
"max", model_id=model, provider_id="gemini"
) == "xhigh", f"{model} max must degrade to xhigh"
def test_max_effort_degrades_to_xhigh_for_pre_adaptive_anthropic():
# Pre-adaptive Claude (3.7 / 4.0-4.5) uses manual thinking whose budget table
# lacks 'max' and falls back to 8k; 'max' must degrade to xhigh instead. (#4627 gate)
for model in (
"claude-3-7-sonnet", "claude-sonnet-4-5", "claude-haiku-4-5",
# date-stamped legacy IDs the Anthropic adapter uses
"claude-3-opus-20240229", "claude-3-5-sonnet-20241022",
"claude-sonnet-4-20250514", "claude-opus-4-20250514",
):
assert cfg.coerce_reasoning_effort_for_model(
"max", model_id=model, provider_id="anthropic"
) == "xhigh", f"{model} max must degrade to xhigh"
def test_max_effort_preserved_for_adaptive_anthropic_and_deepseek():
# Adaptive Claude (4.6+) and DeepSeek genuinely support 'max' — it must NOT degrade.
for model in ("claude-opus-4.6", "claude-sonnet-4.6", "claude-opus-4.7", "claude-opus-latest"):
assert cfg.coerce_reasoning_effort_for_model(
"max", model_id=model, provider_id="anthropic"
) == "max", f"{model} must preserve max"
assert cfg.coerce_reasoning_effort_for_model(
"max", model_id="deepseek-reasoner", provider_id="deepseek"
) == "max"
def test_max_degrades_across_all_openai_family_lanes():
# 'max' is WebUI-only; direct OpenAI, openai-api, and Azure Foundry GPT-5 all
# cap at xhigh (o-series at high), not just openai-codex. (#4627 re-gate)
for prov in ("openai", "openai-api", "azure-foundry", "openai-codex"):
assert cfg.coerce_reasoning_effort_for_model(
"max", model_id="gpt-5.1", provider_id=prov
) == "xhigh", f"gpt-5 on {prov} must degrade max->xhigh"
assert cfg.coerce_reasoning_effort_for_model(
"max", model_id="o3", provider_id=prov
) == "high", f"o-series on {prov} must degrade max->high"
def test_max_degrades_for_azure_bedrock_hosted_legacy_claude():
# Legacy Claude via Azure Foundry / Bedrock is still pre-adaptive; the ceiling
# follows the model, not just the provider name. (#4627 re-gate)
for prov in ("azure-foundry", "bedrock"):
assert cfg.coerce_reasoning_effort_for_model(
"max", model_id="claude-sonnet-4-20250514", provider_id=prov
) == "xhigh", f"legacy Claude on {prov} must degrade max->xhigh"
# adaptive Claude via azure preserves max
assert cfg.coerce_reasoning_effort_for_model(
"max", model_id="claude-opus-4.6", provider_id="azure-foundry"
) == "max"
def test_max_degrades_on_unknown_provider_but_other_levels_preserved():
# #3505 default-deny refinement (maintainer call 2026-07-11): 'max' is above
# the universal ceiling, so an unknown/custom provider (empty capability list)
# must degrade 'max'->'xhigh' rather than send an unsupported level — while all
# other levels keep the conservative preserve-verbatim behavior.
assert cfg.coerce_reasoning_effort_for_model(
"max", model_id="some-unknown-model", provider_id="customprovider"
) == "xhigh"
# other levels still preserved verbatim for an unknown provider
for eff in ("minimal", "low", "medium", "high", "xhigh"):
assert cfg.coerce_reasoning_effort_for_model(
eff, model_id="some-unknown-model", provider_id="customprovider"
) == eff, f"{eff} must be preserved verbatim on unknown provider (#3505)"
def test_max_only_offered_in_ui_when_actually_supported():
# The dropdown gates on resolve_model_reasoning_efforts(): 'max' appears ONLY
# for models whose supported list includes it (adaptive Claude, DeepSeek), and
# is absent for legacy/capped models and unknown providers.
assert "max" in cfg.resolve_model_reasoning_efforts("claude-opus-4.6", provider_id="anthropic")
assert "max" in cfg.resolve_model_reasoning_efforts("deepseek-reasoner", provider_id="deepseek")
assert "max" not in cfg.resolve_model_reasoning_efforts("claude-sonnet-4-5", provider_id="anthropic")
assert "max" not in cfg.resolve_model_reasoning_efforts("gpt-5.1", provider_id="openai")
assert "max" not in cfg.resolve_model_reasoning_efforts("gemini-3-pro", provider_id="gemini")
+3 -9
View File
@@ -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):