feat(cliproxy-stats): Implement detailed stats fetching and integrate into UI

This commit is contained in:
kaitranntt
2025-12-16 05:57:27 -05:00
parent 6e4ee805da
commit 3216a0e847
5 changed files with 131 additions and 37 deletions
+81 -3
View File
@@ -7,10 +7,28 @@
import { CCS_CONTROL_PANEL_SECRET, CLIPROXY_DEFAULT_PORT } from './config-generator';
/** Per-account usage statistics */
export interface AccountUsageStats {
/** Account email or identifier */
source: string;
/** Number of successful requests */
successCount: number;
/** Number of failed requests */
failureCount: number;
/** Total tokens used */
totalTokens: number;
/** Last request timestamp */
lastUsedAt?: string;
}
/** Usage statistics from CLIProxyAPI */
export interface CliproxyStats {
/** Total number of requests processed */
totalRequests: number;
/** Total successful requests */
successCount: number;
/** Total failed requests */
failureCount: number;
/** Token counts */
tokens: {
input: number;
@@ -21,6 +39,8 @@ export interface CliproxyStats {
requestsByModel: Record<string, number>;
/** Requests grouped by provider */
requestsByProvider: Record<string, number>;
/** Per-account usage breakdown */
accountStats: Record<string, AccountUsageStats>;
/** Number of quota exceeded (429) events */
quotaExceededCount: number;
/** Number of request retries */
@@ -29,6 +49,21 @@ export interface CliproxyStats {
collectedAt: string;
}
/** Request detail from CLIProxyAPI */
interface RequestDetail {
timestamp: string;
source: string;
auth_index: number;
tokens: {
input_tokens: number;
output_tokens: number;
reasoning_tokens: number;
cached_tokens: number;
total_tokens: number;
};
failed: boolean;
}
/** Usage API response from CLIProxyAPI /v0/management/usage endpoint */
interface UsageApiResponse {
failed_requests?: number;
@@ -47,6 +82,7 @@ interface UsageApiResponse {
{
total_requests?: number;
total_tokens?: number;
details?: RequestDetail[];
}
>;
}
@@ -83,9 +119,14 @@ export async function fetchCliproxyStats(
const data = (await response.json()) as UsageApiResponse;
const usage = data.usage;
// Extract models and providers from the nested API structure
// Extract models, providers, and per-account stats from the nested API structure
const requestsByModel: Record<string, number> = {};
const requestsByProvider: Record<string, number> = {};
const accountStats: Record<string, AccountUsageStats> = {};
let totalSuccessCount = 0;
let totalFailureCount = 0;
let totalInputTokens = 0;
let totalOutputTokens = 0;
if (usage?.apis) {
for (const [provider, providerData] of Object.entries(usage.apis)) {
@@ -93,6 +134,40 @@ export async function fetchCliproxyStats(
if (providerData.models) {
for (const [model, modelData] of Object.entries(providerData.models)) {
requestsByModel[model] = modelData.total_requests ?? 0;
// Aggregate per-account stats from request details
if (modelData.details) {
for (const detail of modelData.details) {
const source = detail.source || 'unknown';
// Initialize account stats if not exists
if (!accountStats[source]) {
accountStats[source] = {
source,
successCount: 0,
failureCount: 0,
totalTokens: 0,
};
}
// Update account stats
if (detail.failed) {
accountStats[source].failureCount++;
totalFailureCount++;
} else {
accountStats[source].successCount++;
totalSuccessCount++;
}
const tokens = detail.tokens?.total_tokens ?? 0;
accountStats[source].totalTokens += tokens;
accountStats[source].lastUsedAt = detail.timestamp;
// Aggregate token breakdowns
totalInputTokens += detail.tokens?.input_tokens ?? 0;
totalOutputTokens += detail.tokens?.output_tokens ?? 0;
}
}
}
}
}
@@ -101,13 +176,16 @@ export async function fetchCliproxyStats(
// Normalize the response to our interface
return {
totalRequests: usage?.total_requests ?? 0,
successCount: totalSuccessCount,
failureCount: totalFailureCount,
tokens: {
input: 0, // API doesn't provide input/output breakdown
output: 0,
input: totalInputTokens,
output: totalOutputTokens,
total: usage?.total_tokens ?? 0,
},
requestsByModel,
requestsByProvider,
accountStats,
quotaExceededCount: usage?.failure_count ?? data.failed_requests ?? 0,
retryCount: 0, // API doesn't track retries separately
collectedAt: new Date().toISOString(),
+13 -22
View File
@@ -41,34 +41,25 @@ interface ConnectionEvent {
latencyMs?: number;
}
/** Generate mock connection events based on account data */
/** Generate connection events from real account data */
function generateConnectionEvents(accounts: AccountData[]): ConnectionEvent[] {
const events: ConnectionEvent[] = [];
const now = new Date();
accounts.forEach((account) => {
// Generate events based on success/failure counts
const successEvents = Math.min(account.successCount, 3);
const failEvents = Math.min(account.failureCount, 2);
// Only show events for accounts that have actual request data
const hasActivity = account.successCount > 0 || account.failureCount > 0;
if (!hasActivity) return;
for (let i = 0; i < successEvents; i++) {
events.push({
id: `${account.id}-s-${i}`,
timestamp: new Date(now.getTime() - Math.random() * 3600000), // Last hour
accountEmail: account.email,
status: 'success',
latencyMs: Math.floor(Math.random() * 500) + 50,
});
}
// Create a single consolidated event per account showing its current status
const lastUsed = account.lastUsedAt ? new Date(account.lastUsedAt) : new Date();
const hasFailures = account.failureCount > 0;
for (let i = 0; i < failEvents; i++) {
events.push({
id: `${account.id}-f-${i}`,
timestamp: new Date(now.getTime() - Math.random() * 7200000), // Last 2 hours
accountEmail: account.email,
status: 'failed',
});
}
events.push({
id: `${account.id}-status`,
timestamp: lastUsed,
accountEmail: account.email,
status: hasFailures && account.failureCount > account.successCount ? 'failed' : 'success',
});
});
// Sort by timestamp descending (most recent first)
+16 -6
View File
@@ -6,6 +6,7 @@
import { useState, useMemo } from 'react';
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
import { useCliproxyStats, type AccountUsageStats } from '@/hooks/use-cliproxy-stats';
import { cn, STATUS_COLORS } from '@/lib/utils';
import { getProviderDisplayName, PROVIDER_COLORS } from '@/lib/provider-config';
import { Skeleton } from '@/components/ui/skeleton';
@@ -63,9 +64,16 @@ const ACCOUNT_COLORS = [
export function AuthMonitor() {
const { data, isLoading, error } = useCliproxyAuth();
const { data: statsData, isLoading: statsLoading } = useCliproxyStats();
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const [hoveredProvider, setHoveredProvider] = useState<string | null>(null);
// Build a map of account email -> usage stats from CLIProxy
const accountStatsMap = useMemo(() => {
if (!statsData?.accountStats) return new Map<string, AccountUsageStats>();
return new Map(Object.entries(statsData.accountStats));
}, [statsData?.accountStats]);
// Transform auth status data into account rows
const { accounts, totalSuccess, totalFailure, totalRequests, providerStats } = useMemo(() => {
if (!data?.authStatus) {
@@ -96,9 +104,11 @@ export function AuthMonitor() {
if (!providerData) return;
status.accounts?.forEach((account: OAuthAccount) => {
// Mock stats - in production, fetch from CLIProxy /usage endpoint
const success = Math.floor(Math.random() * 2000) + 100;
const failure = account.isDefault ? Math.floor(Math.random() * 50) : 0;
// Get real stats from CLIProxy - try email first, then id
const accountEmail = account.email || account.id;
const realStats = accountStatsMap.get(accountEmail);
const success = realStats?.successCount ?? 0;
const failure = realStats?.failureCount ?? 0;
tSuccess += success;
tFailure += failure;
providerData.success += success;
@@ -112,7 +122,7 @@ export function AuthMonitor() {
isDefault: account.isDefault,
successCount: success,
failureCount: failure,
lastUsedAt: account.lastUsedAt,
lastUsedAt: realStats?.lastUsedAt ?? account.lastUsedAt,
color: ACCOUNT_COLORS[colorIndex % ACCOUNT_COLORS.length],
};
accountsList.push(row);
@@ -144,7 +154,7 @@ export function AuthMonitor() {
totalRequests: tSuccess + tFailure,
providerStats: providerStatsArr,
};
}, [data?.authStatus]);
}, [data?.authStatus, accountStatsMap]);
const overallSuccessRate =
totalRequests > 0 ? Math.round((totalSuccess / totalRequests) * 100) : 100;
@@ -154,7 +164,7 @@ export function AuthMonitor() {
? providerStats.find((ps) => ps.provider === selectedProvider)
: null;
if (isLoading) {
if (isLoading || statsLoading) {
return (
<div className="rounded-xl border border-border overflow-hidden font-mono text-[13px] bg-card/50 dark:bg-zinc-900/60 backdrop-blur-sm">
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
+3 -6
View File
@@ -31,20 +31,17 @@ export function ProfileDeck() {
);
}
// Mock data for demonstration
const mockProfiles = profiles.map((profile, index: number) => ({
// Use real profile data directly
const normalizedProfiles = profiles.map((profile) => ({
...profile,
configured: profile.configured ?? false, // Ensure configured is always boolean
isActive: index === 0, // First profile is active
lastUsed: index === 0 ? '2 hours ago' : index === 1 ? '1 day ago' : undefined,
model: index === 0 ? 'claude-3.5-sonnet' : index === 1 ? 'claude-3-haiku' : undefined,
}));
return (
<div className="space-y-4">
<h2 className="text-lg font-semibold">Profiles</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{mockProfiles.map((profile) => (
{normalizedProfiles.map((profile) => (
<ProfileCard
key={profile.name}
profile={profile}
+18
View File
@@ -4,9 +4,25 @@
import { useQuery } from '@tanstack/react-query';
/** Per-account usage statistics */
export interface AccountUsageStats {
/** Account email or identifier */
source: string;
/** Number of successful requests */
successCount: number;
/** Number of failed requests */
failureCount: number;
/** Total tokens used */
totalTokens: number;
/** Last request timestamp */
lastUsedAt?: string;
}
/** CLIProxy usage statistics */
export interface CliproxyStats {
totalRequests: number;
successCount: number;
failureCount: number;
tokens: {
input: number;
output: number;
@@ -14,6 +30,8 @@ export interface CliproxyStats {
};
requestsByModel: Record<string, number>;
requestsByProvider: Record<string, number>;
/** Per-account usage breakdown */
accountStats: Record<string, AccountUsageStats>;
quotaExceededCount: number;
retryCount: number;
collectedAt: string;