feat: add token usage tracking, context display, and dynamic context length (#132)
* fix: specify TS_NODE_PROJECT for dev:server script ts-node/register resolves tsconfig from the entry file upward, finding the root solution-style tsconfig.json (no compilerOptions). This causes target to default to ES3, breaking MapIterator spread syntax (TS2802). Set TS_NODE_PROJECT env var to point to the server tsconfig which targets ES2024. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add token usage tracking, context display, and dynamic context length - Intercept SSE proxy to capture run.completed events and persist token usage (input_tokens, output_tokens) per session to SQLite/JSON store - Display context usage bar in ChatInput showing used/total/remaining tokens - Resolve actual context length from Hermes models_dev_cache.json based on the active profile's default model (fallback 200K), with 5min in-memory cache - Move sessions-db.ts to db/hermes/ for unified database layer - Add usage store with SQLite + JSON fallback (auto-migration via ensureTable) - Fix proxy SSE path regex to match rewritten upstream path - Fix route ordering: /sessions/usage before /sessions/:id to avoid 404 - Fetch per-session usage on session enter instead of batch - Add unit tests for usage-store, db index, and proxy SSE interception Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { startRun, streamRunEvents, type ChatMessage, type RunEvent } from '@/api/hermes/chat'
|
||||
import { deleteSession as deleteSessionApi, fetchSession, fetchSessions, type HermesMessage, type SessionSummary } from '@/api/hermes/sessions'
|
||||
import { deleteSession as deleteSessionApi, fetchSession, fetchSessions, fetchSessionUsageSingle, type HermesMessage, type SessionSummary } from '@/api/hermes/sessions'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useAppStore } from './app'
|
||||
@@ -155,8 +155,6 @@ function mapHermesSession(s: SessionSummary): Session {
|
||||
model: s.model,
|
||||
provider: (s as any).billing_provider || '',
|
||||
messageCount: s.message_count,
|
||||
inputTokens: s.input_tokens,
|
||||
outputTokens: s.output_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,8 +338,6 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|| (serverUsers === localUsers && serverAssistantLen >= localAssistantLen)
|
||||
if (serverIsAhead) {
|
||||
target.messages = mapped
|
||||
target.inputTokens = detail.input_tokens
|
||||
target.outputTokens = detail.output_tokens
|
||||
if (detail.title && !target.title) target.title = detail.title
|
||||
if (sid === activeSessionId.value) persistActiveMessages()
|
||||
}
|
||||
@@ -363,8 +359,6 @@ export const useChatStore = defineStore('chat', () => {
|
||||
// our "don't retreat" guard above skipped it — the server is
|
||||
// now the authoritative source of truth.
|
||||
target.messages = mapped
|
||||
target.inputTokens = detail.input_tokens
|
||||
target.outputTokens = detail.output_tokens
|
||||
if (detail.title) target.title = detail.title
|
||||
if (sid === activeSessionId.value) persistActiveMessages()
|
||||
clearInFlight(sid)
|
||||
@@ -447,8 +441,6 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if (!target) return false
|
||||
const mapped = mapHermesMessages(detail.messages || [])
|
||||
target.messages = mapped
|
||||
target.inputTokens = detail.input_tokens
|
||||
target.outputTokens = detail.output_tokens
|
||||
if (detail.title) target.title = detail.title
|
||||
persistActiveMessages()
|
||||
return true
|
||||
@@ -531,8 +523,6 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if (serverIsAhead) {
|
||||
activeSession.value.messages = mapped
|
||||
}
|
||||
activeSession.value.inputTokens = detail.input_tokens
|
||||
activeSession.value.outputTokens = detail.output_tokens
|
||||
// Update title: use Hermes title, or fallback to first user message
|
||||
if (detail.title) {
|
||||
activeSession.value.title = detail.title
|
||||
@@ -557,6 +547,15 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if (readInFlight(sessionId) && !streamStates.value.has(sessionId)) {
|
||||
startPolling(sessionId)
|
||||
}
|
||||
|
||||
// Fetch token usage for this session from web-ui DB
|
||||
try {
|
||||
const usage = await fetchSessionUsageSingle(sessionId)
|
||||
if (usage) {
|
||||
activeSession.value.inputTokens = usage.input_tokens
|
||||
activeSession.value.outputTokens = usage.output_tokens
|
||||
}
|
||||
} catch { /* non-critical */ }
|
||||
}
|
||||
|
||||
function newChat() {
|
||||
@@ -785,9 +784,15 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if (lastMsg?.isStreaming) {
|
||||
updateMessage(sid, lastMsg.id, { isStreaming: false })
|
||||
}
|
||||
if (evt.usage) {
|
||||
const target = sessions.value.find(s => s.id === sid)
|
||||
if (target) {
|
||||
target.inputTokens = evt.usage.input_tokens
|
||||
target.outputTokens = evt.usage.output_tokens
|
||||
}
|
||||
}
|
||||
cleanup()
|
||||
updateSessionTitle(sid)
|
||||
// IMPORTANT ordering: persist the final cache BEFORE clearing
|
||||
// the in-flight marker. If the browser is reloading right now
|
||||
// and kills us between the two localStorage writes, we want
|
||||
// the next page load to still see in-flight === true (so
|
||||
|
||||
Reference in New Issue
Block a user