Bug Description
Status-bar tokens/sec is inaccurate for streaming / OpenAI-compatible (local) providers
Summary
The status-bar tokens/sec reads well below the model's real generation speed when using a
local / OpenAI-compatible provider (e.g. LM Studio). tokenSpeedLabel correctly prefers a
provider-measured rate, but that value is never available for these providers, so it falls
back to a turn-duration estimate that includes prompt submission, prefill,
time-to-first-token, and tool-call gaps — heavily diluting the decode rate.
Environment
- Hermes desktop 0.18.0 (packaged build,
sourceMode: false)
- OS: Windows 11
- Provider: custom OpenAI-compatible endpoint "Local (localhost:1234)" = LM Studio
- Model:
qwen3.6-35b-a3b-mtp (35B-A3B MoE + MTP speculative decoding)
Steps to reproduce
- Add a local OpenAI-compatible provider (LM Studio at
http://localhost:1234/v1).
- Load a model with a non-trivial context; send a turn with a few thousand tokens of prompt
and/or a tool call or two.
- Compare the status-bar tokens/sec against the model's true generation speed.
Expected vs actual
- Expected: ≈ the model's decode rate. LM Studio's server log reports
eval time … N tokens ( X tokens per second ) at 76–123 tok/s for this model.
- Actual: the status bar shows materially less, and swings with prompt size / tool usage
rather than tracking the real decode rate.
Root cause
apps/desktop/src/app/shell/statusbar-controls.tsx
Call site (~line 465):
const speed = tokenSpeedLabel(usage.output, turnStartedAt, now, usage.tokens_per_second)
tokenSpeedLabel (~line 242) prefers measured (4th arg); otherwise
output / max(1, (now - since)/1000) with since = turnStartedAt.
Two facts make the fallback fire and be wrong:
usage.tokens_per_second is never populated for OpenAI-compatible / local providers.
Verified against LM Studio /v1/chat/completions: response has usage
{prompt_tokens, completion_tokens, total_tokens} and a stats object holding only
draft-token counts (total/accepted/rejected_draft_tokens_count) — no tokens_per_second.
So it always falls through to the self-timed branch.
- The fallback anchors to
turnStartedAt (stamped at turn start in
use-message-stream/gateway-event.ts, setTurnStartedAt(Date.now())). That window includes
prompt submission + prefill + time-to-first-token + tool-call idle. There is no first-token
timestamp anywhere in the session code, so decode-only timing isn't possible today.
For a local model this is large: a ~14k-token prompt prefills at ~2000 tok/s (~7 s) before the
first output token, and that 7 s is folded into now − turnStartedAt, dragging the shown rate
far below the true ~80–120 tok/s decode speed.
Suggested fix (provider-agnostic, preferred)
Measure decode-only speed:
- In the streaming handler, record
firstTokenAt = Date.now() on the first assistant
content delta of the turn (once per turn).
- In the fallback, divide by the decode window:
const seconds = Math.max(0.001, (now - firstTokenAt) / 1000)
const speed = output / seconds
- Optional: subtract tool-call idle spans so multi-tool turns don't dilute the rate.
No change to tokenSpeedLabel's display logic — only pass firstTokenAt as since instead of
turnStartedAt.
Secondary (when a provider exposes a real rate)
Keep the existing measured-first path. For LM Studio the rate isn't in the OpenAI /v1 body
but is available via its native REST API (/api/v0/...) and server logs; wiring that is optional.
Note
tokenSpeedLabel's "prefer measured, else compute" structure is correct and should stay. The
defect is purely (a) the fallback's timing anchor and (b) the missing first-token timestamp.
Steps to Reproduce
Steps to reproduce
- Add a local OpenAI-compatible provider (LM Studio at
http://localhost:1234/v1).
- Load a model with a non-trivial context; send a turn with a few thousand tokens of prompt
and/or a tool call or two.
- Compare the status-bar tokens/sec against the model's true generation speed.
Expected Behavior
Expected vs actual
- Expected: ≈ the model's decode rate. LM Studio's server log reports
eval time … N tokens ( X tokens per second ) at 76–123 tok/s for this model.
Actual Behavior
- Actual: the status bar shows materially less, and swings with prompt size / tool usage
rather than tracking the real decode rate.
Affected Component
CLI (interactive chat), Agent Core (conversation loop, context compression, memory)
Messaging Platform (if gateway-related)
N/A (CLI only)
Debug Report
Operating System
windows 11
Python Version
3.14
Hermes Version
0.18.0
Additional Logs / Traceback (optional)
Root Cause Analysis (optional)
Root cause
apps/desktop/src/app/shell/statusbar-controls.tsx
Call site (~line 465):
const speed = tokenSpeedLabel(usage.output, turnStartedAt, now, usage.tokens_per_second)
tokenSpeedLabel (~line 242) prefers measured (4th arg); otherwise
output / max(1, (now - since)/1000) with since = turnStartedAt.
Proposed Fix (optional)
Suggested fix (provider-agnostic, preferred)
Measure decode-only speed:
- In the streaming handler, record
firstTokenAt = Date.now() on the first assistant
content delta of the turn (once per turn).
- In the fallback, divide by the decode window:
const seconds = Math.max(0.001, (now - firstTokenAt) / 1000)
const speed = output / seconds
- Optional: subtract tool-call idle spans so multi-tool turns don't dilute the rate.
Are you willing to submit a PR for this?
Bug Description
Status-bar tokens/sec is inaccurate for streaming / OpenAI-compatible (local) providers
Summary
The status-bar tokens/sec reads well below the model's real generation speed when using a
local / OpenAI-compatible provider (e.g. LM Studio).
tokenSpeedLabelcorrectly prefers aprovider-measured rate, but that value is never available for these providers, so it falls
back to a turn-duration estimate that includes prompt submission, prefill,
time-to-first-token, and tool-call gaps — heavily diluting the decode rate.
Environment
sourceMode: false)qwen3.6-35b-a3b-mtp(35B-A3B MoE + MTP speculative decoding)Steps to reproduce
http://localhost:1234/v1).and/or a tool call or two.
Expected vs actual
eval time … N tokens ( X tokens per second )at 76–123 tok/s for this model.rather than tracking the real decode rate.
Root cause
apps/desktop/src/app/shell/statusbar-controls.tsxCall site (~line 465):
const speed = tokenSpeedLabel(usage.output, turnStartedAt, now, usage.tokens_per_second)
tokenSpeedLabel(~line 242) prefersmeasured(4th arg); otherwiseoutput / max(1, (now - since)/1000)withsince = turnStartedAt.Two facts make the fallback fire and be wrong:
usage.tokens_per_secondis never populated for OpenAI-compatible / local providers.Verified against LM Studio
/v1/chat/completions: response hasusage{prompt_tokens, completion_tokens, total_tokens} and a
statsobject holding onlydraft-token counts (total/accepted/rejected_draft_tokens_count) — no
tokens_per_second.So it always falls through to the self-timed branch.
turnStartedAt(stamped at turn start inuse-message-stream/gateway-event.ts,setTurnStartedAt(Date.now())). That window includesprompt submission + prefill + time-to-first-token + tool-call idle. There is no first-token
timestamp anywhere in the session code, so decode-only timing isn't possible today.
For a local model this is large: a ~14k-token prompt prefills at ~2000 tok/s (~7 s) before the
first output token, and that 7 s is folded into
now − turnStartedAt, dragging the shown ratefar below the true ~80–120 tok/s decode speed.
Suggested fix (provider-agnostic, preferred)
Measure decode-only speed:
firstTokenAt = Date.now()on the first assistantcontent delta of the turn (once per turn).
const seconds = Math.max(0.001, (now - firstTokenAt) / 1000)
const speed = output / seconds
No change to
tokenSpeedLabel's display logic — only passfirstTokenAtassinceinstead ofturnStartedAt.Secondary (when a provider exposes a real rate)
Keep the existing
measured-first path. For LM Studio the rate isn't in the OpenAI/v1bodybut is available via its native REST API (
/api/v0/...) and server logs; wiring that is optional.Note
tokenSpeedLabel's "prefer measured, else compute" structure is correct and should stay. Thedefect is purely (a) the fallback's timing anchor and (b) the missing first-token timestamp.
Steps to Reproduce
Steps to reproduce
http://localhost:1234/v1).and/or a tool call or two.
Expected Behavior
Expected vs actual
eval time … N tokens ( X tokens per second )at 76–123 tok/s for this model.Actual Behavior
rather than tracking the real decode rate.
Affected Component
CLI (interactive chat), Agent Core (conversation loop, context compression, memory)
Messaging Platform (if gateway-related)
N/A (CLI only)
Debug Report
Operating System
windows 11
Python Version
3.14
Hermes Version
0.18.0
Additional Logs / Traceback (optional)
Root Cause Analysis (optional)
Root cause
apps/desktop/src/app/shell/statusbar-controls.tsxCall site (~line 465):
const speed = tokenSpeedLabel(usage.output, turnStartedAt, now, usage.tokens_per_second)
tokenSpeedLabel(~line 242) prefersmeasured(4th arg); otherwiseoutput / max(1, (now - since)/1000)withsince = turnStartedAt.Proposed Fix (optional)
Suggested fix (provider-agnostic, preferred)
Measure decode-only speed:
firstTokenAt = Date.now()on the first assistantcontent delta of the turn (once per turn).
const seconds = Math.max(0.001, (now - firstTokenAt) / 1000)
const speed = output / seconds
Are you willing to submit a PR for this?