diff --git a/src/cliproxy/stats-fetcher.ts b/src/cliproxy/stats-fetcher.ts index 2002151a..aaf00058 100644 --- a/src/cliproxy/stats-fetcher.ts +++ b/src/cliproxy/stats-fetcher.ts @@ -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; /** Requests grouped by provider */ requestsByProvider: Record; + /** Per-account usage breakdown */ + accountStats: Record; /** 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 = {}; const requestsByProvider: Record = {}; + const accountStats: Record = {}; + 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(), diff --git a/ui/src/components/account-flow-viz.tsx b/ui/src/components/account-flow-viz.tsx index 3951dd40..f12fb059 100644 --- a/ui/src/components/account-flow-viz.tsx +++ b/ui/src/components/account-flow-viz.tsx @@ -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) diff --git a/ui/src/components/auth-monitor.tsx b/ui/src/components/auth-monitor.tsx index 26d83769..ae17e7c7 100644 --- a/ui/src/components/auth-monitor.tsx +++ b/ui/src/components/auth-monitor.tsx @@ -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(null); const [hoveredProvider, setHoveredProvider] = useState(null); + // Build a map of account email -> usage stats from CLIProxy + const accountStatsMap = useMemo(() => { + if (!statsData?.accountStats) return new Map(); + 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 (
diff --git a/ui/src/components/profile-deck.tsx b/ui/src/components/profile-deck.tsx index 20523469..4a3255b0 100644 --- a/ui/src/components/profile-deck.tsx +++ b/ui/src/components/profile-deck.tsx @@ -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 (

Profiles

- {mockProfiles.map((profile) => ( + {normalizedProfiles.map((profile) => ( ; requestsByProvider: Record; + /** Per-account usage breakdown */ + accountStats: Record; quotaExceededCount: number; retryCount: number; collectedAt: string;