mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
feat: Change /stats to include more detailed breakdowns (#2615)
This commit is contained in:
247
packages/cli/src/ui/utils/computeStats.test.ts
Normal file
247
packages/cli/src/ui/utils/computeStats.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
84
packages/cli/src/ui/utils/computeStats.ts
Normal file
84
packages/cli/src/ui/utils/computeStats.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
58
packages/cli/src/ui/utils/displayUtils.test.ts
Normal file
58
packages/cli/src/ui/utils/displayUtils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
32
packages/cli/src/ui/utils/displayUtils.ts
Normal file
32
packages/cli/src/ui/utils/displayUtils.ts
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user