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,
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>
+4
View File
@@ -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",
+4
View File
@@ -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": "Суммарная задержка",
+4
View File
@@ -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": "总延迟",
+4
View File
@@ -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": "總延遲",
+25
View File
@@ -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
View File
@@ -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,