From a82eb1622f8504c5b83b50ff7133079b8ae249f3 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Thu, 11 Jun 2026 18:46:17 -0400 Subject: [PATCH] fix(#3958): keep reasoning effort status on the active session model --- api/config.py | 14 +++- api/routes.py | 13 +++- static/ui.js | 16 ++-- ...ssue3958_reasoning_post_session_context.py | 75 +++++++++++++++++++ 4 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 tests/test_issue3958_reasoning_post_session_context.py diff --git a/api/config.py b/api/config.py index 0260fb67..93e39f9c 100644 --- a/api/config.py +++ b/api/config.py @@ -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 ``: 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: diff --git a/api/routes.py b/api/routes.py index c4d90406..23a5f6dd 100644 --- a/api/routes.py +++ b/api/routes.py @@ -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)) diff --git a/static/ui.js b/static/ui.js index 7c9d0132..7fd09535 100644 --- a/static/ui.js +++ b/static/ui.js @@ -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)); diff --git a/tests/test_issue3958_reasoning_post_session_context.py b/tests/test_issue3958_reasoning_post_session_context.py new file mode 100644 index 00000000..6741e698 --- /dev/null +++ b/tests/test_issue3958_reasoning_post_session_context.py @@ -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