feat: added performance statistics info when using embedded LLM

This commit is contained in:
Xiaohan-Tian
2026-05-14 19:05:13 -07:00
parent 98c2b97413
commit eb1c7bd2fc
8 changed files with 58 additions and 8 deletions
+3 -2
View File
@@ -110,6 +110,7 @@ export class AgentCore {
let assistantTextContent = ''; let assistantTextContent = '';
const accumulatedToolCalls: ToolCall[] = []; const accumulatedToolCalls: ToolCall[] = [];
let finishReason = 'stop'; let finishReason = 'stop';
let performanceInfo: StreamChunk['performanceInfo'];
for await (const chunk of this.llmProvider.generateStream(conversationHistory, systemPrompt, tools)) { for await (const chunk of this.llmProvider.generateStream(conversationHistory, systemPrompt, tools)) {
if (chunk.type === 'text') { if (chunk.type === 'text') {
@@ -120,6 +121,7 @@ export class AgentCore {
accumulatedToolCalls.push(chunk.toolCall); accumulatedToolCalls.push(chunk.toolCall);
} else if (chunk.type === 'done') { } else if (chunk.type === 'done') {
finishReason = chunk.finishReason ?? 'stop'; finishReason = chunk.finishReason ?? 'stop';
performanceInfo = chunk.performanceInfo;
} }
} }
@@ -163,10 +165,9 @@ export class AgentCore {
// LLM finished with text response (stop reason) // LLM finished with text response (stop reason)
this.agentState.updateMessage(this.currentAssistantMessageId, assistantTextContent); this.agentState.updateMessage(this.currentAssistantMessageId, assistantTextContent);
continueLoop = false; continueLoop = false;
yield { type: 'done', content: '', finishReason, performanceInfo };
} }
} }
yield { type: 'done', content: '', finishReason: 'stop' };
} finally { } finally {
this.currentUserMessageId = null; this.currentUserMessageId = null;
this.currentAssistantMessageId = null; this.currentAssistantMessageId = null;
+9 -1
View File
@@ -296,6 +296,14 @@ export class LocalBrowserLLMProvider implements LLMProvider {
yield { type: 'tool_call', content: '', toolCall }; yield { type: 'tool_call', content: '', toolCall };
} }
yield { type: 'done', content: '', finishReason }; yield {
type: 'done',
content: '',
finishReason,
performanceInfo: {
prefillTps,
generationTps,
},
};
} }
} }
+7 -1
View File
@@ -4,10 +4,16 @@
import type { ToolCall } from '../core/AgentState'; import type { ToolCall } from '../core/AgentState';
export interface PerformanceInfo {
prefillTps?: number;
generationTps?: number;
}
export interface StreamChunk { export interface StreamChunk {
type: 'text' | 'tool_call' | 'tool_result' | 'done'; type: 'text' | 'tool_call' | 'tool_result' | 'done';
content: string; content: string;
toolCall?: ToolCall; toolCall?: ToolCall;
toolResult?: { name: string; success: boolean; result: string }; toolResult?: { name: string; success: boolean; result: string };
finishReason?: string; // 'stop' | 'tool_calls' — present on 'done' chunks performanceInfo?: PerformanceInfo;
finishReason?: string;
} }
+6
View File
@@ -226,6 +226,12 @@
font-weight: bold; font-weight: bold;
} }
.message-performance-info {
margin-top: 8px;
font-size: 10px;
color: #909090;
}
/* Abort link styling */ /* Abort link styling */
.abort-link { .abort-link {
background: none !important; background: none !important;
+1
View File
@@ -434,6 +434,7 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
key={message.id} key={message.id}
content={message.content} content={message.content}
isStreaming={message.isStreaming} isStreaming={message.isStreaming}
performanceInfo={message.performanceInfo}
onAbort={message.isStreaming ? handleAbort : undefined} onAbort={message.isStreaming ? handleAbort : undefined}
/> />
) )
+22 -1
View File
@@ -3,11 +3,13 @@ import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import type { PerformanceInfo } from '../../agent/llm/StreamingTypes';
interface AssistantMessageProps { interface AssistantMessageProps {
content: string; content: string;
isStreaming?: boolean; isStreaming?: boolean;
onAbort?: () => void; onAbort?: () => void;
performanceInfo?: PerformanceInfo;
} }
// Memoized code component to prevent SyntaxHighlighter re-renders // Memoized code component to prevent SyntaxHighlighter re-renders
@@ -30,7 +32,19 @@ const CodeComponent = memo(({ inline, className, children, ...props }: any) => {
); );
}); });
const AssistantMessage: React.FC<AssistantMessageProps> = ({ content, isStreaming, onAbort }) => { const formatTps = (value?: number): string | null => {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
return null;
}
return value.toFixed(1);
};
const AssistantMessage: React.FC<AssistantMessageProps> = ({ content, isStreaming, onAbort, performanceInfo }) => {
const prefillTps = formatTps(performanceInfo?.prefillTps);
const generationTps = formatTps(performanceInfo?.generationTps);
const hasPerformanceInfo = Boolean(prefillTps || generationTps);
const renderContent = () => { const renderContent = () => {
// Handle special abort link for streaming messages // Handle special abort link for streaming messages
if (isStreaming && onAbort && content.includes('click here to abort')) { if (isStreaming && onAbort && content.includes('click here to abort')) {
@@ -83,6 +97,13 @@ const AssistantMessage: React.FC<AssistantMessageProps> = ({ content, isStreamin
<div className="message-container message-assistant"> <div className="message-container message-assistant">
<div className="message-content"> <div className="message-content">
{renderContent()} {renderContent()}
{hasPerformanceInfo && (
<div className="message-performance-info">
{prefillTps ? `Prefill: ${prefillTps} t/s` : 'Prefill: -'}
{' · '}
{generationTps ? `Generation: ${generationTps} t/s` : 'Generation: -'}
</div>
)}
</div> </div>
</div> </div>
); );
+7 -2
View File
@@ -38,6 +38,7 @@ export const useStreamProcessor = (options: StreamProcessorOptions): StreamProce
let assistantResponse = ''; let assistantResponse = '';
let tokenCount = 0; let tokenCount = 0;
let hasTextContent = false; let hasTextContent = false;
let performanceInfo: ChatMessage['performanceInfo'];
console.log(`------------ ${logPrefix} ------------`); console.log(`------------ ${logPrefix} ------------`);
console.log(input); console.log(input);
@@ -65,7 +66,8 @@ export const useStreamProcessor = (options: StreamProcessorOptions): StreamProce
...msg, ...msg,
content: assistantResponse, content: assistantResponse,
isStreaming: false, isStreaming: false,
tokenCount: undefined tokenCount: undefined,
performanceInfo
})); }));
console.log('------------ ASSISTANT ------------'); console.log('------------ ASSISTANT ------------');
@@ -98,19 +100,22 @@ export const useStreamProcessor = (options: StreamProcessorOptions): StreamProce
assistantResponse = ''; assistantResponse = '';
tokenCount = 0; tokenCount = 0;
hasTextContent = false; hasTextContent = false;
performanceInfo = undefined;
// Create a fresh streaming placeholder for the next LLM response // Create a fresh streaming placeholder for the next LLM response
const nextMsg = createStreamingMessage(); const nextMsg = createStreamingMessage();
currentStreamingId = nextMsg.id; currentStreamingId = nextMsg.id;
onMessageAdd(nextMsg); onMessageAdd(nextMsg);
} else if (chunk.type === 'done') { } else if (chunk.type === 'done') {
performanceInfo = chunk.performanceInfo;
// Finalize the streaming message // Finalize the streaming message
if (hasTextContent) { if (hasTextContent) {
onMessageUpdate(currentStreamingId, (msg) => ({ onMessageUpdate(currentStreamingId, (msg) => ({
...msg, ...msg,
content: assistantResponse, content: assistantResponse,
isStreaming: false, isStreaming: false,
tokenCount: undefined tokenCount: undefined,
performanceInfo
})); }));
} else { } else {
// No text in final response — remove empty placeholder // No text in final response — remove empty placeholder
+2
View File
@@ -1,4 +1,5 @@
import { Transform, type TransformFnParams } from 'class-transformer'; import { Transform, type TransformFnParams } from 'class-transformer';
import type { PerformanceInfo } from '../agent/llm/StreamingTypes';
export interface TimeSignature { export interface TimeSignature {
numerator: number; numerator: number;
@@ -11,6 +12,7 @@ export interface ChatMessage {
content: string; content: string;
isStreaming?: boolean; isStreaming?: boolean;
tokenCount?: number; tokenCount?: number;
performanceInfo?: PerformanceInfo;
} }
/** /**