-
Notifications
You must be signed in to change notification settings - Fork 82
feat(tui): show output token throughput #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": minor | ||
| --- | ||
|
|
||
| Show recent model output speed in the footer after each completed response. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -235,6 +235,7 @@ import { installTerminalFocusTracking } from './utils/terminal-focus'; | |
| import { notifyTerminalOnce } from './utils/terminal-notification'; | ||
| import { createTerminalState, type TerminalState } from './utils/terminal-state'; | ||
| import { installTerminalThemeTracking } from './utils/terminal-theme'; | ||
| import { outputTokensPerSecond } from './utils/token-throughput'; | ||
| import { detectTmuxKeyboardWarning } from './utils/tmux-keyboard'; | ||
| import { nextTranscriptId } from './utils/transcript-id'; | ||
|
|
||
|
|
@@ -380,6 +381,8 @@ export interface TUIState { | |
| externalEditorRunning: boolean; | ||
| currentTurnId: string | undefined; | ||
| currentStep: number; | ||
| currentStepFirstDeltaAtMs: number | undefined; | ||
| currentStepLastDeltaAtMs: number | undefined; | ||
| assistantDraft: string; | ||
| assistantStreamActive: boolean; | ||
| thinkingDraft: string; | ||
|
|
@@ -405,6 +408,7 @@ function createInitialAppState(input: KimiTUIStartupInput): AppState { | |
| contextUsage: 0, | ||
| contextTokens: 0, | ||
| maxContextTokens: 0, | ||
| outputTokensPerSecond: null, | ||
| isStreaming: false, | ||
| isCompacting: false, | ||
| isReplaying: false, | ||
|
|
@@ -491,6 +495,8 @@ export function createTUIState(options: KimiTUIOptions): TUIState { | |
| externalEditorRunning: false, | ||
| currentTurnId: undefined, | ||
| currentStep: 0, | ||
| currentStepFirstDeltaAtMs: undefined, | ||
| currentStepLastDeltaAtMs: undefined, | ||
| assistantDraft: '', | ||
| assistantStreamActive: false, | ||
| thinkingDraft: '', | ||
|
|
@@ -947,6 +953,7 @@ export class KimiTUI { | |
| contextTokens: 0, | ||
| maxContextTokens: 0, | ||
| contextUsage: 0, | ||
| outputTokensPerSecond: null, | ||
| sessionTitle: null, | ||
| }); | ||
| this.state.startupNotice = combineStartupNotice( | ||
|
|
@@ -994,6 +1001,7 @@ export class KimiTUI { | |
| sessionId: '', | ||
| model: '', | ||
| sessionTitle: null, | ||
| outputTokensPerSecond: null, | ||
| }); | ||
| await this.refreshSkillCommands(); | ||
| } | ||
|
|
@@ -1037,6 +1045,7 @@ export class KimiTUI { | |
| maxContextTokens: 0, | ||
| contextUsage: 0, | ||
| contextTokens: 0, | ||
| outputTokensPerSecond: null, | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -1545,6 +1554,7 @@ export class KimiTUI { | |
| // Resets request-scoped state before submitting work to the active session. | ||
| private beginSessionRequest(): void { | ||
| this.state.currentTurnId = undefined; | ||
| this.resetCurrentStepModelTiming(); | ||
| this.resetLiveTextRuntime(); | ||
| this.resetLiveToolUiState(); | ||
| this.resetToolCallState(); | ||
|
|
@@ -1558,6 +1568,7 @@ export class KimiTUI { | |
| isStreaming: true, | ||
| streamingPhase: 'waiting', | ||
| streamingStartTime: Date.now(), | ||
| outputTokensPerSecond: null, | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -1938,6 +1949,7 @@ export class KimiTUI { | |
| contextTokens: status.contextTokens, | ||
| maxContextTokens: status.maxContextTokens, | ||
| contextUsage: status.contextUsage, | ||
| outputTokensPerSecond: null, | ||
| sessionTitle: session.summary?.title ?? null, | ||
| }); | ||
| } | ||
|
|
@@ -2033,6 +2045,7 @@ export class KimiTUI { | |
| this.setTodoList([]); | ||
| this.state.currentTurnId = undefined; | ||
| this.state.currentStep = 0; | ||
| this.resetCurrentStepModelTiming(); | ||
| this.resetLiveTextRuntime(); | ||
| this.updateQueueDisplay(); | ||
| } | ||
|
|
@@ -2355,6 +2368,7 @@ export class KimiTUI { | |
| void _event; | ||
| this.resetLiveToolUiState(); | ||
| this.state.currentStep = 0; | ||
| this.resetCurrentStepModelTiming(); | ||
| this.patchLivePane({ | ||
| mode: 'waiting', | ||
| pendingApproval: null, | ||
|
|
@@ -2364,6 +2378,7 @@ export class KimiTUI { | |
| isStreaming: true, | ||
| streamingPhase: 'waiting', | ||
| streamingStartTime: Date.now(), | ||
| outputTokensPerSecond: null, | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -2383,6 +2398,7 @@ export class KimiTUI { | |
| private handleStepBegin(event: TurnStepStartedEvent): void { | ||
| this.flushStreamingUiUpdatesNow(); | ||
| this.state.currentStep = event.step; | ||
| this.resetCurrentStepModelTiming(); | ||
| this.resetLiveToolUiState(); | ||
| this.finalizeLiveTextBuffers('waiting'); | ||
| this.patchLivePane({ | ||
|
|
@@ -2393,6 +2409,7 @@ export class KimiTUI { | |
| this.setAppState({ | ||
| streamingPhase: 'waiting', | ||
| streamingStartTime: Date.now(), | ||
| outputTokensPerSecond: null, | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -2406,6 +2423,7 @@ export class KimiTUI { | |
| // notice pointing at the config knob. | ||
| private handleStepCompleted(event: TurnStepCompletedEvent): void { | ||
| this.flushStreamingUiUpdatesNow(); | ||
| this.updateOutputTokenThroughput(event.usage); | ||
| if (event.finishReason !== 'max_tokens') return; | ||
|
|
||
| // Scope the truncation marking to tool calls that belong to the | ||
|
|
@@ -2443,6 +2461,30 @@ export class KimiTUI { | |
| this.showNotice(title, detail); | ||
| } | ||
|
|
||
| private updateOutputTokenThroughput(usage: TurnStepCompletedEvent['usage']): void { | ||
| const startedAtMs = this.state.currentStepFirstDeltaAtMs; | ||
| const endedAtMs = this.state.currentStepLastDeltaAtMs; | ||
| if (startedAtMs === undefined || endedAtMs === undefined) return; | ||
| const tokensPerSecond = outputTokensPerSecond( | ||
| usage, | ||
| startedAtMs, | ||
| endedAtMs, | ||
|
Comment on lines
+2468
to
+2471
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| ); | ||
| if (tokensPerSecond === null) return; | ||
| this.setAppState({ outputTokensPerSecond: tokensPerSecond }); | ||
| } | ||
|
|
||
| private resetCurrentStepModelTiming(): void { | ||
| this.state.currentStepFirstDeltaAtMs = undefined; | ||
| this.state.currentStepLastDeltaAtMs = undefined; | ||
| } | ||
|
|
||
| private recordCurrentStepModelDelta(): void { | ||
| const now = Date.now(); | ||
| this.state.currentStepFirstDeltaAtMs ??= now; | ||
| this.state.currentStepLastDeltaAtMs = now; | ||
| } | ||
|
|
||
| private isAnthropicSessionActive(): boolean { | ||
| const providerKey = this.state.appState.availableModels[this.state.appState.model]?.provider; | ||
| if (providerKey === undefined) return false; | ||
|
|
@@ -2469,6 +2511,9 @@ export class KimiTUI { | |
|
|
||
| // Appends a thinking delta to the live thinking block. | ||
| private handleThinkingDelta(event: ThinkingDeltaEvent): void { | ||
| if (event.delta.length > 0) { | ||
| this.recordCurrentStepModelDelta(); | ||
| } | ||
| this.state.thinkingDraft += event.delta; | ||
| this.pendingThinkingFlush = true; | ||
| this.patchLivePane({ mode: 'idle' }); | ||
|
|
@@ -2480,6 +2525,9 @@ export class KimiTUI { | |
|
|
||
| // Appends an assistant text delta to the live assistant block. | ||
| private handleAssistantDelta(event: AssistantDeltaEvent): void { | ||
| if (event.delta.length > 0) { | ||
| this.recordCurrentStepModelDelta(); | ||
| } | ||
| if (this.state.thinkingDraft.length > 0) { | ||
| this.flushThinkingToTranscript('idle'); | ||
| } | ||
|
|
@@ -2558,6 +2606,9 @@ export class KimiTUI { | |
| // Accumulates streaming tool-call arguments and updates the rendered call. | ||
| private handleToolCallDelta(event: ToolCallDeltaEvent): void { | ||
| if (event.toolCallId.length === 0) return; | ||
| if ((event.argumentsPart?.length ?? 0) > 0 || (event.name?.length ?? 0) > 0) { | ||
| this.recordCurrentStepModelDelta(); | ||
| } | ||
| const id = event.toolCallId; | ||
| const existing = this.state.streamingToolCallArguments.get(id); | ||
| const argumentsText = appendStreamingArgsPreview( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import type { TokenUsage } from '@moonshot-ai/kimi-code-sdk'; | ||
|
|
||
| export function outputTokensPerSecond( | ||
| usage: TokenUsage | undefined, | ||
| startedAtMs: number, | ||
| endedAtMs: number, | ||
| ): number | null { | ||
| const outputTokens = usage?.output; | ||
| if (typeof outputTokens !== 'number' || !Number.isFinite(outputTokens) || outputTokens <= 0) { | ||
| return null; | ||
| } | ||
| if (!Number.isFinite(startedAtMs) || !Number.isFinite(endedAtMs)) return null; | ||
|
|
||
| const elapsedMs = endedAtMs - startedAtMs; | ||
| if (elapsedMs <= 0) return null; | ||
|
|
||
| return outputTokens / (elapsedMs / 1000); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This throughput path uses
currentStepLastDeltaAtMsas the end timestamp, so when a step emits exactly one model delta (a common pattern in this repo’s agent-core event snapshots),startedAtMsandendedAtMsare identical andoutputTokensPerSecondreturnsnull. In that case the new footer speed metric silently disappears even thoughturn.step.completed.usage.outputis positive, so short or single-chunk responses never show a speed value.Useful? React with 👍 / 👎.