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:
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,
|
||||
)
|
||||
@@ -29,6 +29,7 @@ import com.m57.hermescontrol.data.model.EnvVarDeleteRequest
|
||||
import com.m57.hermescontrol.data.model.EnvVarRevealRequest
|
||||
import com.m57.hermescontrol.data.model.EnvVarRevealResponse
|
||||
import com.m57.hermescontrol.data.model.EnvVarUpdate
|
||||
import com.m57.hermescontrol.data.model.HealthStatus
|
||||
import com.m57.hermescontrol.data.model.HookResponse
|
||||
import com.m57.hermescontrol.data.model.KanbanBoardResponse
|
||||
import com.m57.hermescontrol.data.model.KanbanBoardsResponse
|
||||
@@ -46,6 +47,7 @@ import com.m57.hermescontrol.data.model.MessagingPlatformUpdate
|
||||
import com.m57.hermescontrol.data.model.MoaConfigResponse
|
||||
import com.m57.hermescontrol.data.model.ModelAssignmentRequest
|
||||
import com.m57.hermescontrol.data.model.ModelAssignmentResponse
|
||||
import com.m57.hermescontrol.data.model.ModelInfoResponse
|
||||
import com.m57.hermescontrol.data.model.ModelOptionsResponse
|
||||
import com.m57.hermescontrol.data.model.ModelsAnalyticsResponse
|
||||
import com.m57.hermescontrol.data.model.OAuthCancelResponse
|
||||
@@ -64,6 +66,7 @@ import com.m57.hermescontrol.data.model.RawConfigResponse
|
||||
import com.m57.hermescontrol.data.model.RecentUnlock
|
||||
import com.m57.hermescontrol.data.model.SaveSkillContentRequest
|
||||
import com.m57.hermescontrol.data.model.ScanStatus
|
||||
import com.m57.hermescontrol.data.model.SessionDetailResponse
|
||||
import com.m57.hermescontrol.data.model.SessionListResponse
|
||||
import com.m57.hermescontrol.data.model.SessionMessagesResponse
|
||||
import com.m57.hermescontrol.data.model.SessionPromptResponse
|
||||
@@ -123,6 +126,9 @@ interface HermesApiService {
|
||||
@GET("api/status")
|
||||
suspend fun getStatus(): Response<StatusResponse>
|
||||
|
||||
@GET("api/health")
|
||||
suspend fun getHealth(): Response<HealthStatus>
|
||||
|
||||
@GET("api/sessions")
|
||||
suspend fun getSessions(
|
||||
@Query("limit") limit: Int = 20,
|
||||
@@ -181,6 +187,17 @@ interface HermesApiService {
|
||||
@Path("id", encoded = true) sessionId: String,
|
||||
): Response<SessionPromptResponse>
|
||||
|
||||
@GET("api/sessions/{id}")
|
||||
suspend fun getSessionDetail(
|
||||
// Preserve slashes in session IDs — backend generates IDs containing '/' characters (issue #468).
|
||||
// Contract: The server-generated sessionId must only contain URL-safe characters (no ?, #, or spaces).
|
||||
@Path("id", encoded = true) sessionId: String,
|
||||
@Query("profile") profile: String? = null,
|
||||
): Response<SessionDetailResponse>
|
||||
|
||||
@GET("api/model/info")
|
||||
suspend fun getModelInfo(): Response<ModelInfoResponse>
|
||||
|
||||
@GET("api/system/stats")
|
||||
suspend fun getSystemStats(): Response<SystemStatsResponse>
|
||||
|
||||
|
||||
Reference in new issue
Block a user