Skip to content

Commit 1fe7f43

Browse files
committed
Show tokens/sec alongside token count and cost per message
Wall-clock the stream from send to each usage snapshot and derive a tok/s figure in formatUsage(), since providers don't report generation speed themselves.
1 parent 01c5cca commit 1fe7f43

2 files changed

Lines changed: 18 additions & 2 deletions

File tree

‎frontend/src/pages/Chat.tsx‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,14 @@ const MessageBubble = memo(function MessageBubble({
335335
function formatUsage(usage: UsageInfo, provider: ProviderId | undefined, modelId: string | undefined): string {
336336
const total = (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0);
337337
const tokenLabel = `${total.toLocaleString()} tokens`;
338-
if (provider === "ollama" || provider === "llamacpp") return `${tokenLabel} · local`;
338+
const speed =
339+
usage.completionTokens && usage.elapsedMs && usage.elapsedMs > 0
340+
? `${(usage.completionTokens / (usage.elapsedMs / 1000)).toFixed(1)} tok/s`
341+
: null;
342+
const withSpeed = (label: string) => (speed ? `${label} · ${speed}` : label);
343+
if (provider === "ollama" || provider === "llamacpp") return withSpeed(`${tokenLabel} · local`);
339344
const cost = modelId ? estimateCost(modelId, usage.promptTokens, usage.completionTokens) : null;
340-
return cost !== null ? `${tokenLabel} · ~${formatCost(cost)}` : tokenLabel;
345+
return withSpeed(cost !== null ? `${tokenLabel} · ~${formatCost(cost)}` : tokenLabel);
341346
}
342347

343348
export default function Chat() {
@@ -775,6 +780,11 @@ export default function Chat() {
775780
) {
776781
const parsed = parseModelRef(model);
777782
if (!parsed || !sessionId) return;
783+
// Timing an in-flight network stream, not computing render output —
784+
// the react-hooks purity rule can't tell this closure only runs once
785+
// per user-initiated send, not during render.
786+
// eslint-disable-next-line react-hooks/purity
787+
const streamStartedAt = Date.now();
778788

779789
setMessages([...baseMessages, { role: "assistant", content: "" }]);
780790
setIsStreaming(true);
@@ -803,6 +813,7 @@ export default function Chat() {
803813
? {
804814
promptTokens: chunk.usage.promptTokens ?? last.usage?.promptTokens,
805815
completionTokens: chunk.usage.completionTokens ?? last.usage?.completionTokens,
816+
elapsedMs: Date.now() - streamStartedAt,
806817
}
807818
: last.usage,
808819
toolCalls: chunk.toolCalls ? [...(last.toolCalls ?? []), ...chunk.toolCalls] : last.toolCalls,

‎frontend/src/types/electron.d.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ export interface OllamaModel {
77
export interface UsageInfo {
88
promptTokens?: number;
99
completionTokens?: number;
10+
// Wall-clock time from request start to this usage snapshot, measured
11+
// client-side (providers don't report generation speed themselves) — used
12+
// to derive a tokens/sec figure. Not persisted meaning across app
13+
// restarts in any special way; it's just another number on the message.
14+
elapsedMs?: number;
1015
}
1116

1217
export interface MessageImage {

0 commit comments

Comments
 (0)