diff --git a/gateway/run.py b/gateway/run.py index 2672ab43e95a..4a5330708c93 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -9566,6 +9566,8 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g context_tokens=agent_result.get("last_prompt_tokens", 0) or 0, context_length=agent_result.get("context_length") or None, cwd=os.environ.get("TERMINAL_CWD", ""), + provider=agent_result.get("provider"), + reasoning_effort=agent_result.get("reasoning_effort"), ) except Exception as _footer_err: logger.debug("runtime_footer build failed: %s", _footer_err) @@ -15813,6 +15815,23 @@ def _approval_notify_sync(approval_data: dict) -> None: _output_toks = getattr(_agent, "session_completion_tokens", 0) _context_length = getattr(_agent.context_compressor, "context_length", 0) or 0 _resolved_model = getattr(_agent, "model", None) if _agent else None + _resolved_provider = getattr(_agent, "provider", None) if _agent else None + # Reasoning effort: extract from agent's reasoning_config, then config.yaml + _resolved_effort = None + if _agent: + _rc = getattr(_agent, "reasoning_config", None) + if isinstance(_rc, dict) and _rc.get("enabled") and _rc.get("effort"): + _resolved_effort = _rc["effort"] + if not _resolved_effort: + try: + _eff_cfg = cfg_get(user_config, "agent", "reasoning_effort", default="") + if _eff_cfg: + from hermes_constants import parse_reasoning_effort + _parsed = parse_reasoning_effort(str(_eff_cfg)) + if _parsed and _parsed.get("effort"): + _resolved_effort = _parsed["effort"] + except Exception: + pass # Sync session_id immediately after run_conversation(). Compression # can rotate before a follow-up model call fails; the failure return @@ -15890,6 +15909,8 @@ def _approval_notify_sync(approval_data: dict) -> None: "output_tokens": _output_toks, "model": _resolved_model, "context_length": _context_length, + "provider": _resolved_provider, + "reasoning_effort": _resolved_effort, } # Scan tool results for MEDIA: tags that need to be delivered @@ -15990,6 +16011,8 @@ def _title_failure_cb(task: str, exc: BaseException) -> None: "model": _resolved_model, "context_length": _context_length, "session_id": effective_session_id, + "provider": _resolved_provider, + "reasoning_effort": _resolved_effort, "response_previewed": result.get("response_previewed", False), "response_transformed": result.get("response_transformed", False), } diff --git a/gateway/runtime_footer.py b/gateway/runtime_footer.py index 024cf74d6817..9b89eab51f20 100644 --- a/gateway/runtime_footer.py +++ b/gateway/runtime_footer.py @@ -1,15 +1,15 @@ """Gateway runtime-metadata footer. -Renders a compact footer showing runtime state (model, context %, cwd) and -appends it to the FINAL message of an agent turn when enabled. Off by default -to keep replies minimal. +Renders a compact footer showing runtime state (provider, model, reasoning +effort, context %, cwd) and appends it to the FINAL message of an agent turn +when enabled. Off by default to keep replies minimal. Config (``~/.hermes/config.yaml``):: display: runtime_footer: enabled: true # off by default - fields: [model, context_pct, cwd] # order shown; drop any to hide + fields: [provider, model, effort, context_pct, cwd] # order shown; drop any to hide Per-platform overrides live under ``display.platforms..runtime_footer``. Users can toggle the global setting with ``/footer on|off`` from both the CLI @@ -28,7 +28,7 @@ import os from typing import Any, Iterable, Optional -_DEFAULT_FIELDS: tuple[str, ...] = ("model", "context_pct", "cwd") +_DEFAULT_FIELDS: tuple[str, ...] = ("provider", "model", "effort", "context_pct", "cwd") _SEP = " · " @@ -53,6 +53,32 @@ def _model_short(model: Optional[str]) -> str: return model.rsplit("/", 1)[-1] +def _provider_short(provider: Optional[str]) -> str: + """Shorten provider name for the footer. + + Drops common suffixes (``-direct``, ``-payg``) and maps long names to + compact aliases. + """ + if not provider: + return "" + aliases = { + "openai-codex": "codex", + "opencode-go": "oc-go", + "opencode-zen": "oc-zen", + "google-gemini": "gemini", + "google-gemini-cli": "gemini", + "anthropic": "anthropic", + } + return aliases.get(provider, provider) + + +def _effort_short(effort: Optional[str]) -> str: + """Format reasoning effort for the footer. Empty string when unset.""" + if not effort: + return "" + return effort + + def resolve_footer_config( user_config: dict[str, Any] | None, platform_key: str | None = None, @@ -95,6 +121,8 @@ def format_runtime_footer( context_length: Optional[int], cwd: Optional[str] = None, fields: Iterable[str] = _DEFAULT_FIELDS, + provider: Optional[str] = None, + reasoning_effort: Optional[str] = None, ) -> str: """Render the footer line, or return "" if no fields have data. @@ -103,10 +131,18 @@ def format_runtime_footer( """ parts: list[str] = [] for field in fields: - if field == "model": + if field == "provider": + p = _provider_short(provider) + if p: + parts.append(p) + elif field == "model": m = _model_short(model) if m: parts.append(m) + elif field == "effort": + e = _effort_short(reasoning_effort) + if e: + parts.append(e) elif field == "context_pct": if context_length and context_length > 0 and context_tokens >= 0: pct = max(0, min(100, round((context_tokens / context_length) * 100))) @@ -130,6 +166,8 @@ def build_footer_line( context_tokens: int, context_length: Optional[int], cwd: Optional[str] = None, + provider: Optional[str] = None, + reasoning_effort: Optional[str] = None, ) -> str: """Top-level entry point used by gateway/run.py. @@ -146,4 +184,6 @@ def build_footer_line( context_length=context_length, cwd=cwd, fields=cfg.get("fields") or _DEFAULT_FIELDS, + provider=provider, + reasoning_effort=reasoning_effort, ) diff --git a/tests/gateway/test_runtime_footer.py b/tests/gateway/test_runtime_footer.py index 9c36706f71b4..c38de7c97419 100644 --- a/tests/gateway/test_runtime_footer.py +++ b/tests/gateway/test_runtime_footer.py @@ -153,14 +153,14 @@ def test_format_footer_unknown_field_silently_ignored(): def test_resolve_defaults_off_empty_config(): cfg = resolve_footer_config({}, "telegram") - assert cfg == {"enabled": False, "fields": ["model", "context_pct", "cwd"]} + assert cfg == {"enabled": False, "fields": ["provider", "model", "effort", "context_pct", "cwd"]} def test_resolve_global_enable(): user = {"display": {"runtime_footer": {"enabled": True}}} cfg = resolve_footer_config(user, "telegram") assert cfg["enabled"] is True - assert cfg["fields"] == ["model", "context_pct", "cwd"] + assert cfg["fields"] == ["provider", "model", "effort", "context_pct", "cwd"] def test_resolve_platform_override_wins(): @@ -189,7 +189,7 @@ def test_resolve_platform_can_add_fields_only(): } tg = resolve_footer_config(user, "telegram") assert tg["enabled"] is True - assert tg["fields"] == ["model", "context_pct", "cwd"] + assert tg["fields"] == ["provider", "model", "effort", "context_pct", "cwd"] dc = resolve_footer_config(user, "discord") assert dc["enabled"] is True assert dc["fields"] == ["context_pct"] @@ -249,7 +249,7 @@ def test_build_footer_per_platform_off_suppresses(): def test_build_footer_no_data_returns_empty_even_when_enabled(): - # Enabled, but context_length is None AND cwd empty AND model empty ⇒ no fields + # Enabled, but context_length is None AND cwd empty AND model empty AND no provider ⇒ no fields out = build_footer_line( user_config={"display": {"runtime_footer": {"enabled": True}}}, platform_key="telegram", @@ -260,3 +260,72 @@ def test_build_footer_no_data_returns_empty_even_when_enabled(): # With no TERMINAL_CWD env either if not os.environ.get("TERMINAL_CWD"): assert out == "" + + +# --------------------------------------------------------------------------- +# provider and reasoning_effort fields +# --------------------------------------------------------------------------- + +def test_format_footer_provider_and_effort(): + out = format_runtime_footer( + model="gpt-5.5", + context_tokens=19000, + context_length=100000, + provider="openai-codex", + reasoning_effort="high", + fields=["provider", "model", "effort", "context_pct"], + ) + assert out == "codex · gpt-5.5 · high · 19%" + + +def test_format_footer_provider_aliases(): + assert format_runtime_footer( + model="m", context_tokens=1, context_length=100, + provider="opencode-go", fields=["provider"], + ) == "oc-go" + assert format_runtime_footer( + model="m", context_tokens=1, context_length=100, + provider="google-gemini", fields=["provider"], + ) == "gemini" + assert format_runtime_footer( + model="m", context_tokens=1, context_length=100, + provider="some-unknown-provider", fields=["provider"], + ) == "some-unknown-provider" + + +def test_format_footer_effort_skipped_when_none(): + out = format_runtime_footer( + model="gpt-5.5", + context_tokens=10, + context_length=100, + provider="openai-codex", + reasoning_effort=None, + fields=["provider", "model", "effort", "context_pct"], + ) + assert out == "codex · gpt-5.5 · 10%" + + +def test_format_footer_provider_skipped_when_none(): + out = format_runtime_footer( + model="gpt-5.5", + context_tokens=10, + context_length=100, + provider=None, + reasoning_effort="medium", + fields=["provider", "model", "effort", "context_pct"], + ) + assert out == "gpt-5.5 · medium · 10%" + + +def test_build_footer_passes_provider_and_effort(monkeypatch, tmp_path): + monkeypatch.setenv("HOME", str(tmp_path)) + out = build_footer_line( + user_config={"display": {"runtime_footer": {"enabled": True, "fields": ["provider", "model", "effort", "context_pct"]}}}, + platform_key="telegram", + model="gpt-5.5", + context_tokens=50000, + context_length=128000, + provider="openai-codex", + reasoning_effort="xhigh", + ) + assert out == "codex · gpt-5.5 · xhigh · 39%"