Files
hermes-agent/tests/hermes_cli/test_aux_assignment_reasoning_effort.py
T
teknium1 1d33a4fee6 feat(desktop): persist auxiliary reasoning_effort through the models router
Backend half of the per-task effort control, on today's layout: POST /api/model/set
distinguishes omitted (leave the task's override alone) from explicit null (clear →
inherit) via model_fields_set, canonicalises a level through parse_reasoning_effort
(400 on an unknown one), and "Reset all to main" also drops every override. GET
/api/model/auxiliary returns reasoning_effort per task and the row summary shows it.

The inherit row reads "inherit · main model effort" (own i18n key in all six locales)
rather than reusing the provider's "auto · use main model" copy — the two mean
different things and the reused string read as "use the main model" for the effort.

Runtime already consumes auxiliary.<task>.reasoning_effort (agent/auxiliary_client.py)
and hermes model writes the same key (#110346), so Desktop and CLI now edit one value.

Closes #89259. Salvages #90649 by @higgs1729.
2026-09-13 19:19:32 -07:00

53 lines
2.8 KiB
Python

"""Desktop Settings → Model → Auxiliary can set a task's reasoning effort (#89259, salvage #90649).
``POST /api/model/set`` carries ``reasoning_effort`` for an auxiliary task: omitted → the task's
override is left alone; explicit null → cleared (inherit); a level → set. Runtime reads it from
``auxiliary.<task>.reasoning_effort`` (``agent/auxiliary_client.py``).
"""
import pytest
from fastapi import HTTPException
from hermes_cli.web_server_config import _UNSET, _apply_aux_assignment_sync
@pytest.fixture
def saved(monkeypatch):
store: dict = {}
monkeypatch.setattr("hermes_cli.config.save_config", lambda cfg: store.update(cfg))
return store
def test_reasoning_effort_field_semantics_omitted_null_and_level(saved):
cfg = {"auxiliary": {"vision": {"provider": "openrouter", "model": "m1", "reasoning_effort": "low"}}}
# A plain provider/model re-assignment leaves an existing override alone.
_apply_aux_assignment_sync(cfg, "openrouter", "m2", "vision", "", "")
assert cfg["auxiliary"]["vision"] == {"provider": "openrouter", "model": "m2", "reasoning_effort": "low"}
# A level sets it (canonicalised) and the response echoes it; a disable is stored as the explicit "none".
out = _apply_aux_assignment_sync(cfg, "openrouter", "m2", "vision", "", "", reasoning_effort="HIGH")
assert cfg["auxiliary"]["vision"]["reasoning_effort"] == "high" and out["reasoning_effort"] == "high"
_apply_aux_assignment_sync(cfg, "openrouter", "m2", "vision", "", "", reasoning_effort="disabled")
assert cfg["auxiliary"]["vision"]["reasoning_effort"] == "none"
# Explicit null clears only this task's key; siblings and the pick survive.
cfg["auxiliary"]["compression"] = {"provider": "openrouter", "model": "m3", "reasoning_effort": "max"}
_apply_aux_assignment_sync(cfg, "openrouter", "m2", "vision", "", "", reasoning_effort=None)
assert "reasoning_effort" not in cfg["auxiliary"]["vision"]
assert cfg["auxiliary"]["vision"]["model"] == "m2"
assert cfg["auxiliary"]["compression"]["reasoning_effort"] == "max"
assert saved["auxiliary"] == cfg["auxiliary"]
def test_unknown_level_is_rejected_and_reset_clears_overrides(saved):
cfg = {"auxiliary": {"vision": {"provider": "openrouter", "model": "m1"}}}
with pytest.raises(HTTPException) as exc:
_apply_aux_assignment_sync(cfg, "openrouter", "m1", "vision", "", "", reasoning_effort="turbo")
assert exc.value.status_code == 400 and "reasoning_effort" in exc.value.detail
assert "reasoning_effort" not in cfg["auxiliary"]["vision"]
cfg["auxiliary"]["vision"]["reasoning_effort"] = "high"
_apply_aux_assignment_sync(cfg, "", "", "__reset__", "", "", reasoning_effort=_UNSET)
assert all("reasoning_effort" not in slot for slot in cfg["auxiliary"].values())