feat: Change /stats to include more detailed breakdowns (#2615)

This commit is contained in:
Abhi
2025-06-29 20:44:33 -04:00
committed by GitHub
parent 0fd602eb43
commit 770f862832
36 changed files with 3218 additions and 758 deletions

View File

@@ -0,0 +1,247 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import {
calculateAverageLatency,
calculateCacheHitRate,
calculateErrorRate,
computeSessionStats,
} from './computeStats.js';
import { ModelMetrics, SessionMetrics } from '../contexts/SessionContext.js';
describe('calculateErrorRate', () => {
it('should return 0 if totalRequests is 0', () => {
const metrics: ModelMetrics = {
api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 0 },
tokens: {
prompt: 0,
candidates: 0,
total: 0,
cached: 0,
thoughts: 0,
tool: 0,
},
};
expect(calculateErrorRate(metrics)).toBe(0);
});
it('should calculate the error rate correctly', () => {
const metrics: ModelMetrics = {
api: { totalRequests: 10, totalErrors: 2, totalLatencyMs: 0 },
tokens: {
prompt: 0,
candidates: 0,
total: 0,
cached: 0,
thoughts: 0,
tool: 0,
},
};
expect(calculateErrorRate(metrics)).toBe(20);
});
});
describe('calculateAverageLatency', () => {
it('should return 0 if totalRequests is 0', () => {
const metrics: ModelMetrics = {
api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 1000 },
tokens: {
prompt: 0,
candidates: 0,
total: 0,
cached: 0,
thoughts: 0,
tool: 0,
},
};
expect(calculateAverageLatency(metrics)).toBe(0);
});
it('should calculate the average latency correctly', () => {
const metrics: ModelMetrics = {
api: { totalRequests: 10, totalErrors: 0, totalLatencyMs: 1500 },
tokens: {
prompt: 0,
candidates: 0,
total: 0,
cached: 0,
thoughts: 0,
tool: 0,
},
};
expect(calculateAverageLatency(metrics)).toBe(150);
});
});
describe('calculateCacheHitRate', () => {
it('should return 0 if prompt tokens is 0', () => {
const metrics: ModelMetrics = {
api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 0 },
tokens: {
prompt: 0,
candidates: 0,
total: 0,
cached: 100,
thoughts: 0,
tool: 0,
},
};
expect(calculateCacheHitRate(metrics)).toBe(0);
});
it('should calculate the cache hit rate correctly', () => {
const metrics: ModelMetrics = {
api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 0 },
tokens: {
prompt: 200,
candidates: 0,
total: 0,
cached: 50,
thoughts: 0,
tool: 0,
},
};
expect(calculateCacheHitRate(metrics)).toBe(25);
});
});
describe('computeSessionStats', () => {
it('should return all zeros for initial empty metrics', () => {
const metrics: SessionMetrics = {
models: {},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
totalDecisions: { accept: 0, reject: 0, modify: 0 },
byName: {},
},
};
const result = computeSessionStats(metrics);
expect(result).toEqual({
totalApiTime: 0,
totalToolTime: 0,
agentActiveTime: 0,
apiTimePercent: 0,
toolTimePercent: 0,
cacheEfficiency: 0,
totalDecisions: 0,
successRate: 0,
agreementRate: 0,
totalPromptTokens: 0,
totalCachedTokens: 0,
});
});
it('should correctly calculate API and tool time percentages', () => {
const metrics: SessionMetrics = {
models: {
'gemini-pro': {
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 750 },
tokens: {
prompt: 10,
candidates: 10,
total: 20,
cached: 0,
thoughts: 0,
tool: 0,
},
},
},
tools: {
totalCalls: 1,
totalSuccess: 1,
totalFail: 0,
totalDurationMs: 250,
totalDecisions: { accept: 0, reject: 0, modify: 0 },
byName: {},
},
};
const result = computeSessionStats(metrics);
expect(result.totalApiTime).toBe(750);
expect(result.totalToolTime).toBe(250);
expect(result.agentActiveTime).toBe(1000);
expect(result.apiTimePercent).toBe(75);
expect(result.toolTimePercent).toBe(25);
});
it('should correctly calculate cache efficiency', () => {
const metrics: SessionMetrics = {
models: {
'gemini-pro': {
api: { totalRequests: 2, totalErrors: 0, totalLatencyMs: 1000 },
tokens: {
prompt: 150,
candidates: 10,
total: 160,
cached: 50,
thoughts: 0,
tool: 0,
},
},
},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
totalDecisions: { accept: 0, reject: 0, modify: 0 },
byName: {},
},
};
const result = computeSessionStats(metrics);
expect(result.cacheEfficiency).toBeCloseTo(33.33); // 50 / 150
});
it('should correctly calculate success and agreement rates', () => {
const metrics: SessionMetrics = {
models: {},
tools: {
totalCalls: 10,
totalSuccess: 8,
totalFail: 2,
totalDurationMs: 1000,
totalDecisions: { accept: 6, reject: 2, modify: 2 },
byName: {},
},
};
const result = computeSessionStats(metrics);
expect(result.successRate).toBe(80); // 8 / 10
expect(result.agreementRate).toBe(60); // 6 / 10
});
it('should handle division by zero gracefully', () => {
const metrics: SessionMetrics = {
models: {},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
totalDecisions: { accept: 0, reject: 0, modify: 0 },
byName: {},
},
};
const result = computeSessionStats(metrics);
expect(result.apiTimePercent).toBe(0);
expect(result.toolTimePercent).toBe(0);
expect(result.cacheEfficiency).toBe(0);
expect(result.successRate).toBe(0);
expect(result.agreementRate).toBe(0);
});
});

View File

@@ -0,0 +1,84 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
SessionMetrics,
ComputedSessionStats,
ModelMetrics,
} from '../contexts/SessionContext.js';
export function calculateErrorRate(metrics: ModelMetrics): number {
if (metrics.api.totalRequests === 0) {
return 0;
}
return (metrics.api.totalErrors / metrics.api.totalRequests) * 100;
}
export function calculateAverageLatency(metrics: ModelMetrics): number {
if (metrics.api.totalRequests === 0) {
return 0;
}
return metrics.api.totalLatencyMs / metrics.api.totalRequests;
}
export function calculateCacheHitRate(metrics: ModelMetrics): number {
if (metrics.tokens.prompt === 0) {
return 0;
}
return (metrics.tokens.cached / metrics.tokens.prompt) * 100;
}
export const computeSessionStats = (
metrics: SessionMetrics,
): ComputedSessionStats => {
const { models, tools } = metrics;
const totalApiTime = Object.values(models).reduce(
(acc, model) => acc + model.api.totalLatencyMs,
0,
);
const totalToolTime = tools.totalDurationMs;
const agentActiveTime = totalApiTime + totalToolTime;
const apiTimePercent =
agentActiveTime > 0 ? (totalApiTime / agentActiveTime) * 100 : 0;
const toolTimePercent =
agentActiveTime > 0 ? (totalToolTime / agentActiveTime) * 100 : 0;
const totalCachedTokens = Object.values(models).reduce(
(acc, model) => acc + model.tokens.cached,
0,
);
const totalPromptTokens = Object.values(models).reduce(
(acc, model) => acc + model.tokens.prompt,
0,
);
const cacheEfficiency =
totalPromptTokens > 0 ? (totalCachedTokens / totalPromptTokens) * 100 : 0;
const totalDecisions =
tools.totalDecisions.accept +
tools.totalDecisions.reject +
tools.totalDecisions.modify;
const successRate =
tools.totalCalls > 0 ? (tools.totalSuccess / tools.totalCalls) * 100 : 0;
const agreementRate =
totalDecisions > 0
? (tools.totalDecisions.accept / totalDecisions) * 100
: 0;
return {
totalApiTime,
totalToolTime,
agentActiveTime,
apiTimePercent,
toolTimePercent,
cacheEfficiency,
totalDecisions,
successRate,
agreementRate,
totalCachedTokens,
totalPromptTokens,
};
};

View File

@@ -0,0 +1,58 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import {
getStatusColor,
TOOL_SUCCESS_RATE_HIGH,
TOOL_SUCCESS_RATE_MEDIUM,
USER_AGREEMENT_RATE_HIGH,
USER_AGREEMENT_RATE_MEDIUM,
CACHE_EFFICIENCY_HIGH,
CACHE_EFFICIENCY_MEDIUM,
} from './displayUtils.js';
import { Colors } from '../colors.js';
describe('displayUtils', () => {
describe('getStatusColor', () => {
const thresholds = {
green: 80,
yellow: 50,
};
it('should return green for values >= green threshold', () => {
expect(getStatusColor(90, thresholds)).toBe(Colors.AccentGreen);
expect(getStatusColor(80, thresholds)).toBe(Colors.AccentGreen);
});
it('should return yellow for values < green and >= yellow threshold', () => {
expect(getStatusColor(79, thresholds)).toBe(Colors.AccentYellow);
expect(getStatusColor(50, thresholds)).toBe(Colors.AccentYellow);
});
it('should return red for values < yellow threshold', () => {
expect(getStatusColor(49, thresholds)).toBe(Colors.AccentRed);
expect(getStatusColor(0, thresholds)).toBe(Colors.AccentRed);
});
it('should return defaultColor for values < yellow threshold when provided', () => {
expect(
getStatusColor(49, thresholds, { defaultColor: Colors.Foreground }),
).toBe(Colors.Foreground);
});
});
describe('Threshold Constants', () => {
it('should have the correct values', () => {
expect(TOOL_SUCCESS_RATE_HIGH).toBe(95);
expect(TOOL_SUCCESS_RATE_MEDIUM).toBe(85);
expect(USER_AGREEMENT_RATE_HIGH).toBe(75);
expect(USER_AGREEMENT_RATE_MEDIUM).toBe(45);
expect(CACHE_EFFICIENCY_HIGH).toBe(40);
expect(CACHE_EFFICIENCY_MEDIUM).toBe(15);
});
});
});

View File

@@ -0,0 +1,32 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Colors } from '../colors.js';
// --- Thresholds ---
export const TOOL_SUCCESS_RATE_HIGH = 95;
export const TOOL_SUCCESS_RATE_MEDIUM = 85;
export const USER_AGREEMENT_RATE_HIGH = 75;
export const USER_AGREEMENT_RATE_MEDIUM = 45;
export const CACHE_EFFICIENCY_HIGH = 40;
export const CACHE_EFFICIENCY_MEDIUM = 15;
// --- Color Logic ---
export const getStatusColor = (
value: number,
thresholds: { green: number; yellow: number },
options: { defaultColor?: string } = {},
) => {
if (value >= thresholds.green) {
return Colors.AccentGreen;
}
if (value >= thresholds.yellow) {
return Colors.AccentYellow;
}
return options.defaultColor || Colors.AccentRed;
};

View File

@@ -27,7 +27,7 @@ export const formatDuration = (milliseconds: number): string => {
}
if (milliseconds < 1000) {
return `${milliseconds}ms`;
return `${Math.round(milliseconds)}ms`;
}
const totalSeconds = milliseconds / 1000;