Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 174 additions & 7 deletions src/features/monitoring/hooks/useMonitoringData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { authFilesApi } from '@/services/api/authFiles';
import { apiClient } from '@/services/api/client';
import type { AuthFileItem } from '@/types/authFile';
import type { Config } from '@/types/config';
import type { UsageServiceApiKeyMapItem } from '@/services/api/usageService';
import type { CredentialInfo } from '@/types/sourceInfo';
import { buildSourceInfoMap, resolveSourceDisplay } from '@/utils/sourceResolver';
import {
Expand Down Expand Up @@ -317,6 +318,9 @@ export type MonitoringEventRow = {
authIndex: string;
authIndexMasked: string;
authLabel: string;
apiKeyHash: string;
apiKeyLabel: string;
apiKeyMasked: string;
provider: string;
planType: string;
channel: string;
Expand Down Expand Up @@ -395,6 +399,28 @@ export type MonitoringAccountRow = {
models: MonitoringAccountModelSpendRow[];
};

export type MonitoringApiKeyRow = {
id: string;
apiKeyHash: string;
apiKeyLabel: string;
apiKeyMasked: string;
authLabels: string[];
accounts: string[];
channels: string[];
totalCalls: number;
successCalls: number;
failureCalls: number;
successRate: number;
inputTokens: number;
outputTokens: number;
cachedTokens: number;
totalTokens: number;
totalCost: number;
averageLatencyMs: number | null;
lastSeenAt: number;
recentPattern: boolean[];
};

export type MonitoringRealtimeRow = {
id: string;
account: string;
Expand Down Expand Up @@ -434,6 +460,7 @@ export type MonitoringMetadata = {

export interface UseMonitoringDataParams {
usage: unknown;
apiKeyMap: UsageServiceApiKeyMapItem[];
config: Config | null | undefined;
modelPrices: Record<string, ModelPrice>;
timeRange: MonitoringTimeRange;
Expand All @@ -458,6 +485,7 @@ export interface UseMonitoringDataReturn {
failureSourceRows: MonitoringFailureSourceRow[];
taskBuckets: MonitoringTaskBucketRow[];
recentFailures: MonitoringFailureRow[];
apiKeyRows: MonitoringApiKeyRow[];
filteredRows: MonitoringEventRow[];
refreshMeta: (showLoading?: boolean) => Promise<void>;
}
Expand Down Expand Up @@ -640,6 +668,13 @@ const buildHourlyDistribution = (rows: MonitoringEventRow[]) => {
return buckets;
};

const maskClientApiKey = (value: string) => {
const trimmed = value.trim();
if (!trimmed) return '-';
if (trimmed.length <= 10) return trimmed;
return `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
};

const buildRecentPattern = (rows: MonitoringEventRow[], limit = 10) =>
rows
.slice()
Expand Down Expand Up @@ -709,6 +744,107 @@ export const buildMonitoringSummary = (rows: MonitoringEventRow[]): MonitoringSu
};
};

export const buildApiKeyRows = (rows: MonitoringEventRow[]): MonitoringApiKeyRow[] => {
const grouped = new Map<
string,
{
id: string;
apiKeyHash: string;
apiKeyLabel: string;
apiKeyMasked: string;
authLabels: Set<string>;
accounts: Set<string>;
channels: Set<string>;
rows: MonitoringEventRow[];
totalCalls: number;
successCalls: number;
failureCalls: number;
inputTokens: number;
outputTokens: number;
cachedTokens: number;
totalTokens: number;
totalCost: number;
latencySum: number;
latencyCount: number;
lastSeenAt: number;
}
>();

rows.forEach((row) => {
const apiKeyHash = row.apiKeyHash || '-';
const existing = grouped.get(apiKeyHash) ?? {
id: apiKeyHash,
apiKeyHash,
apiKeyLabel: row.apiKeyLabel || '-',
apiKeyMasked: row.apiKeyMasked || '-',
authLabels: new Set<string>(),
accounts: new Set<string>(),
channels: new Set<string>(),
rows: [] as MonitoringEventRow[],
totalCalls: 0,
successCalls: 0,
failureCalls: 0,
inputTokens: 0,
outputTokens: 0,
cachedTokens: 0,
totalTokens: 0,
totalCost: 0,
latencySum: 0,
latencyCount: 0,
lastSeenAt: 0,
};

existing.rows.push(row);
existing.authLabels.add(row.authLabel);
existing.accounts.add(row.accountMasked || row.account || '-');
existing.channels.add(row.channel);
existing.totalCalls += 1;
existing.successCalls += row.failed ? 0 : 1;
existing.failureCalls += row.failed ? 1 : 0;
existing.inputTokens += row.inputTokens;
existing.outputTokens += row.outputTokens;
existing.cachedTokens += row.cachedTokens;
existing.totalTokens += row.totalTokens;
existing.totalCost += row.totalCost;
existing.lastSeenAt = Math.max(existing.lastSeenAt, row.timestampMs);
if (row.latencyMs !== null) {
existing.latencySum += row.latencyMs;
existing.latencyCount += 1;
}

grouped.set(apiKeyHash, existing);
});

return Array.from(grouped.values())
.map((item) => ({
id: item.id,
apiKeyHash: item.apiKeyHash,
apiKeyLabel: item.apiKeyLabel,
apiKeyMasked: item.apiKeyMasked,
authLabels: Array.from(item.authLabels).sort(),
accounts: Array.from(item.accounts).sort(),
channels: Array.from(item.channels).sort(),
totalCalls: item.totalCalls,
successCalls: item.successCalls,
failureCalls: item.failureCalls,
successRate: item.totalCalls > 0 ? item.successCalls / item.totalCalls : 1,
inputTokens: item.inputTokens,
outputTokens: item.outputTokens,
cachedTokens: item.cachedTokens,
totalTokens: item.totalTokens,
totalCost: item.totalCost,
averageLatencyMs: item.latencyCount > 0 ? item.latencySum / item.latencyCount : null,
lastSeenAt: item.lastSeenAt,
recentPattern: buildRecentPattern(item.rows),
}))
.sort(
(left, right) =>
right.lastSeenAt - left.lastSeenAt ||
right.totalCalls - left.totalCalls ||
right.totalCost - left.totalCost
);
};

export const buildAccountRows = (rows: MonitoringEventRow[]): MonitoringAccountRow[] => {
const grouped = new Map<
string,
Expand Down Expand Up @@ -1338,7 +1474,8 @@ const buildEventRows = (
authFileMap: Map<string, CredentialInfo>,
sourceInfoMap: ReturnType<typeof buildSourceInfoMap>,
channelByAuthIndex: Map<string, MonitoringChannelMeta>,
modelPrices: Record<string, ModelPrice>
modelPrices: Record<string, ModelPrice>,
apiKeyHashLabelMap: Map<string, string>
) =>
details
.map((detail, index) => {
Expand All @@ -1357,8 +1494,15 @@ const buildEventRows = (
const sourceMasked = maskEmailLike(sourceLabel);
const account = authMeta?.account || sourceLabel;
const accountMasked = maskEmailLike(account);
const apiKeyHash = readString(detail.api_key_hash);
const resolvedApiKey = apiKeyHashLabelMap.get(apiKeyHash) || '';
const apiKeyLabel = resolvedApiKey || (apiKeyHash ? `sha256:${apiKeyHash.slice(0, 12)}` : '-');
const apiKeyMasked = resolvedApiKey ? maskClientApiKey(resolvedApiKey) : apiKeyLabel;
const channelMeta = channelByAuthIndex.get(authIndex);
const channelLabel = channelMeta?.name || authMeta?.provider || sourceMeta.type || '-';
const detailProvider = readString(detail.provider);
const detailAuthType = readString(detail.auth_type);
const resolvedProvider = authMeta?.provider || detailProvider || detailAuthType || sourceMeta.type || '-';
const channelLabel = channelMeta?.name || resolvedProvider || '-';
const endpoint = readString(detail.__endpoint) || '-';
const endpointMethod = readString(detail.__endpointMethod) || '-';
const endpointPath = readString(detail.__endpointPath) || endpoint;
Expand Down Expand Up @@ -1395,7 +1539,10 @@ const buildEventRows = (
authIndex,
authIndexMasked: maskAuthIndex(authIndex),
authLabel: authMeta?.label || sourceMasked,
provider: authMeta?.provider || sourceMeta.type || '-',
apiKeyHash,
apiKeyLabel,
apiKeyMasked,
provider: resolvedProvider,
planType: authMeta?.planType || '-',
channel: channelLabel,
channelHost: channelMeta?.host || '-',
Expand All @@ -1416,6 +1563,8 @@ const buildEventRows = (
authMeta?.account,
authMeta?.label,
authIndex,
apiKeyLabel,
apiKeyMasked,
channelLabel,
channelMeta?.host,
endpointPath,
Expand Down Expand Up @@ -1474,6 +1623,7 @@ const loadMonitoringMetaPayload = async (

export function useMonitoringData({
usage,
apiKeyMap,
config,
modelPrices,
timeRange,
Expand Down Expand Up @@ -1564,12 +1714,27 @@ export function useMonitoringData({
return map;
}, [channels]);

const apiKeyHashLabelMap = useMemo(() => {
const map = new Map<string, string>();
apiKeyMap.forEach((item) => {
if (!item?.apiKeyHash) return;
map.set(item.apiKeyHash, item.apiKeyLabel || item.apiKeyMasked || item.apiKeyHash);
});
return map;
}, [apiKeyMap]);

const allRows = useMemo(() => {
const details = collectUsageDetailsWithEndpoint(usage);
return buildEventRows(details, authMetaMap, authFileMap, sourceInfoMap, channelByAuthIndex, modelPrices).sort(
(left, right) => right.timestampMs - left.timestampMs
);
}, [authFileMap, authMetaMap, channelByAuthIndex, modelPrices, sourceInfoMap, usage]);
return buildEventRows(
details,
authMetaMap,
authFileMap,
sourceInfoMap,
channelByAuthIndex,
modelPrices,
apiKeyHashLabelMap
).sort((left, right) => right.timestampMs - left.timestampMs);
}, [apiKeyHashLabelMap, authFileMap, authMetaMap, channelByAuthIndex, modelPrices, sourceInfoMap, usage]);

const filteredRows = useMemo(
() => buildRangeFilteredRows(allRows, timeRange, customTimeRange, searchQuery),
Expand All @@ -1589,6 +1754,7 @@ export function useMonitoringData({
const failureSourceRows = useMemo(() => buildFailureSourceRows(statsRows), [statsRows]);
const taskBuckets = useMemo(() => buildTaskBuckets(statsRows), [statsRows]);
const recentFailures = useMemo(() => buildFailureRows(statsRows), [statsRows]);
const apiKeyRows = useMemo(() => buildApiKeyRows(statsRows), [statsRows]);

const metadata = useMemo<MonitoringMetadata>(() => {
const planTypes = Array.from(
Expand Down Expand Up @@ -1628,6 +1794,7 @@ export function useMonitoringData({
failureSourceRows,
taskBuckets,
recentFailures,
apiKeyRows,
filteredRows,
refreshMeta,
};
Expand Down
14 changes: 14 additions & 0 deletions src/features/monitoring/hooks/useUsageData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type ModelPriceSyncResponse,
type UsageExportResponse,
type UsageImportResponse,
type UsageServiceApiKeyMapItem,
} from '@/services/api/usageService';
import { useAuthStore, useUsageServiceStore } from '@/stores';
import { detectApiBaseFromLocation } from '@/utils/connection';
Expand All @@ -24,6 +25,7 @@ export interface UsagePayload {

export interface UseUsageDataReturn {
usage: UsagePayload | null;
apiKeyMap: UsageServiceApiKeyMapItem[];
loading: boolean;
error: string;
lastRefreshedAt: Date | null;
Expand All @@ -42,6 +44,7 @@ export function useUsageData(): UseUsageDataReturn {
const usageServiceEnabled = useUsageServiceStore((state) => state.enabled);
const usageServiceBase = useUsageServiceStore((state) => state.serviceBase);
const [usage, setUsage] = useState<UsagePayload | null>(null);
const [apiKeyMap, setApiKeyMap] = useState<UsageServiceApiKeyMapItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [lastRefreshedAt, setLastRefreshedAt] = useState<Date | null>(null);
Expand Down Expand Up @@ -151,8 +154,18 @@ export function useUsageData(): UseUsageDataReturn {
usageServiceEnabled && usageServiceBase
? await usageServiceApi.getUsage(usageServiceBase, managementKey)
: await apiClient.get<UsagePayload>('/usage');
let nextApiKeyMap: UsageServiceApiKeyMapItem[] = [];
if (usageServiceEnabled && usageServiceBase) {
try {
const mapResponse = await usageServiceApi.getApiKeyMap(usageServiceBase, managementKey);
nextApiKeyMap = Array.isArray(mapResponse.items) ? mapResponse.items : [];
} catch {
nextApiKeyMap = [];
}
}
if (requestIdRef.current !== requestId) return;
setUsage(payload ?? null);
setApiKeyMap(nextApiKeyMap);
setLastRefreshedAt(new Date());
} catch (err) {
if (requestIdRef.current !== requestId) return;
Expand Down Expand Up @@ -189,6 +202,7 @@ export function useUsageData(): UseUsageDataReturn {

return {
usage,
apiKeyMap,
loading,
error,
lastRefreshedAt,
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"model_alias_placeholder": "Model alias (optional)",
"invalid_provider_index": "Invalid provider index.",
"unsaved_changes_title": "Unsaved changes",
"unsaved_changes_message": "You have unsaved changes. Leaving now will discard them. Do you want to leave?"
"unsaved_changes_message": "You have unsaved changes. Leaving now will discard them. Do you want to leave?",
"detail": "Detail"
},
"title": {
"main": "CLI Proxy API Management Center",
Expand Down Expand Up @@ -1190,6 +1191,8 @@
"clear_account_focus": "Clear Account Focus",
"account_overview_title": "Account Overview",
"account_overview_desc": "Realtime account-level totals for calls, success and failure, token structure, and spend, with expandable model cost details and live Codex quota fetches.",
"api_key_overview_title": "Client API Key Overview",
"api_key_overview_desc": "Break down calls, success and failure, token structure, and spend by CLIProxyAPI top-level api-keys.",
"codex_inspection_entry": "Codex Account Inspection",
"codex_inspection_eyebrow": "Codex Account Inspection",
"codex_inspection_title": "Codex Account Inspection",
Expand Down Expand Up @@ -1298,6 +1301,8 @@
"codex_inspection_logs_clear": "Clear logs",
"codex_inspection_logs_jump_latest": "Jump to latest",
"codex_inspection_action_total": "Suggested",
"codex_inspection_probe_total": "Probe Set",
"codex_inspection_sampled_total": "Sampled This Run",
"codex_inspection_sampled_meta_running": "Total {{total}} · {{percent}}%",
"codex_inspection_sampled_meta_done": "Total {{total}} · Completed",
"codex_inspection_sampled_meta_idle": "Waiting to start",
Expand Down
9 changes: 7 additions & 2 deletions src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"model_alias_placeholder": "模型别名 (可选)",
"invalid_provider_index": "无效的提供商索引。",
"unsaved_changes_title": "未保存的更改",
"unsaved_changes_message": "你有未保存的更改,离开后将丢失这些更改。确定要离开吗?"
"unsaved_changes_message": "你有未保存的更改,离开后将丢失这些更改。确定要离开吗?",
"detail": "详情"
},
"title": {
"main": "CLI Proxy API Management Center",
Expand Down Expand Up @@ -1190,6 +1191,8 @@
"clear_account_focus": "取消账号聚焦",
"account_overview_title": "账号汇总",
"account_overview_desc": "按账号实时汇总调用次数、成功失败、Token 结构和总花费,可展开查看模型花费明细,并拉取该账号的 Codex 配额。",
"api_key_overview_title": "客户端密钥汇总",
"api_key_overview_desc": "按 CLIProxyAPI 顶层 api-keys 统计调用次数、成功失败、Token 结构与总花费。",
"codex_inspection_entry": "Codex 账号巡检",
"codex_inspection_eyebrow": "Codex Account Inspection",
"codex_inspection_title": "Codex 账号巡检",
Expand Down Expand Up @@ -1363,7 +1366,9 @@
"pagination_next": "下一页",
"pagination_info": "第 {{current}} / {{total}} 页 · 显示 {{start}}-{{end}} / {{count}}",
"page_size_label": "每页",
"page_size_option": "{{count}} 条/页"
"page_size_option": "{{count}} 条/页",
"codex_inspection_probe_total": "巡检集合",
"codex_inspection_sampled_total": "本次探测"
},
"logs": {
"title": "日志查看",
Expand Down
Loading