Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion agent/transports/chat_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.<name>.models.<model>.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:
Expand Down
21 changes: 21 additions & 0 deletions tests/agent/transports/test_chat_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
40 changes: 40 additions & 0 deletions tests/run_agent/test_run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.<name>.models.<model>.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."""
Expand Down