fix(cliproxy): restore live monitor provider attribution

This commit is contained in:
Tam Nhu Tran
2026-03-26 17:18:47 -04:00
parent 7f84308a41
commit 368a625d92
4 changed files with 236 additions and 50 deletions
+48 -26
View File
@@ -64,7 +64,7 @@ export interface CliproxyStats {
export interface CliproxyRequestDetail {
timestamp: string;
source: string;
auth_index: number;
auth_index: string | number;
tokens: {
input_tokens: number;
output_tokens: number;
@@ -104,6 +104,14 @@ export interface CliproxyUsageApiResponse {
};
}
/** Auth file metadata from CLIProxyAPI /v0/management/auth-files */
export interface CliproxyManagementAuthFile {
auth_index?: string | number;
provider?: string;
email?: string;
name?: string;
}
/**
* Fetch usage statistics from CLIProxyAPI management API
* @param port CLIProxyAPI port (default: 8317)
@@ -111,35 +119,16 @@ export interface CliproxyUsageApiResponse {
*/
export async function fetchCliproxyStats(port?: number): Promise<CliproxyStats | null> {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000); // 3s timeout
const [data, authFiles] = await Promise.all([
fetchCliproxyUsageRaw(port),
fetchCliproxyAuthFiles(port),
]);
// Dynamic target resolution
const target = getProxyTarget();
// Allow port override for local testing only
if (port !== undefined && !target.isRemote) {
target.port = port;
}
const url = buildProxyUrl(target, '/v0/management/usage');
// For management endpoints, use management key for remote, local management secret for local
const headers = target.isRemote
? buildManagementHeaders(target)
: { Accept: 'application/json', Authorization: `Bearer ${getEffectiveManagementSecret()}` };
const response = await fetch(url, {
signal: controller.signal,
headers,
});
clearTimeout(timeoutId);
if (!response.ok) {
if (!data) {
return null;
}
const data = (await response.json()) as CliproxyUsageApiResponse;
return buildCliproxyStatsFromUsageResponse(data);
return buildCliproxyStatsFromUsageResponse(data, { authFiles: authFiles ?? [] });
} catch {
// CLIProxyAPI not running or stats endpoint not available
return null;
@@ -184,6 +173,39 @@ export async function fetchCliproxyUsageRaw(
}
}
async function fetchCliproxyAuthFiles(port?: number): Promise<CliproxyManagementAuthFile[] | null> {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const target = getProxyTarget();
if (port !== undefined && !target.isRemote) {
target.port = port;
}
const url = buildProxyUrl(target, '/v0/management/auth-files');
const headers = target.isRemote
? buildManagementHeaders(target)
: { Accept: 'application/json', Authorization: `Bearer ${getEffectiveManagementSecret()}` };
const response = await fetch(url, {
signal: controller.signal,
headers,
});
clearTimeout(timeoutId);
if (!response.ok) {
return null;
}
const data = (await response.json()) as { files?: CliproxyManagementAuthFile[] };
return Array.isArray(data.files) ? data.files : null;
} catch {
return null;
}
}
/** OpenAI-compatible model object from /v1/models endpoint */
export interface CliproxyModel {
id: string;
+97 -8
View File
@@ -1,20 +1,97 @@
import { buildQualifiedAccountStatsKey } from './account-stats-key';
import type { AccountUsageStats, CliproxyStats, CliproxyUsageApiResponse } from './stats-fetcher';
import { mapExternalProviderName } from './provider-capabilities';
import type {
AccountUsageStats,
CliproxyManagementAuthFile,
CliproxyRequestDetail,
CliproxyStats,
CliproxyUsageApiResponse,
} from './stats-fetcher';
export function buildCliproxyStatsFromUsageResponse(data: CliproxyUsageApiResponse): CliproxyStats {
interface BuildCliproxyStatsOptions {
authFiles?: CliproxyManagementAuthFile[];
}
interface ResolvedAuthFile {
provider: string;
source?: string;
}
function normalizeProvider(provider: string): string {
const normalized = provider.trim().toLowerCase();
if (!normalized) {
return 'unknown';
}
return mapExternalProviderName(normalized) ?? normalized;
}
function buildAuthIndexLookup(
authFiles: CliproxyManagementAuthFile[] | undefined
): ReadonlyMap<string, ResolvedAuthFile> {
const lookup = new Map<string, ResolvedAuthFile>();
for (const authFile of authFiles ?? []) {
if (authFile.auth_index === undefined || authFile.auth_index === null || !authFile.provider) {
continue;
}
lookup.set(String(authFile.auth_index), {
provider: normalizeProvider(authFile.provider),
source: authFile.email?.trim() || authFile.name?.trim() || undefined,
});
}
return lookup;
}
function resolveProviderForDetail(
usageProvider: string,
detail: CliproxyRequestDetail,
authIndexLookup: ReadonlyMap<string, ResolvedAuthFile>
): string {
const resolvedAuthFile = authIndexLookup.get(String(detail.auth_index));
if (resolvedAuthFile?.provider) {
return resolvedAuthFile.provider;
}
return normalizeProvider(usageProvider);
}
function resolveSourceForDetail(
detail: CliproxyRequestDetail,
authIndexLookup: ReadonlyMap<string, ResolvedAuthFile>
): string {
const source = detail.source?.trim();
if (source) {
return source;
}
return authIndexLookup.get(String(detail.auth_index))?.source ?? 'unknown';
}
export function buildCliproxyStatsFromUsageResponse(
data: CliproxyUsageApiResponse,
options: BuildCliproxyStatsOptions = {}
): CliproxyStats {
const usage = data.usage;
const requestsByModel: Record<string, number> = {};
const requestsByProvider: Record<string, number> = {};
const accountStats: Record<string, AccountUsageStats> = {};
const authIndexLookup = buildAuthIndexLookup(options.authFiles);
let totalSuccessCount = 0;
let totalFailureCount = 0;
let totalInputTokens = 0;
let totalOutputTokens = 0;
let sawAnyDetail = false;
if (usage?.apis) {
for (const [provider, providerData] of Object.entries(usage.apis)) {
requestsByProvider[provider] = providerData.total_requests ?? 0;
let sawProviderDetail = false;
if (!providerData.models) {
const normalizedProvider = normalizeProvider(provider);
requestsByProvider[normalizedProvider] =
(requestsByProvider[normalizedProvider] ?? 0) + (providerData.total_requests ?? 0);
continue;
}
@@ -25,13 +102,17 @@ export function buildCliproxyStatsFromUsageResponse(data: CliproxyUsageApiRespon
}
for (const detail of modelData.details) {
const source = detail.source || 'unknown';
const accountKey = buildQualifiedAccountStatsKey(provider, source);
sawAnyDetail = true;
sawProviderDetail = true;
const source = resolveSourceForDetail(detail, authIndexLookup);
const resolvedProvider = resolveProviderForDetail(provider, detail, authIndexLookup);
const accountKey = buildQualifiedAccountStatsKey(resolvedProvider, source);
requestsByProvider[resolvedProvider] = (requestsByProvider[resolvedProvider] ?? 0) + 1;
if (!accountStats[accountKey]) {
accountStats[accountKey] = {
accountKey,
provider,
provider: resolvedProvider,
source,
successCount: 0,
failureCount: 0,
@@ -54,13 +135,21 @@ export function buildCliproxyStatsFromUsageResponse(data: CliproxyUsageApiRespon
totalOutputTokens += detail.tokens?.output_tokens ?? 0;
}
}
if (!sawProviderDetail) {
const normalizedProvider = normalizeProvider(provider);
requestsByProvider[normalizedProvider] =
(requestsByProvider[normalizedProvider] ?? 0) + (providerData.total_requests ?? 0);
}
}
}
return {
totalRequests: usage?.total_requests ?? 0,
successCount: totalSuccessCount,
failureCount: totalFailureCount,
successCount: sawAnyDetail ? totalSuccessCount : (usage?.success_count ?? 0),
failureCount: sawAnyDetail
? totalFailureCount
: (usage?.failure_count ?? data.failed_requests ?? 0),
tokens: {
input: totalInputTokens,
output: totalOutputTokens,
+2 -15
View File
@@ -51,7 +51,7 @@ import {
normalizeKiroAuthMethod,
toKiroManagementMethod,
} from '../../cliproxy/auth/auth-types';
import { getOAuthFlowType } from '../../cliproxy/provider-capabilities';
import { getOAuthFlowType, mapExternalProviderName } from '../../cliproxy/provider-capabilities';
import type { CLIProxyProvider } from '../../cliproxy/types';
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
import {
@@ -294,24 +294,11 @@ router.get('/', async (_req: Request, res: Response): Promise<void> => {
// Fetch CLIProxyAPI usage stats to determine active providers
const stats = await fetchCliproxyStats();
// Map CLIProxyAPI provider names to our internal provider names
const statsProviderMap: Record<string, CLIProxyProvider> = {
gemini: 'gemini',
antigravity: 'agy',
codex: 'codex',
qwen: 'qwen',
iflow: 'iflow',
kiro: 'kiro',
copilot: 'ghcp', // CLIProxyAPI returns 'copilot', we map to 'ghcp'
anthropic: 'claude', // CLIProxyAPI returns 'anthropic', we map to 'claude'
claude: 'claude',
};
// Update lastUsedAt for providers with recent activity
if (stats?.requestsByProvider) {
for (const [statsProvider, requestCount] of Object.entries(stats.requestsByProvider)) {
if (requestCount > 0) {
const provider = statsProviderMap[statsProvider.toLowerCase()];
const provider = mapExternalProviderName(statsProvider.toLowerCase());
if (provider) {
// Touch the default account for this provider (or all accounts)
const accounts = getProviderAccounts(provider);
+89 -1
View File
@@ -1,5 +1,8 @@
import { describe, expect, it } from 'bun:test';
import type { CliproxyUsageApiResponse } from '../../../src/cliproxy/stats-fetcher';
import type {
CliproxyManagementAuthFile,
CliproxyUsageApiResponse,
} from '../../../src/cliproxy/stats-fetcher';
import { buildCliproxyStatsFromUsageResponse } from '../../../src/cliproxy/stats-transformer';
describe('buildCliproxyStatsFromUsageResponse', () => {
@@ -121,4 +124,89 @@ describe('buildCliproxyStatsFromUsageResponse', () => {
expect(stats.failureCount).toBe(2);
expect(stats.requestsByProvider).toEqual({ codex: 3, gemini: 2 });
});
it('resolves canonical providers from auth_index when usage is internally bucketed', () => {
const usage: CliproxyUsageApiResponse = {
usage: {
total_requests: 3,
success_count: 2,
failure_count: 1,
apis: {
'ccs-internal-managed': {
total_requests: 3,
models: {
'gpt-5': {
total_requests: 3,
details: [
{
timestamp: '2026-03-26T10:00:00.000Z',
source: 'shared@example.com',
auth_index: 'codex-1',
tokens: {
input_tokens: 10,
output_tokens: 5,
reasoning_tokens: 0,
cached_tokens: 0,
total_tokens: 15,
},
failed: false,
},
{
timestamp: '2026-03-26T10:01:00.000Z',
source: 'shared@example.com',
auth_index: 'gemini-1',
tokens: {
input_tokens: 12,
output_tokens: 7,
reasoning_tokens: 0,
cached_tokens: 0,
total_tokens: 19,
},
failed: false,
},
{
timestamp: '2026-03-26T10:02:00.000Z',
source: 'shared@example.com',
auth_index: 'agy-1',
tokens: {
input_tokens: 8,
output_tokens: 2,
reasoning_tokens: 0,
cached_tokens: 0,
total_tokens: 10,
},
failed: true,
},
],
},
},
},
},
},
};
const authFiles: CliproxyManagementAuthFile[] = [
{ auth_index: 'codex-1', provider: 'codex', email: 'shared@example.com' },
{ auth_index: 'gemini-1', provider: 'gemini-cli', email: 'shared@example.com' },
{ auth_index: 'agy-1', provider: 'antigravity', email: 'shared@example.com' },
];
const stats = buildCliproxyStatsFromUsageResponse(usage, { authFiles });
expect(stats.accountStats['codex:shared@example.com']).toMatchObject({
provider: 'codex',
successCount: 1,
failureCount: 0,
});
expect(stats.accountStats['gemini:shared@example.com']).toMatchObject({
provider: 'gemini',
successCount: 1,
failureCount: 0,
});
expect(stats.accountStats['agy:shared@example.com']).toMatchObject({
provider: 'agy',
successCount: 0,
failureCount: 1,
});
expect(stats.requestsByProvider).toEqual({ codex: 1, gemini: 1, agy: 1 });
});
});