diff --git a/src/commands/chat/state/state.ts b/src/commands/chat/state/state.ts index ce7a7a9..fe5b820 100644 --- a/src/commands/chat/state/state.ts +++ b/src/commands/chat/state/state.ts @@ -24,8 +24,8 @@ export interface UserChatMessage { export interface AiChatMessage { type: 'ai'; text: string; - responseTime?: number; - usage?: ModelUsage; + responseTime: number; + usage: ModelUsage; cost?: number; data?: unknown; } diff --git a/src/commands/chat/ui/StatusBar.tsx b/src/commands/chat/ui/StatusBar.tsx index 6cde793..1f516a2 100644 --- a/src/commands/chat/ui/StatusBar.tsx +++ b/src/commands/chat/ui/StatusBar.tsx @@ -1,9 +1,9 @@ import React, { useMemo } from 'react'; import { Box, Text } from 'ink'; import type { ModelUsage } from '../../../engine/inference.js'; -import { formatCost, formatTokenCount } from '../../../format.js'; +import { formatCost, formatSpeed, formatTokenCount } from '../../../format.js'; import { calculateUsageCost } from '../../../engine/session.js'; -import { useChatState } from '../state/state.js'; +import { useChatState, type ChatMessage } from '../state/state.js'; export function StatusBar() { const verbose = useChatState((state) => state.verbose); @@ -11,17 +11,8 @@ export function StatusBar() { const provider = useChatState((state) => state.provider); const providerConfig = useChatState((state) => state.providerConfig); - const totalUsage = useMemo(() => { - const usage: ModelUsage = { inputTokens: 0, outputTokens: 0, requests: 0 }; - items.forEach((item) => { - if (item.type === 'ai') { - usage.inputTokens += item.usage?.inputTokens ?? 0; - usage.outputTokens += item.usage?.outputTokens ?? 0; - usage.requests += item.usage?.requests ?? 0; - } - }); - return usage; - }, [items]); + const totalUsage = useMemo(() => calculateTotalUsage(items), [items]); + const totalTime = useMemo(() => calculateTotalResponseTime(items), [items]); const modelPricing = provider.pricing[providerConfig.model]; const totalCost = calculateUsageCost(totalUsage, modelPricing) ?? 0; @@ -30,15 +21,37 @@ export function StatusBar() { LLM: {provider.label}/{providerConfig.model} - Total Cost:{' '} - {formatStats(totalCost, verbose ? totalUsage : undefined)} + {verbose ? formatVerboseStats(totalCost, totalUsage, totalTime) : formatCost(totalCost)} ); } -const formatStats = (cost: number, usage?: ModelUsage) => { +function formatVerboseStats(cost: number, usage: ModelUsage, time: number) { const usageOutput = usage - ? ` (tokens: ${formatTokenCount(usage.inputTokens)} in + ${formatTokenCount(usage.outputTokens)} out, requests: ${usage.requests})` + ? ` (tokens: ${formatTokenCount(usage.inputTokens)} in + ${formatTokenCount(usage.outputTokens)} out, requests: ${usage.requests}, speed: ${formatSpeed(usage.outputTokens, time)})` : ''; return `${formatCost(cost)}${usageOutput}`; -}; +} + +function calculateTotalUsage(messages: ChatMessage[]) { + const usage: ModelUsage = { inputTokens: 0, outputTokens: 0, requests: 0 }; + messages.forEach((message) => { + if (message.type === 'ai') { + usage.inputTokens += message.usage?.inputTokens ?? 0; + usage.outputTokens += message.usage?.outputTokens ?? 0; + usage.requests += message.usage?.requests ?? 0; + } + }); + return usage; +} + +function calculateTotalResponseTime(messages: ChatMessage[]) { + let total = 0; + messages.forEach((message) => { + if (message.type === 'ai') { + total += message.responseTime ?? 0; + } + }); + return total; +} diff --git a/src/commands/chat/ui/list/AiChatMessageItem.tsx b/src/commands/chat/ui/list/AiChatMessageItem.tsx index f2fe7c8..583b20f 100644 --- a/src/commands/chat/ui/list/AiChatMessageItem.tsx +++ b/src/commands/chat/ui/list/AiChatMessageItem.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Text } from 'ink'; -import { formatTime } from '../../../../format.js'; +import { formatSpeed, formatTime } from '../../../../format.js'; import { colors } from '../../../../theme/colors.js'; import { useChatState, type AiChatMessage } from '../../state/state.js'; import { texts } from '../../texts.js'; @@ -19,7 +19,11 @@ export function AiChatMessageItem({ message }: AiChatMessageItemProps) { {message.text} {verbose && message.responseTime != null ? ( - ({formatTime(message.responseTime)}) + + {' '} + ({formatTime(message.responseTime)},{' '} + {formatSpeed(message.usage?.outputTokens, message.responseTime)}) + ) : null} {verbose ? {JSON.stringify(message.data, null, 2)} : null} diff --git a/src/format.ts b/src/format.ts index f6ba3f2..e648e8f 100644 --- a/src/format.ts +++ b/src/format.ts @@ -48,10 +48,14 @@ export function formatSessionCost(cost: SessionCost | undefined) { return `costs: ${formatCost(cost.current)} (total: ${formatCost(cost.total)})`; } -export function formatTime(timeInMs?: number) { - if (timeInMs == null) { - return ''; +export function formatTime(timeInMs: number) { + return `${(timeInMs / 1000).toFixed(1)} s`; +} + +export function formatSpeed(tokens: number, timeInMs: number) { + if (tokens == null || timeInMs == null || timeInMs === 0) { + return '? tok/s'; } - return `${(timeInMs / 1000).toFixed(1)} s`; + return `${((tokens * 1000) / timeInMs).toFixed(1)} tok/s`; }