Skip to content

feat(gateway): add opt-in 'latency' runtime footer field - #71990

Closed
Kyzcreig wants to merge 1 commit into
NousResearch:mainfrom
ANG-Ventures:upstream/footer-latency
Closed

feat(gateway): add opt-in 'latency' runtime footer field#71990
Kyzcreig wants to merge 1 commit into
NousResearch:mainfrom
ANG-Ventures:upstream/footer-latency

Conversation

@Kyzcreig

Copy link
Copy Markdown
Contributor

Symptom

The runtime footer (/footer) shows what model ran and how full the context is, but not how long the turn took. On a messaging platform there is no progress bar and no shell timer — a turn that took 4 seconds and one that took four minutes produce visually identical replies. Users comparing models, providers, or reasoning levels have no at-a-glance signal for the dimension they most often care about, and "was that slow, or did I imagine it?" is unanswerable after the fact.

Change

Adds a latency field to the existing footer machinery, rendering the wall-clock duration of the agent run:

gpt-5.4 · 68% · 1m05s · ~

Format: <1s / 22s / 1m05s.

gateway/run.py measures with time.monotonic() immediately around the self._run_agent(...) await in _handle_message_with_agent — the same function that already builds the footer, so the value is the user-perceived turn duration. monotonic (not time.time()) so it is immune to wall-clock/NTP adjustment mid-turn.

Byte stability — the part I'd most like reviewed

latency is deliberately NOT in _DEFAULT_FIELDS. It is opt-in via display.runtime_footer.fields. Every footer that renders today — including for users who never touch fields — renders byte-identically after this change.

Per AGENTS.md ("a system prompt that is byte-stable for the life of a conversation" / "per-conversation prompt caching is sacred"), I treated that as a hard constraint rather than a nicety, and enforced it with tests rather than asserting it in prose:

Test Pins
test_latency_not_in_default_fields the default tuple itself
test_resolve_footer_config_default_fields_exclude_latency what config resolution yields for an untouched config
test_default_footer_renders_byte_identically 5 exact output strings, while supplying turn_seconds
test_default_build_footer_line_ignores_turn_seconds build_footer_line(...) == build_footer_line(..., turn_seconds=125.0)

The third one is the load-bearing one: it passes a real measured duration and asserts the default-configured footer still doesn't show it. So the guarantee is "opt-in by construction," not "opt-in because no caller happens to pass the value yet."

Adding latency to _DEFAULT_FIELDS fails 11 of these tests. I verified that by actually doing it, not by inspection.

Footprint

  • No new config surface — reuses the existing display.runtime_footer.fields list.
  • No new env vars. No new core tool. No change to the model-facing tool schema.
  • One module-private helper (_format_latency), one keyword arg threaded through the two existing footer functions, 3 lines in gateway/run.py.

turn_seconds defaults to None, and the field is skipped when it is None or negative — so any call site that doesn't measure timing keeps working unchanged, and a future call site can opt in by passing one argument.

On "sibling call paths included"

I checked for other places that render this footer, since fixing a whole class rather than one site is the standard here. On current main build_footer_line / format_runtime_footer have exactly two consumers:

  • gateway/run.py:14175 — the real per-turn render. Wired.
  • gateway/slash_commands.py:3426 — the /footer on toggle preview, which passes context_tokens=0, context_length=None because no turn has run. Deliberately not wired: there is no turn to time, and inventing a duration for a preview line would be misleading.

hermes_cli/cli_commands_mixin.py only reads enabled for the CLI /footer toggle; it doesn't render a footer. So this is the complete set — there is no third path silently missing the field.

Tests

tests/gateway/test_runtime_footer.py (+185): a _format_latency boundary table (sub-second, rounding either side of 59.4/59.6, the m{:02d}s zero-pad, 60m), the render/skip/opt-in matrix, field-order placement, build_footer_line threading, and the byte-stability block above.

RED-proved by mutation — each of these was applied and the listed tests failed:

Mutation Result
latency added to _DEFAULT_FIELDS 11 failed
drop the turn_seconds is not None and >= 0 guard 2 failed
f"{m}m{sec:02d}s"f"{m}m{sec}s" 6 failed
build_footer_line stops threading turn_seconds 1 failed
51 passed   tests/gateway/test_runtime_footer.py
54 passed   + tests/gateway/test_footer_command_mid_run.py
ruff check  All checks passed!

Run hermetically against a temp HERMES_HOME.

Docs

website/docs/user-guide/configuration.md — replaced the stale inline # supported fields: model, context_pct, cwd comment with a real field table, and stated explicitly that the default set is ["model", "context_pct", "cwd"] and that latency is opt-in.

(Drive-by while there: the existing example footer in that section was — claude-opus-4.7 · 12 tool calls · 2m 14s · $0.042, which doesn't match anything the code can render — no tool calls or cost field exists. Replaced with real output. Happy to split that into its own commit if you'd prefer.)

Relationship to #47600

This PR stands alone against main and does not depend on #47600. They can merge in either order.

#47600 (provider_model, context_full, reasoning) touches the same file and the same two functions, so expect a small textual conflict if both land — specifically in three spots: the module docstring's field list, the format_runtime_footer signature's keyword block, and the elif field == ... chain. All three are additive; the resolution is a union in every case (keep both sets of fields), with no semantic decision to make.

I kept them separate on purpose rather than stacking: latency is self-contained and reviewable in a couple of minutes, and stacking would have made it un-mergeable until #47600 is resolved. If you'd rather review them as one unit, say so and I'll rebase this onto #47600 and re-push.

Both PRs share the same byte-stability property — after the review round on #47600 I reverted its _DEFAULT_FIELDS change too, so all new footer fields across both PRs are opt-in and no existing footer's bytes move.

## Symptom

The runtime footer (`/footer`) shows what model ran and how full the context
is, but not how long the turn took. On a messaging platform there is no
progress bar and no shell timer — a turn that took 4 seconds and one that took
four minutes produce visually identical replies. Users comparing models,
providers, or reasoning levels have no at-a-glance signal for the one
dimension they most often care about, and "was that slow or did I imagine it?"
is unanswerable after the fact.

## Change

Adds a `latency` field to the existing footer machinery, rendering the
wall-clock duration of the agent run: `<1s`, `22s`, `1m05s`.

`gateway/run.py` measures with `time.monotonic()` immediately around the
`self._run_agent(...)` await in `_handle_message_with_agent` — the same
function that already builds the footer, so the value is the user-perceived
turn duration (monotonic, so it is immune to wall-clock/NTP adjustment).

## Byte stability

`latency` is deliberately NOT in `_DEFAULT_FIELDS`. It is opt-in via
`display.runtime_footer.fields`. Every existing footer — and every footer a
user has today without touching config — renders byte-identically.

This is enforced by tests, not just asserted:

- `test_latency_not_in_default_fields` pins the default tuple.
- `test_resolve_footer_config_default_fields_exclude_latency` pins what
  config resolution produces for an untouched config.
- `test_default_footer_renders_byte_identically` pins five exact output
  strings for default-config renders **while supplying `turn_seconds`** —
  proving that even when the caller measures timing, a default-configured
  footer does not show it.
- `test_default_build_footer_line_ignores_turn_seconds` asserts
  `build_footer_line(...) == build_footer_line(..., turn_seconds=125.0)`
  under default fields.

Adding `latency` to `_DEFAULT_FIELDS` fails 11 of these tests.

## Footprint

No new config surface (reuses `display.runtime_footer.fields`), no new env
vars, no new core tool, no new model-facing schema. One new module-private
helper (`_format_latency`), one new keyword argument threaded through the two
existing footer functions, and 3 lines in `gateway/run.py`.

`turn_seconds` defaults to `None` and the field is skipped when it is `None`
or negative, so any call site that does not measure timing keeps working
unchanged.

## Tests

`tests/gateway/test_runtime_footer.py` (+185): `_format_latency` boundary
table (sub-second, rounding at 59.4/59.6, the `m{:02d}s` zero-pad, 60m), the
render/skip/opt-in matrix, field-order placement, `build_footer_line`
threading, and the byte-stability block above.

RED-proved by mutation — each of these breaks tests:
- `latency` added to `_DEFAULT_FIELDS` → 11 failures
- dropping the `turn_seconds is not None and >= 0` guard → 2 failures
- `{sec:02d}` → `{sec}` → 6 failures
- `build_footer_line` not threading `turn_seconds` → 1 failure

51 passed in `tests/gateway/test_runtime_footer.py`; 54 passed across the
footer blast radius. `ruff check` clean.
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery area/config Config system, migrations, profiles area/usage-cost Token accounting, usage reporting, billing, cost tracking sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Jul 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: #19922 is the runtime-footer usage-metrics umbrella, while #18188 adds complementary provider/account/quota metadata. This PR adds a separate opt-in latency field with unchanged defaults.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused, opt-in implementation. Current main has no latency renderer in gateway/runtime_footer.py:105-118, and its real gateway call site invokes build_footer_line without elapsed-turn data at gateway/run.py:16879-16886. The PR's time.monotonic() measurement and explicit latency field in 8bbad05827de0889fae36ab1d9cd396d4bf37db8 fit the existing display.runtime_footer.fields mechanism while leaving _DEFAULT_FIELDS unchanged.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users type/perf Performance improvement or optimization labels Jul 30, 2026
@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Thanks @Kyzcreig — reviewed and salvaged as-is in #77611 (your commits, authorship preserved; only positional conflict resolution against current main, zero content changes). The opt-in design (latency NOT in the default fields) matches the footer byte-stability doctrine exactly, and the measurement placement (wrapping only the _run_agent await) is correct. Closing in favor of the salvage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles area/usage-cost Token accounting, usage reporting, billing, cost tracking comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/feature New feature or request type/perf Performance improvement or optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants