mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 02:19:39 +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';
|
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 */
|
/** Usage statistics from CLIProxyAPI */
|
||||||
export interface CliproxyStats {
|
export interface CliproxyStats {
|
||||||
/** Total number of requests processed */
|
/** Total number of requests processed */
|
||||||
totalRequests: number;
|
totalRequests: number;
|
||||||
|
/** Total successful requests */
|
||||||
|
successCount: number;
|
||||||
|
/** Total failed requests */
|
||||||
|
failureCount: number;
|
||||||
/** Token counts */
|
/** Token counts */
|
||||||
tokens: {
|
tokens: {
|
||||||
input: number;
|
input: number;
|
||||||
@@ -21,6 +39,8 @@ export interface CliproxyStats {
|
|||||||
requestsByModel: Record<string, number>;
|
requestsByModel: Record<string, number>;
|
||||||
/** Requests grouped by provider */
|
/** Requests grouped by provider */
|
||||||
requestsByProvider: Record<string, number>;
|
requestsByProvider: Record<string, number>;
|
||||||
|
/** Per-account usage breakdown */
|
||||||
|
accountStats: Record<string, AccountUsageStats>;
|
||||||
/** Number of quota exceeded (429) events */
|
/** Number of quota exceeded (429) events */
|
||||||
quotaExceededCount: number;
|
quotaExceededCount: number;
|
||||||
/** Number of request retries */
|
/** Number of request retries */
|
||||||
@@ -29,6 +49,21 @@ export interface CliproxyStats {
|
|||||||
collectedAt: string;
|
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 */
|
/** Usage API response from CLIProxyAPI /v0/management/usage endpoint */
|
||||||
interface UsageApiResponse {
|
interface UsageApiResponse {
|
||||||
failed_requests?: number;
|
failed_requests?: number;
|
||||||
@@ -47,6 +82,7 @@ interface UsageApiResponse {
|
|||||||
{
|
{
|
||||||
total_requests?: number;
|
total_requests?: number;
|
||||||
total_tokens?: number;
|
total_tokens?: number;
|
||||||
|
details?: RequestDetail[];
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
@@ -83,9 +119,14 @@ export async function fetchCliproxyStats(
|
|||||||
const data = (await response.json()) as UsageApiResponse;
|
const data = (await response.json()) as UsageApiResponse;
|
||||||
const usage = data.usage;
|
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 requestsByModel: Record<string, number> = {};
|
||||||
const requestsByProvider: 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) {
|
if (usage?.apis) {
|
||||||
for (const [provider, providerData] of Object.entries(usage.apis)) {
|
for (const [provider, providerData] of Object.entries(usage.apis)) {
|
||||||
@@ -93,6 +134,40 @@ export async function fetchCliproxyStats(
|
|||||||
if (providerData.models) {
|
if (providerData.models) {
|
||||||
for (const [model, modelData] of Object.entries(providerData.models)) {
|
for (const [model, modelData] of Object.entries(providerData.models)) {
|
||||||
requestsByModel[model] = modelData.total_requests ?? 0;
|
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
|
// Normalize the response to our interface
|
||||||
return {
|
return {
|
||||||
totalRequests: usage?.total_requests ?? 0,
|
totalRequests: usage?.total_requests ?? 0,
|
||||||
|
successCount: totalSuccessCount,
|
||||||
|
failureCount: totalFailureCount,
|
||||||
tokens: {
|
tokens: {
|
||||||
input: 0, // API doesn't provide input/output breakdown
|
input: totalInputTokens,
|
||||||
output: 0,
|
output: totalOutputTokens,
|
||||||
total: usage?.total_tokens ?? 0,
|
total: usage?.total_tokens ?? 0,
|
||||||
},
|
},
|
||||||
requestsByModel,
|
requestsByModel,
|
||||||
requestsByProvider,
|
requestsByProvider,
|
||||||
|
accountStats,
|
||||||
quotaExceededCount: usage?.failure_count ?? data.failed_requests ?? 0,
|
quotaExceededCount: usage?.failure_count ?? data.failed_requests ?? 0,
|
||||||
retryCount: 0, // API doesn't track retries separately
|
retryCount: 0, // API doesn't track retries separately
|
||||||
collectedAt: new Date().toISOString(),
|
collectedAt: new Date().toISOString(),
|
||||||
|
|||||||
@@ -41,34 +41,25 @@ interface ConnectionEvent {
|
|||||||
latencyMs?: number;
|
latencyMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generate mock connection events based on account data */
|
/** Generate connection events from real account data */
|
||||||
function generateConnectionEvents(accounts: AccountData[]): ConnectionEvent[] {
|
function generateConnectionEvents(accounts: AccountData[]): ConnectionEvent[] {
|
||||||
const events: ConnectionEvent[] = [];
|
const events: ConnectionEvent[] = [];
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
accounts.forEach((account) => {
|
accounts.forEach((account) => {
|
||||||
// Generate events based on success/failure counts
|
// Only show events for accounts that have actual request data
|
||||||
const successEvents = Math.min(account.successCount, 3);
|
const hasActivity = account.successCount > 0 || account.failureCount > 0;
|
||||||
const failEvents = Math.min(account.failureCount, 2);
|
if (!hasActivity) return;
|
||||||
|
|
||||||
for (let i = 0; i < successEvents; i++) {
|
// Create a single consolidated event per account showing its current status
|
||||||
events.push({
|
const lastUsed = account.lastUsedAt ? new Date(account.lastUsedAt) : new Date();
|
||||||
id: `${account.id}-s-${i}`,
|
const hasFailures = account.failureCount > 0;
|
||||||
timestamp: new Date(now.getTime() - Math.random() * 3600000), // Last hour
|
|
||||||
accountEmail: account.email,
|
|
||||||
status: 'success',
|
|
||||||
latencyMs: Math.floor(Math.random() * 500) + 50,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < failEvents; i++) {
|
events.push({
|
||||||
events.push({
|
id: `${account.id}-status`,
|
||||||
id: `${account.id}-f-${i}`,
|
timestamp: lastUsed,
|
||||||
timestamp: new Date(now.getTime() - Math.random() * 7200000), // Last 2 hours
|
accountEmail: account.email,
|
||||||
accountEmail: account.email,
|
status: hasFailures && account.failureCount > account.successCount ? 'failed' : 'success',
|
||||||
status: 'failed',
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort by timestamp descending (most recent first)
|
// Sort by timestamp descending (most recent first)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
|
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
|
||||||
|
import { useCliproxyStats, type AccountUsageStats } from '@/hooks/use-cliproxy-stats';
|
||||||
import { cn, STATUS_COLORS } from '@/lib/utils';
|
import { cn, STATUS_COLORS } from '@/lib/utils';
|
||||||
import { getProviderDisplayName, PROVIDER_COLORS } from '@/lib/provider-config';
|
import { getProviderDisplayName, PROVIDER_COLORS } from '@/lib/provider-config';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
@@ -63,9 +64,16 @@ const ACCOUNT_COLORS = [
|
|||||||
|
|
||||||
export function AuthMonitor() {
|
export function AuthMonitor() {
|
||||||
const { data, isLoading, error } = useCliproxyAuth();
|
const { data, isLoading, error } = useCliproxyAuth();
|
||||||
|
const { data: statsData, isLoading: statsLoading } = useCliproxyStats();
|
||||||
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
||||||
const [hoveredProvider, setHoveredProvider] = 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
|
// Transform auth status data into account rows
|
||||||
const { accounts, totalSuccess, totalFailure, totalRequests, providerStats } = useMemo(() => {
|
const { accounts, totalSuccess, totalFailure, totalRequests, providerStats } = useMemo(() => {
|
||||||
if (!data?.authStatus) {
|
if (!data?.authStatus) {
|
||||||
@@ -96,9 +104,11 @@ export function AuthMonitor() {
|
|||||||
if (!providerData) return;
|
if (!providerData) return;
|
||||||
|
|
||||||
status.accounts?.forEach((account: OAuthAccount) => {
|
status.accounts?.forEach((account: OAuthAccount) => {
|
||||||
// Mock stats - in production, fetch from CLIProxy /usage endpoint
|
// Get real stats from CLIProxy - try email first, then id
|
||||||
const success = Math.floor(Math.random() * 2000) + 100;
|
const accountEmail = account.email || account.id;
|
||||||
const failure = account.isDefault ? Math.floor(Math.random() * 50) : 0;
|
const realStats = accountStatsMap.get(accountEmail);
|
||||||
|
const success = realStats?.successCount ?? 0;
|
||||||
|
const failure = realStats?.failureCount ?? 0;
|
||||||
tSuccess += success;
|
tSuccess += success;
|
||||||
tFailure += failure;
|
tFailure += failure;
|
||||||
providerData.success += success;
|
providerData.success += success;
|
||||||
@@ -112,7 +122,7 @@ export function AuthMonitor() {
|
|||||||
isDefault: account.isDefault,
|
isDefault: account.isDefault,
|
||||||
successCount: success,
|
successCount: success,
|
||||||
failureCount: failure,
|
failureCount: failure,
|
||||||
lastUsedAt: account.lastUsedAt,
|
lastUsedAt: realStats?.lastUsedAt ?? account.lastUsedAt,
|
||||||
color: ACCOUNT_COLORS[colorIndex % ACCOUNT_COLORS.length],
|
color: ACCOUNT_COLORS[colorIndex % ACCOUNT_COLORS.length],
|
||||||
};
|
};
|
||||||
accountsList.push(row);
|
accountsList.push(row);
|
||||||
@@ -144,7 +154,7 @@ export function AuthMonitor() {
|
|||||||
totalRequests: tSuccess + tFailure,
|
totalRequests: tSuccess + tFailure,
|
||||||
providerStats: providerStatsArr,
|
providerStats: providerStatsArr,
|
||||||
};
|
};
|
||||||
}, [data?.authStatus]);
|
}, [data?.authStatus, accountStatsMap]);
|
||||||
|
|
||||||
const overallSuccessRate =
|
const overallSuccessRate =
|
||||||
totalRequests > 0 ? Math.round((totalSuccess / totalRequests) * 100) : 100;
|
totalRequests > 0 ? Math.round((totalSuccess / totalRequests) * 100) : 100;
|
||||||
@@ -154,7 +164,7 @@ export function AuthMonitor() {
|
|||||||
? providerStats.find((ps) => ps.provider === selectedProvider)
|
? providerStats.find((ps) => ps.provider === selectedProvider)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading || statsLoading) {
|
||||||
return (
|
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="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">
|
<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
|
// Use real profile data directly
|
||||||
const mockProfiles = profiles.map((profile, index: number) => ({
|
const normalizedProfiles = profiles.map((profile) => ({
|
||||||
...profile,
|
...profile,
|
||||||
configured: profile.configured ?? false, // Ensure configured is always boolean
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-lg font-semibold">Profiles</h2>
|
<h2 className="text-lg font-semibold">Profiles</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{mockProfiles.map((profile) => (
|
{normalizedProfiles.map((profile) => (
|
||||||
<ProfileCard
|
<ProfileCard
|
||||||
key={profile.name}
|
key={profile.name}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
|
|||||||
@@ -4,9 +4,25 @@
|
|||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
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 */
|
/** CLIProxy usage statistics */
|
||||||
export interface CliproxyStats {
|
export interface CliproxyStats {
|
||||||
totalRequests: number;
|
totalRequests: number;
|
||||||
|
successCount: number;
|
||||||
|
failureCount: number;
|
||||||
tokens: {
|
tokens: {
|
||||||
input: number;
|
input: number;
|
||||||
output: number;
|
output: number;
|
||||||
@@ -14,6 +30,8 @@ export interface CliproxyStats {
|
|||||||
};
|
};
|
||||||
requestsByModel: Record<string, number>;
|
requestsByModel: Record<string, number>;
|
||||||
requestsByProvider: Record<string, number>;
|
requestsByProvider: Record<string, number>;
|
||||||
|
/** Per-account usage breakdown */
|
||||||
|
accountStats: Record<string, AccountUsageStats>;
|
||||||
quotaExceededCount: number;
|
quotaExceededCount: number;
|
||||||
retryCount: number;
|
retryCount: number;
|
||||||
collectedAt: string;
|
collectedAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user