/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; import { Avatar, Space, Tag, Tooltip, Popover, Typography, } from '@douyinfe/semi-ui'; import { renderGroup, renderQuota, stringToColor, getLogOther, renderModelTag, renderModelPriceSimple, } from '../../../helpers'; import { IconHelpCircle } from '@douyinfe/semi-icons'; import { Route, Sparkles } from 'lucide-react'; const colors = [ 'amber', 'blue', 'cyan', 'green', 'grey', 'indigo', 'light-blue', 'lime', 'orange', 'pink', 'purple', 'red', 'teal', 'violet', 'yellow', ]; function formatRatio(ratio) { if (ratio === undefined || ratio === null) { return '-'; } if (typeof ratio === 'number') { return ratio.toFixed(4); } return String(ratio); } function buildChannelAffinityTooltip(affinity, t) { if (!affinity) { return null; } const keySource = affinity.key_source || '-'; const keyPath = affinity.key_path || affinity.key_key || '-'; const keyHint = affinity.key_hint || ''; const keyFp = affinity.key_fp ? `#${affinity.key_fp}` : ''; const keyText = `${keySource}:${keyPath}${keyFp}`; const lines = [ t('渠道亲和性'), `${t('规则')}:${affinity.rule_name || '-'}`, `${t('分组')}:${affinity.selected_group || '-'}`, `${t('Key')}:${keyText}`, ...(keyHint ? [`${t('Key 摘要')}:${keyHint}`] : []), ]; return (
{lines.map((line, i) => (
{line}
))}
); } // Render functions function renderType(type, t) { switch (type) { case 1: return ( {t('充值')} ); case 2: return ( {t('消费')} ); case 3: return ( {t('管理')} ); case 4: return ( {t('系统')} ); case 5: return ( {t('错误')} ); case 6: return ( {t('退款')} ); default: return ( {t('未知')} ); } } function renderIsStream(bool, t) { if (bool) { return ( {t('流')} ); } else { return ( {t('非流')} ); } } function renderUseTime(type, t) { const time = parseInt(type); if (time < 101) { return ( {' '} {time} s{' '} ); } else if (time < 300) { return ( {' '} {time} s{' '} ); } else { return ( {' '} {time} s{' '} ); } } function renderFirstUseTime(type, t) { let time = parseFloat(type) / 1000.0; time = time.toFixed(1); if (time < 3) { return ( {' '} {time} s{' '} ); } else if (time < 10) { return ( {' '} {time} s{' '} ); } else { return ( {' '} {time} s{' '} ); } } function renderBillingTag(record, t) { const other = getLogOther(record.other); if (other?.billing_source === 'subscription') { return ( {t('订阅抵扣')} ); } return null; } function renderModelName(record, copyText, t) { let other = getLogOther(record.other); let modelMapped = other?.is_model_mapped && other?.upstream_model_name && other?.upstream_model_name !== ''; if (!modelMapped) { return renderModelTag(record.model_name, { onClick: (event) => { copyText(event, record.model_name).then((r) => {}); }, }); } else { return ( <>
{t('请求并计费模型')}: {renderModelTag(record.model_name, { onClick: (event) => { copyText(event, record.model_name).then((r) => {}); }, })}
{t('实际模型')}: {renderModelTag(other.upstream_model_name, { onClick: (event) => { copyText(event, other.upstream_model_name).then( (r) => {}, ); }, })}
} > {renderModelTag(record.model_name, { onClick: (event) => { copyText(event, record.model_name).then((r) => {}); }, suffixIcon: ( ), })}
); } } function toTokenNumber(value) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { return 0; } return parsed; } function formatTokenCount(value) { return toTokenNumber(value).toLocaleString(); } function getPromptCacheSummary(other) { if (!other || typeof other !== 'object') { return null; } const cacheReadTokens = toTokenNumber(other.cache_tokens); const cacheCreationTokens = toTokenNumber(other.cache_creation_tokens); const cacheCreationTokens5m = toTokenNumber(other.cache_creation_tokens_5m); const cacheCreationTokens1h = toTokenNumber(other.cache_creation_tokens_1h); const hasSplitCacheCreation = cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0; const cacheWriteTokens = hasSplitCacheCreation ? cacheCreationTokens5m + cacheCreationTokens1h : cacheCreationTokens; if (cacheReadTokens <= 0 && cacheWriteTokens <= 0) { return null; } return { cacheReadTokens, cacheWriteTokens, }; } function normalizeDetailText(detail) { return String(detail || '') .replace(/\n\r/g, '\n') .replace(/\r\n/g, '\n'); } function getUsageLogGroupSummary(groupRatio, userGroupRatio, t) { const parsedUserGroupRatio = Number(userGroupRatio); const useUserGroupRatio = Number.isFinite(parsedUserGroupRatio) && parsedUserGroupRatio !== -1; const ratio = useUserGroupRatio ? userGroupRatio : groupRatio; if (ratio === undefined || ratio === null || ratio === '') { return ''; } return `${useUserGroupRatio ? t('专属倍率') : t('分组')} ${formatRatio(ratio)}x`; } function renderCompactDetailSummary(summarySegments) { const segments = Array.isArray(summarySegments) ? summarySegments.filter((segment) => segment?.text) : []; if (!segments.length) { return null; } return (
{segments.map((segment, index) => ( {segment.text} ))}
); } function getUsageLogDetailSummary(record, text, billingDisplayMode, t) { const other = getLogOther(record.other); if (record.type === 6) { return { segments: [{ text: t('异步任务退款'), tone: 'primary' }], }; } if (other == null || record.type !== 2) { return null; } if ( other?.violation_fee === true || Boolean(other?.violation_fee_code) || Boolean(other?.violation_fee_marker) ) { const feeQuota = other?.fee_quota ?? record?.quota; const groupText = getUsageLogGroupSummary( other?.group_ratio, other?.user_group_ratio, t, ); return { segments: [ groupText ? { text: groupText, tone: 'primary' } : null, { text: t('违规扣费'), tone: 'primary' }, { text: `${t('扣费')}:${renderQuota(feeQuota, 6)}`, tone: 'secondary', }, text ? { text: `${t('详情')}:${text}`, tone: 'secondary' } : null, ].filter(Boolean), }; } return { segments: other?.claude ? renderModelPriceSimple( other.model_ratio, other.model_price, other.group_ratio, other?.user_group_ratio, other.cache_tokens || 0, other.cache_ratio || 1.0, other.cache_creation_tokens || 0, other.cache_creation_ratio || 1.0, other.cache_creation_tokens_5m || 0, other.cache_creation_ratio_5m || other.cache_creation_ratio || 1.0, other.cache_creation_tokens_1h || 0, other.cache_creation_ratio_1h || other.cache_creation_ratio || 1.0, false, 1.0, other?.is_system_prompt_overwritten, 'claude', billingDisplayMode, 'segments', ) : renderModelPriceSimple( other.model_ratio, other.model_price, other.group_ratio, other?.user_group_ratio, other.cache_tokens || 0, other.cache_ratio || 1.0, 0, 1.0, 0, 1.0, 0, 1.0, false, 1.0, other?.is_system_prompt_overwritten, 'openai', billingDisplayMode, 'segments', ), }; } export const getLogsColumns = ({ t, COLUMN_KEYS, copyText, showUserInfoFunc, openChannelAffinityUsageCacheModal, isAdminUser, billingDisplayMode = 'price', }) => { return [ { key: COLUMN_KEYS.TIME, title: t('时间'), dataIndex: 'timestamp2string', }, { key: COLUMN_KEYS.CHANNEL, title: t('渠道'), dataIndex: 'channel', render: (text, record, index) => { let isMultiKey = false; let multiKeyIndex = -1; let content = t('渠道') + `:${record.channel}`; let affinity = null; let showMarker = false; let other = getLogOther(record.other); if (other?.admin_info) { let adminInfo = other.admin_info; if (adminInfo?.is_multi_key) { isMultiKey = true; multiKeyIndex = adminInfo.multi_key_index; } if ( Array.isArray(adminInfo.use_channel) && adminInfo.use_channel.length > 0 ) { content = t('渠道') + `:${adminInfo.use_channel.join('->')}`; } if (adminInfo.channel_affinity) { affinity = adminInfo.channel_affinity; showMarker = true; } } return isAdminUser && (record.type === 0 || record.type === 2 || record.type === 5 || record.type === 6) ? ( {text} {showMarker && (
{content}
{affinity ? (
{buildChannelAffinityTooltip(affinity, t)}
) : null} } > { e.stopPropagation(); openChannelAffinityUsageCacheModal?.(affinity); }} >
)}
{isMultiKey && ( {multiKeyIndex} )}
) : null; }, }, { key: COLUMN_KEYS.USERNAME, title: t('用户'), dataIndex: 'username', render: (text, record, index) => { return isAdminUser ? (
{ event.stopPropagation(); showUserInfoFunc(record.user_id); }} > {typeof text === 'string' && text.slice(0, 1)} {text}
) : ( <> ); }, }, { key: COLUMN_KEYS.TOKEN, title: t('令牌'), dataIndex: 'token_name', render: (text, record, index) => { return record.type === 0 || record.type === 2 || record.type === 5 || record.type === 6 ? (
{ copyText(event, text); }} > {' '} {t(text)}{' '}
) : ( <> ); }, }, { key: COLUMN_KEYS.GROUP, title: t('分组'), dataIndex: 'group', render: (text, record, index) => { if ( record.type === 0 || record.type === 2 || record.type === 5 || record.type === 6 ) { if (record.group) { return <>{renderGroup(record.group)}; } else { let other = null; try { other = JSON.parse(record.other); } catch (e) { console.error( `Failed to parse record.other: "${record.other}".`, e, ); } if (other === null) { return <>; } if (other.group !== undefined) { return <>{renderGroup(other.group)}; } else { return <>; } } } else { return <>; } }, }, { key: COLUMN_KEYS.TYPE, title: t('类型'), dataIndex: 'type', render: (text, record, index) => { return <>{renderType(text, t)}; }, }, { key: COLUMN_KEYS.MODEL, title: t('模型'), dataIndex: 'model_name', render: (text, record, index) => { return record.type === 0 || record.type === 2 || record.type === 5 || record.type === 6 ? ( <>{renderModelName(record, copyText, t)} ) : ( <> ); }, }, { key: COLUMN_KEYS.USE_TIME, title: t('用时/首字'), dataIndex: 'use_time', render: (text, record, index) => { if (!(record.type === 2 || record.type === 5)) { return <>; } if (record.is_stream) { let other = getLogOther(record.other); return ( <> {renderUseTime(text, t)} {renderFirstUseTime(other?.frt, t)} {renderIsStream(record.is_stream, t)} ); } else { return ( <> {renderUseTime(text, t)} {renderIsStream(record.is_stream, t)} ); } }, }, { key: COLUMN_KEYS.PROMPT, title: (
{t('输入')}
), dataIndex: 'prompt_tokens', render: (text, record, index) => { const other = getLogOther(record.other); const cacheSummary = getPromptCacheSummary(other); const hasCacheRead = (cacheSummary?.cacheReadTokens || 0) > 0; const hasCacheWrite = (cacheSummary?.cacheWriteTokens || 0) > 0; let cacheText = ''; if (hasCacheRead && hasCacheWrite) { cacheText = `${t('缓存读')} ${formatTokenCount(cacheSummary.cacheReadTokens)} · ${t('写')} ${formatTokenCount(cacheSummary.cacheWriteTokens)}`; } else if (hasCacheRead) { cacheText = `${t('缓存读')} ${formatTokenCount(cacheSummary.cacheReadTokens)}`; } else if (hasCacheWrite) { cacheText = `${t('缓存写')} ${formatTokenCount(cacheSummary.cacheWriteTokens)}`; } return record.type === 0 || record.type === 2 || record.type === 5 || record.type === 6 ? (
{text} {cacheText ? ( {cacheText} ) : null}
) : ( <> ); }, }, { key: COLUMN_KEYS.COMPLETION, title: t('输出'), dataIndex: 'completion_tokens', render: (text, record, index) => { return parseInt(text) > 0 && (record.type === 0 || record.type === 2 || record.type === 5 || record.type === 6) ? ( <>{ {text} } ) : ( <> ); }, }, { key: COLUMN_KEYS.COST, title: t('花费'), dataIndex: 'quota', render: (text, record, index) => { if ( !( record.type === 0 || record.type === 2 || record.type === 5 || record.type === 6 ) ) { return <>; } const other = getLogOther(record.other); const isSubscription = other?.billing_source === 'subscription'; if (isSubscription) { // Subscription billed: show only tag (no $0), but keep tooltip for equivalent cost. return ( {renderBillingTag(record, t)} ); } return <>{renderQuota(text, 6)}; }, }, { key: COLUMN_KEYS.IP, title: (
{t('IP')}
), dataIndex: 'ip', render: (text, record, index) => { return (record.type === 2 || record.type === 5) && text ? ( { copyText(event, text); }} > {text} ) : ( <> ); }, }, { key: COLUMN_KEYS.RETRY, title: t('重试'), dataIndex: 'retry', render: (text, record, index) => { if (!(record.type === 2 || record.type === 5)) { return <>; } let content = t('渠道') + `:${record.channel}`; if (record.other !== '') { let other = JSON.parse(record.other); if (other === null) { return <>; } if (other.admin_info !== undefined) { if ( other.admin_info.use_channel !== null && other.admin_info.use_channel !== undefined && other.admin_info.use_channel !== '' ) { let useChannel = other.admin_info.use_channel; let useChannelStr = useChannel.join('->'); content = t('渠道') + `:${useChannelStr}`; } } } return isAdminUser ?
{content}
: <>; }, }, { key: COLUMN_KEYS.DETAILS, title: t('详情'), dataIndex: 'content', fixed: 'right', width: 200, render: (text, record, index) => { const detailSummary = getUsageLogDetailSummary( record, text, billingDisplayMode, t, ); if (!detailSummary) { return ( {text} ); } return renderCompactDetailSummary(detailSummary.segments); }, }, ]; };