feat(chat): display token usage and tps in message footers (#1094)
This commit is contained in:
24 files changed
+448
-3
No files matched your search
@@ -42,4 +42,8 @@ data class ChatMessageEntity(
|
||||
val isStreaming: Boolean = false,
|
||||
@ColumnInfo(name = "display_kind")
|
||||
val displayKind: String? = null,
|
||||
@ColumnInfo(name = "token_count")
|
||||
val tokenCount: Int? = null,
|
||||
@ColumnInfo(name = "tps")
|
||||
val tps: Double? = null,
|
||||
)
|
||||
@@ -35,6 +35,8 @@ fun ChatMessageEntity.toUiModel(): ChatMessage =
|
||||
else -> null
|
||||
},
|
||||
displayKind = displayKind,
|
||||
tokenCount = tokenCount,
|
||||
tps = tps,
|
||||
)
|
||||
|
||||
fun ChatMessage.toEntity(sessionId: String): ChatMessageEntity =
|
||||
@@ -50,4 +52,6 @@ fun ChatMessage.toEntity(sessionId: String): ChatMessageEntity =
|
||||
toolStatus = toolStatus?.name,
|
||||
isStreaming = isStreaming,
|
||||
displayKind = displayKind,
|
||||
tokenCount = tokenCount,
|
||||
tps = tps,
|
||||
)
|
||||
@@ -12,7 +12,7 @@ import java.io.File
|
||||
|
||||
@Database(
|
||||
entities = [ChatMessageEntity::class],
|
||||
version = 6,
|
||||
version = 7,
|
||||
exportSchema = true,
|
||||
)
|
||||
abstract class HermesDatabase : RoomDatabase() {
|
||||
@@ -67,6 +67,18 @@ abstract class HermesDatabase : RoomDatabase() {
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_6_7: Migration =
|
||||
object : Migration(6, 7) {
|
||||
override suspend fun migrate(connection: SQLiteConnection) {
|
||||
connection.execSQL(
|
||||
"ALTER TABLE `chat_messages` ADD COLUMN `token_count` INTEGER",
|
||||
)
|
||||
connection.execSQL(
|
||||
"ALTER TABLE `chat_messages` ADD COLUMN `tps` REAL",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun get(context: Context): HermesDatabase =
|
||||
instance ?: synchronized(this) {
|
||||
// SQLCipher can't open plaintext SQLite databases — if an old
|
||||
@@ -97,6 +109,7 @@ abstract class HermesDatabase : RoomDatabase() {
|
||||
MIGRATION_3_4,
|
||||
MIGRATION_4_5,
|
||||
MIGRATION_5_6,
|
||||
MIGRATION_6_7,
|
||||
).fallbackToDestructiveMigration(false)
|
||||
.build()
|
||||
.also { instance = it }
|
||||
|
||||
@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
|
||||
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
|
||||
|
||||
@Serializable
|
||||
@@ -50,10 +51,18 @@ data class SessionMessage(
|
||||
val display_kind: String? = null,
|
||||
/** Display-only metadata for the marker (e.g. delegation result counts). */
|
||||
val display_metadata: JsonElement? = null,
|
||||
/** Token count recorded by the backend. */
|
||||
val token_count: JsonElement? = null,
|
||||
) {
|
||||
val timestampText: String?
|
||||
get() = (timestamp as? JsonPrimitive)?.content
|
||||
|
||||
val tokenCount: Int?
|
||||
get() {
|
||||
val prim = token_count as? JsonPrimitive ?: return null
|
||||
return prim.intOrNull ?: prim.content.toIntOrNull()
|
||||
}
|
||||
|
||||
val timestampEpochMs: Long?
|
||||
get() {
|
||||
val prim = timestamp as? JsonPrimitive ?: return null
|
||||
|
||||
@@ -20,6 +20,8 @@ data class UsageSnapshotResponse(
|
||||
val contextMax: Long? = null,
|
||||
/** Cumulative session total tokens. */
|
||||
val totalTokens: Long? = null,
|
||||
/** Rolling tokens-per-second generation speed. */
|
||||
val avgTps: Double? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -38,5 +40,6 @@ fun parseUsageSnapshot(result: Any?): UsageSnapshotResponse? {
|
||||
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(),
|
||||
)
|
||||
}
|
||||
@@ -87,7 +87,7 @@ object EventParser {
|
||||
"message.complete" -> {
|
||||
val text = payload?.get("text") as? String ?: ""
|
||||
val reasoning = payload?.get("reasoning") as? String
|
||||
WsEvent.MessageComplete(text, sessionId, reasoning)
|
||||
WsEvent.MessageComplete(text, sessionId, reasoning, rawPayload = payload)
|
||||
}
|
||||
|
||||
"message.done" -> {
|
||||
|
||||
@@ -61,6 +61,8 @@ sealed class WsEvent {
|
||||
val reasoning: String? = null,
|
||||
/** Stored session id captured before background disconnect clears the active mapping. */
|
||||
val storedSessionId: String? = null,
|
||||
/** Full raw payload map (including usage/avg_tps) emitted with message.complete. */
|
||||
val rawPayload: Map<String, Any?>? = null,
|
||||
) : WsEvent()
|
||||
|
||||
data class MessageDone(
|
||||
|
||||
Reference in new issue
Block a user