diff --git a/src/components/usage/RequestEventsDetailsCard.tsx b/src/components/usage/RequestEventsDetailsCard.tsx index b19404e..bd2b321 100644 --- a/src/components/usage/RequestEventsDetailsCard.tsx +++ b/src/components/usage/RequestEventsDetailsCard.tsx @@ -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(() => { 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>(); 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({ {t('usage_stats.request_events_auth_index')} {t('usage_stats.request_events_result')} {hasLatencyData && {t('usage_stats.time')}} + {t('usage_stats.thinking_intensity')} {t('usage_stats.input_tokens')} {t('usage_stats.output_tokens')} {t('usage_stats.reasoning_tokens')} @@ -529,6 +571,34 @@ export function RequestEventsDetailsCard({ {hasLatencyData && ( {formatDurationMs(row.latencyMs)} )} + + + {row.thinkingLabel} + + {row.inputTokens.toLocaleString()} {row.outputTokens.toLocaleString()} {row.reasoningTokens.toLocaleString()} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index c4af0f2..abccabc 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 0210709..76e6fd9 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -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": "Суммарная задержка", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index dabfd9b..6e85244 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -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": "总延迟", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index f760f71..1a51632 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -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": "總延遲", diff --git a/src/pages/UsagePage.module.scss b/src/pages/UsagePage.module.scss index bf533c4..963952b 100644 --- a/src/pages/UsagePage.module.scss +++ b/src/pages/UsagePage.module.scss @@ -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; diff --git a/src/utils/usage.ts b/src/utils/usage.ts index 3515044..6817b79 100644 --- a/src/utils/usage.ts +++ b/src/utils/usage.ts @@ -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 | 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,