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
23 changes: 23 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:<path> tags that need to be delivered
Expand Down Expand Up @@ -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),
}
Expand Down
52 changes: 46 additions & 6 deletions gateway/runtime_footer.py
Original file line number Diff line number Diff line change
@@ -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.<platform>.runtime_footer``.
Users can toggle the global setting with ``/footer on|off`` from both the CLI
Expand All @@ -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 = " · "


Expand All @@ -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,
Expand Down Expand Up @@ -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.

Expand All @@ -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)))
Expand All @@ -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.

Expand All @@ -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,
)
77 changes: 73 additions & 4 deletions tests/gateway/test_runtime_footer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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",
Expand All @@ -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%"