mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-21 03:35:36 +00:00
Feat: ai proxy monitor (#4985)
* Aiproxy ModelBoard (#4983) * Aiproxy ModelBoard * Add components LineChartComponent and Make some revisions * perf: ai proxy dashboard * doc * remove invalid i18n * remove invalid i18n --------- Co-authored-by: Zhuangzai fa <143257420+ctrlz526@users.noreply.github.com>
This commit is contained in:
@@ -9,6 +9,7 @@ weight: 789
|
||||
|
||||
## 🚀 新增内容
|
||||
|
||||
1. AI proxy 服务,支持以图表形式展示模型调用情况。
|
||||
1. 商业版支持知识库分块时,LLM 进行自动分段识别。
|
||||
|
||||
## ⚙️ 优化
|
||||
|
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"Hunyuan": "Tencent Hunyuan",
|
||||
"aipoint_usage": "AI points",
|
||||
"all": "All",
|
||||
"api_key": "API key",
|
||||
"azure": "Azure",
|
||||
"base_url": "Base url",
|
||||
@@ -16,6 +18,10 @@
|
||||
"confirm_delete_channel": "Confirm the deletion of the [{{name}}] channel?",
|
||||
"copy_model_id_success": "Copyed model id",
|
||||
"create_channel": "Added channels",
|
||||
"dashboard_error_calls": "Error Calls",
|
||||
"dashboard_model": "Model",
|
||||
"dashboard_points": "points",
|
||||
"dashboard_token_usage": "Tokens",
|
||||
"default_url": "Default address",
|
||||
"detail": "Detail",
|
||||
"duration": "Duration",
|
||||
@@ -23,6 +29,7 @@
|
||||
"edit_channel": "Channel configuration",
|
||||
"enable_channel": "Enable",
|
||||
"forbid_channel": "Disabled",
|
||||
"input": "Input",
|
||||
"key_type": "API key format:",
|
||||
"log": "Call log",
|
||||
"log_detail": "Log details",
|
||||
@@ -33,9 +40,14 @@
|
||||
"maxToken_tip": "Model max_tokens parameter",
|
||||
"max_temperature_tip": "If the model temperature parameter is not filled in, it means that the model does not support the temperature parameter.",
|
||||
"model": "Model",
|
||||
"model_error_rate": "Error rate",
|
||||
"model_error_request_times": "Number of failures",
|
||||
"model_name": "Model name",
|
||||
"model_request_times": "Request times",
|
||||
"model_test": "Model testing",
|
||||
"model_tokens": "Input/Output tokens",
|
||||
"monitoring": "Monitoring",
|
||||
"output": "Output",
|
||||
"request_at": "Request time",
|
||||
"request_duration": "Request duration: {{duration}}s",
|
||||
"retry_times": "Number of retry times",
|
||||
|
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"Hunyuan": "腾讯混元",
|
||||
"all": "全部",
|
||||
"api_key": "API 密钥",
|
||||
"azure": "微软 Azure",
|
||||
"base_url": "代理地址",
|
||||
@@ -16,6 +17,11 @@
|
||||
"confirm_delete_channel": "确认删除 【{{name}}】渠道?",
|
||||
"copy_model_id_success": "已复制模型id",
|
||||
"create_channel": "新增渠道",
|
||||
"aipoint_usage": "积分消耗",
|
||||
"dashboard_error_calls": "错误次数",
|
||||
"dashboard_model": "模型",
|
||||
"dashboard_points": "积分",
|
||||
"dashboard_token_usage": "Tokens 消耗",
|
||||
"default_url": "默认地址",
|
||||
"detail": "详情",
|
||||
"duration": "耗时",
|
||||
@@ -23,6 +29,7 @@
|
||||
"edit_channel": "渠道配置",
|
||||
"enable_channel": "启用",
|
||||
"forbid_channel": "禁用",
|
||||
"input": "输入",
|
||||
"key_type": "API key 格式: ",
|
||||
"log": "调用日志",
|
||||
"log_detail": "日志详情",
|
||||
@@ -33,9 +40,14 @@
|
||||
"maxToken_tip": "模型 max_tokens 参数",
|
||||
"max_temperature_tip": "模型 temperature 参数,不填则代表模型不支持 temperature 参数。",
|
||||
"model": "模型",
|
||||
"model_error_rate": "失败率",
|
||||
"model_error_request_times": "失败次数",
|
||||
"model_name": "模型名",
|
||||
"model_request_times": "请求次数",
|
||||
"model_test": "模型测试",
|
||||
"model_tokens": "输入/输出 Tokens",
|
||||
"monitoring": "监控",
|
||||
"output": "输出",
|
||||
"request_at": "请求时间",
|
||||
"request_duration": "请求时长: {{duration}}s",
|
||||
"retry_times": "重试次数",
|
||||
|
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"Hunyuan": "騰訊混元",
|
||||
"aipoint_usage": "積分消耗",
|
||||
"all": "全部",
|
||||
"api_key": "API 金鑰",
|
||||
"azure": "Azure",
|
||||
"base_url": "代理地址",
|
||||
@@ -16,6 +18,7 @@
|
||||
"confirm_delete_channel": "確認刪除【{{name}}】管道?",
|
||||
"copy_model_id_success": "已復制模型 id",
|
||||
"create_channel": "新增管道",
|
||||
"dashboard_token_usage": "Tokens 消耗",
|
||||
"default_url": "預設地址",
|
||||
"detail": "詳細資訊",
|
||||
"duration": "耗時",
|
||||
@@ -23,6 +26,7 @@
|
||||
"edit_channel": "管道設定",
|
||||
"enable_channel": "啟用",
|
||||
"forbid_channel": "停用",
|
||||
"input": "輸入",
|
||||
"key_type": "API key 格式:",
|
||||
"log": "呼叫日誌",
|
||||
"log_detail": "日誌詳細資訊",
|
||||
@@ -33,9 +37,14 @@
|
||||
"maxToken_tip": "模型 max_tokens 參數",
|
||||
"max_temperature_tip": "模型 temperature 參數,不填則代表模型不支援 temperature 參數。",
|
||||
"model": "模型",
|
||||
"model_error_rate": "失敗率",
|
||||
"model_error_request_times": "失敗次數",
|
||||
"model_name": "模型名",
|
||||
"model_request_times": "請求次數",
|
||||
"model_test": "模型測試",
|
||||
"model_tokens": "輸入/輸出 Tokens",
|
||||
"monitoring": "監控",
|
||||
"output": "輸出",
|
||||
"request_at": "請求時間",
|
||||
"request_duration": "請求時長:{{duration}}s",
|
||||
"retry_times": "重試次數",
|
||||
|
10
projects/app/src/global/aiproxy/type.d.ts
vendored
10
projects/app/src/global/aiproxy/type.d.ts
vendored
@@ -52,3 +52,13 @@ export type ChannelLogListItemType = {
|
||||
content?: string;
|
||||
retry_times?: number;
|
||||
};
|
||||
|
||||
export type DashboardDataItemType = {
|
||||
model: string;
|
||||
request_count: number;
|
||||
used_amount: number;
|
||||
exception_count: number;
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
total_tokens?: number;
|
||||
};
|
||||
|
@@ -0,0 +1,169 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Box, HStack, useTheme } from '@chakra-ui/react';
|
||||
import {
|
||||
ResponsiveContainer,
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
type TooltipProps
|
||||
} from 'recharts';
|
||||
import { type NameType, type ValueType } from 'recharts/types/component/DefaultTooltipContent';
|
||||
import { formatNumber } from '@fastgpt/global/common/math/tools';
|
||||
|
||||
type XAxisConfig = {
|
||||
dataKey: string;
|
||||
};
|
||||
|
||||
type LineConfig = {
|
||||
dataKey: string;
|
||||
name: string;
|
||||
color: string;
|
||||
gradient?: boolean;
|
||||
};
|
||||
|
||||
type TooltipItem = {
|
||||
label: string;
|
||||
dataKey: string;
|
||||
color: string;
|
||||
formatter?: (value: number) => string;
|
||||
customValue?: (data: any) => number;
|
||||
};
|
||||
|
||||
type LineChartComponentProps = {
|
||||
data: Record<string, any>[];
|
||||
title: string;
|
||||
HeaderRightChildren?: React.ReactNode;
|
||||
lines: LineConfig[];
|
||||
tooltipItems?: TooltipItem[];
|
||||
};
|
||||
|
||||
const CustomTooltip = ({
|
||||
active,
|
||||
payload,
|
||||
tooltipItems
|
||||
}: TooltipProps<ValueType, NameType> & { tooltipItems?: TooltipItem[] }) => {
|
||||
const data = payload?.[0]?.payload;
|
||||
|
||||
if (active && data && tooltipItems) {
|
||||
return (
|
||||
<Box bg={'white'} p={3} borderRadius={'md'} border={'base'} boxShadow={'sm'}>
|
||||
<Box fontSize={'sm'} color={'myGray.900'} mb={2}>
|
||||
{data.x}
|
||||
</Box>
|
||||
{tooltipItems.map((item, index) => {
|
||||
const value = (() => {
|
||||
if (item.customValue) {
|
||||
return item.customValue(data);
|
||||
} else {
|
||||
return data[item.dataKey];
|
||||
}
|
||||
})();
|
||||
|
||||
const displayValue = item.formatter ? item.formatter(value) : formatNumber(value);
|
||||
|
||||
return (
|
||||
<HStack key={index} fontSize={'sm'} _notLast={{ mb: 1 }}>
|
||||
<Box w={2} h={2} borderRadius={'full'} bg={item.color} />
|
||||
<Box>{item.label}</Box>
|
||||
<Box>{displayValue}</Box>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const LineChartComponent = ({
|
||||
data,
|
||||
title,
|
||||
HeaderRightChildren,
|
||||
lines,
|
||||
tooltipItems
|
||||
}: LineChartComponentProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// Y-axis number formatter function
|
||||
const formatYAxisNumber = useCallback((value: number): string => {
|
||||
if (value >= 1000000) {
|
||||
return value / 1000000 + 'M';
|
||||
} else if (value >= 1000) {
|
||||
return value / 1000 + 'K';
|
||||
}
|
||||
return value.toString();
|
||||
}, []);
|
||||
|
||||
// Generate gradient definitions
|
||||
const gradientDefs = useMemo(() => {
|
||||
return (
|
||||
<defs>
|
||||
{lines.map((line, index) => (
|
||||
<linearGradient
|
||||
key={`gradient-${line.color}`}
|
||||
id={`gradient-${line.color}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="0%" stopColor={line.color} stopOpacity={0.25} />
|
||||
<stop offset="100%" stopColor={line.color} stopOpacity={0.01} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
);
|
||||
}, [lines]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HStack mb={4} justifyContent={'space-between'} alignItems={'flex-start'}>
|
||||
<Box fontSize={'sm'} color={'myGray.900'} fontWeight={'medium'}>
|
||||
{title}
|
||||
</Box>
|
||||
{HeaderRightChildren && HeaderRightChildren}
|
||||
</HStack>
|
||||
<ResponsiveContainer width="100%" height={'100%'}>
|
||||
<AreaChart
|
||||
data={data}
|
||||
margin={{ top: 5, right: 30, left: 0, bottom: HeaderRightChildren ? 30 : 15 }}
|
||||
>
|
||||
{gradientDefs}
|
||||
<XAxis
|
||||
dataKey={'x'}
|
||||
tickMargin={10}
|
||||
tick={{ fontSize: '12px', color: theme.colors.myGray['500'], fontWeight: '500' }}
|
||||
interval={'preserveStartEnd'}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickSize={0}
|
||||
tickMargin={10}
|
||||
tick={{ fontSize: '12px', color: theme.colors.myGray['500'], fontWeight: '500' }}
|
||||
interval={'preserveStartEnd'}
|
||||
tickFormatter={formatYAxisNumber}
|
||||
/>
|
||||
<CartesianGrid strokeDasharray="3 3" horizontal={true} vertical={false} />
|
||||
{tooltipItems && <Tooltip content={<CustomTooltip tooltipItems={tooltipItems} />} />}
|
||||
{lines.map((line, index) => (
|
||||
<Area
|
||||
key={index}
|
||||
type="monotone"
|
||||
name={line.name}
|
||||
dataKey={line.dataKey}
|
||||
stroke={line.color}
|
||||
strokeWidth={2}
|
||||
fill={`url(#gradient-${line.color})`}
|
||||
dot={false}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LineChartComponent;
|
@@ -0,0 +1,405 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
import { Box, Flex, Grid, HStack, useTheme } from '@chakra-ui/react';
|
||||
import { formatNumber } from '@fastgpt/global/common/math/tools';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { addDays } from 'date-fns';
|
||||
import dayjs from 'dayjs';
|
||||
import DateRangePicker, {
|
||||
type DateRangeType
|
||||
} from '@fastgpt/web/components/common/DateRangePicker';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { getChannelList, getDashboardV2 } from '@/web/core/ai/channel';
|
||||
import { getSystemModelList } from '@/web/core/ai/config';
|
||||
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
|
||||
import LineChartComponent from './LineChartComponent';
|
||||
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
export type ModelDashboardData = {
|
||||
x: string;
|
||||
totalCalls: number;
|
||||
errorCalls: number;
|
||||
errorRate: number;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
totalTokens: number;
|
||||
totalCost: number;
|
||||
};
|
||||
|
||||
const ChartsBoxStyles: BoxProps = {
|
||||
px: 5,
|
||||
pt: 4,
|
||||
pb: 8,
|
||||
h: '300px',
|
||||
border: 'base',
|
||||
borderRadius: 'md',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
// Displays model usage statistics, token consumption and cost visualization
|
||||
const ModelDashboard = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const [filterProps, setFilterProps] = useState<{
|
||||
channelId?: string;
|
||||
model?: string;
|
||||
dateRange: DateRangeType;
|
||||
}>({
|
||||
channelId: undefined,
|
||||
model: undefined,
|
||||
dateRange: {
|
||||
from: (() => {
|
||||
const today = addDays(new Date(), -7);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return today;
|
||||
})(),
|
||||
to: (() => {
|
||||
const today = new Date();
|
||||
today.setHours(23, 59, 59, 999);
|
||||
return today;
|
||||
})()
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch channel list with "All" option
|
||||
const { data: channelList = [] } = useRequest2(
|
||||
async () => {
|
||||
const res = await getChannelList().then((res) =>
|
||||
res.map((item) => ({
|
||||
label: item.name,
|
||||
value: `${item.id}`
|
||||
}))
|
||||
);
|
||||
return [
|
||||
{
|
||||
label: t('common:All'),
|
||||
value: ''
|
||||
},
|
||||
...res
|
||||
];
|
||||
},
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
// Get model list filtered by selected channel
|
||||
const { data: systemModelList = [] } = useRequest2(getSystemModelList, {
|
||||
manual: false
|
||||
});
|
||||
const modelList = useMemo(() => {
|
||||
const res = systemModelList
|
||||
.map((item) => {
|
||||
const provider = getModelProvider(item.provider);
|
||||
return {
|
||||
order: provider.order,
|
||||
icon: provider.avatar,
|
||||
label: item.model,
|
||||
value: item.model
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.order - b.order);
|
||||
return [
|
||||
{
|
||||
label: t('common:All'),
|
||||
value: ''
|
||||
},
|
||||
...res
|
||||
];
|
||||
}, [systemModelList, t]);
|
||||
|
||||
// Fetch dashboard data with date range and channel filters
|
||||
const { data: dashboardData = [], loading: isLoading } = useRequest2(
|
||||
async () => {
|
||||
const params = {
|
||||
channel: filterProps.channelId ? parseInt(filterProps.channelId) : 0,
|
||||
start_timestamp: filterProps.dateRange.from
|
||||
? Math.floor(filterProps.dateRange.from.getTime())
|
||||
: undefined,
|
||||
end_timestamp: filterProps.dateRange.to
|
||||
? Math.floor(filterProps.dateRange.to.getTime())
|
||||
: undefined,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
timespan: 'day' as const
|
||||
};
|
||||
|
||||
let data = await getDashboardV2(params);
|
||||
|
||||
if (filterProps.model) {
|
||||
data = data.map((item) => {
|
||||
const filterModels = item.models.filter((model) => model.model === filterProps.model);
|
||||
return {
|
||||
...item,
|
||||
models: filterModels
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [filterProps.channelId, filterProps.dateRange, filterProps.model]
|
||||
}
|
||||
);
|
||||
|
||||
// Process chart data - aggregate daily model calls, token usage and cost data
|
||||
const chartData: ModelDashboardData[] = useMemo(() => {
|
||||
if (dashboardData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Model price map
|
||||
const modelPriceMap = new Map<
|
||||
string,
|
||||
{
|
||||
inputPrice?: number;
|
||||
outputPrice?: number;
|
||||
charsPointsPrice?: number;
|
||||
}
|
||||
>();
|
||||
systemModelList.forEach((model) => {
|
||||
modelPriceMap.set(model.model, {
|
||||
inputPrice: model.inputPrice,
|
||||
outputPrice: model.outputPrice,
|
||||
charsPointsPrice: model.charsPointsPrice
|
||||
});
|
||||
});
|
||||
|
||||
return dashboardData.map((item) => {
|
||||
const date = dayjs(item.timestamp * 1000).format('MM-DD');
|
||||
const totalCalls = item.models.reduce((acc, model) => acc + model.request_count, 0);
|
||||
const errorCalls = item.models.reduce((acc, model) => acc + model.exception_count, 0);
|
||||
const errorRate = Number((errorCalls / totalCalls).toFixed(2));
|
||||
|
||||
const inputTokens = item.models.reduce((acc, model) => acc + (model?.input_tokens || 0), 0);
|
||||
const outputTokens = item.models.reduce((acc, model) => acc + (model?.output_tokens || 0), 0);
|
||||
const totalTokens = item.models.reduce((acc, model) => acc + (model?.total_tokens || 0), 0);
|
||||
|
||||
const totalCost = item.models.reduce((acc, model) => {
|
||||
const modelPricing = modelPriceMap.get(model.model);
|
||||
|
||||
if (modelPricing) {
|
||||
const inputTokens = model.input_tokens || 0;
|
||||
const outputTokens = model.output_tokens || 0;
|
||||
const isIOPriceType =
|
||||
typeof modelPricing.inputPrice === 'number' && modelPricing.inputPrice > 0;
|
||||
|
||||
const totalPoints = isIOPriceType
|
||||
? (modelPricing.inputPrice || 0) * (inputTokens / 1000) +
|
||||
(modelPricing.outputPrice || 0) * (outputTokens / 1000)
|
||||
: ((modelPricing.charsPointsPrice || 0) * (inputTokens + outputTokens)) / 1000;
|
||||
|
||||
return acc + totalPoints;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
x: date,
|
||||
totalCalls,
|
||||
errorCalls,
|
||||
errorRate,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
totalTokens,
|
||||
totalCost
|
||||
};
|
||||
});
|
||||
}, [dashboardData, systemModelList]);
|
||||
|
||||
const [tokensUsageType, setTokensUsageType] = useState<
|
||||
'inputTokens' | 'outputTokens' | 'totalTokens'
|
||||
>('totalTokens');
|
||||
console.log(chartData);
|
||||
return (
|
||||
<>
|
||||
<Box>{Tab}</Box>
|
||||
|
||||
<HStack spacing={4}>
|
||||
<HStack>
|
||||
<FormLabel>{t('common:user.Time')}</FormLabel>
|
||||
<Box>
|
||||
<DateRangePicker
|
||||
defaultDate={filterProps.dateRange}
|
||||
dateRange={filterProps.dateRange}
|
||||
position="bottom"
|
||||
onSuccess={(e) => setFilterProps({ ...filterProps, dateRange: e })}
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
<HStack>
|
||||
<FormLabel>{t('account_model:channel_name')}</FormLabel>
|
||||
<Box flex={'1 0 0'}>
|
||||
<MySelect<string>
|
||||
bg={'myGray.50'}
|
||||
isSearch
|
||||
list={channelList}
|
||||
placeholder={t('account_model:select_channel')}
|
||||
value={filterProps.channelId}
|
||||
onChange={(val) => setFilterProps({ ...filterProps, channelId: val })}
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
<HStack>
|
||||
<FormLabel>{t('account_model:model_name')}</FormLabel>
|
||||
<Box flex={'1 0 0'}>
|
||||
<MySelect<string>
|
||||
bg={'myGray.50'}
|
||||
isSearch
|
||||
list={modelList}
|
||||
placeholder={t('account_model:select_model')}
|
||||
value={filterProps.model}
|
||||
onChange={(val) => setFilterProps({ ...filterProps, model: val })}
|
||||
/>
|
||||
</Box>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
<MyBox flex={'1 0 0'} h={0} overflowY={'auto'} isLoading={isLoading}>
|
||||
{dashboardData && dashboardData.length > 0 && (
|
||||
<>
|
||||
<Box {...ChartsBoxStyles}>
|
||||
<LineChartComponent
|
||||
data={chartData}
|
||||
title={t('account_model:model_request_times')}
|
||||
lines={[
|
||||
{
|
||||
dataKey: 'totalCalls',
|
||||
name: t('account_model:model_request_times'),
|
||||
color: theme.colors.primary['600']
|
||||
}
|
||||
]}
|
||||
tooltipItems={[
|
||||
{
|
||||
label: t('account_model:model_request_times'),
|
||||
dataKey: 'totalCalls',
|
||||
color: theme.colors.primary['600']
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gap={5}>
|
||||
<Box {...ChartsBoxStyles}>
|
||||
<LineChartComponent
|
||||
data={chartData}
|
||||
title={t('account_model:model_error_request_times')}
|
||||
lines={[
|
||||
{
|
||||
dataKey: 'errorCalls',
|
||||
name: t('account_model:model_error_request_times'),
|
||||
color: '#f98e1a'
|
||||
}
|
||||
]}
|
||||
tooltipItems={[
|
||||
{
|
||||
label: t('account_model:model_error_request_times'),
|
||||
dataKey: 'errorCalls',
|
||||
color: '#f98e1a'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
<Box {...ChartsBoxStyles}>
|
||||
<LineChartComponent
|
||||
data={chartData}
|
||||
title={t('account_model:model_error_rate')}
|
||||
lines={[
|
||||
{
|
||||
dataKey: 'errorRate',
|
||||
name: t('account_model:model_error_rate'),
|
||||
color: '#e84738'
|
||||
}
|
||||
]}
|
||||
tooltipItems={[
|
||||
{
|
||||
label: t('account_model:model_error_rate'),
|
||||
dataKey: 'errorRate',
|
||||
color: '#e84738'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Box mt={5} {...ChartsBoxStyles}>
|
||||
<LineChartComponent
|
||||
data={chartData}
|
||||
title={t('account_model:dashboard_token_usage')}
|
||||
lines={[
|
||||
{
|
||||
dataKey: tokensUsageType,
|
||||
name: t('account_model:dashboard_token_usage'),
|
||||
color: theme.colors.primary['600']
|
||||
}
|
||||
]}
|
||||
tooltipItems={[
|
||||
{
|
||||
label: t('account_model:dashboard_token_usage'),
|
||||
dataKey: tokensUsageType,
|
||||
color: theme.colors.primary['600']
|
||||
}
|
||||
]}
|
||||
HeaderRightChildren={
|
||||
<FillRowTabs<'inputTokens' | 'outputTokens' | 'totalTokens'>
|
||||
list={[
|
||||
{
|
||||
label: t('account_model:all'),
|
||||
value: 'totalTokens'
|
||||
},
|
||||
{
|
||||
label: t('account_model:input'),
|
||||
value: 'inputTokens'
|
||||
},
|
||||
{
|
||||
label: t('account_model:output'),
|
||||
value: 'outputTokens'
|
||||
}
|
||||
]}
|
||||
py={1}
|
||||
px={5}
|
||||
value={tokensUsageType}
|
||||
onChange={(val) => setTokensUsageType(val)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{feConfigs?.isPlus && (
|
||||
<Box mt={5} {...ChartsBoxStyles}>
|
||||
<LineChartComponent
|
||||
data={chartData}
|
||||
title={t('account_model:aipoint_usage')}
|
||||
lines={[
|
||||
{
|
||||
dataKey: 'totalCost',
|
||||
name: t('account_model:aipoint_usage'),
|
||||
color: '#8774EE'
|
||||
}
|
||||
]}
|
||||
tooltipItems={[
|
||||
{
|
||||
label: t('account_model:aipoint_usage'),
|
||||
dataKey: 'totalCost',
|
||||
color: '#8774EE'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</MyBox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ModelDashboard);
|
@@ -13,8 +13,9 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
const ModelConfigTable = dynamic(() => import('@/pageComponents/account/model/ModelConfigTable'));
|
||||
const ChannelTable = dynamic(() => import('@/pageComponents/account/model/Channel'));
|
||||
const ChannelLog = dynamic(() => import('@/pageComponents/account/model/Log'));
|
||||
const ModelDashboard = dynamic(() => import('@/pageComponents/account/model/ModelDashboard'));
|
||||
|
||||
type TabType = 'model' | 'config' | 'channel' | 'channel_log';
|
||||
type TabType = 'model' | 'config' | 'channel' | 'channel_log' | 'account_model';
|
||||
|
||||
const ModelProvider = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -32,7 +33,8 @@ const ModelProvider = () => {
|
||||
...(feConfigs?.show_aiproxy
|
||||
? [
|
||||
{ label: t('account:channel'), value: 'channel' },
|
||||
{ label: t('account_model:log'), value: 'channel_log' }
|
||||
{ label: t('account_model:log'), value: 'channel_log' },
|
||||
{ label: t('account_model:monitoring'), value: 'account_model' }
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
@@ -50,6 +52,7 @@ const ModelProvider = () => {
|
||||
{tab === 'config' && <ModelConfigTable Tab={Tab} />}
|
||||
{tab === 'channel' && <ChannelTable Tab={Tab} />}
|
||||
{tab === 'channel_log' && <ChannelLog Tab={Tab} />}
|
||||
{tab === 'account_model' && <ModelDashboard Tab={Tab} />}
|
||||
</Flex>
|
||||
</AccountContainer>
|
||||
);
|
||||
|
@@ -7,6 +7,11 @@ import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
|
||||
const baseUrl = process.env.AIPROXY_API_ENDPOINT;
|
||||
const token = process.env.AIPROXY_API_TOKEN;
|
||||
|
||||
// 特殊路径映射,标记需要在末尾保留斜杠的路径
|
||||
const endPathMap: Record<string, boolean> = {
|
||||
'api/dashboardv2': true
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await authSystemAdmin({ req });
|
||||
@@ -22,9 +27,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
const queryStr = new URLSearchParams(query).toString();
|
||||
const requestPath = queryStr
|
||||
? `/${path?.join('/')}?${new URLSearchParams(query).toString()}`
|
||||
: `/${path?.join('/')}`;
|
||||
// Determine whether the base path requires a trailing slash.
|
||||
const basePath = `/${path?.join('/')}${endPathMap[path?.join('/')] ? '/' : ''}`;
|
||||
const requestPath = queryStr ? `${basePath}?${queryStr}` : basePath;
|
||||
|
||||
const parsedUrl = new URL(baseUrl);
|
||||
delete req.headers?.cookie;
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import axios, { type Method, type AxiosResponse } from 'axios';
|
||||
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
||||
import {
|
||||
type ChannelInfoType,
|
||||
type ChannelListResponseType,
|
||||
type ChannelLogListItemType,
|
||||
type CreateChannelProps
|
||||
import type {
|
||||
DashboardDataItemType,
|
||||
ChannelInfoType,
|
||||
ChannelListResponseType,
|
||||
ChannelLogListItemType,
|
||||
CreateChannelProps
|
||||
} from '@/global/aiproxy/type';
|
||||
import type { ChannelStatusEnum } from '@/global/aiproxy/constants';
|
||||
|
||||
@@ -187,3 +188,23 @@ export const getLogDetail = (id: number) =>
|
||||
request_body: string;
|
||||
response_body: string;
|
||||
}>(`/logs/detail/${id}`);
|
||||
|
||||
export const getDashboardV2 = (params: {
|
||||
channel?: number;
|
||||
start_timestamp?: number;
|
||||
end_timestamp?: number;
|
||||
timezone?: string;
|
||||
timespan?: 'day' | 'hour';
|
||||
}) =>
|
||||
GET<
|
||||
{
|
||||
timestamp: number;
|
||||
models: DashboardDataItemType[];
|
||||
}[]
|
||||
>('/dashboardv2/', {
|
||||
channel: params.channel,
|
||||
start_timestamp: params.start_timestamp,
|
||||
end_timestamp: params.end_timestamp,
|
||||
timezone: params.timezone || 'Local',
|
||||
timespan: params.timespan || 'day'
|
||||
});
|
||||
|
Reference in New Issue
Block a user