Repository navigation
Description
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
- Construct a completed assistant message with
tokens.output = 20,tokens.reasoning = 80, and a 10-second interval betweentime.createdandtime.streamed. - Pass it to
turnTokensPerSecondinpackages/tui/src/routes/session/rows.ts, with no other assistant messages in the current prompt's history. - The calculation returns
2 tok/s:20 / 10. Counting all 100 generated tokens over the same interval would give10 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:
opencode/packages/tui/src/routes/session/rows.ts
Lines 349 to 369 in 2df0095
| 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:
opencode/packages/core/src/session/usage.ts
Lines 11 to 19 in 2df0095
| 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
Hona commentedon Sep 22, 2026
Fixed by #48689 (
fix(tui): include reasoning tokens in throughput), merged tov2as b278ef6 on 2026-09-18. Closing manually becausev2is not the default branch, so the PR did not auto-close this issue.