Skip to content

tui: tok/s excludes reported reasoning tokens in V2 #48705

Closed

Description

@dukebw

Description

OpenCode V2's tok/s indicator excludes reported reasoning tokens from the numerator. This understates total generated-token throughput for reasoning-heavy responses.

The footer sums assistant.tokens.output, which is populated from Usage.visibleOutputTokens. That value is the provider's inclusive output count minus its reported reasoning count; reasoning is stored separately in assistant.tokens.reasoning.

The indicator should count both non-reasoning output and reasoning tokens, using a duration that covers the generation of both. For providers such as OpenAI, the raw API output count already includes reasoning, so the calculation must use OpenCode's normalized counters without double-counting the provider total.

Plugins

No plugins are required for the code-level reproduction below.

OpenCode version

V2, upstream source at 2df00955cb933e977427535d2505e50cbc689c69.

Steps to reproduce

  1. Construct a completed assistant message with tokens.output = 20, tokens.reasoning = 80, and a 10-second interval between time.created and time.streamed.
  2. Pass it to turnTokensPerSecond in packages/tui/src/routes/session/rows.ts, with no other assistant messages in the current prompt's history.
  3. The calculation returns 2 tok/s: 20 / 10. Counting all 100 generated tokens over the same interval would give 10 tok/s.

This isolates the missing reasoning count from provider-specific timing behavior. In normal use, it affects the footer whenever a provider reports reasoning tokens separately and the tok/s indicator is enabled.

Screenshot and/or share link

Calculation:

export function turnTokensPerSecond(
message: SessionMessageAssistant,
messages: SessionMessageInfo[],
position?: number,
) {
const index = position ?? messages.findIndex((item) => item.id === message.id)
const end = index === -1 ? messages.length : index + 1
const start = inputIndex(messages, end)
const steps = messages
.slice(start + 1, end)
.filter((item): item is SessionMessageAssistant => item.type === "assistant")
const durations = steps.flatMap((step) =>
step.time.streamed === undefined ? [] : [Math.max(0, step.time.streamed - step.time.created)],
)
if (steps.length === 0 || durations.length !== steps.length) return
const output = steps.reduce((total, step) => total + (step.tokens?.output ?? 0), 0)
const duration = durations.reduce((total, value) => total + value, 0)
if (output <= 0 || duration <= 0) return
return output / (duration / 1_000)
}

Session usage normalization:

export const tokens = (usage: Usage | undefined): TokenUsage.Info => ({
input: safe(usage?.nonCachedInputTokens),
output: safe(usage?.visibleOutputTokens),
reasoning: safe(usage?.reasoningTokens),
cache: {
read: safe(usage?.cacheReadInputTokens),
write: safe(usage?.cacheWriteInputTokens),
},
})

Operating System

macOS (Darwin 25.6.0, arm64); the calculation is platform-independent.

Terminal

iTerm2

Activity

  1. Hona commented on Sep 22, 2026

    @Hona
    Member

    Fixed by #48689 (fix(tui): include reasoning tokens in throughput), merged to v2 as b278ef6 on 2026-09-18. Closing manually because v2 is not the default branch, so the PR did not auto-close this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions