* 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>
76 lines
2.8 KiB
TypeScript
76 lines
2.8 KiB
TypeScript
import { isSqliteAvailable, ensureTable, getDb, jsonSet, jsonGet, jsonGetAll, jsonDelete } from '../index'
|
|
|
|
const TABLE = 'session_usage'
|
|
|
|
const SCHEMA = {
|
|
session_id: 'TEXT PRIMARY KEY',
|
|
input_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
|
output_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
|
updated_at: 'INTEGER NOT NULL',
|
|
}
|
|
|
|
export function initUsageStore(): void {
|
|
if (isSqliteAvailable()) {
|
|
ensureTable(TABLE, SCHEMA)
|
|
}
|
|
}
|
|
|
|
export function updateUsage(sessionId: string, inputTokens: number, outputTokens: number): void {
|
|
const record = { input_tokens: inputTokens, output_tokens: outputTokens, updated_at: Date.now() }
|
|
if (isSqliteAvailable()) {
|
|
const db = getDb()!
|
|
db.prepare(
|
|
`INSERT INTO ${TABLE} (session_id, input_tokens, output_tokens, updated_at)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
input_tokens = excluded.input_tokens,
|
|
output_tokens = excluded.output_tokens,
|
|
updated_at = excluded.updated_at`,
|
|
).run(sessionId, inputTokens, outputTokens, record.updated_at)
|
|
} else {
|
|
jsonSet(TABLE, sessionId, record)
|
|
}
|
|
}
|
|
|
|
export function getUsage(sessionId: string): { input_tokens: number; output_tokens: number } | undefined {
|
|
if (isSqliteAvailable()) {
|
|
return getDb()!.prepare(
|
|
`SELECT input_tokens, output_tokens FROM ${TABLE} WHERE session_id = ?`,
|
|
).get(sessionId) as { input_tokens: number; output_tokens: number } | undefined
|
|
}
|
|
const row = jsonGet(TABLE, sessionId)
|
|
if (!row) return undefined
|
|
return { input_tokens: row.input_tokens ?? 0, output_tokens: row.output_tokens ?? 0 }
|
|
}
|
|
|
|
export function getUsageBatch(
|
|
sessionIds: string[],
|
|
): Record<string, { input_tokens: number; output_tokens: number }> {
|
|
if (sessionIds.length === 0) return {}
|
|
if (isSqliteAvailable()) {
|
|
const db = getDb()!
|
|
const placeholders = sessionIds.map(() => '?').join(',')
|
|
const rows = db.prepare(
|
|
`SELECT session_id, input_tokens, output_tokens FROM ${TABLE} WHERE session_id IN (${placeholders})`,
|
|
).all(...sessionIds) as Array<{ session_id: string; input_tokens: number; output_tokens: number }>
|
|
const map: Record<string, { input_tokens: number; output_tokens: number }> = {}
|
|
for (const r of rows) map[r.session_id] = { input_tokens: r.input_tokens, output_tokens: r.output_tokens }
|
|
return map
|
|
}
|
|
const all = jsonGetAll(TABLE)
|
|
const map: Record<string, { input_tokens: number; output_tokens: number }> = {}
|
|
for (const id of sessionIds) {
|
|
const row = all[id]
|
|
if (row) map[id] = { input_tokens: row.input_tokens ?? 0, output_tokens: row.output_tokens ?? 0 }
|
|
}
|
|
return map
|
|
}
|
|
|
|
export function deleteUsage(sessionId: string): void {
|
|
if (isSqliteAvailable()) {
|
|
getDb()!.prepare(`DELETE FROM ${TABLE} WHERE session_id = ?`).run(sessionId)
|
|
} else {
|
|
jsonDelete(TABLE, sessionId)
|
|
}
|
|
}
|