Files
hermes-agent/agent/reasoning_effort.py
Teknium 336b11584c fix: replay prior reasoning to every chat-completions route that can read it
Every model reasons and intends that reasoning to be replayed, but the
chat-completions wire echoed reasoning_content only to DeepSeek/Kimi/MiMo/
Ollama (plus a hardcoded Portal stealth model) and reasoning_details only to
OpenRouter; turn_context popped `reasoning` everywhere, so Ollama's /v1
(reads only `reasoning`) never saw any.

- message_sanitization owns one route->carrier table
  (_REASONING_CARRIER_ROUTES): strict schemas (Mistral, Groq, Cerebras,
  SambaNova) get nothing, Fireworks reasoning_content only, OpenRouter-format
  gateways (OpenRouter, Kilo, Vercel AI Gateway, Nous Portal) all three,
  Novita/MiniMax /v1/TokenHub + reasoning_details, local/Ollama/Upstage +
  reasoning, everyone else reasoning_content.
- The " " pad stays limited to the must-echo tier (DeepSeek, Kimi, MiMo);
  Ollama leaves it (no echo requirement) and the stealth hardcode is gone.
- Reactive recovery: a 4xx unknown-field rejection that NAMES a reasoning
  key this request carried is recorded per (provider, host, model) and the
  retry re-shapes api_messages without it. Generic upstream errors never
  count; a recorded key falls through, so no loop. History is untouched.
- Nous Portal reads reasoning_details again: the #118182 "replay budget"
  does not exist in the Portal and does not reproduce live.
- Gemini thought_signature survives on Gemini hosts/providers even when the
  model id is an alias.
- Vendor keep-reasoning flags (docs-only): GLM thinking.clear_thinking=false,
  DashScope preserve_thinking / clear_thinking, Kimi K2.x thinking.keep=all.
- stale_thinking_reaches_wire follows the same table, so compaction trigger
  and tail walks agree with what is sent.
2026-10-10 19:22:07 -07:00

275 lines
15 KiB
Python

"""Canonical reasoning-effort vocabulary and wire clamping.
Hermes' internal effort ladder (``VALID_REASONING_EFFORTS`` plus ``none``) is wider than any
single provider wire accepts; hand-rolled per-transport maps leaked new levels (``ultra``) to
wires that 400 and inverted the ladder (unknown → weak default). Single source of truth:
:data:`EFFORT_LADDER` (low→high), :func:`clamp_effort` (verbatim if supported, else the
nearest WEAKER level; only when nothing weaker exists the weakest supported), and named
wire-vocabulary constants so call sites declare data. Rules: wire shape stays local, only the
vocabulary math lives here; unset stays unset (never invent an effort); when a provider
rejects a level fix its declared set, never a predicate.
"""
from __future__ import annotations
import re
from typing import Optional, Sequence
#: Matches ``k3`` as a delimited token (``k3``, ``k3-256k``, ``kimi-k3-cot``), never K2-era names (``kimi-k2.6``).
# From #76427 by @ruizanthony.
_KIMI_K3_SLUG_RE = re.compile(r"(?:^|[^a-z0-9])k3(?:[^a-z0-9]|$)")
# Canonical low→high ordering for nearest-level clamping. Includes "none" so an explicit
# disable can be clamped when a provider publishes it as a level. ``ultra`` is Hermes-internal
# (the Codex product tier): no wire accepts it, every declared set stops at ``max``.
EFFORT_LADDER: tuple[str, ...] = ("none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra")
#: Widest OpenAI-compatible wire vocabulary (OpenRouter, Nous Portal).
OPENAI_COMPAT_WIRE_EFFORTS: tuple[str, ...] = ("none", "minimal", "low", "medium", "high", "xhigh", "max")
#: OpenAI/Codex Responses per model generation (live-verified): ``minimal`` is rejected by
#: both (clamps to low); ``max`` is gpt-5.6 / gpt-6-tier only (legacy = 5.5 and older).
CODEX_GPT56_EFFORTS: tuple[str, ...] = ("none", "low", "medium", "high", "xhigh", "max")
CODEX_LEGACY_EFFORTS: tuple[str, ...] = ("none", "low", "medium", "high", "xhigh")
# GPT-6 Astra is account-gated and its Responses API accepts no disable/minimal
# wire level; callers normalize those requests to ``low`` at the transport boundary.
CODEX_ASTRA_EFFORTS: tuple[str, ...] = ("low", "medium", "high", "xhigh", "max")
ASTRA_MODEL_IDS: frozenset[str] = frozenset({"gpt-6-astra", "gpt-6-astra-900k"})
#: GPT-6 Sol/Terra/Luna (the 5.6 successors; ``-pro``/``-900k``/dated snapshots share the prefix).
GPT6_TIER_PREFIXES: tuple[str, ...] = ("gpt-6-sol", "gpt-6-luna")
#: GPT-6.1 Sol takes Astra's ``low..max`` ladder (``none`` 400s, live 2026-09-29) without Astra's
#: account gating, so it stays in the static catalogs.
NO_DISABLE_TIER_PREFIXES: tuple[str, ...] = ("gpt-6.1-sol",)
DAYBREAK_MODEL_IDS: frozenset[str] = frozenset(
{"gpt-daybreak-blue-latest", "gpt-daybreak-blue-latest-900k"}
)
#: xAI Responses — Grok 4.6+ accepts xhigh; older Grok tops out at high.
XAI_GROK46_EFFORTS: tuple[str, ...] = ("low", "medium", "high", "xhigh")
XAI_LEGACY_EFFORTS: tuple[str, ...] = ("low", "medium", "high")
#: Actual Computer relays (SGLang/vLLM).
ACTUAL_RELAY_EFFORTS: tuple[str, ...] = ("none", "low", "medium", "high", "max")
#: Moonshot/Kimi K3 (server default high) vs K2-era models. K3 quirks: ``high`` is K3's
#: positional middle AND server default, so ``medium`` rounds to it rather than down to
#: ``low``; ``xhigh`` rounds up to ``max`` (K3's top tier).
KIMI_K3_EFFORTS: tuple[str, ...] = ("low", "high", "max")
KIMI_K2_EFFORTS: tuple[str, ...] = ("low", "medium", "high")
KIMI_K3_OVERRIDES: dict[str, str] = {"medium": "high", "xhigh": "max"}
#: OpenCode "Ox Alpha" (x-preview-f-free): thinking cannot be disabled and the wire accepts
#: exactly low/high/max (medium/none/xhigh 400); xhigh rounds up.
OX_ALPHA_EFFORTS: tuple[str, ...] = ("low", "high", "max")
OX_ALPHA_OVERRIDES: dict[str, str] = {"xhigh": "max"}
#: Tencent TokenHub / Nebius Token Factory / Upstage Solar: plain three-level knobs.
TOKENHUB_EFFORTS: tuple[str, ...] = ("low", "medium", "high")
NEBIUS_EFFORTS: tuple[str, ...] = ("low", "medium", "high")
SOLAR_EFFORTS: tuple[str, ...] = ("low", "medium", "high")
#: GLM-5.2 native knob: exactly ``high`` (its minimum thinking level) and ``max``; GLM-5.3
#: widens it to a graded scale (live-verified, monotonic). ``xhigh`` requests the top tier.
GLM52_EFFORTS: tuple[str, ...] = ("high", "max")
GLM52_OVERRIDES: dict[str, str] = {"xhigh": "max"}
# : GLM-5.3 widens the knob to a graded low/medium/high/max scale — verified : live on
# api.z.ai/api/coding/paas/v4 (issue #91789, 2026-08-21): every : level accepted with monotonic
# reasoning-token scaling (low=4, medium=11, : high=98, max=125 on the probe prompt).
GLM53_EFFORTS: tuple[str, ...] = ("low", "medium", "high", "max")
GLM53_OVERRIDES: dict[str, str] = {"xhigh": "max"}
#: DeepSeek V4 OpenAI-compat endpoint; ``xhigh`` requests the top tier.
DEEPSEEK_V4_EFFORTS: tuple[str, ...] = ("low", "medium", "high", "max")
DEEPSEEK_V4_OVERRIDES: dict[str, str] = {"xhigh": "max"}
#: Ollama Cloud /v1/chat/completions: rejects ``minimal`` with HTTP 400.
OLLAMA_CLOUD_EFFORTS: tuple[str, ...] = ("none", "low", "medium", "high", "max")
OLLAMA_CLOUD_OVERRIDES: dict[str, str] = {"xhigh": "max"}
#: Meta Model API (Muse): rejects ``none``.
META_AI_EFFORTS: tuple[str, ...] = ("minimal", "low", "medium", "high", "xhigh")
def is_astra_model(model: Optional[str]) -> bool:
"""``gpt-6-astra`` or its Hermes-side ``-900k`` picker alias, with or without a ``vendor/`` prefix.
The single home for the slug set: picker gating, effort vocabulary and the request sanitizer all
key off it, so a new Astra alias is one edit."""
return (model or "").strip().lower().rsplit("/", 1)[-1] in ASTRA_MODEL_IDS
def codex_supported_efforts(model: Optional[str]) -> tuple[str, ...]:
"""Supported effort set for an OpenAI/Codex Responses model."""
bare = (model or "").strip().lower().rsplit("/", 1)[-1]
if is_astra_model(model) or bare.startswith(NO_DISABLE_TIER_PREFIXES):
return CODEX_ASTRA_EFFORTS
return (
CODEX_GPT56_EFFORTS
if "gpt-5.6" in bare or bare.startswith(GPT6_TIER_PREFIXES) or bare in DAYBREAK_MODEL_IDS
else CODEX_LEGACY_EFFORTS
)
def kimi_supported_efforts(model: Optional[str]) -> tuple[str, ...]:
"""Supported effort set for a Moonshot/Kimi slug (bare ``k3``, ``k3-256k``, ``kimi-k3*`` → K3).
K3 is served as the bare slug ``k3``, plan variants like ``k3-256k``, and the ``kimi-k3*`` aliases; its
documented set is low/high/max. Everything earlier speaks low/medium/high. Boundary-matched so K2-era
names (``kimi-k2.6``) never match (detection regex from #76427 by @ruizanthony).
"""
m = (model or "").strip().lower().split("/")[-1]
return KIMI_K3_EFFORTS if _KIMI_K3_SLUG_RE.search(m) else KIMI_K2_EFFORTS
def clamp_effort(
effort: Optional[str], supported: Optional[Sequence[str]], overrides: Optional[dict[str, str]] = None,
) -> Optional[str]:
"""Clamp a requested reasoning effort onto a wire's supported levels.
``overrides`` (a declared vendor mapping, e.g. Kimi K3 ``medium → high``) is consulted
first. Otherwise the request passes through unchanged when it is supported, when the
supported set is unknown/empty, or when it isn't a recognized ladder level (custom
providers may use bespoke names). Else the **nearest weaker** supported level is returned
so a clamp never escalates cost; when nothing weaker exists, the weakest supported level
(the provider's floor is the closest honest match). Monotonic: a stronger request never
resolves weaker than a weaker request would.
"""
requested = str(effort or "").strip().lower()
if not requested or not supported:
return effort
supported_norm = [lvl for lvl in (str(s).strip().lower() for s in supported) if lvl in EFFORT_LADDER]
if not supported_norm or requested in supported_norm:
return effort
if overrides and overrides.get(requested) in supported_norm:
return overrides[requested]
if requested not in EFFORT_LADDER:
return effort
# "none" disables reasoning — never a degradation target for an enabled ask
# (clamping "minimal" to "none" would silently switch thinking off).
candidates = [level for level in supported_norm if level != "none"]
if not candidates:
return effort
requested_idx = EFFORT_LADDER.index(requested)
below = [level for level in candidates if EFFORT_LADDER.index(level) < requested_idx]
return max(below, key=EFFORT_LADDER.index) if below else min(candidates, key=EFFORT_LADDER.index)
def route_supported_efforts(
provider: Optional[str], model: Optional[str], api_mode: Optional[str] = None,
) -> tuple[str, ...]:
"""Levels the (provider, model) route's ENTRY clamp accepts: the Codex/OpenAI Responses set per
model generation, else the widest OpenAI-compatible vocabulary (narrower providers clamp again
downstream, never upward). On the Codex app-server runtime ``ultra`` is codex's own harness mode,
accepted wherever the model's ladder reaches ``max`` (codex runs it at ``max``; a model without
``max`` rejects it), so it is sent verbatim there."""
if (provider or "").strip().lower() == "openai-codex":
supported = codex_supported_efforts(model)
return (*supported, "ultra") if api_mode == "codex_app_server" and "max" in supported else supported
return OPENAI_COMPAT_WIRE_EFFORTS
def effort_display_label(
effort: Optional[str], provider: Optional[str] = None, model: Optional[str] = None,
api_mode: Optional[str] = None,
) -> str:
"""Picker / ``/reasoning`` status label for a ladder level: the level itself when the route sends
it verbatim, else ``"<level> (sends <clamped> on this route)"`` so a Hermes-internal step such as
``ultra`` (#61634) is never presented as a distinct wire level the route does not have."""
requested = str(effort or "").strip().lower()
clamped = clamp_effort(requested, route_supported_efforts(provider, model, api_mode))
return requested if not requested or clamped == requested else f"{requested} (sends {clamped} on this route)"
def requested_effort(reasoning_config: Optional[dict]) -> Optional[str]:
"""The user's explicit effort, or None (absent/malformed config, no effort, or reasoning
disabled) — callers then omit the wire field."""
if not isinstance(reasoning_config, dict) or reasoning_config.get("enabled") is False:
return None
return str(reasoning_config.get("effort") or "").strip().lower() or None
def tokenhub_effort(requested: Optional[str]) -> str:
"""TokenHub's top-level ``reasoning_effort``: ``high`` when no level was chosen, else the
request clamped to low/medium/high. Shared by the tencent-tokenhub profile and the host-based
branch base_url-only agents take; callers handle thinking-off themselves."""
return "high" if requested is None else clamp_effort(requested, TOKENHUB_EFFORTS)
def generic_nested_reasoning(reasoning_config: Optional[dict]) -> dict:
"""The OpenAI-compatible ``extra_body`` fallback for a route whose profile declares no
reasoning shape: ``{}`` with no config, ``{"reasoning": {"enabled": False}}`` when disabled,
else ``{"reasoning": {"enabled": True, "effort": <effort or medium>}}``. Used by auxiliary
calls, and by a profile hook that wants that fallback (``handles_reasoning`` skips it for any
profile overriding ``build_api_kwargs_extras``)."""
if not reasoning_config or not isinstance(reasoning_config, dict):
return {}
if reasoning_config.get("enabled") is False:
return {"reasoning": {"enabled": False}}
return {"reasoning": {"enabled": True, "effort": reasoning_config.get("effort") or "medium"}}
def clamp_reasoning_config(reasoning_config: Optional[dict], supported: Sequence[str] = OPENAI_COMPAT_WIRE_EFFORTS) -> Optional[dict]:
"""Return ``reasoning_config`` with its ``effort`` clamped onto ``supported`` (non-dicts and
configs without an effort pass through untouched).
The entry clamp for an OpenAI-compatible chat-completions request builder: Hermes-internal
``ultra`` never reaches a wire (#89503 main transport, #112010 aux/MoA), while provider
profiles with narrower vocabularies clamp again downstream. Unset stays unset.
"""
if not isinstance(reasoning_config, dict):
return reasoning_config
effort = str(reasoning_config.get("effort") or "").strip().lower()
clamped = clamp_effort(effort, supported) if effort else effort
return {**reasoning_config, "effort": clamped} if clamped != effort else reasoning_config
def thinking_toggle_extras(
reasoning_config: Optional[dict],
efforts: Sequence[str],
overrides: Optional[dict[str, str]] = None,
*,
always_emit_toggle: bool = False,
) -> tuple[dict, dict]:
"""Translate a reasoning config onto the Moonshot/DeepSeek chat_completions wire:
``extra_body.thinking`` toggle and top-level ``reasoning_effort``.
Moonshot 400s when both are sent, so by default the effort (when it lands in
``efforts``) replaces the toggle. DeepSeek instead requires the toggle on every
request (an omitted toggle defaults thinking on and then demands
``reasoning_content`` echoes), hence ``always_emit_toggle``. A requested effort of
``none`` is not a level on these wires; it falls back to the plain toggle.
"""
if isinstance(reasoning_config, dict) and reasoning_config.get("enabled") is False:
return {"thinking": {"type": "disabled"}}, {}
effort = requested_effort(reasoning_config)
clamped = clamp_effort(None if effort == "none" else effort, efforts, overrides)
if clamped in efforts:
return ({"thinking": {"type": "enabled"}} if always_emit_toggle else {}), {"reasoning_effort": clamped}
return {"thinking": {"type": "enabled"}}, {}
# DashScope documents ``preserve_thinking`` (Qwen3.6+ and the Kimi K2.6/K2.7 it hosts) and a
# top-level ``clear_thinking`` (its GLM-5.x) as the switches that make the server KEEP replayed
# prior-turn ``reasoning_content``; without them the replay is accepted and dropped.
# https://www.alibabacloud.com/help/en/model-studio/deep-thinking
_DASHSCOPE_PRESERVE_MODELS = ("qwen3.6", "qwen3.7", "qwen3.8", "kimi-k2.6", "kimi-k2.7")
def dashscope_preserve_thinking_extras(model: Optional[str]) -> dict:
"""``extra_body`` keys that keep replayed reasoning on a DashScope-hosted model, else ``{}``."""
m = (model or "").strip().lower()
if any(token in m for token in _DASHSCOPE_PRESERVE_MODELS):
return {"preserve_thinking": True}
if m.startswith("glm-5"):
return {"clear_thinking": False}
return {}
def ox_alpha_reasoning_extras(reasoning_config: Optional[dict], model: Optional[str]) -> tuple[dict, dict]:
"""Ox Alpha (``x-preview-f-free``) ``reasoning_effort`` translation for the
opencode-zen profile (low/high/max only; anything else 400s)."""
if (model or "").strip().rsplit("/", 1)[-1].lower() != "x-preview-f-free":
return {}, {}
effort = requested_effort(reasoning_config)
clamped = clamp_effort(None if effort == "none" else effort, OX_ALPHA_EFFORTS, OX_ALPHA_OVERRIDES)
return ({}, {"reasoning_effort": clamped}) if clamped in OX_ALPHA_EFFORTS else ({}, {})