Commit 101a6e95 by Archer Committed by GitHub

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>
parent 01ff56b4
...@@ -9,6 +9,7 @@ weight: 789 ...@@ -9,6 +9,7 @@ weight: 789
## 🚀 新增内容 ## 🚀 新增内容
1. AI proxy 服务,支持以图表形式展示模型调用情况。
1. 商业版支持知识库分块时,LLM 进行自动分段识别。 1. 商业版支持知识库分块时,LLM 进行自动分段识别。
## ⚙️ 优化 ## ⚙️ 优化
......
{ {
"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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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",
......
{ {
"Hunyuan": "腾讯混元", "Hunyuan": "腾讯混元",
"all": "全部",
"api_key": "API 密钥", "api_key": "API 密钥",
"azure": "微软 Azure", "azure": "微软 Azure",
"base_url": "代理地址", "base_url": "代理地址",
...@@ -16,6 +17,11 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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": "重试次数",
......
{ {
"Hunyuan": "騰訊混元", "Hunyuan": "騰訊混元",
"aipoint_usage": "積分消耗",
"all": "全部",
"api_key": "API 金鑰", "api_key": "API 金鑰",
"azure": "Azure", "azure": "Azure",
"base_url": "代理地址", "base_url": "代理地址",
...@@ -16,6 +18,7 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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": "重試次數",
......
...@@ -52,3 +52,13 @@ export type ChannelLogListItemType = { ...@@ -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;
};
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;
...@@ -13,8 +13,9 @@ import { useSystemStore } from '@/web/common/system/useSystemStore'; ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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'; ...@@ -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) ...@@ -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;
......
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) => ...@@ -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'
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment