From cff93966f4442a87967c8733ed5b700fca96233d Mon Sep 17 00:00:00 2001 From: Andre Kurait Date: Thu, 23 Apr 2026 20:29:03 +0000 Subject: [PATCH] feat(config): expose "max" as a valid reasoning effort level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Anthropic's adaptive-thinking API exposes five effort levels on Claude 4.6+ / 4.7 models: low, medium, high, xhigh, max (xhigh was added by 4.7; max has existed since 4.6.) `agent/anthropic_adapter.py` already maps all five correctly via `ADAPTIVE_EFFORT_MAP` (including a `xhigh → max` downgrade path for pre-4.7 models) and its `THINKING_BUDGET` + documentation treat "max" as a first-class value. However, the user-facing surface only lists four of them. `VALID_REASONING_EFFORTS` in `hermes_constants.py` stops at `"xhigh"`, so `parse_reasoning_effort("max")` silently returns `None`, the caller falls back to the default (medium), and the adapter never receives the max level. The same hardcoded shorter list is duplicated in four other places: `/reasoning` slash-command subcommand tuple, the gateway handler's allowed-value check, the gateway's help text, the Discord slash command description, and the CLI effort-picker's canonical order. ## Fix Add `"max"` to `VALID_REASONING_EFFORTS` and update every duplicated surface to match. While here, replace the gateway's open-coded `("minimal", "low", "medium", "high", "xhigh")` tuple with a reference to `VALID_REASONING_EFFORTS` so future additions (when Anthropic ships a new level) only need one edit. No behavior change for existing values — any string that was valid before remains valid and parses identically. The only new behavior is that `"max"` is now accepted end-to-end: `/reasoning max` sets `agent.reasoning_effort: max` in `config.yaml`, which Anthropic's `output_config.effort` then receives directly. ## Changes Made - `hermes_constants.py` - `VALID_REASONING_EFFORTS`: add `"max"`. - `parse_reasoning_effort()` docstring: list `"max"`. - `gateway/run.py` - `_handle_reasoning_command()`: replace hardcoded tuple with `VALID_REASONING_EFFORTS`; error message now derives the valid list from the tuple instead of hardcoding a stale copy. - Docstring + help banner + "_Usage:_" line: mention `max`. - `_load_reasoning_config()` docstring: mention `max`. - `hermes_cli/commands.py`: add `"max"` to the `/reasoning` `CommandDef` subcommand tuple (drives tab-completion). - `hermes_cli/main.py`: add `"max"` to the canonical-order tuple in `_prompt_reasoning_effort_selection()` (drives `/reasoning` picker ordering). - `gateway/platforms/discord.py`: add `max` to the Discord slash command's `app_commands.describe(effort=...)` docstring. - `tests/test_hermes_constants.py`: new `TestParseReasoningEffort` class with 7 tests covering empty/none/case-insensitivity/unknown inputs and explicitly verifying `"max"` parses end-to-end and is present in `VALID_REASONING_EFFORTS`. ## Testing ```bash pytest tests/test_hermes_constants.py -q # 18 passed in 1.74s ``` Manually: ```bash # Before: /reasoning max → "Unknown argument: max" error # After: /reasoning max → "Reasoning effort set to `max` (saved to config)" ``` Signed-off-by: Andre Kurait --- gateway/platforms/discord.py | 2 +- gateway/run.py | 11 +++++---- hermes_cli/commands.py | 2 +- hermes_cli/main.py | 2 +- hermes_constants.py | 4 ++-- tests/test_hermes_constants.py | 44 ++++++++++++++++++++++++++++++++++ 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index a148c5f4b..763bff3aa 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -1997,7 +1997,7 @@ class DiscordAdapter(BasePlatformAdapter): await self._run_simple_slash(interaction, f"/model {name}".strip()) @tree.command(name="reasoning", description="Show or change reasoning effort") - @discord.app_commands.describe(effort="Reasoning effort: none, minimal, low, medium, high, or xhigh.") + @discord.app_commands.describe(effort="Reasoning effort: none, minimal, low, medium, high, xhigh, or max.") async def slash_reasoning(interaction: discord.Interaction, effort: str = ""): await self._run_simple_slash(interaction, f"/reasoning {effort}".strip()) diff --git a/gateway/run.py b/gateway/run.py index dcee18e51..fb26140e1 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1344,7 +1344,7 @@ class GatewayRunner: """Load reasoning effort from config.yaml. Reads agent.reasoning_effort from config.yaml. Valid: "none", - "minimal", "low", "medium", "high", "xhigh". Returns None to use + "minimal", "low", "medium", "high", "xhigh", "max". Returns None to use default (medium). """ from hermes_constants import parse_reasoning_effort @@ -6788,7 +6788,7 @@ class GatewayRunner: Usage: /reasoning Show current effort level and display state - /reasoning Set reasoning effort (none, minimal, low, medium, high, xhigh) + /reasoning Set reasoning effort (none, minimal, low, medium, high, xhigh, max) /reasoning show|on Show model reasoning in responses /reasoning hide|off Hide model reasoning from responses """ @@ -6833,7 +6833,7 @@ class GatewayRunner: "🧠 **Reasoning Settings**\n\n" f"**Effort:** `{level}`\n" f"**Display:** {display_state}\n\n" - "_Usage:_ `/reasoning `" + "_Usage:_ `/reasoning `" ) # Display toggle (per-platform) @@ -6853,14 +6853,15 @@ class GatewayRunner: # Effort level change effort = args.strip() + from hermes_constants import VALID_REASONING_EFFORTS if effort == "none": parsed = {"enabled": False} - elif effort in ("minimal", "low", "medium", "high", "xhigh"): + elif effort in VALID_REASONING_EFFORTS: parsed = {"enabled": True, "effort": effort} else: return ( f"⚠️ Unknown argument: `{effort}`\n\n" - "**Valid levels:** none, minimal, low, medium, high, xhigh\n" + f"**Valid levels:** none, {', '.join(VALID_REASONING_EFFORTS)}\n" "**Display:** show, hide" ) diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 87d73af58..4c61dc38a 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -119,7 +119,7 @@ COMMAND_REGISTRY: list[CommandDef] = [ "Configuration"), CommandDef("reasoning", "Manage reasoning effort and display", "Configuration", args_hint="[level|show|hide]", - subcommands=("none", "minimal", "low", "medium", "high", "xhigh", "show", "hide", "on", "off")), + subcommands=("none", "minimal", "low", "medium", "high", "xhigh", "max", "show", "hide", "on", "off")), CommandDef("fast", "Toggle fast mode — OpenAI Priority Processing / Anthropic Fast Mode (Normal/Fast)", "Configuration", args_hint="[normal|fast|status]", subcommands=("normal", "fast", "status", "on", "off")), diff --git a/hermes_cli/main.py b/hermes_cli/main.py index cb70261b4..a855286b7 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -2984,7 +2984,7 @@ def _prompt_reasoning_effort_selection(efforts, current_effort=""): str(effort).strip().lower() for effort in efforts if str(effort).strip() ) ) - canonical_order = ("minimal", "low", "medium", "high", "xhigh") + canonical_order = ("minimal", "low", "medium", "high", "xhigh", "max") ordered = [effort for effort in canonical_order if effort in deduped] ordered.extend(effort for effort in deduped if effort not in canonical_order) if not ordered: diff --git a/hermes_constants.py b/hermes_constants.py index 35dbf86ab..26fd66b73 100644 --- a/hermes_constants.py +++ b/hermes_constants.py @@ -138,13 +138,13 @@ def get_subprocess_home() -> str | None: return None -VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh") +VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh", "max") def parse_reasoning_effort(effort: str) -> dict | None: """Parse a reasoning effort level into a config dict. - Valid levels: "none", "minimal", "low", "medium", "high", "xhigh". + Valid levels: "none", "minimal", "low", "medium", "high", "xhigh", "max". Returns None when the input is empty or unrecognized (caller uses default). Returns {"enabled": False} for "none". Returns {"enabled": True, "effort": } for valid effort levels. diff --git a/tests/test_hermes_constants.py b/tests/test_hermes_constants.py index d49dff813..a095c00f0 100644 --- a/tests/test_hermes_constants.py +++ b/tests/test_hermes_constants.py @@ -111,3 +111,47 @@ class TestIsContainer: # Even if we make os.path.exists return False, cached value wins monkeypatch.setattr(os.path, "exists", lambda p: False) assert is_container() is True + + +class TestParseReasoningEffort: + """Tests for parse_reasoning_effort() — reasoning effort parsing and validation.""" + + def test_empty_string_returns_none(self): + """Empty input defers to caller's default.""" + from hermes_constants import parse_reasoning_effort + assert parse_reasoning_effort("") is None + assert parse_reasoning_effort(" ") is None + + def test_none_disables_reasoning(self): + """'none' explicitly disables reasoning.""" + from hermes_constants import parse_reasoning_effort + assert parse_reasoning_effort("none") == {"enabled": False} + assert parse_reasoning_effort("NONE") == {"enabled": False} + + def test_standard_levels(self): + """Each canonical effort level round-trips correctly.""" + from hermes_constants import parse_reasoning_effort + for level in ("minimal", "low", "medium", "high", "xhigh"): + assert parse_reasoning_effort(level) == {"enabled": True, "effort": level} + + def test_max_level_accepted(self): + """'max' is the strongest adaptive-thinking level on Claude 4.6+/4.7.""" + from hermes_constants import parse_reasoning_effort + assert parse_reasoning_effort("max") == {"enabled": True, "effort": "max"} + + def test_case_insensitive(self): + """Effort strings are case-insensitive.""" + from hermes_constants import parse_reasoning_effort + assert parse_reasoning_effort("MAX") == {"enabled": True, "effort": "max"} + assert parse_reasoning_effort("XHigh") == {"enabled": True, "effort": "xhigh"} + + def test_unknown_level_returns_none(self): + """Unrecognized levels return None so caller can fall back to default.""" + from hermes_constants import parse_reasoning_effort + assert parse_reasoning_effort("extreme") is None + assert parse_reasoning_effort("ultra") is None + + def test_valid_reasoning_efforts_includes_max(self): + """VALID_REASONING_EFFORTS exposes 'max' as a supported level.""" + from hermes_constants import VALID_REASONING_EFFORTS + assert "max" in VALID_REASONING_EFFORTS