Files
hermes-agent/agent/auxiliary_reasoning_floor.py
teknium1 2768f31628 fix: step reasoning up to the floor when a route refuses to disable it
Endpoints that understand the reasoning field but refuse the OFF ("Reasoning is
mandatory for this endpoint and cannot be disabled" -- the Nous Portal on
gpt-6-astra) got two different wrong answers:

* auxiliary lanes (title generation sends reasoning_config={"enabled": False})
  400'd outright -- the #112781 strip rung only matched "unsupported/unknown
  field" wordings, so every session on such a route stayed untitled;
* the main loop dropped the disable and let the route pick its default effort,
  the opposite of what a thinking-off user asked for.

Both now step the effort up to the lowest level every reasoning wire accepts
(low) instead. The aux ladder gets a rung ordered before the strip
(agent/auxiliary_reasoning_floor.py: lifts reasoning_effort / extra_body.reasoning
/ _reasoning_config, memoises the (route, model) so the next thinking-off aux call
starts at the floor without the guaranteed 400); the main loop's reasoning_mandatory
recovery records whether the route said mandatory (floor) or unknown-field (drop,
unchanged). error_classifier.is_reasoning_required_rejection separates the two.

Live on the portal (proxy wire capture): title none->400->low->200 titled;
main loop none->400->low->200; second aux call on the route sends low up front.
2026-09-19 02:37:36 -07:00

97 lines
4.8 KiB
Python

"""Reasoning floor for auxiliary requests: when a route refuses to switch reasoning OFF, step it UP.
Aux lanes that want speed over thought (title generation: ``max_tokens=64``, JSON body) send the
provider's thinking-off encoding — top-level ``reasoning_effort: "none"`` on the custom profile,
``extra_body.reasoning: {"enabled": false}`` on OpenRouter-shaped relays, ``_reasoning_config`` on the
Anthropic Messages adapters. Some endpoints understand the field but refuse the disable ("Reasoning is
mandatory for this endpoint and cannot be disabled" — the Nous Portal on gpt-6-astra); before this
module every title call there 400'd and the session stayed untitled.
The recovery is a *step up*, not a strip: the same request goes out again at the lowest effort every
reasoning wire accepts (``low``; ``minimal`` is rejected by o-series and Claude), and the (route, model)
pair is memoised so the next disabled-reasoning aux call on it starts at the floor instead of burning
the guaranteed 400 first. Dropping the field would also succeed once, but it says nothing about the
next call and hands the effort choice back to the provider default (often ``medium`` or higher, the
opposite of what a thinking-off caller asked for).
"""
from __future__ import annotations
import logging
from typing import Any, Dict, Optional
from urllib.parse import urlparse
logger = logging.getLogger(__name__)
REASONING_FLOOR_EFFORT = "low"
# (route key, model) pairs that refused a reasoning disable in this process.
_FLOORED_ROUTES: set[tuple[str, str]] = set()
_DISABLED_EFFORTS = {"none", "off", "disabled", "false", "0"}
def _route_key(provider: Optional[str], base_url: Optional[str]) -> str:
"""Endpoint host:port when known (a base_url override turns a named provider into ``custom``), else
the provider name — the same key shape ``auxiliary_structured_output`` uses."""
return (urlparse(base_url or "").netloc or "").lower() or str(provider or "").strip().lower()
def _is_disabled(reasoning_config: Any) -> bool:
return isinstance(reasoning_config, dict) and reasoning_config.get("enabled") is False
def floor_reasoning_config(reasoning_config: Any) -> Dict[str, Any]:
"""The caller's disabled ``reasoning_config`` lifted to the floor; anything else returned as-is."""
if _is_disabled(reasoning_config):
return {"enabled": True, "effort": REASONING_FLOOR_EFFORT}
return reasoning_config
def with_reasoning_floor(kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Copy of *kwargs* with every thinking-OFF encoding lifted to ``REASONING_FLOOR_EFFORT``:
top-level ``reasoning_effort``, ``extra_body.reasoning`` (OpenRouter shape) and the adapter's private
``_reasoning_config``. ``None`` when nothing was disabled, so the ladder never re-sends an unchanged
request."""
changed = False
retry = dict(kwargs)
if str(retry.get("reasoning_effort", "")).strip().lower() in _DISABLED_EFFORTS:
retry["reasoning_effort"] = REASONING_FLOOR_EFFORT
changed = True
if _is_disabled(retry.get("_reasoning_config")):
retry["_reasoning_config"] = floor_reasoning_config(retry["_reasoning_config"])
changed = True
extra_body = retry.get("extra_body")
if isinstance(extra_body, dict):
reasoning = extra_body.get("reasoning")
if _is_disabled(reasoning) or (
isinstance(reasoning, dict) and str(reasoning.get("effort", "")).strip().lower() in _DISABLED_EFFORTS
):
retry["extra_body"] = {**extra_body, "reasoning": {"enabled": True, "effort": REASONING_FLOOR_EFFORT}}
changed = True
return retry if changed else None
def remember_reasoning_floor(
provider: Optional[str], base_url: Optional[str], rejected_kwargs: Dict[str, Any], error: BaseException,
) -> None:
"""Record that this route's ``rejected_kwargs["model"]`` refuses to disable reasoning (the ladder
calls this after the stepped-up retry succeeded)."""
_FLOORED_ROUTES.add((_route_key(provider, base_url), str(rejected_kwargs.get("model") or "")))
def known_reasoning_floor(
reasoning_config: Any, provider: Optional[str], base_url: Optional[str], model: Optional[str],
task: Optional[str] = None,
) -> Any:
"""*reasoning_config* lifted to the floor when this route+model is known to refuse a disable; unchanged
otherwise. Runs before the profile projection so every wire shape starts at the floor."""
if not _is_disabled(reasoning_config):
return reasoning_config
if (_route_key(provider, base_url), str(model or "")) not in _FLOORED_ROUTES:
return reasoning_config
logger.info(
"Auxiliary %s: %s (%s) cannot disable reasoning; sending effort=%s up front",
task or "call", _route_key(provider, base_url) or "provider", model or "model", REASONING_FLOOR_EFFORT,
)
return floor_reasoning_config(reasoning_config)