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.
This commit is contained in:
teknium1
2026-09-13 17:27:40 -07:00
committed by Teknium
parent cc7ee8d1f5
commit 1d33a4fee6
12 changed files with 111 additions and 8 deletions
+35 -5
View File
@@ -685,7 +685,26 @@ def _apply_main_assignment_sync(cfg: dict, provider: str, model: str, base_url:
}
def _apply_aux_assignment_sync(cfg: dict, provider: str, model: str, task: str, base_url: str, api_key: str) -> dict:
# "Field omitted" sentinel for optional assignment fields whose None means "clear".
_UNSET: Any = object()
def _normalize_aux_reasoning_effort(value: Optional[str]) -> Optional[str]:
"""``auxiliary.<task>.reasoning_effort`` value for an assignment: None clears (inherit), else the
canonical level (``none`` for a disable), 400 on an unknown level."""
if value is None:
return None
from hermes_constants import parse_reasoning_effort
parsed = parse_reasoning_effort(value)
if parsed is None:
from hermes_constants import VALID_REASONING_EFFORTS
raise HTTPException(status_code=400,
detail=f"reasoning_effort must be one of: none, {', '.join(VALID_REASONING_EFFORTS)}")
return "none" if parsed.get("enabled") is False else parsed["effort"]
def _apply_aux_assignment_sync(cfg: dict, provider: str, model: str, task: str, base_url: str, api_key: str,
reasoning_effort: Optional[str] = _UNSET) -> dict:
from hermes_cli.config import save_config
aux = cfg.get("auxiliary")
if not isinstance(aux, dict):
@@ -695,12 +714,15 @@ def _apply_aux_assignment_sync(cfg: dict, provider: str, model: str, task: str,
slot_cfg = aux.get(slot)
return slot_cfg if isinstance(slot_cfg, dict) else {}
effort = _normalize_aux_reasoning_effort(reasoning_effort) if reasoning_effort is not _UNSET else _UNSET
if task == "__reset__":
# Reset every slot to provider="auto", model="" — keeps other fields intact.
# Reset every slot to provider="auto", model="", no effort override — keeps other fields intact.
for slot in _AUX_TASK_SLOTS:
slot_cfg = _slot(slot)
slot_cfg["provider"] = "auto"
slot_cfg["model"] = ""
slot_cfg.pop("reasoning_effort", None)
slot_cfg.pop("base_url", None)
clear_model_endpoint_credentials(slot_cfg)
aux[slot] = slot_cfg
@@ -734,15 +756,23 @@ def _apply_aux_assignment_sync(cfg: dict, provider: str, model: str, task: str,
elif new_provider != prev_provider and new_provider != "custom":
slot_cfg.pop("base_url", None)
clear_model_endpoint_credentials(slot_cfg)
if effort is None:
slot_cfg.pop("reasoning_effort", None)
elif effort is not _UNSET:
slot_cfg["reasoning_effort"] = effort
aux[slot] = slot_cfg
cfg["auxiliary"] = aux
save_config(cfg)
return {"ok": True, "scope": "auxiliary", "tasks": targets, "provider": provider, "model": model}
result = {"ok": True, "scope": "auxiliary", "tasks": targets, "provider": provider, "model": model}
if effort is not _UNSET:
result["reasoning_effort"] = effort
return result
def _apply_model_assignment_sync(
scope: str, provider: str, model: str, task: str, base_url: str, api_key: str = ""
scope: str, provider: str, model: str, task: str, base_url: str, api_key: str = "",
reasoning_effort: Optional[str] = _UNSET,
):
"""Synchronous body of POST /api/model/set.
@@ -753,7 +783,7 @@ def _apply_model_assignment_sync(
cfg = load_config()
if scope == "main":
return _apply_main_assignment_sync(cfg, provider, model, base_url, api_key)
return _apply_aux_assignment_sync(cfg, provider, model, task, base_url, api_key)
return _apply_aux_assignment_sync(cfg, provider, model, task, base_url, api_key, reasoning_effort)
def _infer_provider_on_model_change(model_val: str, prev_provider: str) -> tuple[str, str]: