feat(config): expose "max" as a valid reasoning effort level

## 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 <andrekurait@gmail.com>
This commit is contained in:
Andre Kurait 2026-04-23 20:29:03 +00:00
parent b6ca3c28dc
commit cff93966f4
No known key found for this signature in database
6 changed files with 55 additions and 10 deletions

View file

@ -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())

View file

@ -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 <level> Set reasoning effort (none, minimal, low, medium, high, xhigh)
/reasoning <level> 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 <none|minimal|low|medium|high|xhigh|show|hide>`"
"_Usage:_ `/reasoning <none|minimal|low|medium|high|xhigh|max|show|hide>`"
)
# 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"
)

View file

@ -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")),

View file

@ -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:

View file

@ -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": <level>} for valid effort levels.

View file

@ -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