Merge pull request #3036 from nesquena/release/stage-batch32
stage-batch32: v0.51.150 / Release DV — single-PR reasoning-effort agent metadata
This commit is contained in:
3 files changed
+189
-37
No files matched your search
@@ -3,6 +3,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.150] — 2026-05-28 — Release DV (stage-batch32 — single-PR reasoning-effort agent metadata)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reasoning-effort capability detection now consults Hermes Agent's `models.dev` metadata before falling back to WebUI-local provider/model prefix heuristics. xAI OAuth Grok models (e.g. `grok-4.3`) and other native/provider-specific catalogs that Hermes Agent already knows about now show the reasoning-effort chip without requiring a per-provider WebUI allowlist update. Existing exact-resolver paths (ACP unsupported, Copilot/GitHub effort subsets, OpenAI Codex, LM Studio live probing) keep their authoritative behavior — the new metadata lookup sits between those resolvers and the broad heuristic fallback. Metadata `supports_reasoning=False` is treated as authoritative so known non-reasoning variants stay hidden. Also: the no-query boot path of `/api/reasoning` now hydrates against the configured default model so the chip can populate before the front-end has session model context. (#3017)
|
||||
|
||||
## [v0.51.149] — 2026-05-28 — Release DU (stage-batch31 — hyphenated session ids + prefill role consistency)
|
||||
|
||||
### Fixed
|
||||
|
||||
+73
-37
@@ -2134,6 +2134,38 @@ def _heuristic_reasoning_efforts(model_id: str, provider_id: str) -> list[str]:
|
||||
return []
|
||||
|
||||
|
||||
def _models_dev_reasoning_efforts(model_id: str, provider_id: str) -> list[str] | None:
|
||||
"""Return reasoning efforts from Hermes Agent model metadata when known.
|
||||
|
||||
``None`` means the metadata source is unavailable or has no answer, so the
|
||||
caller should continue to compatibility fallbacks. A concrete list (including
|
||||
``[]``) is authoritative.
|
||||
"""
|
||||
model = _strip_provider_hint_for_reasoning(model_id)
|
||||
provider = str(provider_id or "").strip().lower()
|
||||
if not model or not provider:
|
||||
return None
|
||||
|
||||
try:
|
||||
from agent.models_dev import get_model_capabilities
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
capabilities = get_model_capabilities(provider=provider, model=model)
|
||||
except Exception:
|
||||
return None
|
||||
if capabilities is None:
|
||||
return None
|
||||
|
||||
supports_reasoning = getattr(capabilities, "supports_reasoning", None)
|
||||
if supports_reasoning is True:
|
||||
return list(VALID_REASONING_EFFORTS)
|
||||
if supports_reasoning is False:
|
||||
return []
|
||||
return None
|
||||
|
||||
|
||||
def resolve_model_reasoning_efforts(
|
||||
model_id: str | None = None,
|
||||
provider_id: str | None = None,
|
||||
@@ -2156,51 +2188,42 @@ def resolve_model_reasoning_efforts(
|
||||
if provider in {"cursor-acp", "copilot-acp"}:
|
||||
return []
|
||||
|
||||
hinted_model = _strip_provider_hint_for_reasoning(model)
|
||||
|
||||
try:
|
||||
from hermes_cli.models import (
|
||||
github_model_reasoning_efforts,
|
||||
lmstudio_model_reasoning_options,
|
||||
)
|
||||
except Exception:
|
||||
return _heuristic_reasoning_efforts(model, provider)
|
||||
if provider in {"copilot", "github-copilot"}:
|
||||
return _heuristic_reasoning_efforts(hinted_model, provider)
|
||||
else:
|
||||
if provider in {"copilot", "github-copilot"}:
|
||||
return github_model_reasoning_efforts(hinted_model)
|
||||
|
||||
hinted_model = _strip_provider_hint_for_reasoning(model)
|
||||
if provider in {"copilot", "github-copilot"}:
|
||||
return github_model_reasoning_efforts(hinted_model)
|
||||
if provider == "openai-codex":
|
||||
bare = hinted_model.rsplit("/", 1)[-1]
|
||||
return github_model_reasoning_efforts(bare)
|
||||
|
||||
if provider == "openai-codex":
|
||||
bare = hinted_model.rsplit("/", 1)[-1]
|
||||
return github_model_reasoning_efforts(bare)
|
||||
|
||||
if provider == "lmstudio":
|
||||
probe_base = resolved_base_url or _get_provider_base_url(provider)
|
||||
opts = lmstudio_model_reasoning_options(model, probe_base)
|
||||
normalized = [str(opt).strip().lower() for opt in opts if str(opt).strip()]
|
||||
if not normalized or set(normalized).issubset({"off"}):
|
||||
if provider == "lmstudio":
|
||||
probe_base = resolved_base_url or _get_provider_base_url(provider)
|
||||
opts = lmstudio_model_reasoning_options(hinted_model, probe_base)
|
||||
normalized = [str(opt).strip().lower() for opt in opts if str(opt).strip()]
|
||||
if not normalized or set(normalized).issubset({"off"}):
|
||||
return []
|
||||
level_opts = [opt for opt in normalized if opt in VALID_REASONING_EFFORTS]
|
||||
if level_opts:
|
||||
return list(dict.fromkeys(level_opts))
|
||||
if set(normalized).issubset({"off", "on"}):
|
||||
return []
|
||||
return []
|
||||
level_opts = [opt for opt in normalized if opt in VALID_REASONING_EFFORTS]
|
||||
if level_opts:
|
||||
return list(dict.fromkeys(level_opts))
|
||||
if set(normalized).issubset({"off", "on"}):
|
||||
return []
|
||||
return []
|
||||
|
||||
model_lower = model.lower()
|
||||
prefixes = (
|
||||
"deepseek/",
|
||||
"anthropic/",
|
||||
"openai/",
|
||||
"x-ai/",
|
||||
"google/gemini-2",
|
||||
"google/gemma-4",
|
||||
"qwen/qwen3",
|
||||
"tencent/hy3-preview",
|
||||
"xiaomi/",
|
||||
)
|
||||
if any(model_lower.startswith(prefix) for prefix in prefixes):
|
||||
return list(VALID_REASONING_EFFORTS)
|
||||
metadata_efforts = _models_dev_reasoning_efforts(hinted_model, provider)
|
||||
if metadata_efforts is not None:
|
||||
return metadata_efforts
|
||||
|
||||
return []
|
||||
return _heuristic_reasoning_efforts(hinted_model, provider)
|
||||
|
||||
|
||||
def get_reasoning_status(
|
||||
@@ -2221,10 +2244,23 @@ def get_reasoning_status(
|
||||
agent_cfg = config_data.get("agent") or {}
|
||||
show_raw = display_cfg.get("show_reasoning") if isinstance(display_cfg, dict) else None
|
||||
effort_raw = agent_cfg.get("reasoning_effort") if isinstance(agent_cfg, dict) else None
|
||||
|
||||
resolve_model = model_id
|
||||
resolve_provider = provider_id
|
||||
resolve_base_url = base_url
|
||||
if not resolve_model:
|
||||
model_cfg = config_data.get("model") or {}
|
||||
if isinstance(model_cfg, dict):
|
||||
resolve_model = str(model_cfg.get("default") or "").strip() or None
|
||||
if not resolve_provider and model_cfg.get("provider"):
|
||||
resolve_provider = str(model_cfg["provider"]).strip()
|
||||
if not resolve_base_url and model_cfg.get("base_url"):
|
||||
resolve_base_url = str(model_cfg["base_url"]).strip()
|
||||
|
||||
supported_efforts = resolve_model_reasoning_efforts(
|
||||
model_id,
|
||||
provider_id=provider_id,
|
||||
base_url=base_url,
|
||||
resolve_model,
|
||||
provider_id=resolve_provider,
|
||||
base_url=resolve_base_url,
|
||||
)
|
||||
return {
|
||||
# Match CLI default (True if unset in config.yaml)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Regression coverage for Agent model-metadata reasoning capability lookup."""
|
||||
|
||||
import sys
|
||||
import types
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
def _install_fake_models_dev(monkeypatch, fake_fn):
|
||||
fake_agent = types.ModuleType("agent")
|
||||
fake_models_dev = types.ModuleType("agent.models_dev")
|
||||
setattr(fake_models_dev, "get_model_capabilities", fake_fn)
|
||||
setattr(fake_agent, "models_dev", fake_models_dev)
|
||||
monkeypatch.setitem(sys.modules, "agent", fake_agent)
|
||||
monkeypatch.setitem(sys.modules, "agent.models_dev", fake_models_dev)
|
||||
|
||||
|
||||
def test_models_dev_true_returns_full_efforts(monkeypatch):
|
||||
_install_fake_models_dev(
|
||||
monkeypatch,
|
||||
lambda provider, model: SimpleNamespace(supports_reasoning=True),
|
||||
)
|
||||
|
||||
import api.config as cfg
|
||||
|
||||
assert cfg._models_dev_reasoning_efforts("grok-4.3", "xai-oauth") == list(
|
||||
cfg.VALID_REASONING_EFFORTS
|
||||
)
|
||||
|
||||
|
||||
def test_models_dev_false_returns_authoritative_empty(monkeypatch):
|
||||
_install_fake_models_dev(
|
||||
monkeypatch,
|
||||
lambda provider, model: SimpleNamespace(supports_reasoning=False),
|
||||
)
|
||||
|
||||
import api.config as cfg
|
||||
|
||||
assert cfg._models_dev_reasoning_efforts("grok-4.20-non-reasoning", "xai-oauth") == []
|
||||
|
||||
|
||||
def test_models_dev_unknown_allows_compatibility_fallback(monkeypatch):
|
||||
_install_fake_models_dev(monkeypatch, lambda provider, model: None)
|
||||
|
||||
import api.config as cfg
|
||||
|
||||
assert cfg.resolve_model_reasoning_efforts(
|
||||
"x-ai/grok-4", provider_id="openrouter"
|
||||
) == list(cfg.VALID_REASONING_EFFORTS)
|
||||
|
||||
|
||||
def test_xai_oauth_grok_uses_agent_metadata(monkeypatch):
|
||||
seen = []
|
||||
|
||||
def fake_capabilities(provider, model):
|
||||
seen.append((provider, model))
|
||||
return SimpleNamespace(supports_reasoning=True)
|
||||
|
||||
_install_fake_models_dev(monkeypatch, fake_capabilities)
|
||||
|
||||
import api.config as cfg
|
||||
|
||||
assert cfg.resolve_model_reasoning_efforts(
|
||||
"@xai-oauth:grok-4.3", provider_id="xai-oauth"
|
||||
) == list(cfg.VALID_REASONING_EFFORTS)
|
||||
assert seen == [("xai-oauth", "grok-4.3")]
|
||||
|
||||
|
||||
def test_models_dev_false_suppresses_prefix_heuristic(monkeypatch):
|
||||
_install_fake_models_dev(
|
||||
monkeypatch,
|
||||
lambda provider, model: SimpleNamespace(supports_reasoning=False),
|
||||
)
|
||||
|
||||
import api.config as cfg
|
||||
|
||||
assert cfg.resolve_model_reasoning_efforts(
|
||||
"x-ai/grok-4-non-reasoning", provider_id="openrouter"
|
||||
) == []
|
||||
|
||||
|
||||
def test_get_reasoning_status_uses_config_default_model(monkeypatch, tmp_path):
|
||||
_install_fake_models_dev(
|
||||
monkeypatch,
|
||||
lambda provider, model: SimpleNamespace(supports_reasoning=True)
|
||||
if (provider, model) == ("xai-oauth", "grok-4.3")
|
||||
else None,
|
||||
)
|
||||
|
||||
import api.config as cfg
|
||||
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text(
|
||||
"""
|
||||
model:
|
||||
default: grok-4.3
|
||||
provider: xai-oauth
|
||||
agent:
|
||||
reasoning_effort: medium
|
||||
display:
|
||||
show_reasoning: true
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(cfg, "_get_config_path", lambda: config_path)
|
||||
|
||||
status = cfg.get_reasoning_status()
|
||||
|
||||
assert status["reasoning_effort"] == "medium"
|
||||
assert status["supported_efforts"] == list(cfg.VALID_REASONING_EFFORTS)
|
||||
assert status["supports_reasoning_effort"] is True
|
||||
Reference in new issue
Block a user