Skip to content

Commit 4845fea

Browse files
committed
🤖 fix: consolidate per-model entries when not splitting by mode
- Keep per-model+mode breakdown for 'Split by mode' - When the toggle is off, aggregate stats across mode/unknown keys so each model appears once Change-Id: I0e7f6c712dd864862fe185e9fe6b425d96e242b0 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent d431db3 commit 4845fea

File tree

1 file changed

+119
-62
lines changed

1 file changed

+119
-62
lines changed

src/browser/components/RightSidebar/StatsTab.tsx

Lines changed: 119 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,12 @@ const StatsTabComponent: React.FC<StatsTabProps> = ({ workspaceId }) => {
260260
}
261261

262262
// Get per-model breakdown (works for both session and last-request)
263-
const modelBreakdown: ModelBreakdownEntry[] = (() => {
263+
const modelBreakdownData: {
264+
/** Per-model+mode entries (no consolidation; keys may be model:mode) */
265+
byKey: ModelBreakdownEntry[];
266+
/** Consolidated per-model entries (mode ignored) */
267+
byModel: ModelBreakdownEntry[];
268+
} = (() => {
264269
if (viewMode === "session") {
265270
// Session view: aggregate from all completed streams + active
266271
interface BreakdownEntry {
@@ -335,9 +340,11 @@ const StatsTabComponent: React.FC<StatsTabProps> = ({ workspaceId }) => {
335340
breakdown[activeKey] = existing;
336341
}
337342

338-
// Convert to display format
339-
return Object.entries(breakdown).map(([key, stats]) => {
340-
const { model, mode } = parseStatsKey(key);
343+
const toModelBreakdownEntry = (
344+
model: string,
345+
stats: BreakdownEntry,
346+
mode?: "plan" | "exec"
347+
): ModelBreakdownEntry => {
341348
const modelTime = Math.max(0, stats.totalDuration - stats.toolExecutionMs);
342349
const avgTtft = stats.ttftCount > 0 ? stats.ttftSum / stats.ttftCount : null;
343350
const tokensPerSec = calculateAverageTPS(
@@ -354,6 +361,7 @@ const StatsTabComponent: React.FC<StatsTabProps> = ({ workspaceId }) => {
354361
stats.responseCount > 0 && stats.totalReasoningTokens > 0
355362
? Math.round(stats.totalReasoningTokens / stats.responseCount)
356363
: null;
364+
357365
return {
358366
model,
359367
displayName: getModelDisplayName(model),
@@ -367,68 +375,117 @@ const StatsTabComponent: React.FC<StatsTabProps> = ({ workspaceId }) => {
367375
tokensPerSec,
368376
avgTokensPerMsg,
369377
avgReasoningPerMsg,
370-
mode: mode ?? stats.mode,
378+
mode,
371379
};
380+
};
381+
382+
const byKey = Object.entries(breakdown).map(([key, stats]) => {
383+
const { model, mode } = parseStatsKey(key);
384+
return toModelBreakdownEntry(model, stats, mode ?? stats.mode);
372385
});
373-
} else {
374-
// Last Request view: show single model from the last/active request
375-
if (!timingStats) return [];
376-
377-
const elapsed = timingStats.isActive
378-
? now - timingStats.startTime
379-
: timingStats.endTime! - timingStats.startTime;
380-
const modelTime = Math.max(0, elapsed - timingStats.toolExecutionMs);
381-
const ttft =
382-
timingStats.firstTokenTime !== null
383-
? timingStats.firstTokenTime - timingStats.startTime
384-
: null;
385-
386-
// For active streams: use live token data
387-
// For completed: use stored token counts
388-
const outputTokens = timingStats.isActive
389-
? (timingStats.liveTokenCount ?? 0)
390-
: (timingStats.outputTokens ?? 0);
391-
const reasoningTokens = timingStats.reasoningTokens ?? 0;
392-
393-
// For active streams: streaming time excludes tool execution
394-
// For completed: use stored streamingMs (already excludes tools)
395-
const rawStreamingMs = timingStats.isActive
396-
? timingStats.firstTokenTime !== null
397-
? now - timingStats.firstTokenTime
398-
: 0
399-
: (timingStats.streamingMs ?? 0);
400-
const streamingMs = timingStats.isActive
401-
? Math.max(0, rawStreamingMs - timingStats.toolExecutionMs)
402-
: rawStreamingMs;
403-
404-
// Calculate TPS with fallback for old data
405-
const tokensPerSec = calculateAverageTPS(
406-
streamingMs,
407-
modelTime,
408-
outputTokens,
409-
timingStats.isActive ? (timingStats.liveTPS ?? null) : null
410-
);
411-
412-
return [
413-
{
414-
model: timingStats.model,
415-
displayName: getModelDisplayName(timingStats.model),
416-
totalDuration: elapsed,
417-
toolExecutionMs: timingStats.toolExecutionMs,
418-
modelTime,
419-
avgTtft: ttft,
420-
responseCount: 1,
421-
totalOutputTokens: outputTokens,
422-
totalReasoningTokens: reasoningTokens,
423-
tokensPerSec,
424-
avgTokensPerMsg: outputTokens > 0 ? outputTokens : null,
425-
avgReasoningPerMsg: reasoningTokens > 0 ? reasoningTokens : null,
426-
mode: timingStats.mode,
427-
},
428-
];
386+
387+
// Consolidate by model when not splitting by mode
388+
const consolidated: Record<string, BreakdownEntry> = {};
389+
for (const [key, stats] of Object.entries(breakdown)) {
390+
const { model } = parseStatsKey(key);
391+
const existing = consolidated[model] ?? {
392+
totalDuration: 0,
393+
toolExecutionMs: 0,
394+
streamingMs: 0,
395+
responseCount: 0,
396+
totalOutputTokens: 0,
397+
totalReasoningTokens: 0,
398+
ttftSum: 0,
399+
ttftCount: 0,
400+
liveTPS: null,
401+
liveTokenCount: 0,
402+
};
403+
404+
existing.totalDuration += stats.totalDuration;
405+
existing.toolExecutionMs += stats.toolExecutionMs;
406+
existing.streamingMs += stats.streamingMs;
407+
existing.responseCount += stats.responseCount;
408+
existing.totalOutputTokens += stats.totalOutputTokens;
409+
existing.totalReasoningTokens += stats.totalReasoningTokens;
410+
existing.ttftSum += stats.ttftSum;
411+
existing.ttftCount += stats.ttftCount;
412+
413+
// Preserve live data if present (only expected for the active stream)
414+
existing.liveTPS = stats.liveTPS ?? existing.liveTPS;
415+
existing.liveTokenCount += stats.liveTokenCount;
416+
417+
consolidated[model] = existing;
418+
}
419+
420+
const byModel = Object.entries(consolidated).map(([model, stats]) => {
421+
return toModelBreakdownEntry(model, stats);
422+
});
423+
424+
return { byKey, byModel };
429425
}
426+
427+
// Last Request view: show single model from the last/active request
428+
if (!timingStats) return { byKey: [], byModel: [] };
429+
430+
const elapsed = timingStats.isActive
431+
? now - timingStats.startTime
432+
: timingStats.endTime! - timingStats.startTime;
433+
const modelTime = Math.max(0, elapsed - timingStats.toolExecutionMs);
434+
const ttft =
435+
timingStats.firstTokenTime !== null
436+
? timingStats.firstTokenTime - timingStats.startTime
437+
: null;
438+
439+
// For active streams: use live token data
440+
// For completed: use stored token counts
441+
const outputTokens = timingStats.isActive
442+
? (timingStats.liveTokenCount ?? 0)
443+
: (timingStats.outputTokens ?? 0);
444+
const reasoningTokens = timingStats.reasoningTokens ?? 0;
445+
446+
// For active streams: streaming time excludes tool execution
447+
// For completed: use stored streamingMs (already excludes tools)
448+
const rawStreamingMs = timingStats.isActive
449+
? timingStats.firstTokenTime !== null
450+
? now - timingStats.firstTokenTime
451+
: 0
452+
: (timingStats.streamingMs ?? 0);
453+
const streamingMs = timingStats.isActive
454+
? Math.max(0, rawStreamingMs - timingStats.toolExecutionMs)
455+
: rawStreamingMs;
456+
457+
const tokensPerSec = calculateAverageTPS(
458+
streamingMs,
459+
modelTime,
460+
outputTokens,
461+
timingStats.isActive ? (timingStats.liveTPS ?? null) : null
462+
);
463+
464+
const entry: ModelBreakdownEntry = {
465+
model: timingStats.model,
466+
displayName: getModelDisplayName(timingStats.model),
467+
totalDuration: elapsed,
468+
toolExecutionMs: timingStats.toolExecutionMs,
469+
modelTime,
470+
avgTtft: ttft,
471+
responseCount: 1,
472+
totalOutputTokens: outputTokens,
473+
totalReasoningTokens: reasoningTokens,
474+
tokensPerSec,
475+
avgTokensPerMsg: outputTokens > 0 ? outputTokens : null,
476+
avgReasoningPerMsg: reasoningTokens > 0 ? reasoningTokens : null,
477+
mode: timingStats.mode,
478+
};
479+
480+
return { byKey: [entry], byModel: [entry] };
430481
})();
431482

483+
const hasModeData = viewMode === "session" && modelBreakdownData.byKey.some((m) => m.mode);
484+
const modelBreakdown =
485+
viewMode === "session" && !showModeBreakdown
486+
? modelBreakdownData.byModel
487+
: modelBreakdownData.byKey;
488+
432489
return (
433490
<div className="text-light font-primary text-[13px] leading-relaxed">
434491
<div data-testid="timing-section" className="mb-6">
@@ -538,7 +595,7 @@ const StatsTabComponent: React.FC<StatsTabProps> = ({ workspaceId }) => {
538595
<div className="flex items-center justify-between">
539596
<span className="text-foreground font-medium">By Model</span>
540597
{/* Only show toggle in session view when we have mode data */}
541-
{viewMode === "session" && modelBreakdown.some((m) => m.mode) && (
598+
{viewMode === "session" && hasModeData && (
542599
<label className="text-muted-light flex cursor-pointer items-center gap-1.5 text-[10px]">
543600
<input
544601
type="checkbox"

0 commit comments

Comments
 (0)