mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Merge pull request #234 from vvrfxyz/feature/usage-thinking-column
Show thinking intensity in request events
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
formatDurationMs,
|
||||
LATENCY_SOURCE_FIELD,
|
||||
normalizeAuthIndex,
|
||||
type UsageThinking,
|
||||
} from '@/utils/usage';
|
||||
import { downloadBlob } from '@/utils/download';
|
||||
import styles from '@/pages/UsagePage.module.scss';
|
||||
@@ -37,6 +38,8 @@ type RequestEventRow = {
|
||||
authIndex: string;
|
||||
failed: boolean;
|
||||
latencyMs: number | null;
|
||||
thinking: UsageThinking | null;
|
||||
thinkingLabel: string;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
reasoningTokens: number;
|
||||
@@ -60,6 +63,37 @@ const toNumber = (value: unknown): number => {
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const normalizeThinkingText = (value: unknown): string => {
|
||||
if (typeof value !== 'string') return '';
|
||||
return value.trim();
|
||||
};
|
||||
|
||||
const formatThinkingLabel = (thinking: UsageThinking | null): string => {
|
||||
if (!thinking) return '-';
|
||||
|
||||
const intensity = normalizeThinkingText(thinking.intensity);
|
||||
const level = normalizeThinkingText(thinking.level);
|
||||
const mode = normalizeThinkingText(thinking.mode);
|
||||
const budget =
|
||||
typeof thinking.budget === 'number' && Number.isFinite(thinking.budget)
|
||||
? thinking.budget
|
||||
: null;
|
||||
const label = intensity || level || (budget !== null ? String(budget) : mode);
|
||||
const budgetLabel = budget !== null ? budget.toLocaleString() : null;
|
||||
|
||||
if (!label) return '-';
|
||||
if (budgetLabel !== null && label === String(budget)) {
|
||||
return budgetLabel;
|
||||
}
|
||||
if (mode === 'budget' && budget !== null && budget > 0) {
|
||||
return `${label} (${budgetLabel})`;
|
||||
}
|
||||
if (budget === -1 && label !== 'auto') {
|
||||
return `${label} (-1)`;
|
||||
}
|
||||
return label;
|
||||
};
|
||||
|
||||
const encodeCsv = (value: string | number): string => {
|
||||
const text = String(value ?? '');
|
||||
const trimmedLeft = text.replace(/^\s+/, '');
|
||||
@@ -127,63 +161,61 @@ export function RequestEventsDetailsCard({
|
||||
const rows = useMemo<RequestEventRow[]>(() => {
|
||||
const details = collectUsageDetails(usage);
|
||||
|
||||
const baseRows = details
|
||||
.map((detail, index) => {
|
||||
const timestamp = detail.timestamp;
|
||||
const timestampMs =
|
||||
typeof detail.__timestampMs === 'number' && detail.__timestampMs > 0
|
||||
? detail.__timestampMs
|
||||
: parseTimestampMs(timestamp);
|
||||
const date = Number.isNaN(timestampMs) ? null : new Date(timestampMs);
|
||||
const sourceRaw = String(detail.source ?? '').trim();
|
||||
const authIndexRaw = detail.auth_index as unknown;
|
||||
const authIndex =
|
||||
authIndexRaw === null || authIndexRaw === undefined || authIndexRaw === ''
|
||||
? '-'
|
||||
: String(authIndexRaw);
|
||||
const sourceInfo = resolveSourceDisplay(
|
||||
sourceRaw,
|
||||
authIndexRaw,
|
||||
sourceInfoMap,
|
||||
authFileMap
|
||||
);
|
||||
const source = sourceInfo.displayName;
|
||||
const sourceKey = sourceInfo.identityKey ?? `source:${sourceRaw || source}`;
|
||||
const sourceType = sourceInfo.type;
|
||||
const model = String(detail.__modelName ?? '').trim() || '-';
|
||||
const inputTokens = Math.max(toNumber(detail.tokens?.input_tokens), 0);
|
||||
const outputTokens = Math.max(toNumber(detail.tokens?.output_tokens), 0);
|
||||
const reasoningTokens = Math.max(toNumber(detail.tokens?.reasoning_tokens), 0);
|
||||
const cachedTokens = Math.max(
|
||||
Math.max(toNumber(detail.tokens?.cached_tokens), 0),
|
||||
Math.max(toNumber(detail.tokens?.cache_tokens), 0)
|
||||
);
|
||||
const totalTokens = Math.max(
|
||||
toNumber(detail.tokens?.total_tokens),
|
||||
extractTotalTokens(detail)
|
||||
);
|
||||
const latencyMs = extractLatencyMs(detail);
|
||||
const baseRows = details.map((detail, index) => {
|
||||
const timestamp = detail.timestamp;
|
||||
const timestampMs =
|
||||
typeof detail.__timestampMs === 'number' && detail.__timestampMs > 0
|
||||
? detail.__timestampMs
|
||||
: parseTimestampMs(timestamp);
|
||||
const date = Number.isNaN(timestampMs) ? null : new Date(timestampMs);
|
||||
const sourceRaw = String(detail.source ?? '').trim();
|
||||
const authIndexRaw = detail.auth_index as unknown;
|
||||
const authIndex =
|
||||
authIndexRaw === null || authIndexRaw === undefined || authIndexRaw === ''
|
||||
? '-'
|
||||
: String(authIndexRaw);
|
||||
const sourceInfo = resolveSourceDisplay(sourceRaw, authIndexRaw, sourceInfoMap, authFileMap);
|
||||
const source = sourceInfo.displayName;
|
||||
const sourceKey = sourceInfo.identityKey ?? `source:${sourceRaw || source}`;
|
||||
const sourceType = sourceInfo.type;
|
||||
const model = String(detail.__modelName ?? '').trim() || '-';
|
||||
const inputTokens = Math.max(toNumber(detail.tokens?.input_tokens), 0);
|
||||
const outputTokens = Math.max(toNumber(detail.tokens?.output_tokens), 0);
|
||||
const reasoningTokens = Math.max(toNumber(detail.tokens?.reasoning_tokens), 0);
|
||||
const cachedTokens = Math.max(
|
||||
Math.max(toNumber(detail.tokens?.cached_tokens), 0),
|
||||
Math.max(toNumber(detail.tokens?.cache_tokens), 0)
|
||||
);
|
||||
const totalTokens = Math.max(
|
||||
toNumber(detail.tokens?.total_tokens),
|
||||
extractTotalTokens(detail)
|
||||
);
|
||||
const latencyMs = extractLatencyMs(detail);
|
||||
const thinking = detail.thinking ?? null;
|
||||
const thinkingLabel = formatThinkingLabel(thinking);
|
||||
|
||||
return {
|
||||
id: `${timestamp}-${model}-${sourceKey}-${authIndex}-${index}`,
|
||||
timestamp,
|
||||
timestampMs: Number.isNaN(timestampMs) ? 0 : timestampMs,
|
||||
timestampLabel: date ? date.toLocaleString(i18n.language) : timestamp || '-',
|
||||
model,
|
||||
sourceKey,
|
||||
sourceRaw: sourceRaw || '-',
|
||||
source,
|
||||
sourceType,
|
||||
authIndex,
|
||||
failed: detail.failed === true,
|
||||
latencyMs,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
reasoningTokens,
|
||||
cachedTokens,
|
||||
totalTokens,
|
||||
};
|
||||
});
|
||||
return {
|
||||
id: `${timestamp}-${model}-${sourceKey}-${authIndex}-${index}`,
|
||||
timestamp,
|
||||
timestampMs: Number.isNaN(timestampMs) ? 0 : timestampMs,
|
||||
timestampLabel: date ? date.toLocaleString(i18n.language) : timestamp || '-',
|
||||
model,
|
||||
sourceKey,
|
||||
sourceRaw: sourceRaw || '-',
|
||||
source,
|
||||
sourceType,
|
||||
authIndex,
|
||||
failed: detail.failed === true,
|
||||
latencyMs,
|
||||
thinking,
|
||||
thinkingLabel,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
reasoningTokens,
|
||||
cachedTokens,
|
||||
totalTokens,
|
||||
};
|
||||
});
|
||||
|
||||
const sourceLabelKeyMap = new Map<string, Set<string>>();
|
||||
baseRows.forEach((row) => {
|
||||
@@ -319,6 +351,10 @@ export function RequestEventsDetailsCard({
|
||||
'auth_index',
|
||||
'result',
|
||||
...(hasLatencyData ? ['latency_ms'] : []),
|
||||
'thinking_intensity',
|
||||
'thinking_mode',
|
||||
'thinking_level',
|
||||
'thinking_budget',
|
||||
'input_tokens',
|
||||
'output_tokens',
|
||||
'reasoning_tokens',
|
||||
@@ -335,6 +371,10 @@ export function RequestEventsDetailsCard({
|
||||
row.authIndex,
|
||||
row.failed ? 'failed' : 'success',
|
||||
...(hasLatencyData ? [row.latencyMs ?? ''] : []),
|
||||
row.thinking?.intensity ?? '',
|
||||
row.thinking?.mode ?? '',
|
||||
row.thinking?.level ?? '',
|
||||
row.thinking?.budget ?? '',
|
||||
row.inputTokens,
|
||||
row.outputTokens,
|
||||
row.reasoningTokens,
|
||||
@@ -364,6 +404,7 @@ export function RequestEventsDetailsCard({
|
||||
auth_index: row.authIndex,
|
||||
failed: row.failed,
|
||||
...(hasLatencyData && row.latencyMs !== null ? { latency_ms: row.latencyMs } : {}),
|
||||
...(row.thinking ? { thinking: row.thinking } : {}),
|
||||
tokens: {
|
||||
input_tokens: row.inputTokens,
|
||||
output_tokens: row.outputTokens,
|
||||
@@ -492,6 +533,7 @@ export function RequestEventsDetailsCard({
|
||||
<th>{t('usage_stats.request_events_auth_index')}</th>
|
||||
<th>{t('usage_stats.request_events_result')}</th>
|
||||
{hasLatencyData && <th title={latencyHint}>{t('usage_stats.time')}</th>}
|
||||
<th>{t('usage_stats.thinking_intensity')}</th>
|
||||
<th>{t('usage_stats.input_tokens')}</th>
|
||||
<th>{t('usage_stats.output_tokens')}</th>
|
||||
<th>{t('usage_stats.reasoning_tokens')}</th>
|
||||
@@ -529,6 +571,34 @@ export function RequestEventsDetailsCard({
|
||||
{hasLatencyData && (
|
||||
<td className={styles.durationCell}>{formatDurationMs(row.latencyMs)}</td>
|
||||
)}
|
||||
<td>
|
||||
<span
|
||||
className={
|
||||
row.thinking
|
||||
? styles.requestEventsThinkingBadge
|
||||
: styles.requestEventsThinkingEmpty
|
||||
}
|
||||
title={
|
||||
row.thinking
|
||||
? [
|
||||
row.thinking.mode
|
||||
? `${t('usage_stats.thinking_mode')}: ${row.thinking.mode}`
|
||||
: '',
|
||||
row.thinking.level
|
||||
? `${t('usage_stats.thinking_level')}: ${row.thinking.level}`
|
||||
: '',
|
||||
typeof row.thinking.budget === 'number'
|
||||
? `${t('usage_stats.thinking_budget')}: ${row.thinking.budget.toLocaleString()}`
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{row.thinkingLabel}
|
||||
</span>
|
||||
</td>
|
||||
<td>{row.inputTokens.toLocaleString()}</td>
|
||||
<td>{row.outputTokens.toLocaleString()}</td>
|
||||
<td>{row.reasoningTokens.toLocaleString()}</td>
|
||||
|
||||
@@ -1025,6 +1025,10 @@
|
||||
"request_events_source": "Source",
|
||||
"request_events_auth_index": "Auth Index",
|
||||
"request_events_result": "Result",
|
||||
"thinking_intensity": "Thinking",
|
||||
"thinking_mode": "Thinking Mode",
|
||||
"thinking_level": "Thinking Level",
|
||||
"thinking_budget": "Thinking Budget",
|
||||
"time": "Latency",
|
||||
"avg_time": "Avg Latency",
|
||||
"total_time": "Total Latency",
|
||||
|
||||
@@ -1022,6 +1022,10 @@
|
||||
"request_events_source": "Источник",
|
||||
"request_events_auth_index": "Auth Index",
|
||||
"request_events_result": "Результат",
|
||||
"thinking_intensity": "Рассуждение",
|
||||
"thinking_mode": "Режим рассуждения",
|
||||
"thinking_level": "Уровень рассуждения",
|
||||
"thinking_budget": "Бюджет рассуждения",
|
||||
"time": "Задержка",
|
||||
"avg_time": "Средняя задержка",
|
||||
"total_time": "Суммарная задержка",
|
||||
|
||||
@@ -1025,6 +1025,10 @@
|
||||
"request_events_source": "来源",
|
||||
"request_events_auth_index": "认证索引",
|
||||
"request_events_result": "结果",
|
||||
"thinking_intensity": "思考强度",
|
||||
"thinking_mode": "思考模式",
|
||||
"thinking_level": "思考等级",
|
||||
"thinking_budget": "思考预算",
|
||||
"time": "延迟",
|
||||
"avg_time": "平均延迟",
|
||||
"total_time": "总延迟",
|
||||
|
||||
@@ -1051,6 +1051,10 @@
|
||||
"request_events_source": "來源",
|
||||
"request_events_auth_index": "驗證索引",
|
||||
"request_events_result": "結果",
|
||||
"thinking_intensity": "思考強度",
|
||||
"thinking_mode": "思考模式",
|
||||
"thinking_level": "思考等級",
|
||||
"thinking_budget": "思考預算",
|
||||
"time": "延遲",
|
||||
"avg_time": "平均延遲",
|
||||
"total_time": "總延遲",
|
||||
|
||||
@@ -969,6 +969,31 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.requestEventsThinkingBadge,
|
||||
.requestEventsThinkingEmpty {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 48px;
|
||||
padding: 2px 8px;
|
||||
border-radius: $radius-full;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.requestEventsThinkingBadge {
|
||||
color: var(--primary-color);
|
||||
background: color-mix(in srgb, var(--primary-color) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--primary-color) 28%, transparent);
|
||||
}
|
||||
|
||||
.requestEventsThinkingEmpty {
|
||||
color: var(--text-tertiary);
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.chartLineHeader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
+41
-10
@@ -39,6 +39,13 @@ export interface TokenBreakdown {
|
||||
reasoningTokens: number;
|
||||
}
|
||||
|
||||
export interface UsageThinking {
|
||||
intensity?: string;
|
||||
mode?: string;
|
||||
level?: string;
|
||||
budget?: number;
|
||||
}
|
||||
|
||||
export interface RateStats {
|
||||
rpm: number;
|
||||
tpm: number;
|
||||
@@ -66,6 +73,7 @@ export interface UsageDetail {
|
||||
cache_tokens?: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
thinking?: UsageThinking | null;
|
||||
failed: boolean;
|
||||
__modelName?: string;
|
||||
__timestampMs?: number;
|
||||
@@ -123,6 +131,29 @@ const getApisRecord = (usageData: unknown): Record<string, unknown> | null => {
|
||||
return isRecord(apisRaw) ? apisRaw : null;
|
||||
};
|
||||
|
||||
const normalizeUsageThinking = (value: unknown): UsageThinking | null => {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const intensity = typeof value.intensity === 'string' ? value.intensity.trim() : '';
|
||||
const mode = typeof value.mode === 'string' ? value.mode.trim() : '';
|
||||
const level = typeof value.level === 'string' ? value.level.trim() : '';
|
||||
const budget =
|
||||
typeof value.budget === 'number' && Number.isFinite(value.budget) ? value.budget : undefined;
|
||||
|
||||
if (!intensity && !mode && !level && budget === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...(intensity ? { intensity } : {}),
|
||||
...(mode ? { mode } : {}),
|
||||
...(level ? { level } : {}),
|
||||
...(budget !== undefined ? { budget } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
interface UsageSummary {
|
||||
totalRequests: number;
|
||||
successCount: number;
|
||||
@@ -552,13 +583,13 @@ export function collectUsageDetails(usageData: unknown): UsageDetail[] {
|
||||
details.push({
|
||||
timestamp,
|
||||
source: normalizeSource(detailRaw.source),
|
||||
auth_index:
|
||||
(detailRaw?.auth_index ??
|
||||
detailRaw?.authIndex ??
|
||||
detailRaw?.AuthIndex ??
|
||||
null) as UsageDetail['auth_index'],
|
||||
auth_index: (detailRaw?.auth_index ??
|
||||
detailRaw?.authIndex ??
|
||||
detailRaw?.AuthIndex ??
|
||||
null) as UsageDetail['auth_index'],
|
||||
latency_ms: latencyMs ?? undefined,
|
||||
tokens: tokensRaw as unknown as UsageDetail['tokens'],
|
||||
thinking: normalizeUsageThinking(detailRaw.thinking),
|
||||
failed: detailRaw.failed === true,
|
||||
__modelName: modelName,
|
||||
__timestampMs: Number.isNaN(timestampMs) ? 0 : timestampMs,
|
||||
@@ -629,13 +660,13 @@ export function collectUsageDetailsWithEndpoint(usageData: unknown): UsageDetail
|
||||
details.push({
|
||||
timestamp,
|
||||
source: normalizeSource(detailRaw.source),
|
||||
auth_index:
|
||||
(detailRaw?.auth_index ??
|
||||
detailRaw?.authIndex ??
|
||||
detailRaw?.AuthIndex ??
|
||||
null) as UsageDetail['auth_index'],
|
||||
auth_index: (detailRaw?.auth_index ??
|
||||
detailRaw?.authIndex ??
|
||||
detailRaw?.AuthIndex ??
|
||||
null) as UsageDetail['auth_index'],
|
||||
latency_ms: latencyMs ?? undefined,
|
||||
tokens: tokensRaw as unknown as UsageDetail['tokens'],
|
||||
thinking: normalizeUsageThinking(detailRaw.thinking),
|
||||
failed: detailRaw.failed === true,
|
||||
__modelName: modelName,
|
||||
__endpoint: endpoint,
|
||||
|
||||
Reference in New Issue
Block a user