Skip to content

Commit 5b2cf14

Browse files
authored
Merge pull request #104 from kaitranntt/kai/feat/auth-monitor-design
feat(ui): auth monitor with real-time account flow visualization
2 parents 8ec7e99 + 197848a commit 5b2cf14

19 files changed

+1916
-273
lines changed

src/cliproxy/account-manager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,12 +473,14 @@ export function discoverExistingAccounts(): void {
473473
const stats = fs.statSync(filePath);
474474

475475
// Register account with auto-generated nickname
476+
// Use mtime as lastUsedAt (when token was last modified = last auth/refresh)
477+
const lastModified = stats.mtime || stats.birthtime || new Date();
476478
providerAccounts.accounts[accountId] = {
477479
email,
478480
nickname: generateNickname(email),
479481
tokenFile: file,
480482
createdAt: stats.birthtime?.toISOString() || new Date().toISOString(),
481-
lastUsedAt: stats.mtime?.toISOString(),
483+
lastUsedAt: lastModified.toISOString(),
482484
};
483485
} catch {
484486
// Skip invalid files

src/cliproxy/stats-fetcher.ts

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,28 @@
77

88
import { CCS_CONTROL_PANEL_SECRET, CLIPROXY_DEFAULT_PORT } from './config-generator';
99

10+
/** Per-account usage statistics */
11+
export interface AccountUsageStats {
12+
/** Account email or identifier */
13+
source: string;
14+
/** Number of successful requests */
15+
successCount: number;
16+
/** Number of failed requests */
17+
failureCount: number;
18+
/** Total tokens used */
19+
totalTokens: number;
20+
/** Last request timestamp */
21+
lastUsedAt?: string;
22+
}
23+
1024
/** Usage statistics from CLIProxyAPI */
1125
export interface CliproxyStats {
1226
/** Total number of requests processed */
1327
totalRequests: number;
28+
/** Total successful requests */
29+
successCount: number;
30+
/** Total failed requests */
31+
failureCount: number;
1432
/** Token counts */
1533
tokens: {
1634
input: number;
@@ -21,6 +39,8 @@ export interface CliproxyStats {
2139
requestsByModel: Record<string, number>;
2240
/** Requests grouped by provider */
2341
requestsByProvider: Record<string, number>;
42+
/** Per-account usage breakdown */
43+
accountStats: Record<string, AccountUsageStats>;
2444
/** Number of quota exceeded (429) events */
2545
quotaExceededCount: number;
2646
/** Number of request retries */
@@ -29,6 +49,21 @@ export interface CliproxyStats {
2949
collectedAt: string;
3050
}
3151

52+
/** Request detail from CLIProxyAPI */
53+
interface RequestDetail {
54+
timestamp: string;
55+
source: string;
56+
auth_index: number;
57+
tokens: {
58+
input_tokens: number;
59+
output_tokens: number;
60+
reasoning_tokens: number;
61+
cached_tokens: number;
62+
total_tokens: number;
63+
};
64+
failed: boolean;
65+
}
66+
3267
/** Usage API response from CLIProxyAPI /v0/management/usage endpoint */
3368
interface UsageApiResponse {
3469
failed_requests?: number;
@@ -47,6 +82,7 @@ interface UsageApiResponse {
4782
{
4883
total_requests?: number;
4984
total_tokens?: number;
85+
details?: RequestDetail[];
5086
}
5187
>;
5288
}
@@ -83,16 +119,55 @@ export async function fetchCliproxyStats(
83119
const data = (await response.json()) as UsageApiResponse;
84120
const usage = data.usage;
85121

86-
// Extract models and providers from the nested API structure
122+
// Extract models, providers, and per-account stats from the nested API structure
87123
const requestsByModel: Record<string, number> = {};
88124
const requestsByProvider: Record<string, number> = {};
125+
const accountStats: Record<string, AccountUsageStats> = {};
126+
let totalSuccessCount = 0;
127+
let totalFailureCount = 0;
128+
let totalInputTokens = 0;
129+
let totalOutputTokens = 0;
89130

90131
if (usage?.apis) {
91132
for (const [provider, providerData] of Object.entries(usage.apis)) {
92133
requestsByProvider[provider] = providerData.total_requests ?? 0;
93134
if (providerData.models) {
94135
for (const [model, modelData] of Object.entries(providerData.models)) {
95136
requestsByModel[model] = modelData.total_requests ?? 0;
137+
138+
// Aggregate per-account stats from request details
139+
if (modelData.details) {
140+
for (const detail of modelData.details) {
141+
const source = detail.source || 'unknown';
142+
143+
// Initialize account stats if not exists
144+
if (!accountStats[source]) {
145+
accountStats[source] = {
146+
source,
147+
successCount: 0,
148+
failureCount: 0,
149+
totalTokens: 0,
150+
};
151+
}
152+
153+
// Update account stats
154+
if (detail.failed) {
155+
accountStats[source].failureCount++;
156+
totalFailureCount++;
157+
} else {
158+
accountStats[source].successCount++;
159+
totalSuccessCount++;
160+
}
161+
162+
const tokens = detail.tokens?.total_tokens ?? 0;
163+
accountStats[source].totalTokens += tokens;
164+
accountStats[source].lastUsedAt = detail.timestamp;
165+
166+
// Aggregate token breakdowns
167+
totalInputTokens += detail.tokens?.input_tokens ?? 0;
168+
totalOutputTokens += detail.tokens?.output_tokens ?? 0;
169+
}
170+
}
96171
}
97172
}
98173
}
@@ -101,13 +176,16 @@ export async function fetchCliproxyStats(
101176
// Normalize the response to our interface
102177
return {
103178
totalRequests: usage?.total_requests ?? 0,
179+
successCount: totalSuccessCount,
180+
failureCount: totalFailureCount,
104181
tokens: {
105-
input: 0, // API doesn't provide input/output breakdown
106-
output: 0,
182+
input: totalInputTokens,
183+
output: totalOutputTokens,
107184
total: usage?.total_tokens ?? 0,
108185
},
109186
requestsByModel,
110187
requestsByProvider,
188+
accountStats,
111189
quotaExceededCount: usage?.failure_count ?? data.failed_requests ?? 0,
112190
retryCount: 0, // API doesn't track retries separately
113191
collectedAt: new Date().toISOString(),

src/web-server/routes.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
getProviderAccounts,
3737
setDefaultAccount as setDefaultAccountFn,
3838
removeAccount as removeAccountFn,
39+
touchAccount,
3940
} from '../cliproxy/account-manager';
4041
import type { CLIProxyProvider } from '../cliproxy/types';
4142
import { getClaudeEnvVars } from '../cliproxy/config-generator';
@@ -469,11 +470,43 @@ apiRoutes.delete('/cliproxy/:name', (req: Request, res: Response): void => {
469470

470471
/**
471472
* GET /api/cliproxy/auth - Get auth status for built-in CLIProxy profiles
473+
* Also fetches CLIProxyAPI stats to update lastUsedAt for active providers
472474
*/
473-
apiRoutes.get('/cliproxy/auth', (_req: Request, res: Response) => {
475+
apiRoutes.get('/cliproxy/auth', async (_req: Request, res: Response) => {
474476
// Initialize accounts from existing tokens on first request
475477
initializeAccounts();
476478

479+
// Fetch CLIProxyAPI usage stats to determine active providers
480+
const stats = await fetchCliproxyStats();
481+
482+
// Map CLIProxyAPI provider names to our internal provider names
483+
const statsProviderMap: Record<string, CLIProxyProvider> = {
484+
gemini: 'gemini',
485+
antigravity: 'agy',
486+
codex: 'codex',
487+
qwen: 'qwen',
488+
iflow: 'iflow',
489+
};
490+
491+
// Update lastUsedAt for providers with recent activity
492+
if (stats?.requestsByProvider) {
493+
for (const [statsProvider, requestCount] of Object.entries(stats.requestsByProvider)) {
494+
if (requestCount > 0) {
495+
const provider = statsProviderMap[statsProvider.toLowerCase()];
496+
if (provider) {
497+
// Touch the default account for this provider (or all accounts)
498+
const accounts = getProviderAccounts(provider);
499+
for (const account of accounts) {
500+
// Only touch if this is the default account (most likely being used)
501+
if (account.isDefault) {
502+
touchAccount(provider, account.id);
503+
}
504+
}
505+
}
506+
}
507+
}
508+
}
509+
477510
const statuses = getAllAuthStatus();
478511

479512
const authStatus = statuses.map((status) => {

0 commit comments

Comments
 (0)