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".
77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
from types import SimpleNamespace
|
|
|
|
from hermes_cli.model_switch import ModelSwitchResult
|
|
|
|
|
|
def _bound(fn, instance):
|
|
return fn.__get__(instance, type(instance))
|
|
|
|
|
|
def test_prompt_toolkit_model_picker_defers_confirmation_off_key_handler(monkeypatch):
|
|
import cli as cli_mod
|
|
|
|
result = ModelSwitchResult(
|
|
success=True,
|
|
new_model="openai/gpt-5.5-pro",
|
|
target_provider="nous",
|
|
)
|
|
monkeypatch.setattr(
|
|
"hermes_cli.model_switch.switch_model",
|
|
lambda **_kwargs: result,
|
|
)
|
|
|
|
captured = {}
|
|
|
|
class _Thread:
|
|
def __init__(self, *, target, args, daemon):
|
|
captured["target"] = target
|
|
captured["args"] = args
|
|
captured["daemon"] = daemon
|
|
|
|
def start(self):
|
|
captured["started"] = True
|
|
|
|
monkeypatch.setattr(cli_mod.threading, "Thread", _Thread)
|
|
|
|
self_ = SimpleNamespace(
|
|
_app=object(),
|
|
_model_picker_state={
|
|
"stage": "model",
|
|
"provider_data": {"slug": "nous"},
|
|
"model_list": ["openai/gpt-5.5-pro"],
|
|
"selected": 0,
|
|
"user_provs": None,
|
|
"custom_provs": None,
|
|
},
|
|
provider="nous",
|
|
model="openai/gpt-5.5",
|
|
base_url="",
|
|
api_key="",
|
|
_restore_modal_input_snapshot=lambda: None,
|
|
_invalidate=lambda **_kwargs: None,
|
|
)
|
|
self_._close_model_picker = _bound(cli_mod.HermesCLI._close_model_picker, self_)
|
|
self_._commit_picker_result = _bound(cli_mod.HermesCLI._commit_picker_result, self_)
|
|
self_._confirm_and_apply_model_switch_result = (
|
|
lambda *_args: captured.setdefault("ran_inline", True)
|
|
)
|
|
|
|
# The key handler now resolves persistence via resolve_persist_behavior,
|
|
# which defaults to True (persist-by-default). Simulate that call.
|
|
_bound(cli_mod.HermesCLI._handle_model_picker_selection, self_)(persist_global=True)
|
|
|
|
# Picking a model opens the reasoning-effort step (no commit yet); "Keep current effort"
|
|
# (the last effort row) commits with the historical arity.
|
|
from hermes_cli.cli_model_switch_mixin import _picker_reasoning_rows
|
|
assert self_._model_picker_state["stage"] == "reasoning"
|
|
assert "started" not in captured
|
|
self_._model_picker_state["selected"] = len(_picker_reasoning_rows()) - 1
|
|
_bound(cli_mod.HermesCLI._handle_model_picker_selection, self_)(persist_global=True)
|
|
|
|
assert self_._model_picker_state is None
|
|
assert captured["started"] is True
|
|
assert captured["daemon"] is True
|
|
# Third arg is the fresh picker custom_providers snapshot (None here).
|
|
assert captured["args"] == (result, True, None)
|
|
assert "ran_inline" not in captured
|