fix(usage): include auth-index-only usage in credential stats

This commit is contained in:
Supra4E8C
2026-02-13 15:21:16 +08:00
parent 705e6dac54
commit c53a231c41

View File

@@ -1,7 +1,12 @@
import { useMemo, useState, useEffect } from 'react'; import { useMemo, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { computeKeyStats, collectUsageDetails, buildCandidateUsageSourceIds, formatCompactNumber } from '@/utils/usage'; import {
computeKeyStats,
collectUsageDetails,
buildCandidateUsageSourceIds,
formatCompactNumber
} from '@/utils/usage';
import { authFilesApi } from '@/services/api/authFiles'; import { authFilesApi } from '@/services/api/authFiles';
import type { GeminiKeyConfig, ProviderKeyConfig, OpenAIProviderConfig } from '@/types'; import type { GeminiKeyConfig, ProviderKeyConfig, OpenAIProviderConfig } from '@/types';
import type { AuthFileItem } from '@/types/authFile'; import type { AuthFileItem } from '@/types/authFile';
@@ -33,6 +38,11 @@ interface CredentialRow {
successRate: number; successRate: number;
} }
interface CredentialBucket {
success: number;
failure: number;
}
function normalizeAuthIndexValue(value: unknown): string | null { function normalizeAuthIndexValue(value: unknown): string | null {
if (typeof value === 'number' && Number.isFinite(value)) { if (typeof value === 'number' && Number.isFinite(value)) {
return value.toString(); return value.toString();
@@ -87,8 +97,21 @@ export function CredentialStatsCard({
const rows = useMemo((): CredentialRow[] => { const rows = useMemo((): CredentialRow[] => {
if (!usage) return []; if (!usage) return [];
const { bySource } = computeKeyStats(usage); const { bySource } = computeKeyStats(usage);
const details = collectUsageDetails(usage);
const result: CredentialRow[] = []; const result: CredentialRow[] = [];
const consumedSourceIds = new Set<string>(); const consumedSourceIds = new Set<string>();
const authIndexToRowIndex = new Map<string, number>();
const sourceToAuthIndex = new Map<string, string>();
const fallbackByAuthIndex = new Map<string, CredentialBucket>();
const mergeBucketToRow = (index: number, bucket: CredentialBucket) => {
const target = result[index];
if (!target) return;
target.success += bucket.success;
target.failure += bucket.failure;
target.total = target.success + target.failure;
target.successRate = target.total > 0 ? (target.success / target.total) * 100 : 100;
};
// Aggregate all candidate source IDs for one provider config into a single row // Aggregate all candidate source IDs for one provider config into a single row
const addConfigRow = ( const addConfigRow = (
@@ -140,26 +163,38 @@ export function CredentialStatsCard({
}); });
// Build source → auth file name mapping for remaining unmatched entries. // Build source → auth file name mapping for remaining unmatched entries.
// Cross-reference via usage details: each detail has both source and auth_index. // Also collect fallback stats for details without source but with auth_index.
const sourceToAuthFile = new Map<string, CredentialInfo>(); const sourceToAuthFile = new Map<string, CredentialInfo>();
if (authFileMap.size > 0) { details.forEach((d) => {
const details = collectUsageDetails(usage); const authIdx = normalizeAuthIndexValue(d.auth_index);
details.forEach((d) => { if (!d.source) {
if (consumedSourceIds.has(d.source) || sourceToAuthFile.has(d.source)) return; if (!authIdx) return;
const authIdx = normalizeAuthIndexValue(d.auth_index); const fallback = fallbackByAuthIndex.get(authIdx) ?? { success: 0, failure: 0 };
if (authIdx) { if (d.failed === true) {
const mapped = authFileMap.get(authIdx); fallback.failure += 1;
if (mapped) sourceToAuthFile.set(d.source, mapped); } else {
fallback.success += 1;
} }
}); fallbackByAuthIndex.set(authIdx, fallback);
} return;
}
if (!authIdx || consumedSourceIds.has(d.source)) return;
if (!sourceToAuthIndex.has(d.source)) {
sourceToAuthIndex.set(d.source, authIdx);
}
if (!sourceToAuthFile.has(d.source)) {
const mapped = authFileMap.get(authIdx);
if (mapped) sourceToAuthFile.set(d.source, mapped);
}
});
// Remaining unmatched bySource entries — resolve name from auth files if possible // Remaining unmatched bySource entries — resolve name from auth files if possible
Object.entries(bySource).forEach(([key, bucket]) => { Object.entries(bySource).forEach(([key, bucket]) => {
if (consumedSourceIds.has(key)) return; if (consumedSourceIds.has(key)) return;
const total = bucket.success + bucket.failure; const total = bucket.success + bucket.failure;
const authFile = sourceToAuthFile.get(key); const authFile = sourceToAuthFile.get(key);
result.push({ const row = {
key, key,
displayName: authFile?.name || (key.startsWith('t:') ? key.slice(2) : key), displayName: authFile?.name || (key.startsWith('t:') ? key.slice(2) : key),
type: authFile?.type || '', type: authFile?.type || '',
@@ -167,7 +202,46 @@ export function CredentialStatsCard({
failure: bucket.failure, failure: bucket.failure,
total, total,
successRate: total > 0 ? (bucket.success / total) * 100 : 100, successRate: total > 0 ? (bucket.success / total) * 100 : 100,
}); };
const rowIndex = result.push(row) - 1;
const authIdx = sourceToAuthIndex.get(key);
if (authIdx && !authIndexToRowIndex.has(authIdx)) {
authIndexToRowIndex.set(authIdx, rowIndex);
}
});
// Include requests that have auth_index but missing source.
fallbackByAuthIndex.forEach((bucket, authIdx) => {
if (bucket.success + bucket.failure === 0) return;
const mapped = authFileMap.get(authIdx);
let targetRowIndex = authIndexToRowIndex.get(authIdx);
if (targetRowIndex === undefined && mapped) {
const matchedIndex = result.findIndex(
(row) => row.displayName === mapped.name && row.type === mapped.type
);
if (matchedIndex >= 0) {
targetRowIndex = matchedIndex;
authIndexToRowIndex.set(authIdx, matchedIndex);
}
}
if (targetRowIndex !== undefined) {
mergeBucketToRow(targetRowIndex, bucket);
return;
}
const total = bucket.success + bucket.failure;
const rowIndex = result.push({
key: `auth:${authIdx}`,
displayName: mapped?.name || authIdx,
type: mapped?.type || '',
success: bucket.success,
failure: bucket.failure,
total,
successRate: (bucket.success / total) * 100
}) - 1;
authIndexToRowIndex.set(authIdx, rowIndex);
}); });
return result.sort((a, b) => b.total - a.total); return result.sort((a, b) => b.total - a.total);