Merge pull request #234 from vvrfxyz/feature/usage-thinking-column

Show thinking intensity in request events
This commit is contained in:
Supra4E8C
2026-04-29 00:15:41 +08:00
committed by GitHub
Unverified
7 changed files with 208 additions and 66 deletions
+126 -56
View File
@@ -17,6 +17,7 @@ import {
formatDurationMs, formatDurationMs,
LATENCY_SOURCE_FIELD, LATENCY_SOURCE_FIELD,
normalizeAuthIndex, normalizeAuthIndex,
type UsageThinking,
} from '@/utils/usage'; } from '@/utils/usage';
import { downloadBlob } from '@/utils/download'; import { downloadBlob } from '@/utils/download';
import styles from '@/pages/UsagePage.module.scss'; import styles from '@/pages/UsagePage.module.scss';
@@ -37,6 +38,8 @@ type RequestEventRow = {
authIndex: string; authIndex: string;
failed: boolean; failed: boolean;
latencyMs: number | null; latencyMs: number | null;
thinking: UsageThinking | null;
thinkingLabel: string;
inputTokens: number; inputTokens: number;
outputTokens: number; outputTokens: number;
reasoningTokens: number; reasoningTokens: number;
@@ -60,6 +63,37 @@ const toNumber = (value: unknown): number => {
return parsed; 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 encodeCsv = (value: string | number): string => {
const text = String(value ?? ''); const text = String(value ?? '');
const trimmedLeft = text.replace(/^\s+/, ''); const trimmedLeft = text.replace(/^\s+/, '');
@@ -127,63 +161,61 @@ export function RequestEventsDetailsCard({
const rows = useMemo<RequestEventRow[]>(() => { const rows = useMemo<RequestEventRow[]>(() => {
const details = collectUsageDetails(usage); const details = collectUsageDetails(usage);
const baseRows = details const baseRows = details.map((detail, index) => {
.map((detail, index) => { const timestamp = detail.timestamp;
const timestamp = detail.timestamp; const timestampMs =
const timestampMs = typeof detail.__timestampMs === 'number' && detail.__timestampMs > 0
typeof detail.__timestampMs === 'number' && detail.__timestampMs > 0 ? detail.__timestampMs
? detail.__timestampMs : parseTimestampMs(timestamp);
: parseTimestampMs(timestamp); const date = Number.isNaN(timestampMs) ? null : new Date(timestampMs);
const date = Number.isNaN(timestampMs) ? null : new Date(timestampMs); const sourceRaw = String(detail.source ?? '').trim();
const sourceRaw = String(detail.source ?? '').trim(); const authIndexRaw = detail.auth_index as unknown;
const authIndexRaw = detail.auth_index as unknown; const authIndex =
const authIndex = authIndexRaw === null || authIndexRaw === undefined || authIndexRaw === ''
authIndexRaw === null || authIndexRaw === undefined || authIndexRaw === '' ? '-'
? '-' : String(authIndexRaw);
: String(authIndexRaw); const sourceInfo = resolveSourceDisplay(sourceRaw, authIndexRaw, sourceInfoMap, authFileMap);
const sourceInfo = resolveSourceDisplay( const source = sourceInfo.displayName;
sourceRaw, const sourceKey = sourceInfo.identityKey ?? `source:${sourceRaw || source}`;
authIndexRaw, const sourceType = sourceInfo.type;
sourceInfoMap, const model = String(detail.__modelName ?? '').trim() || '-';
authFileMap const inputTokens = Math.max(toNumber(detail.tokens?.input_tokens), 0);
); const outputTokens = Math.max(toNumber(detail.tokens?.output_tokens), 0);
const source = sourceInfo.displayName; const reasoningTokens = Math.max(toNumber(detail.tokens?.reasoning_tokens), 0);
const sourceKey = sourceInfo.identityKey ?? `source:${sourceRaw || source}`; const cachedTokens = Math.max(
const sourceType = sourceInfo.type; Math.max(toNumber(detail.tokens?.cached_tokens), 0),
const model = String(detail.__modelName ?? '').trim() || '-'; Math.max(toNumber(detail.tokens?.cache_tokens), 0)
const inputTokens = Math.max(toNumber(detail.tokens?.input_tokens), 0); );
const outputTokens = Math.max(toNumber(detail.tokens?.output_tokens), 0); const totalTokens = Math.max(
const reasoningTokens = Math.max(toNumber(detail.tokens?.reasoning_tokens), 0); toNumber(detail.tokens?.total_tokens),
const cachedTokens = Math.max( extractTotalTokens(detail)
Math.max(toNumber(detail.tokens?.cached_tokens), 0), );
Math.max(toNumber(detail.tokens?.cache_tokens), 0) const latencyMs = extractLatencyMs(detail);
); const thinking = detail.thinking ?? null;
const totalTokens = Math.max( const thinkingLabel = formatThinkingLabel(thinking);
toNumber(detail.tokens?.total_tokens),
extractTotalTokens(detail)
);
const latencyMs = extractLatencyMs(detail);
return { return {
id: `${timestamp}-${model}-${sourceKey}-${authIndex}-${index}`, id: `${timestamp}-${model}-${sourceKey}-${authIndex}-${index}`,
timestamp, timestamp,
timestampMs: Number.isNaN(timestampMs) ? 0 : timestampMs, timestampMs: Number.isNaN(timestampMs) ? 0 : timestampMs,
timestampLabel: date ? date.toLocaleString(i18n.language) : timestamp || '-', timestampLabel: date ? date.toLocaleString(i18n.language) : timestamp || '-',
model, model,
sourceKey, sourceKey,
sourceRaw: sourceRaw || '-', sourceRaw: sourceRaw || '-',
source, source,
sourceType, sourceType,
authIndex, authIndex,
failed: detail.failed === true, failed: detail.failed === true,
latencyMs, latencyMs,
inputTokens, thinking,
outputTokens, thinkingLabel,
reasoningTokens, inputTokens,
cachedTokens, outputTokens,
totalTokens, reasoningTokens,
}; cachedTokens,
}); totalTokens,
};
});
const sourceLabelKeyMap = new Map<string, Set<string>>(); const sourceLabelKeyMap = new Map<string, Set<string>>();
baseRows.forEach((row) => { baseRows.forEach((row) => {
@@ -319,6 +351,10 @@ export function RequestEventsDetailsCard({
'auth_index', 'auth_index',
'result', 'result',
...(hasLatencyData ? ['latency_ms'] : []), ...(hasLatencyData ? ['latency_ms'] : []),
'thinking_intensity',
'thinking_mode',
'thinking_level',
'thinking_budget',
'input_tokens', 'input_tokens',
'output_tokens', 'output_tokens',
'reasoning_tokens', 'reasoning_tokens',
@@ -335,6 +371,10 @@ export function RequestEventsDetailsCard({
row.authIndex, row.authIndex,
row.failed ? 'failed' : 'success', row.failed ? 'failed' : 'success',
...(hasLatencyData ? [row.latencyMs ?? ''] : []), ...(hasLatencyData ? [row.latencyMs ?? ''] : []),
row.thinking?.intensity ?? '',
row.thinking?.mode ?? '',
row.thinking?.level ?? '',
row.thinking?.budget ?? '',
row.inputTokens, row.inputTokens,
row.outputTokens, row.outputTokens,
row.reasoningTokens, row.reasoningTokens,
@@ -364,6 +404,7 @@ export function RequestEventsDetailsCard({
auth_index: row.authIndex, auth_index: row.authIndex,
failed: row.failed, failed: row.failed,
...(hasLatencyData && row.latencyMs !== null ? { latency_ms: row.latencyMs } : {}), ...(hasLatencyData && row.latencyMs !== null ? { latency_ms: row.latencyMs } : {}),
...(row.thinking ? { thinking: row.thinking } : {}),
tokens: { tokens: {
input_tokens: row.inputTokens, input_tokens: row.inputTokens,
output_tokens: row.outputTokens, 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_auth_index')}</th>
<th>{t('usage_stats.request_events_result')}</th> <th>{t('usage_stats.request_events_result')}</th>
{hasLatencyData && <th title={latencyHint}>{t('usage_stats.time')}</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.input_tokens')}</th>
<th>{t('usage_stats.output_tokens')}</th> <th>{t('usage_stats.output_tokens')}</th>
<th>{t('usage_stats.reasoning_tokens')}</th> <th>{t('usage_stats.reasoning_tokens')}</th>
@@ -529,6 +571,34 @@ export function RequestEventsDetailsCard({
{hasLatencyData && ( {hasLatencyData && (
<td className={styles.durationCell}>{formatDurationMs(row.latencyMs)}</td> <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.inputTokens.toLocaleString()}</td>
<td>{row.outputTokens.toLocaleString()}</td> <td>{row.outputTokens.toLocaleString()}</td>
<td>{row.reasoningTokens.toLocaleString()}</td> <td>{row.reasoningTokens.toLocaleString()}</td>
+4
View File
@@ -1025,6 +1025,10 @@
"request_events_source": "Source", "request_events_source": "Source",
"request_events_auth_index": "Auth Index", "request_events_auth_index": "Auth Index",
"request_events_result": "Result", "request_events_result": "Result",
"thinking_intensity": "Thinking",
"thinking_mode": "Thinking Mode",
"thinking_level": "Thinking Level",
"thinking_budget": "Thinking Budget",
"time": "Latency", "time": "Latency",
"avg_time": "Avg Latency", "avg_time": "Avg Latency",
"total_time": "Total Latency", "total_time": "Total Latency",
+4
View File
@@ -1022,6 +1022,10 @@
"request_events_source": "Источник", "request_events_source": "Источник",
"request_events_auth_index": "Auth Index", "request_events_auth_index": "Auth Index",
"request_events_result": "Результат", "request_events_result": "Результат",
"thinking_intensity": "Рассуждение",
"thinking_mode": "Режим рассуждения",
"thinking_level": "Уровень рассуждения",
"thinking_budget": "Бюджет рассуждения",
"time": "Задержка", "time": "Задержка",
"avg_time": "Средняя задержка", "avg_time": "Средняя задержка",
"total_time": "Суммарная задержка", "total_time": "Суммарная задержка",
+4
View File
@@ -1025,6 +1025,10 @@
"request_events_source": "来源", "request_events_source": "来源",
"request_events_auth_index": "认证索引", "request_events_auth_index": "认证索引",
"request_events_result": "结果", "request_events_result": "结果",
"thinking_intensity": "思考强度",
"thinking_mode": "思考模式",
"thinking_level": "思考等级",
"thinking_budget": "思考预算",
"time": "延迟", "time": "延迟",
"avg_time": "平均延迟", "avg_time": "平均延迟",
"total_time": "总延迟", "total_time": "总延迟",
+4
View File
@@ -1051,6 +1051,10 @@
"request_events_source": "來源", "request_events_source": "來源",
"request_events_auth_index": "驗證索引", "request_events_auth_index": "驗證索引",
"request_events_result": "結果", "request_events_result": "結果",
"thinking_intensity": "思考強度",
"thinking_mode": "思考模式",
"thinking_level": "思考等級",
"thinking_budget": "思考預算",
"time": "延遲", "time": "延遲",
"avg_time": "平均延遲", "avg_time": "平均延遲",
"total_time": "總延遲", "total_time": "總延遲",
+25
View File
@@ -969,6 +969,31 @@
white-space: nowrap; 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 { .chartLineHeader {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
+41 -10
View File
@@ -39,6 +39,13 @@ export interface TokenBreakdown {
reasoningTokens: number; reasoningTokens: number;
} }
export interface UsageThinking {
intensity?: string;
mode?: string;
level?: string;
budget?: number;
}
export interface RateStats { export interface RateStats {
rpm: number; rpm: number;
tpm: number; tpm: number;
@@ -66,6 +73,7 @@ export interface UsageDetail {
cache_tokens?: number; cache_tokens?: number;
total_tokens: number; total_tokens: number;
}; };
thinking?: UsageThinking | null;
failed: boolean; failed: boolean;
__modelName?: string; __modelName?: string;
__timestampMs?: number; __timestampMs?: number;
@@ -123,6 +131,29 @@ const getApisRecord = (usageData: unknown): Record<string, unknown> | null => {
return isRecord(apisRaw) ? apisRaw : 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 { interface UsageSummary {
totalRequests: number; totalRequests: number;
successCount: number; successCount: number;
@@ -552,13 +583,13 @@ export function collectUsageDetails(usageData: unknown): UsageDetail[] {
details.push({ details.push({
timestamp, timestamp,
source: normalizeSource(detailRaw.source), source: normalizeSource(detailRaw.source),
auth_index: auth_index: (detailRaw?.auth_index ??
(detailRaw?.auth_index ?? detailRaw?.authIndex ??
detailRaw?.authIndex ?? detailRaw?.AuthIndex ??
detailRaw?.AuthIndex ?? null) as UsageDetail['auth_index'],
null) as UsageDetail['auth_index'],
latency_ms: latencyMs ?? undefined, latency_ms: latencyMs ?? undefined,
tokens: tokensRaw as unknown as UsageDetail['tokens'], tokens: tokensRaw as unknown as UsageDetail['tokens'],
thinking: normalizeUsageThinking(detailRaw.thinking),
failed: detailRaw.failed === true, failed: detailRaw.failed === true,
__modelName: modelName, __modelName: modelName,
__timestampMs: Number.isNaN(timestampMs) ? 0 : timestampMs, __timestampMs: Number.isNaN(timestampMs) ? 0 : timestampMs,
@@ -629,13 +660,13 @@ export function collectUsageDetailsWithEndpoint(usageData: unknown): UsageDetail
details.push({ details.push({
timestamp, timestamp,
source: normalizeSource(detailRaw.source), source: normalizeSource(detailRaw.source),
auth_index: auth_index: (detailRaw?.auth_index ??
(detailRaw?.auth_index ?? detailRaw?.authIndex ??
detailRaw?.authIndex ?? detailRaw?.AuthIndex ??
detailRaw?.AuthIndex ?? null) as UsageDetail['auth_index'],
null) as UsageDetail['auth_index'],
latency_ms: latencyMs ?? undefined, latency_ms: latencyMs ?? undefined,
tokens: tokensRaw as unknown as UsageDetail['tokens'], tokens: tokensRaw as unknown as UsageDetail['tokens'],
thinking: normalizeUsageThinking(detailRaw.thinking),
failed: detailRaw.failed === true, failed: detailRaw.failed === true,
__modelName: modelName, __modelName: modelName,
__endpoint: endpoint, __endpoint: endpoint,