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. 商业版支持知识库分块时,LLM 进行自动分段识别。
|
||||||
|
|
||||||
## ⚙️ 优化
|
## ⚙️ 优化
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Hunyuan": "Tencent Hunyuan",
|
"Hunyuan": "Tencent Hunyuan",
|
||||||
|
"aipoint_usage": "AI points",
|
||||||
|
"all": "All",
|
||||||
"api_key": "API key",
|
"api_key": "API key",
|
||||||
"azure": "Azure",
|
"azure": "Azure",
|
||||||
"base_url": "Base url",
|
"base_url": "Base url",
|
||||||
@@ -16,6 +18,10 @@
|
|||||||
"confirm_delete_channel": "Confirm the deletion of the [{{name}}] channel?",
|
"confirm_delete_channel": "Confirm the deletion of the [{{name}}] channel?",
|
||||||
"copy_model_id_success": "Copyed model id",
|
"copy_model_id_success": "Copyed model id",
|
||||||
"create_channel": "Added channels",
|
"create_channel": "Added channels",
|
||||||
|
"dashboard_error_calls": "Error Calls",
|
||||||
|
"dashboard_model": "Model",
|
||||||
|
"dashboard_points": "points",
|
||||||
|
"dashboard_token_usage": "Tokens",
|
||||||
"default_url": "Default address",
|
"default_url": "Default address",
|
||||||
"detail": "Detail",
|
"detail": "Detail",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
@@ -23,6 +29,7 @@
|
|||||||
"edit_channel": "Channel configuration",
|
"edit_channel": "Channel configuration",
|
||||||
"enable_channel": "Enable",
|
"enable_channel": "Enable",
|
||||||
"forbid_channel": "Disabled",
|
"forbid_channel": "Disabled",
|
||||||
|
"input": "Input",
|
||||||
"key_type": "API key format:",
|
"key_type": "API key format:",
|
||||||
"log": "Call log",
|
"log": "Call log",
|
||||||
"log_detail": "Log details",
|
"log_detail": "Log details",
|
||||||
@@ -33,9 +40,14 @@
|
|||||||
"maxToken_tip": "Model max_tokens parameter",
|
"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.",
|
"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": "Model",
|
||||||
|
"model_error_rate": "Error rate",
|
||||||
|
"model_error_request_times": "Number of failures",
|
||||||
"model_name": "Model name",
|
"model_name": "Model name",
|
||||||
|
"model_request_times": "Request times",
|
||||||
"model_test": "Model testing",
|
"model_test": "Model testing",
|
||||||
"model_tokens": "Input/Output tokens",
|
"model_tokens": "Input/Output tokens",
|
||||||
|
"monitoring": "Monitoring",
|
||||||
|
"output": "Output",
|
||||||
"request_at": "Request time",
|
"request_at": "Request time",
|
||||||
"request_duration": "Request duration: {{duration}}s",
|
"request_duration": "Request duration: {{duration}}s",
|
||||||
"retry_times": "Number of retry times",
|
"retry_times": "Number of retry times",
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"Hunyuan": "腾讯混元",
|
"Hunyuan": "腾讯混元",
|
||||||
|
"all": "全部",
|
||||||
"api_key": "API 密钥",
|
"api_key": "API 密钥",
|
||||||
"azure": "微软 Azure",
|
"azure": "微软 Azure",
|
||||||
"base_url": "代理地址",
|
"base_url": "代理地址",
|
||||||
@@ -16,6 +17,11 @@
|
|||||||
"confirm_delete_channel": "确认删除 【{{name}}】渠道?",
|
"confirm_delete_channel": "确认删除 【{{name}}】渠道?",
|
||||||
"copy_model_id_success": "已复制模型id",
|
"copy_model_id_success": "已复制模型id",
|
||||||
"create_channel": "新增渠道",
|
"create_channel": "新增渠道",
|
||||||
|
"aipoint_usage": "积分消耗",
|
||||||
|
"dashboard_error_calls": "错误次数",
|
||||||
|
"dashboard_model": "模型",
|
||||||
|
"dashboard_points": "积分",
|
||||||
|
"dashboard_token_usage": "Tokens 消耗",
|
||||||
"default_url": "默认地址",
|
"default_url": "默认地址",
|
||||||
"detail": "详情",
|
"detail": "详情",
|
||||||
"duration": "耗时",
|
"duration": "耗时",
|
||||||
@@ -23,6 +29,7 @@
|
|||||||
"edit_channel": "渠道配置",
|
"edit_channel": "渠道配置",
|
||||||
"enable_channel": "启用",
|
"enable_channel": "启用",
|
||||||
"forbid_channel": "禁用",
|
"forbid_channel": "禁用",
|
||||||
|
"input": "输入",
|
||||||
"key_type": "API key 格式: ",
|
"key_type": "API key 格式: ",
|
||||||
"log": "调用日志",
|
"log": "调用日志",
|
||||||
"log_detail": "日志详情",
|
"log_detail": "日志详情",
|
||||||
@@ -33,9 +40,14 @@
|
|||||||
"maxToken_tip": "模型 max_tokens 参数",
|
"maxToken_tip": "模型 max_tokens 参数",
|
||||||
"max_temperature_tip": "模型 temperature 参数,不填则代表模型不支持 temperature 参数。",
|
"max_temperature_tip": "模型 temperature 参数,不填则代表模型不支持 temperature 参数。",
|
||||||
"model": "模型",
|
"model": "模型",
|
||||||
|
"model_error_rate": "失败率",
|
||||||
|
"model_error_request_times": "失败次数",
|
||||||
"model_name": "模型名",
|
"model_name": "模型名",
|
||||||
|
"model_request_times": "请求次数",
|
||||||
"model_test": "模型测试",
|
"model_test": "模型测试",
|
||||||
"model_tokens": "输入/输出 Tokens",
|
"model_tokens": "输入/输出 Tokens",
|
||||||
|
"monitoring": "监控",
|
||||||
|
"output": "输出",
|
||||||
"request_at": "请求时间",
|
"request_at": "请求时间",
|
||||||
"request_duration": "请求时长: {{duration}}s",
|
"request_duration": "请求时长: {{duration}}s",
|
||||||
"retry_times": "重试次数",
|
"retry_times": "重试次数",
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Hunyuan": "騰訊混元",
|
"Hunyuan": "騰訊混元",
|
||||||
|
"aipoint_usage": "積分消耗",
|
||||||
|
"all": "全部",
|
||||||
"api_key": "API 金鑰",
|
"api_key": "API 金鑰",
|
||||||
"azure": "Azure",
|
"azure": "Azure",
|
||||||
"base_url": "代理地址",
|
"base_url": "代理地址",
|
||||||
@@ -16,6 +18,7 @@
|
|||||||
"confirm_delete_channel": "確認刪除【{{name}}】管道?",
|
"confirm_delete_channel": "確認刪除【{{name}}】管道?",
|
||||||
"copy_model_id_success": "已復制模型 id",
|
"copy_model_id_success": "已復制模型 id",
|
||||||
"create_channel": "新增管道",
|
"create_channel": "新增管道",
|
||||||
|
"dashboard_token_usage": "Tokens 消耗",
|
||||||
"default_url": "預設地址",
|
"default_url": "預設地址",
|
||||||
"detail": "詳細資訊",
|
"detail": "詳細資訊",
|
||||||
"duration": "耗時",
|
"duration": "耗時",
|
||||||
@@ -23,6 +26,7 @@
|
|||||||
"edit_channel": "管道設定",
|
"edit_channel": "管道設定",
|
||||||
"enable_channel": "啟用",
|
"enable_channel": "啟用",
|
||||||
"forbid_channel": "停用",
|
"forbid_channel": "停用",
|
||||||
|
"input": "輸入",
|
||||||
"key_type": "API key 格式:",
|
"key_type": "API key 格式:",
|
||||||
"log": "呼叫日誌",
|
"log": "呼叫日誌",
|
||||||
"log_detail": "日誌詳細資訊",
|
"log_detail": "日誌詳細資訊",
|
||||||
@@ -33,9 +37,14 @@
|
|||||||
"maxToken_tip": "模型 max_tokens 參數",
|
"maxToken_tip": "模型 max_tokens 參數",
|
||||||
"max_temperature_tip": "模型 temperature 參數,不填則代表模型不支援 temperature 參數。",
|
"max_temperature_tip": "模型 temperature 參數,不填則代表模型不支援 temperature 參數。",
|
||||||
"model": "模型",
|
"model": "模型",
|
||||||
|
"model_error_rate": "失敗率",
|
||||||
|
"model_error_request_times": "失敗次數",
|
||||||
"model_name": "模型名",
|
"model_name": "模型名",
|
||||||
|
"model_request_times": "請求次數",
|
||||||
"model_test": "模型測試",
|
"model_test": "模型測試",
|
||||||
"model_tokens": "輸入/輸出 Tokens",
|
"model_tokens": "輸入/輸出 Tokens",
|
||||||
|
"monitoring": "監控",
|
||||||
|
"output": "輸出",
|
||||||
"request_at": "請求時間",
|
"request_at": "請求時間",
|
||||||
"request_duration": "請求時長:{{duration}}s",
|
"request_duration": "請求時長:{{duration}}s",
|
||||||
"retry_times": "重試次數",
|
"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;
|
content?: string;
|
||||||
retry_times?: number;
|
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 ModelConfigTable = dynamic(() => import('@/pageComponents/account/model/ModelConfigTable'));
|
||||||
const ChannelTable = dynamic(() => import('@/pageComponents/account/model/Channel'));
|
const ChannelTable = dynamic(() => import('@/pageComponents/account/model/Channel'));
|
||||||
const ChannelLog = dynamic(() => import('@/pageComponents/account/model/Log'));
|
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 ModelProvider = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -32,7 +33,8 @@ const ModelProvider = () => {
|
|||||||
...(feConfigs?.show_aiproxy
|
...(feConfigs?.show_aiproxy
|
||||||
? [
|
? [
|
||||||
{ label: t('account:channel'), value: 'channel' },
|
{ 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 === 'config' && <ModelConfigTable Tab={Tab} />}
|
||||||
{tab === 'channel' && <ChannelTable Tab={Tab} />}
|
{tab === 'channel' && <ChannelTable Tab={Tab} />}
|
||||||
{tab === 'channel_log' && <ChannelLog Tab={Tab} />}
|
{tab === 'channel_log' && <ChannelLog Tab={Tab} />}
|
||||||
|
{tab === 'account_model' && <ModelDashboard Tab={Tab} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
</AccountContainer>
|
</AccountContainer>
|
||||||
);
|
);
|
||||||
|
@@ -7,6 +7,11 @@ import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
|
|||||||
const baseUrl = process.env.AIPROXY_API_ENDPOINT;
|
const baseUrl = process.env.AIPROXY_API_ENDPOINT;
|
||||||
const token = process.env.AIPROXY_API_TOKEN;
|
const token = process.env.AIPROXY_API_TOKEN;
|
||||||
|
|
||||||
|
// 特殊路径映射,标记需要在末尾保留斜杠的路径
|
||||||
|
const endPathMap: Record<string, boolean> = {
|
||||||
|
'api/dashboardv2': true
|
||||||
|
};
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
await authSystemAdmin({ req });
|
await authSystemAdmin({ req });
|
||||||
@@ -22,9 +27,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const queryStr = new URLSearchParams(query).toString();
|
const queryStr = new URLSearchParams(query).toString();
|
||||||
const requestPath = queryStr
|
// Determine whether the base path requires a trailing slash.
|
||||||
? `/${path?.join('/')}?${new URLSearchParams(query).toString()}`
|
const basePath = `/${path?.join('/')}${endPathMap[path?.join('/')] ? '/' : ''}`;
|
||||||
: `/${path?.join('/')}`;
|
const requestPath = queryStr ? `${basePath}?${queryStr}` : basePath;
|
||||||
|
|
||||||
const parsedUrl = new URL(baseUrl);
|
const parsedUrl = new URL(baseUrl);
|
||||||
delete req.headers?.cookie;
|
delete req.headers?.cookie;
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import axios, { type Method, type AxiosResponse } from 'axios';
|
import axios, { type Method, type AxiosResponse } from 'axios';
|
||||||
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
||||||
import {
|
import type {
|
||||||
type ChannelInfoType,
|
DashboardDataItemType,
|
||||||
type ChannelListResponseType,
|
ChannelInfoType,
|
||||||
type ChannelLogListItemType,
|
ChannelListResponseType,
|
||||||
type CreateChannelProps
|
ChannelLogListItemType,
|
||||||
|
CreateChannelProps
|
||||||
} from '@/global/aiproxy/type';
|
} from '@/global/aiproxy/type';
|
||||||
import type { ChannelStatusEnum } from '@/global/aiproxy/constants';
|
import type { ChannelStatusEnum } from '@/global/aiproxy/constants';
|
||||||
|
|
||||||
@@ -187,3 +188,23 @@ export const getLogDetail = (id: number) =>
|
|||||||
request_body: string;
|
request_body: string;
|
||||||
response_body: string;
|
response_body: string;
|
||||||
}>(`/logs/detail/${id}`);
|
}>(`/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