Files
hermes-agent/gateway/runtime_footer.py
teknium1 bafb778edc feat(gateway): opt-in served_model footer field shows the model that really answered
What: a new opt-in gateway runtime-footer field `served_model` rendered as
`alias → served`. It is populated from the `x-litellm-model-id` response header
(fallback `x-litellm-model-api-base`) that routing proxies send on every chat
completion, captured through an httpx response hook installed on the agent's
OpenAI client (agent/served_model.py, wired in create_openai_client), and from
Hermes' own provider fallback (primary runtime model → active model) when no
header is present. The turn result carries `requested_model` / `served_model`;
gateway/run_turn.py passes them to the footer. Off unless listed in
`display.runtime_footer.fields`; the default field set renders exactly as before.

Why: behind a routing proxy (or during a silent Hermes fallback) every reply
shows the configured alias, so operators cannot see which deployment actually
served a request (#54864). The SDK's parsed objects drop response headers, so
the capture has to sit on the transport.
2026-09-19 10:38:30 -07:00

121 lines
5.7 KiB
Python

"""Gateway runtime-metadata footer (model · context % · cwd), off by default to keep replies
minimal. Config: ``display.runtime_footer: {enabled: bool, fields: [model, context_pct, cwd]}``
(order shown; drop any to hide), per-platform override ``display.platforms.<p>.runtime_footer``,
toggled by ``/footer on|off``. Fields: ``model`` (vendor prefix dropped), ``context_pct`` (last-call
occupancy), ``latency`` (turn wall-clock, opt-in — NOT in the default set so an unset ``fields``
renders exactly as before), ``served_model`` (opt-in, ``alias → served``: the deployment a routing
proxy reported via ``x-litellm-model-id`` / ``x-litellm-model-api-base``, or Hermes' own fallback
route; skipped when the served model is the requested one), ``cwd`` (home-relative). ``gateway/run.py`` appends the footer to the
final response only (never to tool-progress or streaming partials); when streaming already
delivered the text, it goes out as a trailing message via ``send_trailing_footer()``."""
from __future__ import annotations
import os
from typing import Any, Iterable, Optional
_DEFAULT_FIELDS: tuple[str, ...] = ("model", "context_pct", "cwd")
_SEP = " · "
def _home_relative_cwd(cwd: str) -> str:
"""Return *cwd* with ``$HOME`` collapsed to ``~``. Empty string if unset."""
if not cwd:
return ""
try:
home = os.path.expanduser("~")
p = os.path.abspath(cwd)
if home and (p == home or p.startswith(home + os.sep)):
return "~" + p[len(home):]
return p
except Exception:
return cwd
def _model_short(model: Optional[str]) -> str:
"""Drop ``vendor/`` prefix (``openai/gpt-5.4`` → ``gpt-5.4``)."""
return model.rsplit("/", 1)[-1] if model else ""
def _env_cwd() -> str:
try:
from tools.terminal_scope import terminal_env
except ImportError:
return os.environ.get("TERMINAL_CWD", "")
return terminal_env("TERMINAL_CWD", "")
def resolve_footer_config(user_config: dict[str, Any] | None, platform_key: str | None = None) -> dict[str, Any]:
"""Resolve effective footer config: defaults (enabled=False) <
``display.runtime_footer`` < ``display.platforms.<platform_key>.runtime_footer``."""
resolved = {"enabled": False, "fields": list(_DEFAULT_FIELDS)}
cfg = (user_config or {}).get("display") or {}
plat_cfg = (cfg.get("platforms") or {}).get(platform_key) if platform_key else None
sections = [cfg.get("runtime_footer"), plat_cfg.get("runtime_footer") if isinstance(plat_cfg, dict) else None]
for section in sections:
if not isinstance(section, dict):
continue
if "enabled" in section:
resolved["enabled"] = bool(section.get("enabled"))
if isinstance(section.get("fields"), list) and section["fields"]:
resolved["fields"] = [str(f) for f in section["fields"]]
return resolved
def _format_latency(seconds: float) -> str:
"""Humanize a turn duration: ``<1s``, ``22s``, ``1m05s``."""
if seconds < 1:
return "<1s"
total = int(round(seconds))
if total < 60:
return f"{total}s"
m, sec = divmod(total, 60)
return f"{m}m{sec:02d}s"
def format_runtime_footer(*, model: Optional[str], context_tokens: int,
context_length: Optional[int], cwd: Optional[str] = None,
turn_seconds: Optional[float] = None,
requested_model: Optional[str] = None, served_model: Optional[str] = None,
fields: Iterable[str] = _DEFAULT_FIELDS) -> str:
"""Render the footer line, or "" if no fields have data. Fields whose data is missing (and
unknown field names) are skipped silently — a partial footer beats ``?%`` or empty slots."""
def context_pct() -> str:
if context_length and context_length > 0 and context_tokens >= 0:
return f"{max(0, min(100, round((context_tokens / context_length) * 100)))}%"
return ""
def served() -> str:
requested = requested_model or model
alias = _model_short(requested)
if served_model and served_model not in (alias, requested):
return f"{alias} → {served_model}"
return ""
renderers = {
"model": lambda: _model_short(model),
"served_model": served,
"context_pct": context_pct,
# Skipped when the caller did not measure (None) or the value is negative.
"latency": lambda: _format_latency(turn_seconds) if turn_seconds is not None and turn_seconds >= 0 else "",
"cwd": lambda: _home_relative_cwd(cwd or _env_cwd()),
}
return _SEP.join(v for field in fields if (render := renderers.get(field)) and (v := render()))
def build_footer_line(*, user_config: dict[str, Any] | None, platform_key: str | None,
model: Optional[str], context_tokens: int, context_length: Optional[int],
cwd: Optional[str] = None, turn_seconds: Optional[float] = None,
requested_model: Optional[str] = None, served_model: Optional[str] = None) -> str:
"""Entry point for gateway/run.py: footer text, or "" when disabled / no data. Callers append it
to the final response themselves, preserving a single blank line of separation.
``turn_seconds`` is the caller-measured (``time.monotonic()``) run duration; ``None`` skips the
``latency`` field."""
cfg = resolve_footer_config(user_config, platform_key)
if not cfg.get("enabled"):
return ""
return format_runtime_footer(model=model, context_tokens=context_tokens,
context_length=context_length, cwd=cwd, turn_seconds=turn_seconds,
requested_model=requested_model, served_model=served_model,
fields=cfg.get("fields") or _DEFAULT_FIELDS)