Add used/full context meter to chat screen (#717)

* Adopt /api/health REST route (issue #713)

Cheap liveness probe on the connection screen: GET /api/health surfaces
backend version + auth mode (Token vs Gated/OAuth) without the heavier
/api/status payload. Added HermesApiService.getHealth + HealthStatus model;
ConnectViewModel.loadHealth probes on open (best-effort, silent on failure);
ConnectScreen shows a read-only health card when reachable.

Verified: ktlint clean, assembleDebug BUILD SUCCESSFUL locally.

* Add used/full context meter to chat screen

Shows session prompt tokens vs model context window above the composer,
polled every 5s. Color-coded bar warns at 70%/90%.

* fix(chat): fix unit tests for context usage meter

Format >=100k tokens cleanly in ContextUsageChip and stub AuthManager profile ID in ChatViewModelTest.

* Fix context meter: source used tokens from input_tokens

Gateway /api/sessions/{id} has no last_prompt_tokens column; use
input_tokens instead. Show meter as soon as full context is known.

* Add tappable context breakdown sheet

Chip opens a bottom sheet with input/output/cache/read/write/reasoning
token rows + message count. Source breakdown from /api/sessions/{id}.

* Refresh context meter on streaming complete

Trigger fetchContextUsage immediately when a message finishes streaming
instead of waiting for the 5s session-sync poll.

* Fix context meter: source used tokens from live WS session.info

The REST /api/sessions/{id} endpoint reads the DB input_tokens column,
which is stale/0 for active sessions — that's why the meter was stuck at 0.

Switch the meter to the live values the gateway pushes over the WS
session.info payload after each turn:
- used = last_prompt_tokens (real used context for the turn)
- full = context_length (model-accurate window; keep /api/model/info as a
  fallback before the first turn completes)

fetchContextUsage now only tops up the denominator from /api/model/info and
no longer overwrites the live WS value with the stale REST input_tokens.
The 5s poll remains as a fallback.

* Revert "Fix context meter: source used tokens from live WS session.info"

This reverts commit 932393fd80993d57e66fe93552a949460d646674.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-26 01:52:22 +03:00
1 parent c181d1d593
commit 0180fb12c2
15 files changed
+641 -2

No files matched your search

@@ -0,0 +1,17 @@
package com.m57.hermescontrol.data.model
import kotlinx.serialization.Serializable
/**
* Backend liveness probe, mirrored from `GET /api/health` (hermes_cli/web_server.py).
*
* A lightweight alternative to `/api/status` — no gateway PID / remote-health
* probe, no profile scope. `authRequired` tells the client which auth scheme the
* gateway expects (gated/OAuth vs loopback token) before attempting a real call.
*/
@Serializable
data class HealthStatus(
val ok: Boolean = false,
val version: String? = null,
val authRequired: Boolean? = null,
)
@@ -0,0 +1,30 @@
package com.m57.hermescontrol.data.model
import kotlinx.serialization.Serializable
/**
* Response from `GET /api/model/info` (PUBLIC, no auth required).
*
* VERIFIED against the live gateway (:9119, 2026-07-25): the payload carries
* `model`, `provider`, `auto_context_length`, `config_context_length`, and
* `effective_context_length`. The latter is the authoritative full context
* window for the active model — what the chat screen surfaces as "full context"
* in the used/full context meter. `context_length` (raw, per-session) is not
* part of this endpoint; per-session *used* context comes from
* [SessionDetailResponse.last_prompt_tokens] instead.
*
* Decoded with kotlinx `Json { ignoreUnknownKeys = true }` (see
* [com.m57.hermescontrol.data.remote.OkHttpProvider.json]), so unknown backend
* fields are tolerated.
*/
@Serializable
data class ModelInfoResponse(
val model: String? = null,
val provider: String? = null,
/** Context window the runtime resolved from the provider catalog. */
val auto_context_length: Long? = null,
/** User-configured override (0 when unset). */
val config_context_length: Long? = null,
/** Effective context window actually in use (max of auto/config + caps). */
val effective_context_length: Long? = null,
)
@@ -0,0 +1,31 @@
package com.m57.hermescontrol.data.model
import kotlinx.serialization.Serializable
/**
* Response from `GET /api/sessions/{id}` (gated — rides the app's existing
* cookie auth). Only the fields the chat screen needs for the context meter
* are modelled; the backend serializes many more and [kotlinx.serialization]
* ignores the rest.
*
* VERIFIED against the live gateway SQLite store (`/files/agent-vault/hermes/
* state.db`, 2026-07-26): the `sessions` table exposes `input_tokens`,
* `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `reasoning_tokens`,
* `message_count`, etc. — but it does NOT have a `last_prompt_tokens` column
* (that field lives only on the in-memory `SessionEntry` in the gateway
* process, never persisted to the REST response). So the *used* context window
* is sourced from `input_tokens` (cumulative prompt tokens for the session),
* paired with [ModelInfoResponse.effective_context_length] as the denominator.
*/
@Serializable
data class SessionDetailResponse(
val session_id: String? = null,
val session_key: String? = null,
/** Cumulative prompt tokens for the session — the *used* context window. */
val input_tokens: Long? = null,
val output_tokens: Long? = null,
val cache_read_tokens: Long? = null,
val cache_write_tokens: Long? = null,
val reasoning_tokens: Long? = null,
val message_count: Int? = null,
)