Compare commits

..

2 Commits

Author SHA1 Message Date
hkfires
034c086e31 feat(usage): show per-model success/failure counts 2026-01-25 11:29:34 +08:00
LTbinglingfeng
76e9eb4aa0 feat(auth-files): add disabled state styling for file cards 2026-01-25 00:01:15 +08:00
5 changed files with 74 additions and 18 deletions

View File

@@ -6,6 +6,8 @@ import styles from '@/pages/UsagePage.module.scss';
export interface ModelStat { export interface ModelStat {
model: string; model: string;
requests: number; requests: number;
successCount: number;
failureCount: number;
tokens: number; tokens: number;
cost: number; cost: number;
} }
@@ -38,7 +40,15 @@ export function ModelStatsCard({ modelStats, loading, hasPrices }: ModelStatsCar
{modelStats.map((stat) => ( {modelStats.map((stat) => (
<tr key={stat.model}> <tr key={stat.model}>
<td className={styles.modelCell}>{stat.model}</td> <td className={styles.modelCell}>{stat.model}</td>
<td>{stat.requests.toLocaleString()}</td> <td>
<span className={styles.requestCountCell}>
<span>{stat.requests.toLocaleString()}</span>
<span className={styles.requestBreakdown}>
(<span className={styles.statSuccess}>{stat.successCount.toLocaleString()}</span>{' '}
<span className={styles.statFailure}>{stat.failureCount.toLocaleString()}</span>)
</span>
</span>
</td>
<td>{formatTokensInMillions(stat.tokens)}</td> <td>{formatTokensInMillions(stat.tokens)}</td>
{hasPrices && <td>{stat.cost > 0 ? formatUsd(stat.cost) : '--'}</td>} {hasPrices && <td>{stat.cost > 0 ? formatUsd(stat.cost) : '--'}</td>}
</tr> </tr>

View File

@@ -446,6 +446,16 @@
} }
} }
.fileCardDisabled {
opacity: 0.6;
&:hover {
transform: none;
box-shadow: none;
border-color: var(--border-color);
}
}
.cardHeader { .cardHeader {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -1349,19 +1349,22 @@ export function AuthFilesPage() {
}; };
// 渲染单个认证文件卡片 // 渲染单个认证文件卡片
const renderFileCard = (item: AuthFileItem) => { const renderFileCard = (item: AuthFileItem) => {
const fileStats = resolveAuthFileStats(item, keyStats); const fileStats = resolveAuthFileStats(item, keyStats);
const isRuntimeOnly = isRuntimeOnlyAuthFile(item); const isRuntimeOnly = isRuntimeOnlyAuthFile(item);
const isAistudio = (item.type || '').toLowerCase() === 'aistudio'; const isAistudio = (item.type || '').toLowerCase() === 'aistudio';
const showModelsButton = !isRuntimeOnly || isAistudio; const showModelsButton = !isRuntimeOnly || isAistudio;
const typeColor = getTypeColor(item.type || 'unknown'); const typeColor = getTypeColor(item.type || 'unknown');
return ( return (
<div key={item.name} className={styles.fileCard}> <div
<div className={styles.cardHeader}> key={item.name}
<span className={`${styles.fileCard} ${item.disabled ? styles.fileCardDisabled : ''}`}
className={styles.typeBadge} >
style={{ <div className={styles.cardHeader}>
<span
className={styles.typeBadge}
style={{
backgroundColor: typeColor.bg, backgroundColor: typeColor.bg,
color: typeColor.text, color: typeColor.text,
...(typeColor.border ? { border: typeColor.border } : {}), ...(typeColor.border ? { border: typeColor.border } : {}),

View File

@@ -456,6 +456,18 @@
word-break: break-all; word-break: break-all;
} }
.requestCountCell {
display: inline-flex;
align-items: baseline;
gap: 6px;
font-variant-numeric: tabular-nums;
}
.requestBreakdown {
color: var(--text-secondary);
white-space: nowrap;
}
// Pricing Section (80%比例) // Pricing Section (80%比例)
.pricingSection { .pricingSection {
display: flex; display: flex;

View File

@@ -579,6 +579,8 @@ export function getApiStats(usageData: any, modelPrices: Record<string, ModelPri
export function getModelStats(usageData: any, modelPrices: Record<string, ModelPrice>): Array<{ export function getModelStats(usageData: any, modelPrices: Record<string, ModelPrice>): Array<{
model: string; model: string;
requests: number; requests: number;
successCount: number;
failureCount: number;
tokens: number; tokens: number;
cost: number; cost: number;
}> { }> {
@@ -586,20 +588,39 @@ export function getModelStats(usageData: any, modelPrices: Record<string, ModelP
return []; return [];
} }
const modelMap = new Map<string, { requests: number; tokens: number; cost: number }>(); const modelMap = new Map<string, { requests: number; successCount: number; failureCount: number; tokens: number; cost: number }>();
Object.values(usageData.apis as Record<string, any>).forEach(apiData => { Object.values(usageData.apis as Record<string, any>).forEach(apiData => {
const models = apiData?.models || {}; const models = apiData?.models || {};
Object.entries(models as Record<string, any>).forEach(([modelName, modelData]) => { Object.entries(models as Record<string, any>).forEach(([modelName, modelData]) => {
const existing = modelMap.get(modelName) || { requests: 0, tokens: 0, cost: 0 }; const existing = modelMap.get(modelName) || { requests: 0, successCount: 0, failureCount: 0, tokens: 0, cost: 0 };
existing.requests += modelData.total_requests || 0; existing.requests += modelData.total_requests || 0;
existing.tokens += modelData.total_tokens || 0; existing.tokens += modelData.total_tokens || 0;
const details = Array.isArray(modelData.details) ? modelData.details : [];
const price = modelPrices[modelName]; const price = modelPrices[modelName];
if (price) {
const details = Array.isArray(modelData.details) ? modelData.details : []; const hasExplicitCounts =
typeof modelData.success_count === 'number' || typeof modelData.failure_count === 'number';
if (hasExplicitCounts) {
existing.successCount += Number(modelData.success_count) || 0;
existing.failureCount += Number(modelData.failure_count) || 0;
}
if (details.length > 0 && (!hasExplicitCounts || price)) {
details.forEach((detail: any) => { details.forEach((detail: any) => {
existing.cost += calculateCost({ ...detail, __modelName: modelName }, modelPrices); if (!hasExplicitCounts) {
if (detail?.failed === true) {
existing.failureCount += 1;
} else {
existing.successCount += 1;
}
}
if (price) {
existing.cost += calculateCost({ ...detail, __modelName: modelName }, modelPrices);
}
}); });
} }
modelMap.set(modelName, existing); modelMap.set(modelName, existing);