Runtime footer: app-controlled model/context/cwd/latency/cost under replies
Hermes can append a text "runtime footer" (model, context %, workdir, latency, cost) to final replies, but only when display.runtime_footer is enabled in the hermes config. We want the same info but controlled by the APP, not the gateway config. So the gateway now ALWAYS sends the data as a structured `runtime` object on final assistant messages, and the app decides whether/what to show. Gateway (gateway-plugin/): - protocol.py: new runtime_footer() helper + `runtime` field on the message / message.stop frames. Keys (all optional, absent when the data is unavailable — e.g. no cost for local models): model (vendor prefix dropped), context_pct (0-100), cwd (home-relative), latency (seconds), cost (USD). - adapter.py: a post_api_request plugin hook captures the turn's model + prompt tokens + start time (platform-filtered to android so other platforms don't pollute the buffer). _build_runtime_footer() resolves the model's context window (cached, best-effort, off the event loop via asyncio.to_thread with a timeout) and computes context_pct. The runtime object is attached on every final send (streaming message.stop and non-streaming message, plus the fallback paths). - outbox.py: `runtime` preserved in history reconstruction so the footer survives a restart / first open. App (app/shared/): - Protocol.kt: RuntimeMeta data class + `runtime` on MessagePayload / MessageStopPayload / HistoryMessage. - ChatStore.kt: `runtime` on MessageItem, wired through live + history reconciliation. - SecureStore.kt (+ Android/Desktop actuals): runtimeFooterEnabled + runtimeFooterFields (persisted per device). - IrisController.kt: StateFlows + toggleRuntimeFooter() / toggleRuntimeField(); RUNTIME_FIELD_KEYS / default set / parser. - SettingsScreen.kt: "Runtime footer" switch; when on, an expandable chip menu (Model · Context % · Workdir · Latency · Cost) to pick fields. - ChatScreen.kt: footer rendered on the SAME line as the timestamp (footer left, time right, Telegram-style), only for final non-streaming assistant answers; Inspector pane now shows the runtime fields too. Docs: 04-wire-protocol.md + frames.schema.json document the `runtime` object. Verified end-to-end on device: final replies carry `qwen3.8-27B-exl3-4.5bpw · 53% · ~ · 38s` with the time right-aligned on the same line; 69/69 gateway tests pass, Kotlin builds + tests pass.
This commit is contained in:
1 parent
678c0344c8
commit
9286937e2d
13 files changed
+979
-290
No files matched your search
@@ -216,6 +216,7 @@ class Outbox:
|
||||
"reasoning": payload.get("reasoning"),
|
||||
"model": payload.get("model"),
|
||||
"tokens": payload.get("tokens"),
|
||||
"runtime": payload.get("runtime"),
|
||||
"ts": payload.get("ts"),
|
||||
"media": payload.get("media"),
|
||||
}
|
||||
@@ -230,6 +231,7 @@ class Outbox:
|
||||
"reasoning": payload.get("reasoning"),
|
||||
"model": payload.get("model"),
|
||||
"tokens": payload.get("tokens"),
|
||||
"runtime": payload.get("runtime"),
|
||||
"ts": payload.get("ts"),
|
||||
"media": None,
|
||||
}
|
||||
@@ -259,7 +261,7 @@ class Outbox:
|
||||
# Omit absent optional fields (the app's serializer treats a
|
||||
# missing key as its default, but a JSON ``null`` for a
|
||||
# non-nullable field like ``media`` would fail to parse).
|
||||
for key in ("reasoning", "model", "tokens", "ts", "media"):
|
||||
for key in ("reasoning", "model", "tokens", "runtime", "ts", "media"):
|
||||
if m.get(key) is None:
|
||||
m.pop(key, None)
|
||||
return {
|
||||
@@ -310,12 +312,10 @@ class Outbox:
|
||||
cursors.append(int(r["cursor"]))
|
||||
if not cursors:
|
||||
return 0
|
||||
placeholders = ",".join("?" * len(cursors))
|
||||
sql = f"DELETE FROM outbox WHERE cursor IN ({placeholders})"
|
||||
# Safe: ``placeholders`` is only ``?`` markers; every cursor value is
|
||||
# bound as a parameter (no user data in the SQL text).
|
||||
# pi-lens-ignore: python-sql-injection
|
||||
self._conn.execute(sql, cursors)
|
||||
# One bound-parameter delete per cursor (a message spans only a few
|
||||
# frames); same transaction, no string-built SQL.
|
||||
for cursor in cursors:
|
||||
self._conn.execute("DELETE FROM outbox WHERE cursor = ?", (cursor,))
|
||||
self._conn.commit()
|
||||
return len(cursors)
|
||||
|
||||
|
||||
Reference in new issue
Block a user