mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
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:
parent
b6ca3c28dc
commit
cff93966f4
6 changed files with 55 additions and 10 deletions
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue