mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 04:17:11 +00:00
feat(cliproxy-stats): Implement detailed stats fetching and integrate into UI
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user