2c0bec33f9
The Desktop composer got a reasoning-effort pill this morning; every other place a
model is picked still left the effort to a separate command (`/reasoning`) or a
hand edit of config.yaml. `hermes model` had one effort step for Copilot only, and
its auxiliary-model menu had none at all even though every aux block already reads
`auxiliary.<task>.reasoning_effort`.
One request now carries a model pick AND its effort on every surface:
- `hermes_cli/model_switch.py`: the single `/model` parser accepts `--reasoning
<level>` (validated against `parse_reasoning_effort`; unknown level ->
`MODEL_SWITCH_ERR_BAD_REASONING`; Unicode-dash normalized like the other flags).
`ModelSwitchRequest.reasoning_effort` rides with the pick.
- Classic CLI (`cli_model_switch_mixin`, `cli_tui_mixin`): `/model X --reasoning
high` applies the effort AFTER the agent swap (`switch_model` re-resolves
`reasoning_config` from config.yaml, so an earlier write is clobbered) with the
pick's scope (session; config on `--global`; `--once` snapshots and restores it).
The `/model` picker gains a third stage, "Reasoning effort for <model>", built
from `VALID_REASONING_EFFORTS` + none + "Keep current effort"; hidden when the
inventory capability map says the route has no reasoning control.
- TUI gateway (`tui_gateway/model_switch.py`, serves Ink TUI + Desktop):
`config.set model "X --reasoning high"` applies after the swap; session pin
(`create_reasoning_override`) by default, `agent.reasoning_effort` on --global,
one-turn restore carries `reasoning_config`; re-emits `session_info` so the
status bar shows the new effort.
- Ink TUI `ModelPicker`: step 3/3 (same rows, same capability gate) emitting
`<model> --provider <slug> --reasoning <level> <scope>`; the new-session draft
label strips the flag like `--provider`.
- Messaging gateway `/model`: `--reasoning` goes through the existing
`_apply_reasoning_selection` (the `/reasoning` applier) with the pick's scope.
- `hermes model`: one shared post-pick effort step for the MAIN model (replaces
the Copilot-only inline prompt; Copilot keeps its per-model level set via
`github_model_reasoning_efforts`, other routes get the ladder, catalog
`supports_reasoning=False` skips it) plus a "Reasoning effort for the current
model..." row. The auxiliary menu's provider->model and custom-endpoint flows end
with the same step (+ "Provider default"), stored as
`auxiliary.<task>.reasoning_effort` / `delegation.reasoning_effort`, shown in
the task list ("openrouter · model · high"), cleared by "Reset all to auto";
tasks whose block omits the key by design (MoA slots, memory_query_rewrite) skip
it.
Live (temp HERMES_HOME, stub key, no model call):
- `hermes model` -> aux -> Vision -> OpenRouter -> model: before ends at
"Vision: openrouter · <m>", no key written; after adds "Select reasoning effort"
and saves `reasoning_effort: high`.
- `hermes model` -> DeepSeek -> model: before no effort step; after the step
writes `agent.reasoning_effort: xhigh`.
- tui_gateway stdio: `config.set model "... --reasoning high --session"` before
errors "Model names cannot contain spaces"; after switches and `config.get
reasoning` returns high; bad level -> the canonical error text.
- classic CLI `process_command`: before the same spaces error; after "Reasoning
effort: high" under the switch summary, `--global` writes config.
- `hermes --tui` PTY: /model -> step 1/3 -> 2/3 -> 3/3 -> high; transcript
"reasoning: high", status bar "fable 5.1 high".
66 lines
3.1 KiB
Python
66 lines
3.1 KiB
Python
"""``/model <name> --reasoning <level>`` — one request carries a model pick AND its effort.
|
|
|
|
The parser is the single owner (hermes_cli.model_switch.parse_model_switch_args); the CLI and
|
|
TUI-gateway commit steps apply the effort AFTER the agent swap, because ``agent.switch_model``
|
|
re-resolves ``reasoning_config`` from config.yaml and would clobber an earlier write.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from hermes_cli.model_switch import (
|
|
MODEL_SWITCH_ERR_BAD_REASONING,
|
|
ModelSwitchResult,
|
|
parse_model_switch_args,
|
|
)
|
|
|
|
|
|
def test_reasoning_flag_rides_with_the_pick_and_validates():
|
|
req = parse_model_switch_args("sonnet --provider anthropic --reasoning high --session")
|
|
assert req.target == "sonnet"
|
|
assert req.explicit_provider == "anthropic"
|
|
assert req.reasoning_effort == "high"
|
|
assert req.scope == "session"
|
|
assert req.errors == ()
|
|
|
|
bad = parse_model_switch_args("sonnet --reasoning turbo")
|
|
assert MODEL_SWITCH_ERR_BAD_REASONING in bad.errors
|
|
# Unicode dash normalization (Telegram/iOS) covers the new flag too.
|
|
assert parse_model_switch_args("sonnet \u2014reasoning low").reasoning_effort == "low"
|
|
|
|
|
|
def test_cli_commit_applies_effort_after_the_agent_swap(monkeypatch):
|
|
"""The agent's switch_model resets reasoning_config from config; the ride-along effort must
|
|
win over that reset, on both the CLI and the live agent."""
|
|
import cli as cli_mod
|
|
from hermes_cli import cli_model_switch_mixin as mixin
|
|
|
|
class _Agent:
|
|
reasoning_config = {"enabled": True, "effort": "medium"}
|
|
|
|
def switch_model(self, **_kw):
|
|
# Mirrors agent_runtime_helpers._switch_model: re-resolve from config.yaml.
|
|
self.reasoning_config = {"enabled": True, "effort": "medium"}
|
|
|
|
agent = _Agent()
|
|
cli = SimpleNamespace(
|
|
model="old", provider="nous", requested_provider="nous", _explicit_api_key="", _explicit_base_url="",
|
|
api_key="", base_url="", api_mode="", agent=agent, reasoning_config=None,
|
|
_pending_one_turn_model_restore=None, _pending_model_switch_note="",
|
|
_snapshot_model_runtime=lambda: {}, _persist_model_switch_to_session=lambda *_a: None)
|
|
cli._stage_and_swap_model = lambda result, old: cli_mod.HermesCLI._stage_and_swap_model(cli, result, old)
|
|
monkeypatch.setattr(mixin, "_print_switch_summary", lambda *_a, **_k: None)
|
|
monkeypatch.setattr(cli_mod.HermesCLI, "_persist_model_switch_to_session", lambda *_a: None)
|
|
saved = {}
|
|
monkeypatch.setattr(cli_mod, "save_config_value", lambda k, v: saved.setdefault(k, v) or True)
|
|
|
|
result = ModelSwitchResult(success=True, new_model="new", target_provider="nous")
|
|
mixin._commit_model_switch(cli, result, persist_global=False, reasoning_effort="high")
|
|
|
|
assert agent.reasoning_config == {"enabled": True, "effort": "high"}
|
|
assert cli.reasoning_config == {"enabled": True, "effort": "high"}
|
|
assert "agent.reasoning_effort" not in saved # session scope: no config write
|
|
|
|
mixin._commit_model_switch(cli, result, persist_global=True, reasoning_effort="none")
|
|
assert saved.get("agent.reasoning_effort") == "none"
|
|
assert agent.reasoning_config == {"enabled": False}
|