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:
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user