New request
#1966: [Enhancement] Add LM Studio-style token throughput + total token count display in message footer
We'll provision a sandbox, run an agent against the issue, and open a draft PR. You can pull the branch and iterate from there.
Add LM Studio-style real-time token throughput (token/s) and **total token countdisplay in the assistant message footer, showingXX.X token/s · XXXX token` format.
Motivation / Use Case
When using LLM chat interfaces (especially for cost monitoring and performance tuning), users need quick visibility into:
- Token generation speed — how fast the model is responding
- Total tokens consumed per turn — for cost tracking and context window awareness
LM Studio already provides this UX pattern and users familiar with it expect similar information density. Currently Hermes WebUI shows input/output token counts but lacks:
- Real-time generation speed (
token/s) - Aggregated total token count in a prominent position
Proposed Behavior
Display Format
┌─────────────────────────────────────────────────────┐
│ [Assistant response content...] │
├─────────────────────────────────────────────────────┤
│ 15.0k in · 115 out 50.4 token/s · 15.1k token │
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ │
│ Speed (TPS) + Total Tokens │
└─────────────────────────────────────────────────────┘
Two States
| State | Display | When |
|---|---|---|
| Streaming (in progress) | XX.X token/s only | metering events fire, _turnUsage not yet available |
| Completed (done) | XX.X token/s · XXXX token | done event fires, _turnUsage populated |
Formatting Rules
- Speed first, token count second (LM Studio convention)
- Separator:
·(middle dot, consistent with existingin · outformat) - Unit: full word
token(not abbreviatedtok) - Large numbers: auto-format with suffixes (
15000→15.0k,2500000→2.5M) - Position: inside
.msg-foot, after.msg-usage-inlineor.msg-timeelement - Style: muted color, small font (12px), semi-transparent (opacity 0.85), bold weight
Technical Approach
Data Sources (Already Available)
| Data | Source Event | Field |
|---|---|---|
input_tokens | done | _turnUsage.input_tokens |
output_tokens | done | _turnUsage.output_tokens |
total_tokens | computed | input_tokens + output_tokens |
tokens/sec | metering | calculated from token delta / time delta |
Implementation Scope
Single file modification: static/messages.js
-
New helper function
_fmtTokens(n)— ~7 lines- Formats large numbers with k/M suffixes
-
Enhanced function
_injectLiveTpsToMessage(tps)— ~15 lines modification- Reads last assistant message's
_turnUsagefromS.messagesglobal array - Computes
total_tokens = input + output - Conditionally renders speed-only or speed+tokens format
- Creates/updates
.msg-speed-inlineDOM element
- Reads last assistant message's
Total change: ~22 lines of code across 2 functions in 1 file.
No Backend Changes Required
All data is already emitted by the backend via WebSocket events (metering, done). This is purely a frontend enhancement.
Reference Implementation
A working proof-of-concept is available at:
🔗 messages.js (fork with enhancement)
Note: This is a fork for demonstration only. The relevant changes are in
_fmtTokens()and_injectLiveTpsToMessage()functions (~22 lines total, approximately lines 1199-1240).
Tested on Hermes Agent v0.13.0 (2026.5.7) with the following verified results:
- ✅ Streaming phase shows speed only
- ✅ Completion phase shows
XX.X token/s · XXXX token - ✅ Multi-turn conversations display independently per message
- ✅ No duplicate elements, no layout breakage
- ✅ Works with existing
in · outusage display - ✅ Zero backend changes needed
Key Code Snippet (Reference)
// Helper: format large numbers
function _fmtTokens(n) {
if (!n || n < 0) return '0';
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
return String(n);
}
// Enhanced injection (core logic)
function _injectLiveTpsToMessage(tps) {
const activeFoot = document.querySelector('.assistant-turn:last-child .msg-foot');
if (!activeFoot) return;
// Get usage from global S.messages array (reliable data source)
const lastAsstMsg = S.messages && Array.isArray(S.messages)
? [...S.messages].reverse().find(m => m.role === 'assistant') : null;
const usage = lastAsstMsg?._turnUsage || null;
const totalTok = (usage?.input_tokens || 0) + (usage?.output_tokens || 0);
const tpsVal = typeof tps === 'number' ? tps.toFixed(1) : '—';
// ... DOM element creation/update ...
// Render: speed + token (LM Studio style)
speedEl.textContent = totalTok > 0
? `${tpsVal} token/s · ${_fmtTokens(totalTok)} token`
: `${tpsVal} token/s`;
}
Alternatives Considered
| Option | Pros | Cons | Verdict |
|---|---|---|---|
| A: Current proposal (DOM injection in messages.js) | Minimal change, zero backend impact | Not persisted on page refresh | ✅ Selected |
| B: Modify ui.js renderMessages() | Persisted on refresh | Higher risk of breaking changes, larger diff | ❌ Rejected |
| C: Backend-side footer enhancement | Always available | Requires server code change, more complex | ❌ Overkill |
| D: Browser extension | No source change needed | Maintenance burden, distribution problem | ❌ Impractical |
Known Limitations
- Non-persistent on refresh: Speed/token info disappears on page reload (same behavior as current TPS display). Fixing this would require modifying
ui.jsrender logic. - Requires metering events: If streaming doesn't emit metering events, speed won't show (token count from
doneevent still works).
Screenshots / Mockup
During streaming:
15.0k in · 115 out 50.4 token/s 10:42 AM
↑
Speed only (usage not yet available)
After completion:
15.0k in · 115 out 50.4 token/s · 15.1k token 10:43 AM
^^^^^^^^^^^^^^^^^^^^^^^^
Speed + Total Token Count
Checklist for Implementation
- Add
_fmtTokens()helper function tomessages.js - Enhance
_injectLiveTpsToMessage()with token count logic - Test streaming state (speed-only display)
- Test completion state (speed + token display)
- Test multi-turn conversation isolation
- Verify no regression in existing usage display
- Test on Chrome, Firefox, Edge
Labels Suggested
enhancement ui good first issue webui
Note: This is a ready-to-implement feature request with a working reference implementation. The approach requires modifying only messages.js (~22 lines) with no backend changes.
---
Displaying real-time token throughput in the message footer requires frontend streaming UI changes and possibly backend metering event integration.
- static/js/chat.js
- static/js/message_footer.js
- static/css/app.css
- api/streaming.py
- api/usage.py