Files
new-api/web/src/components/table/usage-logs/UsageLogsColumnDefs.js

581 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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 <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import {
Avatar,
Space,
Tag,
Tooltip,
Popover,
Typography
} from '@douyinfe/semi-ui';
import {
timestamp2string,
renderGroup,
renderQuota,
stringToColor,
getLogOther,
renderModelTag,
renderClaudeLogContent,
renderLogContent,
renderModelPriceSimple,
renderAudioModelPrice,
renderClaudeModelPrice,
renderModelPrice
} from '../../../helpers';
import { IconHelpCircle } from '@douyinfe/semi-icons';
import { Route } from 'lucide-react';
const colors = [
'amber',
'blue',
'cyan',
'green',
'grey',
'indigo',
'light-blue',
'lime',
'orange',
'pink',
'purple',
'red',
'teal',
'violet',
'yellow',
];
// Render functions
function renderType(type, t) {
switch (type) {
case 1:
return (
<Tag color='cyan' shape='circle'>
{t('充值')}
</Tag>
);
case 2:
return (
<Tag color='lime' shape='circle'>
{t('消费')}
</Tag>
);
case 3:
return (
<Tag color='orange' shape='circle'>
{t('管理')}
</Tag>
);
case 4:
return (
<Tag color='purple' shape='circle'>
{t('系统')}
</Tag>
);
case 5:
return (
<Tag color='red' shape='circle'>
{t('错误')}
</Tag>
);
default:
return (
<Tag color='grey' shape='circle'>
{t('未知')}
</Tag>
);
}
}
function renderIsStream(bool, t) {
if (bool) {
return (
<Tag color='blue' shape='circle'>
{t('流')}
</Tag>
);
} else {
return (
<Tag color='purple' shape='circle'>
{t('非流')}
</Tag>
);
}
}
function renderUseTime(type, t) {
const time = parseInt(type);
if (time < 101) {
return (
<Tag color='green' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
} else if (time < 300) {
return (
<Tag color='orange' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
}
}
function renderFirstUseTime(type, t) {
let time = parseFloat(type) / 1000.0;
time = time.toFixed(1);
if (time < 3) {
return (
<Tag color='green' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
} else if (time < 10) {
return (
<Tag color='orange' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
}
}
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 (
<>
<Space vertical align={'start'}>
<Popover
content={
<div style={{ padding: 10 }}>
<Space vertical align={'start'}>
<div className='flex items-center'>
<Typography.Text strong style={{ marginRight: 8 }}>
{t('请求并计费模型')}:
</Typography.Text>
{renderModelTag(record.model_name, {
onClick: (event) => {
copyText(event, record.model_name).then((r) => { });
},
})}
</div>
<div className='flex items-center'>
<Typography.Text strong style={{ marginRight: 8 }}>
{t('实际模型')}:
</Typography.Text>
{renderModelTag(other.upstream_model_name, {
onClick: (event) => {
copyText(event, other.upstream_model_name).then(
(r) => { },
);
},
})}
</div>
</Space>
</div>
}
>
{renderModelTag(record.model_name, {
onClick: (event) => {
copyText(event, record.model_name).then((r) => { });
},
suffixIcon: (
<Route
style={{ width: '0.9em', height: '0.9em', opacity: 0.75 }}
/>
),
})}
</Popover>
</Space>
</>
);
}
}
export const getLogsColumns = ({
t,
COLUMN_KEYS,
copyText,
showUserInfoFunc,
isAdminUser,
}) => {
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 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;
}
}
return isAdminUser && (record.type === 0 || record.type === 2 || record.type === 5) ? (
<Space>
<Tooltip content={record.channel_name || t('未知渠道')}>
<span>
<Tag
color={colors[parseInt(text) % colors.length]}
shape='circle'
>
{text}
</Tag>
</span>
</Tooltip>
{isMultiKey && (
<Tag color='white' shape='circle'>
{multiKeyIndex}
</Tag>
)}
</Space>
) : null;
},
},
{
key: COLUMN_KEYS.USERNAME,
title: t('用户'),
dataIndex: 'username',
render: (text, record, index) => {
return isAdminUser ? (
<div>
<Avatar
size='extra-small'
color={stringToColor(text)}
style={{ marginRight: 4 }}
onClick={(event) => {
event.stopPropagation();
showUserInfoFunc(record.user_id);
}}
>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
) : (
<></>
);
},
},
{
key: COLUMN_KEYS.TOKEN,
title: t('令牌'),
dataIndex: 'token_name',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 || record.type === 5 ? (
<div>
<Tag
color='grey'
shape='circle'
onClick={(event) => {
copyText(event, text);
}}
>
{' '}
{t(text)}{' '}
</Tag>
</div>
) : (
<></>
);
},
},
{
key: COLUMN_KEYS.GROUP,
title: t('分组'),
dataIndex: 'group',
render: (text, record, index) => {
if (record.type === 0 || record.type === 2 || record.type === 5) {
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 ? (
<>{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 (
<>
<Space>
{renderUseTime(text, t)}
{renderFirstUseTime(other?.frt, t)}
{renderIsStream(record.is_stream, t)}
</Space>
</>
);
} else {
return (
<>
<Space>
{renderUseTime(text, t)}
{renderIsStream(record.is_stream, t)}
</Space>
</>
);
}
},
},
{
key: COLUMN_KEYS.PROMPT,
title: t('提示'),
dataIndex: 'prompt_tokens',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 || record.type === 5 ? (
<>{<span> {text} </span>}</>
) : (
<></>
);
},
},
{
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) ? (
<>{<span> {text} </span>}</>
) : (
<></>
);
},
},
{
key: COLUMN_KEYS.COST,
title: t('花费'),
dataIndex: 'quota',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 || record.type === 5 ? (
<>{renderQuota(text, 6)}</>
) : (
<></>
);
},
},
{
key: COLUMN_KEYS.IP,
title: (
<div className="flex items-center gap-1">
{t('IP')}
<Tooltip content={t('只有当用户设置开启IP记录时才会进行请求和错误类型日志的IP记录')}>
<IconHelpCircle className="text-gray-400 cursor-help" />
</Tooltip>
</div>
),
dataIndex: 'ip',
render: (text, record, index) => {
return (record.type === 2 || record.type === 5) && text ? (
<Tooltip content={text}>
<span>
<Tag
color='orange'
shape='circle'
onClick={(event) => {
copyText(event, text);
}}
>
{text}
</Tag>
</span>
</Tooltip>
) : (
<></>
);
},
},
{
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 ? <div>{content}</div> : <></>;
},
},
{
key: COLUMN_KEYS.DETAILS,
title: t('详情'),
dataIndex: 'content',
fixed: 'right',
render: (text, record, index) => {
let other = getLogOther(record.other);
if (other == null || record.type !== 2) {
return (
<Typography.Paragraph
ellipsis={{
rows: 2,
showTooltip: {
type: 'popover',
opts: { style: { width: 240 } },
},
}}
style={{ maxWidth: 240 }}
>
{text}
</Typography.Paragraph>
);
}
let content = 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,
false,
1.0,
other?.is_system_prompt_overwritten,
'claude'
)
: 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,
false,
1.0,
other?.is_system_prompt_overwritten,
'openai'
);
return (
<Typography.Paragraph
ellipsis={{
rows: 3,
}}
style={{ maxWidth: 240, whiteSpace: 'pre-line' }}
>
{content}
</Typography.Paragraph>
);
},
},
];
};