diff --git a/agent/transports/chat_completions.py b/agent/transports/chat_completions.py index c0b2a13d250f..63d2dbcc4f5e 100644 --- a/agent/transports/chat_completions.py +++ b/agent/transports/chat_completions.py @@ -421,7 +421,12 @@ def build_kwargs( if gh_reasoning is not None: extra_body["reasoning"] = gh_reasoning else: - extra_body["reasoning"] = {"enabled": True, "effort": "medium"} + _effort = "medium" + if reasoning_config and isinstance(reasoning_config, dict): + _e = (reasoning_config.get("effort") or "").strip().lower() + if _e in {"low", "medium", "high", "xhigh"}: + _effort = _e + extra_body["reasoning"] = {"enabled": True, "effort": _effort} if provider_name == "gemini": raw_thinking_config = _build_gemini_thinking_config(model, reasoning_config) diff --git a/run_agent.py b/run_agent.py index 87ad09dd9158..dc14087401d9 100644 --- a/run_agent.py +++ b/run_agent.py @@ -4862,6 +4862,24 @@ def _supports_reasoning_extra_body(self) -> bool: opts = self._lmstudio_reasoning_options_cached() # "off-only" (or absent) means no real reasoning capability. return any(opt and opt != "off" for opt in opts) + # Custom/user-defined providers: check providers..models..reasoning + # in config.yaml. This lets users opt in to reasoning for models served + # through custom endpoints (CLIProxyAPI, vLLM, Ollama, etc.) without + # adding a dedicated provider profile. + try: + from hermes_cli.config import load_config as _load_rc_cfg + _rc_cfg = _load_rc_cfg() + _rc_providers = _rc_cfg.get("providers") + if isinstance(_rc_providers, dict): + _rc_prov = _rc_providers.get(self.provider or "") + if isinstance(_rc_prov, dict): + _rc_models = _rc_prov.get("models") + if isinstance(_rc_models, dict): + _rc_mcfg = _rc_models.get(self.model or "") + if isinstance(_rc_mcfg, dict) and _rc_mcfg.get("reasoning"): + return True + except Exception: + pass if "openrouter" not in self._base_url_lower: return False if "api.mistral.ai" in self._base_url_lower: diff --git a/tests/agent/transports/test_chat_completions.py b/tests/agent/transports/test_chat_completions.py index da642e2ae17c..280f653af6d2 100644 --- a/tests/agent/transports/test_chat_completions.py +++ b/tests/agent/transports/test_chat_completions.py @@ -252,6 +252,27 @@ def test_reasoning_default(self, transport): ) assert kw["extra_body"]["reasoning"] == {"enabled": True, "effort": "medium"} + def test_reasoning_effort_from_config(self, transport): + """reasoning_config.effort must propagate, not be hardcoded to medium.""" + msgs = [{"role": "user", "content": "Hi"}] + for effort in ("low", "medium", "high", "xhigh"): + kw = transport.build_kwargs( + model="gpt-4o", messages=msgs, + supports_reasoning=True, + reasoning_config={"effort": effort}, + ) + assert kw["extra_body"]["reasoning"] == {"enabled": True, "effort": effort} + + def test_reasoning_effort_invalid_falls_back_to_medium(self, transport): + """Unknown effort values fall back to medium, not crash.""" + msgs = [{"role": "user", "content": "Hi"}] + kw = transport.build_kwargs( + model="gpt-4o", messages=msgs, + supports_reasoning=True, + reasoning_config={"effort": "garbage"}, + ) + assert kw["extra_body"]["reasoning"] == {"enabled": True, "effort": "medium"} + def test_nous_omits_disabled_reasoning(self, transport): from providers import get_provider_profile profile = get_provider_profile("nous") diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index 385a296f8893..fab8971fe0cd 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -6694,6 +6694,46 @@ def test_xiaomi_models_are_treated_as_reasoning_capable(self): agent.model = model assert agent._supports_reasoning_extra_body() is True, model + def test_custom_provider_model_reasoning_opt_in(self): + """providers..models..reasoning: true enables reasoning for custom providers.""" + from unittest.mock import patch as mock_patch + agent = self._make_agent() + agent.provider = "cliproxyapi" + agent.base_url = "http://localhost:8317/v1" + agent._base_url_lower = agent.base_url.lower() + agent.model = "mimo-v2.5-pro" + cfg = { + "providers": { + "cliproxyapi": { + "models": { + "mimo-v2.5-pro": {"reasoning": True}, + } + } + } + } + with mock_patch("hermes_cli.config.load_config", return_value=cfg): + assert agent._supports_reasoning_extra_body() is True + + def test_custom_provider_without_reasoning_flag_returns_false(self): + """Custom provider without reasoning: true must still return False (backwards compat).""" + from unittest.mock import patch as mock_patch + agent = self._make_agent() + agent.provider = "cliproxyapi" + agent.base_url = "http://localhost:8317/v1" + agent._base_url_lower = agent.base_url.lower() + agent.model = "mimo-v2.5-pro" + cfg = { + "providers": { + "cliproxyapi": { + "models": { + "mimo-v2.5-pro": {"context_length": 131072}, + } + } + } + } + with mock_patch("hermes_cli.config.load_config", return_value=cfg): + assert agent._supports_reasoning_extra_body() is False + class TestMemoryContextSanitization: """sanitize_context() helper correctness — used at provider boundaries."""