Merge #4015 — keep reasoning effort status on the active session model (#3958)

This commit is contained in:
nesquena-hermes 2026-06-11 22:48:36 +00:00
commit ff3de220d8
4 changed files with 110 additions and 8 deletions

View file

@ -2756,7 +2756,13 @@ def set_reasoning_display(show: bool) -> dict:
return get_reasoning_status()
def set_reasoning_effort(effort: str) -> dict:
def set_reasoning_effort(
effort: str,
*,
model_id: str | None = None,
provider_id: str | None = None,
base_url: str | None = None,
) -> dict:
"""Persist ``agent.reasoning_effort`` to the active profile's config.yaml.
Mirrors CLI ``/reasoning <level>``: same key, same valid values
@ -2781,7 +2787,11 @@ def set_reasoning_effort(effort: str) -> dict:
config_data["agent"] = agent_cfg
_save_yaml_config_file(config_path, config_data)
reload_config()
return get_reasoning_status()
return get_reasoning_status(
model_id=model_id,
provider_id=provider_id,
base_url=base_url,
)
def set_hermes_default_model(model_id: str) -> dict:

View file

@ -7446,7 +7446,18 @@ def handle_post(handler, parsed) -> bool:
return j(handler, set_reasoning_display(False))
return bad(handler, f"display must be show|hide|on|off (got '{display}')")
if effort is not None:
return j(handler, set_reasoning_effort(effort))
model_id = str(body.get("model") or "").strip() or None
provider_id = str(body.get("provider") or "").strip() or None
base_url = str(body.get("base_url") or "").strip() or None
return j(
handler,
set_reasoning_effort(
effort,
model_id=model_id,
provider_id=provider_id,
base_url=base_url,
),
)
return bad(handler, "reasoning: must supply 'display' or 'effort'")
except ValueError as e:
return bad(handler, str(e))

View file

@ -2004,16 +2004,21 @@ function _formatReasoningEffortLabel(effort){
return effort;
}
function _reasoningEffortQuery(){
function _reasoningEffortContext(){
const sel=$('modelSelect');
const model=(S&&S.session&&S.session.model)||(sel&&sel.value)||'';
let provider=(S&&S.session&&S.session.model_provider)||'';
if(!provider&&sel&&model&&typeof _modelStateForSelect==='function'){
provider=_modelStateForSelect(sel, model).model_provider||'';
}
const params=new URLSearchParams();
if(model) params.set('model', model);
if(provider) params.set('provider', provider);
const ctx={};
if(model) ctx.model=model;
if(provider) ctx.provider=provider;
return ctx;
}
function _reasoningEffortQuery(){
const params=new URLSearchParams(_reasoningEffortContext());
const qs=params.toString();
return qs?('?'+qs):'';
}
@ -2146,7 +2151,8 @@ document.addEventListener('click',function(e){
const opt=e.target.closest('.reasoning-option');
const effort=opt&&opt.dataset.effort;
if(effort){
api('/api/reasoning',{method:'POST',body:JSON.stringify({effort:effort})})
const payload=Object.assign({effort:effort},_reasoningEffortContext());
api('/api/reasoning',{method:'POST',body:JSON.stringify(payload)})
.then(function(st){
_applyReasoningChip((st&&st.reasoning_effort)||effort, st||{});
showToast('🧠 Reasoning effort set to '+((st&&st.reasoning_effort)||effort));

View file

@ -0,0 +1,75 @@
from pathlib import Path
import re
import api.config as cfg
import yaml
def read(path):
return Path(path).read_text(encoding="utf-8")
def test_set_reasoning_effort_returns_status_for_explicit_model(tmp_path, monkeypatch):
cfgfile = tmp_path / "config.yaml"
cfgfile.write_text(
yaml.safe_dump(
{
"model": {"default": "gpt-4o", "provider": "openai"},
"agent": {"reasoning_effort": ""},
},
sort_keys=False,
),
encoding="utf-8",
)
monkeypatch.setattr(cfg, "_get_config_path", lambda: cfgfile)
monkeypatch.setattr(cfg, "reload_config", lambda: None)
seen = {}
def fake_resolve(model_id, provider_id=None, base_url=None):
seen["args"] = (model_id, provider_id, base_url)
if model_id == "claude-opus-4-7":
return ["minimal", "low", "medium", "high", "xhigh", "max"]
return []
monkeypatch.setattr(cfg, "resolve_model_reasoning_efforts", fake_resolve)
status = cfg.set_reasoning_effort(
"high",
model_id="claude-opus-4-7",
provider_id="anthropic",
)
assert seen["args"] == ("claude-opus-4-7", "anthropic", None)
assert status["reasoning_effort"] == "high"
assert status["supported_efforts"] == [
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
]
def test_ui_posts_reasoning_context_with_effort():
src = read("static/ui.js")
assert "function _reasoningEffortContext()" in src
assert "new URLSearchParams(_reasoningEffortContext())" in src
assert "Object.assign({effort:effort},_reasoningEffortContext())" in src
def test_reasoning_post_route_threads_model_context():
src = read("api/routes.py")
match = re.search(
r"if parsed\.path == \"/api/reasoning\":(.*?)return bad\(handler, \"reasoning: must supply 'display' or 'effort'\"\)",
src,
re.DOTALL,
)
assert match, "The /api/reasoning POST route block must exist"
body = match.group(1)
assert 'body.get("model")' in body
assert 'body.get("provider")' in body
assert 'set_reasoning_effort(' in body
assert "model_id=model_id" in body
assert "provider_id=provider_id" in body