fix(chat): keep context meter aligned after model swaps (#1103)
This commit is contained in:
1 parent
4246ae196d
commit
0a2c782139
8 files changed
+720
-127
No files matched your search
@@ -1,5 +1,11 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import com.m57.hermescontrol.data.ws.toAny
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
|
||||
/**
|
||||
* Live context-window occupancy from the `session.context_breakdown` WS RPC —
|
||||
* the same RPC the Hermes desktop app's status-bar meter uses.
|
||||
@@ -30,20 +36,47 @@ data class ContextBreakdownResponse(
|
||||
* Parse the decoded JSON-RPC result of `session.context_breakdown`.
|
||||
*
|
||||
* The WS event parser decodes JSON numbers as [Double] (`JsonRpcModels.toAny`),
|
||||
* so every count is read through [Number]. Returns null when the payload is
|
||||
* so every count is read through [Number]. Also supports raw [JsonObject] from
|
||||
* [HermesWsClient.request]. Returns null when the payload is
|
||||
* not a map (error/malformed response) — callers treat that as unknown and
|
||||
* keep the last known meter values.
|
||||
*/
|
||||
fun parseContextBreakdown(result: Any?): ContextBreakdownResponse? {
|
||||
val map = result as? Map<*, *> ?: return null
|
||||
val rawMap =
|
||||
when (result) {
|
||||
is JsonElement -> result.toAny() as? Map<*, *>
|
||||
is Map<*, *> -> result
|
||||
else -> null
|
||||
} ?: return null
|
||||
|
||||
fun num(key: String): Long? = (map[key] as? Number)?.toLong()
|
||||
fun num(key: String): Long? =
|
||||
when (val value = rawMap[key]) {
|
||||
is Number -> value.toLong()
|
||||
is JsonPrimitive -> value.longOrNull ?: value.intOrNull?.toLong()
|
||||
is String -> value.toLongOrNull()
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun intNum(key: String): Int? =
|
||||
when (val value = rawMap[key]) {
|
||||
is Number -> value.toInt()
|
||||
is JsonPrimitive -> value.intOrNull
|
||||
is String -> value.toIntOrNull()
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun str(key: String): String? =
|
||||
when (val value = rawMap[key]) {
|
||||
is String -> value
|
||||
is JsonPrimitive -> value.content
|
||||
else -> value?.toString()
|
||||
}
|
||||
|
||||
return ContextBreakdownResponse(
|
||||
contextUsed = num("context_used"),
|
||||
contextMax = num("context_max"),
|
||||
contextPercent = (map["context_percent"] as? Number)?.toInt(),
|
||||
contextPercent = intNum("context_percent"),
|
||||
estimatedTotal = num("estimated_total"),
|
||||
model = map["model"] as? String,
|
||||
model = str("model"),
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import com.m57.hermescontrol.data.ws.toAny
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
|
||||
/**
|
||||
* Minimal view of the `session.usage` WS RPC result or `session.usage` push event —
|
||||
* only the fields the context widget consumes today. The backend sends many more keys
|
||||
@@ -29,17 +36,47 @@ data class UsageSnapshotResponse(
|
||||
*
|
||||
* Handles both direct usage dict (from RPC response) and nested `{ "usage": { ... } }`
|
||||
* (from push event payload). The WS event parser decodes JSON numbers as [Double]
|
||||
* (`JsonRpcModels.toAny`), so every count is read through [Number].
|
||||
* (`JsonRpcModels.toAny`), so every count is read through [Number]. Also supports raw [JsonObject].
|
||||
* Returns null when the payload is not a map (error/malformed response) — callers keep the last known value.
|
||||
*/
|
||||
fun parseUsageSnapshot(result: Any?): UsageSnapshotResponse? {
|
||||
val map = result as? Map<*, *> ?: return null
|
||||
val usageMap = (map["usage"] as? Map<*, *>) ?: map
|
||||
val rawMap =
|
||||
when (result) {
|
||||
is JsonElement -> result.toAny() as? Map<*, *>
|
||||
is Map<*, *> -> result
|
||||
else -> null
|
||||
} ?: return null
|
||||
val usageMap = (rawMap["usage"] as? Map<*, *>) ?: rawMap
|
||||
|
||||
fun num(key: String): Long? =
|
||||
when (val value = usageMap[key]) {
|
||||
is Number -> value.toLong()
|
||||
is JsonPrimitive -> value.longOrNull ?: value.intOrNull?.toLong()
|
||||
is String -> value.toLongOrNull()
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun intNum(key: String): Int? =
|
||||
when (val value = usageMap[key]) {
|
||||
is Number -> value.toInt()
|
||||
is JsonPrimitive -> value.intOrNull
|
||||
is String -> value.toIntOrNull()
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun doubleNum(key: String): Double? =
|
||||
when (val value = usageMap[key]) {
|
||||
is Number -> value.toDouble()
|
||||
is JsonPrimitive -> value.doubleOrNull ?: value.longOrNull?.toDouble()
|
||||
is String -> value.toDoubleOrNull()
|
||||
else -> null
|
||||
}
|
||||
|
||||
return UsageSnapshotResponse(
|
||||
compressions = (usageMap["compressions"] as? Number)?.toInt(),
|
||||
contextUsed = (usageMap["context_used"] as? Number)?.toLong(),
|
||||
contextMax = (usageMap["context_max"] as? Number)?.toLong(),
|
||||
totalTokens = (usageMap["total"] as? Number)?.toLong(),
|
||||
avgTps = (usageMap["avg_tps"] as? Number)?.toDouble(),
|
||||
compressions = intNum("compressions"),
|
||||
contextUsed = num("context_used"),
|
||||
contextMax = num("context_max"),
|
||||
totalTokens = num("total"),
|
||||
avgTps = doubleNum("avg_tps"),
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user